From 7659555ce57a47f5f1c4407f2987ab53fefa12c8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 15:55:52 +0200 Subject: [PATCH 001/903] Bump version to 2022.9.0dev0 (#75818) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 24d37b94518..fd3909ca23a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 1 PIP_CACHE_VERSION: 1 - HA_SHORT_VERSION: 2022.8 + HA_SHORT_VERSION: 2022.9 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache diff --git a/homeassistant/const.py b/homeassistant/const.py index 10523aa6d53..7542ce0d77e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 8 +MINOR_VERSION: Final = 9 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index 48d12964414..288bcc5225a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0.dev0" +version = "2022.9.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d99334eb07a8cf87877b6c72cac3bcea17191972 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Wed, 27 Jul 2022 10:37:22 -0400 Subject: [PATCH 002/903] Add LaCrosse View integration (#71896) * Add new LaCrosse View integration * Add new LaCrosse View integration * Add retry logic * Actually use the start time for the retry logic * Get new token after 1 hour * Replace retry logic with more reliable logic * Improve test coverage * Add device info and unique id to config entry * Fix manufacturer name * Improve token refresh and check sensor permission * Improve test cover * Add LaCrosse View to .strict-typing * Remove empty fields in manifest.json * Fix mypy * Add retry logic for get_data * Add missing break statement in retry decorator * Fix requirements * Finish suggestions by Allen Porter * Suggestions by Allen Porter * Fix typing issues with calls to get_locations and get_sensors --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/lacrosse_view/__init__.py | 78 +++++++++ .../components/lacrosse_view/config_flow.py | 128 ++++++++++++++ .../components/lacrosse_view/const.py | 6 + .../components/lacrosse_view/manifest.json | 9 + .../components/lacrosse_view/sensor.py | 136 +++++++++++++++ .../components/lacrosse_view/strings.json | 20 +++ .../lacrosse_view/translations/en.json | 25 +++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/lacrosse_view/__init__.py | 32 ++++ .../lacrosse_view/test_config_flow.py | 163 ++++++++++++++++++ tests/components/lacrosse_view/test_init.py | 102 +++++++++++ tests/components/lacrosse_view/test_sensor.py | 49 ++++++ 17 files changed, 769 insertions(+) create mode 100644 homeassistant/components/lacrosse_view/__init__.py create mode 100644 homeassistant/components/lacrosse_view/config_flow.py create mode 100644 homeassistant/components/lacrosse_view/const.py create mode 100644 homeassistant/components/lacrosse_view/manifest.json create mode 100644 homeassistant/components/lacrosse_view/sensor.py create mode 100644 homeassistant/components/lacrosse_view/strings.json create mode 100644 homeassistant/components/lacrosse_view/translations/en.json create mode 100644 tests/components/lacrosse_view/__init__.py create mode 100644 tests/components/lacrosse_view/test_config_flow.py create mode 100644 tests/components/lacrosse_view/test_init.py create mode 100644 tests/components/lacrosse_view/test_sensor.py diff --git a/.strict-typing b/.strict-typing index c5b4e376414..d3102572eb1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -148,6 +148,7 @@ homeassistant.components.jewish_calendar.* homeassistant.components.kaleidescape.* homeassistant.components.knx.* homeassistant.components.kraken.* +homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* diff --git a/CODEOWNERS b/CODEOWNERS index bd39fd68590..96238f61fbb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -573,6 +573,8 @@ build.json @home-assistant/supervisor /tests/components/kraken/ @eifinger /homeassistant/components/kulersky/ @emlove /tests/components/kulersky/ @emlove +/homeassistant/components/lacrosse_view/ @IceBotYT +/tests/components/lacrosse_view/ @IceBotYT /homeassistant/components/lametric/ @robbiet480 @frenck /homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol /tests/components/launch_library/ @ludeeus @DurgNomis-drol diff --git a/homeassistant/components/lacrosse_view/__init__.py b/homeassistant/components/lacrosse_view/__init__.py new file mode 100644 index 00000000000..0d3147f43a5 --- /dev/null +++ b/homeassistant/components/lacrosse_view/__init__.py @@ -0,0 +1,78 @@ +"""The LaCrosse View integration.""" +from __future__ import annotations + +from datetime import datetime, timedelta + +from lacrosse_view import LaCrosse, Location, LoginError, Sensor + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up LaCrosse View from a config entry.""" + + async def get_data() -> list[Sensor]: + """Get the data from the LaCrosse View.""" + now = datetime.utcnow() + + if hass.data[DOMAIN][entry.entry_id]["last_update"] < now - timedelta( + minutes=59 + ): # Get new token + hass.data[DOMAIN][entry.entry_id]["last_update"] = now + await api.login(entry.data["username"], entry.data["password"]) + + # Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request) + yesterday = now - timedelta(days=1) + yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0) + yesterday_timestamp = datetime.timestamp(yesterday) + + return await api.get_sensors( + location=Location(id=entry.data["id"], name=entry.data["name"]), + tz=hass.config.time_zone, + start=str(int(yesterday_timestamp)), + end=str(int(datetime.timestamp(now))), + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "api": LaCrosse(async_get_clientsession(hass)), + "last_update": datetime.utcnow(), + } + api: LaCrosse = hass.data[DOMAIN][entry.entry_id]["api"] + + try: + await api.login(entry.data["username"], entry.data["password"]) + except LoginError as error: + raise ConfigEntryNotReady from error + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name="LaCrosse View", + update_method=get_data, + update_interval=timedelta(seconds=SCAN_INTERVAL), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id]["coordinator"] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/lacrosse_view/config_flow.py b/homeassistant/components/lacrosse_view/config_flow.py new file mode 100644 index 00000000000..b5b89828e9b --- /dev/null +++ b/homeassistant/components/lacrosse_view/config_flow.py @@ -0,0 +1,128 @@ +"""Config flow for LaCrosse View integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from lacrosse_view import LaCrosse, Location, LoginError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("username"): str, + vol.Required("password"): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> list[Location]: + """Validate the user input allows us to connect.""" + + api = LaCrosse(async_get_clientsession(hass)) + + try: + await api.login(data["username"], data["password"]) + + locations = await api.get_locations() + except LoginError as error: + raise InvalidAuth from error + + if not locations: + raise NoLocations("No locations found for account {}".format(data["username"])) + + return locations + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for LaCrosse View.""" + + VERSION = 1 + data: dict[str, str] = {} + locations: list[Location] = [] + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except InvalidAuth: + errors["base"] = "invalid_auth" + except NoLocations: + errors["base"] = "no_locations" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.data = user_input + self.locations = info + return await self.async_step_location() + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_location( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the location step.""" + + if not user_input: + return self.async_show_form( + step_id="location", + data_schema=vol.Schema( + { + vol.Required("location"): vol.In( + {location.id: location.name for location in self.locations} + ) + } + ), + ) + + location_id = user_input["location"] + + for location in self.locations: + if location.id == location_id: + location_name = location.name + + await self.async_set_unique_id(location_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=location_name, + data={ + "id": location_id, + "name": location_name, + "username": self.data["username"], + "password": self.data["password"], + }, + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class NoLocations(HomeAssistantError): + """Error to indicate there are no locations.""" diff --git a/homeassistant/components/lacrosse_view/const.py b/homeassistant/components/lacrosse_view/const.py new file mode 100644 index 00000000000..cae11315bc7 --- /dev/null +++ b/homeassistant/components/lacrosse_view/const.py @@ -0,0 +1,6 @@ +"""Constants for the LaCrosse View integration.""" +import logging + +DOMAIN = "lacrosse_view" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = 30 diff --git a/homeassistant/components/lacrosse_view/manifest.json b/homeassistant/components/lacrosse_view/manifest.json new file mode 100644 index 00000000000..64f40267c8a --- /dev/null +++ b/homeassistant/components/lacrosse_view/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "lacrosse_view", + "name": "LaCrosse View", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/lacrosse_view", + "requirements": ["lacrosse-view==0.0.9"], + "codeowners": ["@IceBotYT"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py new file mode 100644 index 00000000000..8ccbe0514b4 --- /dev/null +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -0,0 +1,136 @@ +"""Sensor component for LaCrosse View.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from re import sub + +from lacrosse_view import Sensor + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, LOGGER + + +@dataclass +class LaCrosseSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[..., float] + + +@dataclass +class LaCrosseSensorEntityDescription( + SensorEntityDescription, LaCrosseSensorEntityDescriptionMixin +): + """Description for LaCrosse View sensor.""" + + +PARALLEL_UPDATES = 0 +ICON_LIST = { + "Temperature": "mdi:thermometer", + "Humidity": "mdi:water-percent", + "HeatIndex": "mdi:thermometer", + "WindSpeed": "mdi:weather-windy", + "Rain": "mdi:water", +} +UNIT_LIST = { + "degrees_celsius": "°C", + "degrees_fahrenheit": "°F", + "relative_humidity": "%", + "kilometers_per_hour": "km/h", + "inches": "in", +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LaCrosse View from a config entry.""" + coordinator: DataUpdateCoordinator[list[Sensor]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinator"] + sensors: list[Sensor] = coordinator.data + + sensor_list = [] + for i, sensor in enumerate(sensors): + if not sensor.permissions.get("read"): + LOGGER.warning( + "No permission to read sensor %s, are you sure you're signed into the right account?", + sensor.name, + ) + continue + for field in sensor.sensor_field_names: + sensor_list.append( + LaCrosseViewSensor( + coordinator=coordinator, + description=LaCrosseSensorEntityDescription( + key=str(i), + device_class="temperature" if field == "Temperature" else None, + # The regex is to convert CamelCase to Human Case + # e.g. "RelativeHumidity" -> "Relative Humidity" + name=f"{sensor.name} {sub(r'(? None: + """Initialize.""" + super().__init__(coordinator) + sensor = self.coordinator.data[int(description.key)] + + self.entity_description = description + self._attr_unique_id = f"{sensor.location.id}-{description.key}-{field}" + self._attr_name = f"{sensor.location.name} {description.name}" + self._attr_icon = ICON_LIST.get(field, "mdi:thermometer") + self._attr_device_info = { + "identifiers": {(DOMAIN, sensor.sensor_id)}, + "name": sensor.name.split(" ")[0], + "manufacturer": "LaCrosse Technology", + "model": sensor.model, + "via_device": (DOMAIN, sensor.location.id), + } + + @property + def native_value(self) -> float: + """Return the sensor value.""" + return self.entity_description.value_fn() diff --git a/homeassistant/components/lacrosse_view/strings.json b/homeassistant/components/lacrosse_view/strings.json new file mode 100644 index 00000000000..76f1971518a --- /dev/null +++ b/homeassistant/components/lacrosse_view/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": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "no_locations": "No locations found" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json new file mode 100644 index 00000000000..c972d6d12f9 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + "no_locations": "No locations found" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + }, + "location": { + "data": { + "location": "Location" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 28d0ad6b44b..6d92f7cf7e7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -191,6 +191,7 @@ FLOWS = { "kostal_plenticore", "kraken", "kulersky", + "lacrosse_view", "launch_library", "laundrify", "lg_soundbar", diff --git a/mypy.ini b/mypy.ini index 37765023f74..af039c74de3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1351,6 +1351,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lacrosse_view.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lametric.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index ea3f2dd3e6c..bdbff5adabd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -943,6 +943,9 @@ kostal_plenticore==0.2.0 # homeassistant.components.kraken krakenex==2.1.0 +# homeassistant.components.lacrosse_view +lacrosse-view==0.0.9 + # homeassistant.components.eufy lakeside==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f19f813747..3dc23a1e07d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -684,6 +684,9 @@ kostal_plenticore==0.2.0 # homeassistant.components.kraken krakenex==2.1.0 +# homeassistant.components.lacrosse_view +lacrosse-view==0.0.9 + # homeassistant.components.laundrify laundrify_aio==1.1.2 diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py new file mode 100644 index 00000000000..ea01e7a72e3 --- /dev/null +++ b/tests/components/lacrosse_view/__init__.py @@ -0,0 +1,32 @@ +"""Tests for the LaCrosse View integration.""" + +from lacrosse_view import Location, Sensor + +MOCK_ENTRY_DATA = { + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", +} +TEST_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_NO_PERMISSION_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + permissions={"read": False}, + model="Test", +) diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py new file mode 100644 index 00000000000..82178f2801b --- /dev/null +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -0,0 +1,163 @@ +"""Test the LaCrosse View config flow.""" +from unittest.mock import patch + +from lacrosse_view import Location, LoginError + +from homeassistant import config_entries +from homeassistant.components.lacrosse_view.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "location" + assert result2["errors"] is None + + with patch( + "homeassistant.components.lacrosse_view.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "location": "1", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "Test" + assert result3["data"] == { + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_auth_false(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "lacrosse_view.LaCrosse.login", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +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} + ) + + with patch("lacrosse_view.LaCrosse.login", side_effect=LoginError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_login_first(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_locations", side_effect=LoginError + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_no_locations(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "no_locations"} + + +async def test_form_unexpected_error(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lacrosse_view.config_flow.validate_input", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py new file mode 100644 index 00000000000..a719536f737 --- /dev/null +++ b/tests/components/lacrosse_view/test_init.py @@ -0,0 +1,102 @@ +"""Test the LaCrosse View initialization.""" +from datetime import datetime, timedelta +from unittest.mock import patch + +from freezegun import freeze_time +from lacrosse_view import HTTPError, LoginError + +from homeassistant.components.lacrosse_view.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_ENTRY_DATA, TEST_SENSOR + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test the unload entry.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + assert entries[0].state == ConfigEntryState.NOT_LOADED + + +async def test_login_error(hass: HomeAssistant) -> None: + """Test login error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", side_effect=LoginError("Test")): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.SETUP_RETRY + + +async def test_http_error(hass: HomeAssistant) -> None: + """Test http error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", side_effect=HTTPError + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.SETUP_RETRY + + +async def test_new_token(hass: HomeAssistant) -> None: + """Test new token.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + login.assert_called_once() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + one_hour_after = datetime.utcnow() + timedelta(hours=1) + + with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ), freeze_time(one_hour_after): + async_fire_time_changed(hass, one_hour_after) + await hass.async_block_till_done() + + login.assert_called_once() diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py new file mode 100644 index 00000000000..57197662cc9 --- /dev/null +++ b/tests/components/lacrosse_view/test_sensor.py @@ -0,0 +1,49 @@ +"""Test the LaCrosse View sensors.""" +from unittest.mock import patch + +from homeassistant.components.lacrosse_view import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_ENTRY_DATA, TEST_NO_PERMISSION_SENSOR, TEST_SENSOR + +from tests.common import MockConfigEntry + + +async def test_entities_added(hass: HomeAssistant) -> None: + """Test the entities are added.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR] + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get("sensor.test_test_temperature") + + +async def test_sensor_permission(hass: HomeAssistant, caplog) -> None: + """Test if it raises a warning when there is no permission to read the sensor.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_NO_PERMISSION_SENSOR] + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get("sensor.test_test_temperature") is None + assert "No permission to read sensor" in caplog.text From 3a8748bc9398316b389aa0307fb149829eab20fd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:01:00 -0400 Subject: [PATCH 003/903] Bump zwave-js-server-python to 0.40.0 (#75795) --- homeassistant/components/zwave_js/api.py | 4 ++-- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 8b98c61c4b2..28afdeb4db8 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1888,7 +1888,7 @@ async def websocket_get_firmware_update_progress( node: Node, ) -> None: """Get whether firmware update is in progress.""" - connection.send_result(msg[ID], await node.async_get_firmware_update_progress()) + connection.send_result(msg[ID], await node.async_is_firmware_update_in_progress()) def _get_firmware_update_progress_dict( @@ -2012,7 +2012,7 @@ async def websocket_get_any_firmware_update_progress( ) -> None: """Get whether any firmware updates are in progress.""" connection.send_result( - msg[ID], await driver.controller.async_get_any_firmware_update_progress() + msg[ID], await driver.controller.async_is_any_ota_firmware_update_in_progress() ) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 76f5d94b589..ed969e58042 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.39.0"], + "requirements": ["zwave-js-server-python==0.40.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index bdbff5adabd..efc7282d703 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2544,7 +2544,7 @@ zigpy==0.48.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.39.0 +zwave-js-server-python==0.40.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3dc23a1e07d..03bfde54c5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1715,7 +1715,7 @@ zigpy-znp==0.8.1 zigpy==0.48.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.39.0 +zwave-js-server-python==0.40.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 3a2bff54e88..72d8fce7424 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3459,12 +3459,12 @@ async def test_get_firmware_update_progress( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] - assert args["command"] == "node.get_firmware_update_progress" + assert args["command"] == "node.is_firmware_update_in_progress" assert args["nodeId"] == multisensor_6.node_id # Test FailedZWaveCommand is caught with patch( - "zwave_js_server.model.node.Node.async_get_firmware_update_progress", + "zwave_js_server.model.node.Node.async_is_firmware_update_in_progress", side_effect=FailedZWaveCommand("failed_command", 1, "error message"), ): await ws_client.send_json( @@ -3744,11 +3744,11 @@ async def test_get_any_firmware_update_progress( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] - assert args["command"] == "controller.get_any_firmware_update_progress" + assert args["command"] == "controller.is_any_ota_firmware_update_in_progress" # Test FailedZWaveCommand is caught with patch( - "zwave_js_server.model.controller.Controller.async_get_any_firmware_update_progress", + "zwave_js_server.model.controller.Controller.async_is_any_ota_firmware_update_in_progress", side_effect=FailedZWaveCommand("failed_command", 1, "error message"), ): await ws_client.send_json( From 2e4f13996f5393de2e8718f28f7638540dc67246 Mon Sep 17 00:00:00 2001 From: borky Date: Wed, 27 Jul 2022 19:33:07 +0300 Subject: [PATCH 004/903] Set Level for MIOT purifiers as in python-miio (#75814) * Set Level for MIOT purifiers as in python-miio * Refactoring after review suggestion --- .../components/xiaomi_miio/number.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index e7c61044e25..577e82e3fd8 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -104,6 +104,15 @@ class OscillationAngleValues: step: float | None = None +@dataclass +class FavoriteLevelValues: + """A class that describes favorite level values.""" + + max_value: float | None = None + min_value: float | None = None + step: float | None = None + + NUMBER_TYPES = { FEATURE_SET_MOTOR_SPEED: XiaomiMiioNumberDescription( key=ATTR_MOTOR_SPEED, @@ -237,6 +246,11 @@ OSCILLATION_ANGLE_VALUES = { MODEL_FAN_P11: OscillationAngleValues(max_value=140, min_value=30, step=30), } +FAVORITE_LEVEL_VALUES = { + tuple(MODELS_PURIFIER_MIIO): FavoriteLevelValues(max_value=17, min_value=0, step=1), + tuple(MODELS_PURIFIER_MIOT): FavoriteLevelValues(max_value=14, min_value=0, step=1), +} + async def async_setup_entry( hass: HomeAssistant, @@ -280,6 +294,15 @@ async def async_setup_entry( native_min_value=OSCILLATION_ANGLE_VALUES[model].min_value, native_step=OSCILLATION_ANGLE_VALUES[model].step, ) + elif description.key == ATTR_FAVORITE_LEVEL: + for list_models, favorite_level_value in FAVORITE_LEVEL_VALUES.items(): + if model in list_models: + description = dataclasses.replace( + description, + native_max_value=favorite_level_value.max_value, + native_min_value=favorite_level_value.min_value, + native_step=favorite_level_value.step, + ) entities.append( XiaomiNumberEntity( From 04d00a9acde086db5d0890b92f8d2833143cab42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jul 2022 12:59:43 -0700 Subject: [PATCH 005/903] Remove learn more URL from Home Assistant alerts (#75838) --- .../homeassistant_alerts/__init__.py | 5 +-- .../fixtures/alerts_no_url.json | 34 ------------------- .../homeassistant_alerts/test_init.py | 15 +++----- 3 files changed, 5 insertions(+), 49 deletions(-) delete mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_url.json diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 1aedd6c5419..12ba4dce8ba 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -75,7 +75,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, issue_id, is_fixable=False, - learn_more_url=alert.alert_url, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ @@ -112,7 +111,6 @@ class IntegrationAlert: integration: str filename: str date_updated: str | None - alert_url: str | None @property def issue_id(self) -> str: @@ -147,7 +145,7 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) result = {} for alert in alerts: - if "alert_url" not in alert or "integrations" not in alert: + if "integrations" not in alert: continue if "homeassistant" in alert: @@ -177,7 +175,6 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) integration=integration["package"], filename=alert["filename"], date_updated=alert.get("date_updated"), - alert_url=alert["alert_url"], ) result[integration_alert.issue_id] = integration_alert diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json deleted file mode 100644 index 89f277cf69b..00000000000 --- a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "title": "Dark Sky API closed for new users", - "created": "2020-03-31T14:40:00.000Z", - "integrations": [ - { - "package": "darksky" - } - ], - "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", - "homeassistant": { - "package": "homeassistant", - "affected_from_version": "0.30" - }, - "filename": "dark_sky.markdown", - "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" - }, - { - "title": "Hikvision Security Vulnerability", - "created": "2021-09-20T22:08:00.000Z", - "integrations": [ - { - "package": "hikvision" - }, - { - "package": "hikvisioncam" - } - ], - "filename": "hikvision.markdown", - "homeassistant": { - "package": "homeassistant" - } - } -] diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index c0b6f471033..cb39fb73108 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -133,7 +133,7 @@ async def test_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -149,13 +149,6 @@ async def test_alerts( @pytest.mark.parametrize( "ha_version, fixture, expected_alerts", ( - ( - "2022.7.0", - "alerts_no_url.json", - [ - ("dark_sky.markdown", "darksky"), - ], - ), ( "2022.7.0", "alerts_no_integrations.json", @@ -220,7 +213,7 @@ async def test_bad_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -381,7 +374,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -420,7 +413,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { From 4ffd6fc4be7959753b930224a8b16d16b6eabf8d Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 27 Jul 2022 16:06:33 -0400 Subject: [PATCH 006/903] Add Insteon lock and load controller devices (#75632) --- homeassistant/components/insteon/const.py | 1 + homeassistant/components/insteon/ipdb.py | 5 + homeassistant/components/insteon/lock.py | 49 ++++++++ .../components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/insteon/mock_devices.py | 21 +++- tests/components/insteon/test_lock.py | 109 ++++++++++++++++++ 8 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/insteon/lock.py create mode 100644 tests/components/insteon/test_lock.py diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index fb7b2387d73..5337ccd36c3 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -44,6 +44,7 @@ INSTEON_PLATFORMS = [ Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SWITCH, ] diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 6866e052368..7f4ff92380f 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -1,5 +1,6 @@ """Utility methods for the Insteon platform.""" from pyinsteon.device_types import ( + AccessControl_Morningstar, ClimateControl_Thermostat, ClimateControl_WirelessThermostat, DimmableLightingControl, @@ -12,6 +13,7 @@ from pyinsteon.device_types import ( DimmableLightingControl_OutletLinc, DimmableLightingControl_SwitchLinc, DimmableLightingControl_ToggleLinc, + EnergyManagement_LoadController, GeneralController_ControlLinc, GeneralController_MiniRemote_4, GeneralController_MiniRemote_8, @@ -44,11 +46,13 @@ from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.switch import DOMAIN as SWITCH from .const import ON_OFF_EVENTS DEVICE_PLATFORM = { + AccessControl_Morningstar: {LOCK: [1]}, DimmableLightingControl: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_DinRail: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_FanLinc: {LIGHT: [1], FAN: [2], ON_OFF_EVENTS: [1, 2]}, @@ -67,6 +71,7 @@ DEVICE_PLATFORM = { DimmableLightingControl_OutletLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_SwitchLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_ToggleLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + EnergyManagement_LoadController: {SWITCH: [1], BINARY_SENSOR: [2]}, GeneralController_ControlLinc: {ON_OFF_EVENTS: [1]}, GeneralController_MiniRemote_4: {ON_OFF_EVENTS: range(1, 5)}, GeneralController_MiniRemote_8: {ON_OFF_EVENTS: range(1, 9)}, diff --git a/homeassistant/components/insteon/lock.py b/homeassistant/components/insteon/lock.py new file mode 100644 index 00000000000..17a7cf20111 --- /dev/null +++ b/homeassistant/components/insteon/lock.py @@ -0,0 +1,49 @@ +"""Support for INSTEON locks.""" + +from typing import Any + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import SIGNAL_ADD_ENTITIES +from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Insteon locks from a config entry.""" + + @callback + def async_add_insteon_lock_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, LOCK_DOMAIN, InsteonLockEntity, async_add_entities, discovery_info + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{LOCK_DOMAIN}" + async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities) + async_add_insteon_lock_entities() + + +class InsteonLockEntity(InsteonEntity, LockEntity): + """A Class for an Insteon lock entity.""" + + @property + def is_locked(self) -> bool: + """Return the boolean response if the node is on.""" + return bool(self._insteon_device_group.value) + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the device.""" + await self._insteon_device.async_lock() + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the device.""" + await self._insteon_device.async_unlock() diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c48d502c16e..577383e8976 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.3", + "pyinsteon==1.2.0", "insteon-frontend-home-assistant==0.2.0" ], "codeowners": ["@teharris1"], diff --git a/requirements_all.txt b/requirements_all.txt index efc7282d703..04e86f3ab7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1572,7 +1572,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03bfde54c5c..c8ac231bea1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1079,7 +1079,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index ef64b1e0969..417769d6696 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from pyinsteon.address import Address from pyinsteon.constants import ALDBStatus, ResponseStatus from pyinsteon.device_types import ( + AccessControl_Morningstar, DimmableLightingControl_KeypadLinc_8, GeneralController_RemoteLinc, Hub, @@ -59,12 +60,13 @@ class MockDevices: async def async_load(self, *args, **kwargs): """Load the mock devices.""" - if self._connected: + if self._connected and not self._devices: addr0 = Address("AA.AA.AA") addr1 = Address("11.11.11") addr2 = Address("22.22.22") addr3 = Address("33.33.33") addr4 = Address("44.44.44") + addr5 = Address("55.55.55") self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0") self._devices[addr1] = MockSwitchLinc( addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1" @@ -78,9 +80,12 @@ class MockDevices: self._devices[addr4] = SensorsActuators_IOLink( addr4, 0x07, 0x00, 0x00, "Device 44.44.44", "4" ) + self._devices[addr5] = AccessControl_Morningstar( + addr5, 0x0F, 0x0A, 0x00, "Device 55.55.55", "5" + ) for device in [ - self._devices[addr] for addr in [addr1, addr2, addr3, addr4] + self._devices[addr] for addr in [addr1, addr2, addr3, addr4, addr5] ]: device.async_read_config = AsyncMock() device.aldb.async_write = AsyncMock() @@ -99,7 +104,9 @@ class MockDevices: return_value=ResponseStatus.SUCCESS ) - for device in [self._devices[addr] for addr in [addr2, addr3, addr4]]: + for device in [ + self._devices[addr] for addr in [addr2, addr3, addr4, addr5] + ]: device.async_status = AsyncMock() self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) self._devices[addr0].aldb.async_load = AsyncMock() @@ -117,6 +124,12 @@ class MockDevices: return_value=ResponseStatus.FAILURE ) + self._devices[addr5].async_lock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) + self._devices[addr5].async_unlock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) self.modem = self._devices[addr0] self.modem.async_read_config = AsyncMock() @@ -155,6 +168,6 @@ class MockDevices: yield address await asyncio.sleep(0.01) - def subscribe(self, listener): + def subscribe(self, listener, force_strong_ref=False): """Mock the subscribe function.""" subscribe_topic(listener, DEVICE_LIST_CHANGED) diff --git a/tests/components/insteon/test_lock.py b/tests/components/insteon/test_lock.py new file mode 100644 index 00000000000..6f847543a9f --- /dev/null +++ b/tests/components/insteon/test_lock.py @@ -0,0 +1,109 @@ +"""Tests for the Insteon lock.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components import insteon +from homeassistant.components.insteon import ( + DOMAIN, + insteon_entity, + utils as insteon_utils, +) +from homeassistant.components.lock import ( # SERVICE_LOCK,; SERVICE_UNLOCK, + DOMAIN as LOCK_DOMAIN, +) +from homeassistant.const import ( # ATTR_ENTITY_ID,; + EVENT_HOMEASSISTANT_STOP, + STATE_LOCKED, + STATE_UNLOCKED, + Platform, +) +from homeassistant.helpers import entity_registry as er + +from .const import MOCK_USER_INPUT_PLM +from .mock_devices import MockDevices + +from tests.common import MockConfigEntry + +devices = MockDevices() + + +@pytest.fixture(autouse=True) +def lock_platform_only(): + """Only setup the lock and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.insteon.INSTEON_PLATFORMS", + (Platform.LOCK,), + ): + yield + + +@pytest.fixture(autouse=True) +def patch_setup_and_devices(): + """Patch the Insteon setup process and devices.""" + with patch.object(insteon, "async_connect", new=mock_connection), patch.object( + insteon, "async_close" + ), patch.object(insteon, "devices", devices), patch.object( + insteon_utils, "devices", devices + ), patch.object( + insteon_entity, "devices", devices + ): + yield + + +async def mock_connection(*args, **kwargs): + """Return a successful connection.""" + return True + + +async def test_lock_lock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + assert state.state is STATE_UNLOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "lock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_lock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_lock_unlock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + devices["55.55.55"].groups[1].set_value(255) + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + + assert state.state is STATE_LOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "unlock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_unlock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() From b0f877eca2c30d189647dba54109a78556781c69 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jul 2022 13:53:51 -0700 Subject: [PATCH 007/903] Add issue_domain to repairs (#75839) --- .../components/homeassistant_alerts/__init__.py | 1 + homeassistant/components/repairs/issue_handler.py | 2 ++ homeassistant/components/repairs/issue_registry.py | 6 ++++++ tests/components/demo/test_init.py | 5 +++++ tests/components/homeassistant_alerts/test_init.py | 4 ++++ tests/components/repairs/test_init.py | 13 +++++++++++++ tests/components/repairs/test_websocket_api.py | 7 +++++++ 7 files changed, 38 insertions(+) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 12ba4dce8ba..d405b9e257d 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -75,6 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, issue_id, is_fixable=False, + issue_domain=alert.integration, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 8eff4ac64fe..c139026ec48 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -86,6 +86,7 @@ def async_create_issue( domain: str, issue_id: str, *, + issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, learn_more_url: str | None = None, @@ -106,6 +107,7 @@ def async_create_issue( issue_registry.async_get_or_create( domain, issue_id, + issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, learn_more_url=learn_more_url, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index 5c459309cc0..bd201f1007c 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -30,6 +30,8 @@ class IssueEntry: domain: str is_fixable: bool | None issue_id: str + # Used if an integration creates issues for other integrations (ie alerts) + issue_domain: str | None learn_more_url: str | None severity: IssueSeverity | None translation_key: str | None @@ -58,6 +60,7 @@ class IssueRegistry: domain: str, issue_id: str, *, + issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, learn_more_url: str | None = None, @@ -75,6 +78,7 @@ class IssueRegistry: dismissed_version=None, domain=domain, is_fixable=is_fixable, + issue_domain=issue_domain, issue_id=issue_id, learn_more_url=learn_more_url, severity=severity, @@ -93,6 +97,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -155,6 +160,7 @@ class IssueRegistry: domain=issue["domain"], is_fixable=None, issue_id=issue["issue_id"], + issue_domain=None, learn_more_url=None, severity=None, translation_key=None, diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 85ff2a16405..5b322cb776f 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -95,6 +95,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "transmogrifier_deprecated", + "issue_domain": None, "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", "severity": "warning", "translation_key": "transmogrifier_deprecated", @@ -108,6 +109,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": True, "issue_id": "out_of_blinker_fluid", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", "severity": "critical", "translation_key": "out_of_blinker_fluid", @@ -121,6 +123,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "unfixable_problem", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "severity": "warning", "translation_key": "unfixable_problem", @@ -180,6 +183,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "transmogrifier_deprecated", + "issue_domain": None, "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", "severity": "warning", "translation_key": "transmogrifier_deprecated", @@ -193,6 +197,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "unfixable_problem", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "severity": "warning", "translation_key": "unfixable_problem", diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index cb39fb73108..a0fb2e8557d 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -133,6 +133,7 @@ async def test_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -213,6 +214,7 @@ async def test_bad_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -374,6 +376,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -413,6 +416,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 2f82a084968..d70f6c6e11d 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -85,6 +85,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -97,6 +98,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], is_fixable=issues[0]["is_fixable"], + issue_domain="my_issue_domain", learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -113,6 +115,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=None, ignored=False, learn_more_url="blablabla", + issue_domain="my_issue_domain", ) @@ -206,6 +209,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -226,6 +230,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -245,6 +250,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -264,6 +270,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -292,6 +299,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=ha_version, ignored=True, learn_more_url="blablabla", + issue_domain=None, ) # Unignore the same issue @@ -309,6 +317,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=None, ignored=False, learn_more_url="blablabla", + issue_domain=None, ) for issue in issues ] @@ -359,6 +368,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -378,6 +388,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -428,6 +439,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T08:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -501,6 +513,7 @@ async def test_sync_methods( "ignored": False, "is_fixable": True, "issue_id": "sync_issue", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 73d1898fcb7..d778b043832 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -61,6 +61,7 @@ async def create_issues(hass, ws_client): created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -154,6 +155,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: created=ANY, dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -183,6 +185,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -226,6 +229,7 @@ async def test_fix_non_existing_issue( created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -383,6 +387,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "dismissed_version": None, "domain": "test", "issue_id": "issue_3_inactive", + "issue_domain": None, }, ] }, @@ -404,6 +409,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "domain": "test", "is_fixable": True, "issue_id": "issue_1", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -414,6 +420,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "domain": "test", "is_fixable": False, "issue_id": "issue_2", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", From ef9142f37903be4117eab1e0dfa18b0ffffcba90 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 22:58:58 +0200 Subject: [PATCH 008/903] Fix temperature unit in evohome (#75842) --- homeassistant/components/evohome/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 841619af6f1..c1a630d0d05 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -126,6 +126,8 @@ async def async_setup_platform( class EvoClimateEntity(EvoDevice, ClimateEntity): """Base for an evohome Climate device.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Climate device.""" super().__init__(evo_broker, evo_device) @@ -316,7 +318,6 @@ class EvoController(EvoClimateEntity): _attr_icon = "mdi:thermostat" _attr_precision = PRECISION_TENTHS - _attr_temperature_unit = TEMP_CELSIUS def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Controller/Location.""" From b0c17d67df92af09da7119f24cd87e066a5ffe5d Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 27 Jul 2022 16:39:39 -0500 Subject: [PATCH 009/903] Add Leviton as a supported brand of ZwaveJS (#75729) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/manifest.json | 5 ++++- homeassistant/generated/supported_brands.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index ed969e58042..29be66cd024 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -30,5 +30,8 @@ } ], "zeroconf": ["_zwave-js-server._tcp.local."], - "loggers": ["zwave_js_server"] + "loggers": ["zwave_js_server"], + "supported_brands": { + "leviton_z_wave": "Leviton Z-Wave" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 589e0462cf7..4e151f5578d 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -11,5 +11,6 @@ HAS_SUPPORTED_BRANDS = ( "motion_blinds", "overkiz", "renault", - "wemo" + "wemo", + "zwave_js" ) From 44f1d928907727474afed6aeec70095f6b4c46b6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Jul 2022 17:40:44 -0400 Subject: [PATCH 010/903] Add new zwave_js notification parameters (#75796) --- homeassistant/components/zwave_js/__init__.py | 5 +++++ homeassistant/components/zwave_js/const.py | 2 ++ tests/components/zwave_js/test_device_trigger.py | 8 +++++++- tests/components/zwave_js/test_events.py | 13 +++++++++++-- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index fe616e8bdb9..482f635da65 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -43,12 +43,14 @@ from .const import ( ATTR_COMMAND_CLASS, ATTR_COMMAND_CLASS_NAME, ATTR_DATA_TYPE, + ATTR_DATA_TYPE_LABEL, ATTR_DIRECTION, ATTR_ENDPOINT, ATTR_EVENT, ATTR_EVENT_DATA, ATTR_EVENT_LABEL, ATTR_EVENT_TYPE, + ATTR_EVENT_TYPE_LABEL, ATTR_HOME_ID, ATTR_LABEL, ATTR_NODE_ID, @@ -476,7 +478,9 @@ async def setup_driver( # noqa: C901 { ATTR_COMMAND_CLASS_NAME: "Entry Control", ATTR_EVENT_TYPE: notification.event_type, + ATTR_EVENT_TYPE_LABEL: notification.event_type_label, ATTR_DATA_TYPE: notification.data_type, + ATTR_DATA_TYPE_LABEL: notification.data_type_label, ATTR_EVENT_DATA: notification.event_data, } ) @@ -505,6 +509,7 @@ async def setup_driver( # noqa: C901 { ATTR_COMMAND_CLASS_NAME: "Multilevel Switch", ATTR_EVENT_TYPE: notification.event_type, + ATTR_EVENT_TYPE_LABEL: notification.event_type_label, ATTR_DIRECTION: notification.direction, } ) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 1fd8e3e9d14..3e0bdb9c3f6 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -56,6 +56,8 @@ ATTR_OPTIONS = "options" ATTR_TEST_NODE_ID = "test_node_id" ATTR_STATUS = "status" ATTR_ACKNOWLEDGED_FRAMES = "acknowledged_frames" +ATTR_EVENT_TYPE_LABEL = "event_type_label" +ATTR_DATA_TYPE_LABEL = "data_type_label" ATTR_NODE = "node" ATTR_ZWAVE_VALUE = "zwave_value" diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index decac4cd50f..859164aa4c3 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -252,7 +252,13 @@ async def test_if_entry_control_notification_fires( "event": "notification", "nodeId": node.node_id, "ccId": 111, - "args": {"eventType": 5, "dataType": 2, "eventData": "555"}, + "args": { + "eventType": 5, + "eventTypeLabel": "label 1", + "dataType": 2, + "dataTypeLabel": "label 2", + "eventData": "555", + }, }, ) node.receive_event(event) diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 19f38d4aa57..8552f69936d 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -182,7 +182,13 @@ async def test_notifications(hass, hank_binary_switch, integration, client): "event": "notification", "nodeId": 32, "ccId": 111, - "args": {"eventType": 5, "dataType": 2, "eventData": "555"}, + "args": { + "eventType": 5, + "eventTypeLabel": "test1", + "dataType": 2, + "dataTypeLabel": "test2", + "eventData": "555", + }, }, ) @@ -193,7 +199,9 @@ async def test_notifications(hass, hank_binary_switch, integration, client): assert events[1].data["home_id"] == client.driver.controller.home_id assert events[1].data["node_id"] == 32 assert events[1].data["event_type"] == 5 + assert events[1].data["event_type_label"] == "test1" assert events[1].data["data_type"] == 2 + assert events[1].data["data_type_label"] == "test2" assert events[1].data["event_data"] == "555" assert events[1].data["command_class"] == CommandClass.ENTRY_CONTROL assert events[1].data["command_class_name"] == "Entry Control" @@ -206,7 +214,7 @@ async def test_notifications(hass, hank_binary_switch, integration, client): "event": "notification", "nodeId": 32, "ccId": 38, - "args": {"eventType": 4, "direction": "up"}, + "args": {"eventType": 4, "eventTypeLabel": "test1", "direction": "up"}, }, ) @@ -217,6 +225,7 @@ async def test_notifications(hass, hank_binary_switch, integration, client): assert events[2].data["home_id"] == client.driver.controller.home_id assert events[2].data["node_id"] == 32 assert events[2].data["event_type"] == 4 + assert events[2].data["event_type_label"] == "test1" assert events[2].data["direction"] == "up" assert events[2].data["command_class"] == CommandClass.SWITCH_MULTILEVEL assert events[2].data["command_class_name"] == "Multilevel Switch" From 4aa6300b8bd294727a2d4ee8086b5e6a90ff8460 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Jul 2022 17:42:17 -0400 Subject: [PATCH 011/903] Update zwave_js WS API names (#75797) --- homeassistant/components/zwave_js/api.py | 16 +++++++++------- tests/components/zwave_js/test_api.py | 22 +++++++++++----------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 28afdeb4db8..1a9435509f2 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -414,7 +414,9 @@ def async_register_api(hass: HomeAssistant) -> None: ) websocket_api.async_register_command(hass, websocket_data_collection_status) websocket_api.async_register_command(hass, websocket_abort_firmware_update) - websocket_api.async_register_command(hass, websocket_get_firmware_update_progress) + websocket_api.async_register_command( + hass, websocket_is_node_firmware_update_in_progress + ) websocket_api.async_register_command( hass, websocket_subscribe_firmware_update_status ) @@ -422,7 +424,7 @@ def async_register_api(hass: HomeAssistant) -> None: hass, websocket_get_firmware_update_capabilities ) websocket_api.async_register_command( - hass, websocket_get_any_firmware_update_progress + hass, websocket_is_any_ota_firmware_update_in_progress ) websocket_api.async_register_command(hass, websocket_check_for_config_updates) websocket_api.async_register_command(hass, websocket_install_config_update) @@ -1874,20 +1876,20 @@ async def websocket_abort_firmware_update( @websocket_api.require_admin @websocket_api.websocket_command( { - vol.Required(TYPE): "zwave_js/get_firmware_update_progress", + vol.Required(TYPE): "zwave_js/is_node_firmware_update_in_progress", vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @async_handle_failed_command @async_get_node -async def websocket_get_firmware_update_progress( +async def websocket_is_node_firmware_update_in_progress( hass: HomeAssistant, connection: ActiveConnection, msg: dict, node: Node, ) -> None: - """Get whether firmware update is in progress.""" + """Get whether firmware update is in progress for given node.""" connection.send_result(msg[ID], await node.async_is_firmware_update_in_progress()) @@ -1995,14 +1997,14 @@ async def websocket_get_firmware_update_capabilities( @websocket_api.require_admin @websocket_api.websocket_command( { - vol.Required(TYPE): "zwave_js/get_any_firmware_update_progress", + vol.Required(TYPE): "zwave_js/is_any_ota_firmware_update_in_progress", vol.Required(ENTRY_ID): str, } ) @websocket_api.async_response @async_handle_failed_command @async_get_entry -async def websocket_get_any_firmware_update_progress( +async def websocket_is_any_ota_firmware_update_in_progress( hass: HomeAssistant, connection: ActiveConnection, msg: dict, diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 72d8fce7424..68618edfbeb 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3437,10 +3437,10 @@ async def test_abort_firmware_update( assert msg["error"]["code"] == ERR_NOT_FOUND -async def test_get_firmware_update_progress( +async def test_is_node_firmware_update_in_progress( hass, client, multisensor_6, integration, hass_ws_client ): - """Test that the get_firmware_update_progress WS API call works.""" + """Test that the is_firmware_update_in_progress WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) device = get_device(hass, multisensor_6) @@ -3449,7 +3449,7 @@ async def test_get_firmware_update_progress( await ws_client.send_json( { ID: 1, - TYPE: "zwave_js/get_firmware_update_progress", + TYPE: "zwave_js/is_node_firmware_update_in_progress", DEVICE_ID: device.id, } ) @@ -3470,7 +3470,7 @@ async def test_get_firmware_update_progress( await ws_client.send_json( { ID: 2, - TYPE: "zwave_js/get_firmware_update_progress", + TYPE: "zwave_js/is_node_firmware_update_in_progress", DEVICE_ID: device.id, } ) @@ -3487,7 +3487,7 @@ async def test_get_firmware_update_progress( await ws_client.send_json( { ID: 3, - TYPE: "zwave_js/get_firmware_update_progress", + TYPE: "zwave_js/is_node_firmware_update_in_progress", DEVICE_ID: device.id, } ) @@ -3723,10 +3723,10 @@ async def test_get_firmware_update_capabilities( assert msg["error"]["code"] == ERR_NOT_FOUND -async def test_get_any_firmware_update_progress( +async def test_is_any_ota_firmware_update_in_progress( hass, client, integration, hass_ws_client ): - """Test that the get_any_firmware_update_progress WS API call works.""" + """Test that the is_any_ota_firmware_update_in_progress WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) @@ -3734,7 +3734,7 @@ async def test_get_any_firmware_update_progress( await ws_client.send_json( { ID: 1, - TYPE: "zwave_js/get_any_firmware_update_progress", + TYPE: "zwave_js/is_any_ota_firmware_update_in_progress", ENTRY_ID: entry.entry_id, } ) @@ -3754,7 +3754,7 @@ async def test_get_any_firmware_update_progress( await ws_client.send_json( { ID: 2, - TYPE: "zwave_js/get_any_firmware_update_progress", + TYPE: "zwave_js/is_any_ota_firmware_update_in_progress", ENTRY_ID: entry.entry_id, } ) @@ -3771,7 +3771,7 @@ async def test_get_any_firmware_update_progress( await ws_client.send_json( { ID: 3, - TYPE: "zwave_js/get_any_firmware_update_progress", + TYPE: "zwave_js/is_any_ota_firmware_update_in_progress", ENTRY_ID: entry.entry_id, } ) @@ -3784,7 +3784,7 @@ async def test_get_any_firmware_update_progress( await ws_client.send_json( { ID: 4, - TYPE: "zwave_js/get_any_firmware_update_progress", + TYPE: "zwave_js/is_any_ota_firmware_update_in_progress", ENTRY_ID: "invalid_entry", } ) From f07d64e7dbe4c36cc649cede845efaf06b48a141 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 23:49:22 +0200 Subject: [PATCH 012/903] Raise YAML removal issue for Xbox (#75843) --- homeassistant/components/xbox/__init__.py | 12 +++++++++++- homeassistant/components/xbox/manifest.json | 2 +- homeassistant/components/xbox/strings.json | 6 ++++++ homeassistant/components/xbox/translations/en.json | 6 ++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 6e8492e45ca..c49fd55e8c8 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -21,6 +21,7 @@ from xbox.webapi.api.provider.smartglass.models import ( ) from homeassistant.components import application_credentials +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant @@ -74,9 +75,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET] ), ) + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( "Configuration of Xbox integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " + "will be removed in Home Assistant 2022.9.; Your existing configuration " "(including OAuth Application Credentials) has been imported into " "the UI automatically and can be safely removed from your " "configuration.yaml file" diff --git a/homeassistant/components/xbox/manifest.json b/homeassistant/components/xbox/manifest.json index 5adfa54a901..8857a55d66d 100644 --- a/homeassistant/components/xbox/manifest.json +++ b/homeassistant/components/xbox/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xbox", "requirements": ["xbox-webapi==2.0.11"], - "dependencies": ["auth", "application_credentials"], + "dependencies": ["auth", "application_credentials", "repairs"], "codeowners": ["@hunterjm"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/xbox/strings.json b/homeassistant/components/xbox/strings.json index accd6775941..68af0176fa8 100644 --- a/homeassistant/components/xbox/strings.json +++ b/homeassistant/components/xbox/strings.json @@ -13,5 +13,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Xbox YAML configuration is being removed", + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/xbox/translations/en.json b/homeassistant/components/xbox/translations/en.json index 0bb1266bded..2ef065af458 100644 --- a/homeassistant/components/xbox/translations/en.json +++ b/homeassistant/components/xbox/translations/en.json @@ -13,5 +13,11 @@ "title": "Pick Authentication Method" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Xbox YAML configuration is being removed" + } } } \ No newline at end of file From 997f03d0eab98fe25442fb888f44e660f3b104fe Mon Sep 17 00:00:00 2001 From: Rolf Berkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Wed, 27 Jul 2022 23:50:41 +0200 Subject: [PATCH 013/903] Fix fetching MeteoAlarm XML data (#75840) --- homeassistant/components/meteoalarm/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 35333f6ea01..9a3da54d34f 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -2,7 +2,7 @@ "domain": "meteoalarm", "name": "MeteoAlarm", "documentation": "https://www.home-assistant.io/integrations/meteoalarm", - "requirements": ["meteoalertapi==0.2.0"], + "requirements": ["meteoalertapi==0.3.0"], "codeowners": ["@rolfberkenbosch"], "iot_class": "cloud_polling", "loggers": ["meteoalertapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 04e86f3ab7a..b4b84a84fd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1028,7 +1028,7 @@ meater-python==0.0.8 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.2.0 +meteoalertapi==0.3.0 # homeassistant.components.meteo_france meteofrance-api==1.0.2 From 0317cbb388e685ec00d77a49849693c418aa7ee4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 28 Jul 2022 00:25:05 +0000 Subject: [PATCH 014/903] [ci skip] Translation update --- .../components/ambee/translations/de.json | 6 ++++++ .../components/ambee/translations/it.json | 6 ++++++ .../components/ambee/translations/pl.json | 6 ++++++ .../components/ambee/translations/pt-BR.json | 6 ++++++ .../ambee/translations/zh-Hant.json | 6 ++++++ .../components/anthemav/translations/de.json | 6 ++++++ .../components/anthemav/translations/it.json | 6 ++++++ .../anthemav/translations/pt-BR.json | 6 ++++++ .../anthemav/translations/zh-Hant.json | 6 ++++++ .../components/google/translations/it.json | 2 +- .../google/translations/zh-Hant.json | 2 +- .../homeassistant_alerts/translations/de.json | 8 ++++++++ .../homeassistant_alerts/translations/en.json | 8 ++++++++ .../homeassistant_alerts/translations/it.json | 8 ++++++++ .../homeassistant_alerts/translations/pl.json | 8 ++++++++ .../translations/pt-BR.json | 8 ++++++++ .../translations/zh-Hant.json | 8 ++++++++ .../lacrosse_view/translations/de.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/en.json | 9 ++------- .../lacrosse_view/translations/it.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/pt-BR.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/zh-Hant.json | 20 +++++++++++++++++++ .../components/lyric/translations/de.json | 6 ++++++ .../components/lyric/translations/pt-BR.json | 6 ++++++ .../lyric/translations/zh-Hant.json | 6 ++++++ .../components/mitemp_bt/translations/de.json | 2 +- .../components/mitemp_bt/translations/it.json | 8 ++++++++ .../mitemp_bt/translations/pt-BR.json | 2 +- .../openalpr_local/translations/de.json | 8 ++++++++ .../openalpr_local/translations/it.json | 8 ++++++++ .../openalpr_local/translations/pl.json | 8 ++++++++ .../openalpr_local/translations/pt-BR.json | 8 ++++++++ .../openalpr_local/translations/zh-Hant.json | 8 ++++++++ .../radiotherm/translations/it.json | 3 ++- .../components/senz/translations/de.json | 6 ++++++ .../components/senz/translations/pt-BR.json | 6 ++++++ .../components/senz/translations/zh-Hant.json | 6 ++++++ .../soundtouch/translations/de.json | 6 ++++++ .../soundtouch/translations/it.json | 6 ++++++ .../soundtouch/translations/pt-BR.json | 6 ++++++ .../soundtouch/translations/zh-Hant.json | 6 ++++++ .../spotify/translations/zh-Hant.json | 4 ++-- .../steam_online/translations/zh-Hant.json | 4 ++-- .../uscis/translations/zh-Hant.json | 4 ++-- .../components/xbox/translations/it.json | 6 ++++++ .../components/xbox/translations/pt-BR.json | 6 ++++++ .../components/zha/translations/de.json | 2 ++ .../components/zha/translations/pl.json | 2 ++ .../components/zha/translations/pt-BR.json | 2 ++ .../components/zha/translations/zh-Hant.json | 2 ++ 50 files changed, 324 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/de.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/en.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/it.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/pl.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/pt-BR.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/zh-Hant.json create mode 100644 homeassistant/components/lacrosse_view/translations/de.json create mode 100644 homeassistant/components/lacrosse_view/translations/it.json create mode 100644 homeassistant/components/lacrosse_view/translations/pt-BR.json create mode 100644 homeassistant/components/lacrosse_view/translations/zh-Hant.json create mode 100644 homeassistant/components/mitemp_bt/translations/it.json create mode 100644 homeassistant/components/openalpr_local/translations/de.json create mode 100644 homeassistant/components/openalpr_local/translations/it.json create mode 100644 homeassistant/components/openalpr_local/translations/pl.json create mode 100644 homeassistant/components/openalpr_local/translations/pt-BR.json create mode 100644 homeassistant/components/openalpr_local/translations/zh-Hant.json diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json index 4359ab72349..8055ef5210f 100644 --- a/homeassistant/components/ambee/translations/de.json +++ b/homeassistant/components/ambee/translations/de.json @@ -24,5 +24,11 @@ "description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein." } } + }, + "issues": { + "pending_removal": { + "description": "Die Ambee-Integration ist dabei, aus Home Assistant entfernt zu werden und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil Ambee seine kostenlosen (begrenzten) Konten entfernt hat und keine M\u00f6glichkeit mehr f\u00fcr regul\u00e4re Nutzer bietet, sich f\u00fcr einen kostenpflichtigen Plan anzumelden.\n\nEntferne den Ambee-Integrationseintrag aus deiner Instanz, um dieses Problem zu beheben.", + "title": "Die Ambee-Integration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json index fe97ce33686..db330a9b239 100644 --- a/homeassistant/components/ambee/translations/it.json +++ b/homeassistant/components/ambee/translations/it.json @@ -24,5 +24,11 @@ "description": "Configura Ambee per l'integrazione con Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.", + "title": "L'integrazione Ambee verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pl.json b/homeassistant/components/ambee/translations/pl.json index d0b2225cc9a..255d402175d 100644 --- a/homeassistant/components/ambee/translations/pl.json +++ b/homeassistant/components/ambee/translations/pl.json @@ -24,5 +24,11 @@ "description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem." } } + }, + "issues": { + "pending_removal": { + "description": "Integracja Ambee oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c Ambee usun\u0105\u0142 ich bezp\u0142atne (ograniczone) konta i nie zapewnia ju\u017c zwyk\u0142ym u\u017cytkownikom mo\u017cliwo\u015bci zarejestrowania si\u0119 w p\u0142atnym planie. \n\nUsu\u0144 integracj\u0119 Ambee z Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Ambee zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt-BR.json b/homeassistant/components/ambee/translations/pt-BR.json index 2d960e17df2..3220de5104e 100644 --- a/homeassistant/components/ambee/translations/pt-BR.json +++ b/homeassistant/components/ambee/translations/pt-BR.json @@ -24,5 +24,11 @@ "description": "Configure o Ambee para integrar com o Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do Ambee est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, porque a Ambee removeu suas contas gratuitas (limitadas) e n\u00e3o oferece mais uma maneira de usu\u00e1rios regulares se inscreverem em um plano pago. \n\n Remova a entrada de integra\u00e7\u00e3o Ambee de sua inst\u00e2ncia para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o Ambee est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json index 2e1de25fde2..ccebea49c6f 100644 --- a/homeassistant/components/ambee/translations/zh-Hant.json +++ b/homeassistant/components/ambee/translations/zh-Hant.json @@ -24,5 +24,11 @@ "description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" } } + }, + "issues": { + "pending_removal": { + "description": "Ambee \u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc Ambee \u79fb\u9664\u4e86\u5176\u514d\u8cbb\uff08\u6709\u9650\uff09\u5e33\u865f\u3001\u4e26\u4e14\u4e0d\u518d\u63d0\u4f9b\u4e00\u822c\u4f7f\u7528\u8005\u8a3b\u518a\u4ed8\u8cbb\u670d\u52d9\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Ambee \u6574\u5408\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/de.json b/homeassistant/components/anthemav/translations/de.json index 622384629fe..d751349b005 100644 --- a/homeassistant/components/anthemav/translations/de.json +++ b/homeassistant/components/anthemav/translations/de.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Anthem A/V-Receivern mit YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Anthem A/V Receivers YAML Konfiguration aus deiner configuration.yaml Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die YAML-Konfiguration von Anthem A/V Receivers wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json index 12b0df56f0f..b8bec832581 100644 --- a/homeassistant/components/anthemav/translations/it.json +++ b/homeassistant/components/anthemav/translations/it.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Anthem A/V Receivers tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Anthem A/V Receivers verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json index 309aca9b8ef..5a6038bb480 100644 --- a/homeassistant/components/anthemav/translations/pt-BR.json +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o de receptores A/V Anthem usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML dos receptores A/V do Anthem do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML dos receptores A/V do Anthem est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json index d1b286afd81..0751331f82f 100644 --- a/homeassistant/components/anthemav/translations/zh-Hant.json +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index c29eb8d1a2c..6e24178996f 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -36,7 +36,7 @@ "issues": { "deprecated_yaml": { "description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Google Calendar \u00e8 stata rimossa" + "title": "La configurazione YAML di Google Calendar verr\u00e0 rimossa" }, "removed_track_new_yaml": { "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 43c208d69e8..0d2031c368f 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -36,7 +36,7 @@ "issues": { "deprecated_yaml": { "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" }, "removed_track_new_yaml": { "description": "\u65bc configuration.yaml \u5167\u6240\u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u529f\u80fd\uff0c\u7531\u65bc\u4e0d\u518d\u652f\u6301\u3001\u5df2\u7d93\u906d\u5230\u95dc\u9589\u3002\u4e4b\u5f8c\u5fc5\u9808\u624b\u52d5\u900f\u904e\u4ecb\u9762\u5167\u7684\u6574\u5408\u529f\u80fd\u3001\u4ee5\u95dc\u9589\u4efb\u4f55\u65b0\u767c\u73fe\u7684\u5be6\u9ad4\u3002\u8acb\u7531 configuration.yaml \u4e2d\u79fb\u9664R track_new \u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", diff --git a/homeassistant/components/homeassistant_alerts/translations/de.json b/homeassistant/components/homeassistant_alerts/translations/de.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/en.json b/homeassistant/components/homeassistant_alerts/translations/en.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/it.json b/homeassistant/components/homeassistant_alerts/translations/it.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pl.json b/homeassistant/components/homeassistant_alerts/translations/pl.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pt-BR.json b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/de.json b/homeassistant/components/lacrosse_view/translations/de.json new file mode 100644 index 00000000000..d9aa1210fa0 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_locations": "Keine Standorte gefunden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json index c972d6d12f9..a2a7fd23272 100644 --- a/homeassistant/components/lacrosse_view/translations/en.json +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -5,8 +5,8 @@ }, "error": { "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error", - "no_locations": "No locations found" + "no_locations": "No locations found", + "unknown": "Unexpected error" }, "step": { "user": { @@ -14,11 +14,6 @@ "password": "Password", "username": "Username" } - }, - "location": { - "data": { - "location": "Location" - } } } } diff --git a/homeassistant/components/lacrosse_view/translations/it.json b/homeassistant/components/lacrosse_view/translations/it.json new file mode 100644 index 00000000000..9ce6c75dcbc --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "no_locations": "Nessuna localit\u00e0 trovata", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/pt-BR.json b/homeassistant/components/lacrosse_view/translations/pt-BR.json new file mode 100644 index 00000000000..29b458e5599 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_locations": "Nenhum local encontrado", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/zh-Hant.json b/homeassistant/components/lacrosse_view/translations/zh-Hant.json new file mode 100644 index 00000000000..78235452297 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_locations": "\u627e\u4e0d\u5230\u5ea7\u6a19", + "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/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json index b067b299145..1ef7e65fbf4 100644 --- a/homeassistant/components/lyric/translations/de.json +++ b/homeassistant/components/lyric/translations/de.json @@ -17,5 +17,11 @@ "title": "Integration erneut authentifizieren" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Honeywell Lyric mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Honeywell Lyric YAML-Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pt-BR.json b/homeassistant/components/lyric/translations/pt-BR.json index 907a396d5e2..d70832bfbf6 100644 --- a/homeassistant/components/lyric/translations/pt-BR.json +++ b/homeassistant/components/lyric/translations/pt-BR.json @@ -17,5 +17,11 @@ "title": "Reautenticar Integra\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Honeywell Lyric usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Honeywell Lyric YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json index 850507ec0b3..bb7fbc3aed6 100644 --- a/homeassistant/components/lyric/translations/zh-Hant.json +++ b/homeassistant/components/lyric/translations/zh-Hant.json @@ -17,5 +17,11 @@ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Honeywell Lyric \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Honeywell Lyric YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/de.json b/homeassistant/components/mitemp_bt/translations/de.json index 7d4887ab638..3c9e6960aeb 100644 --- a/homeassistant/components/mitemp_bt/translations/de.json +++ b/homeassistant/components/mitemp_bt/translations/de.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t manuell mit der neuen Integration hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t mit der neuen Integration manuell hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", "title": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors wurde ersetzt" } } diff --git a/homeassistant/components/mitemp_bt/translations/it.json b/homeassistant/components/mitemp_bt/translations/it.json new file mode 100644 index 00000000000..cc383e4184c --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor \u00e8 stata sostituita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pt-BR.json b/homeassistant/components/mitemp_bt/translations/pt-BR.json index 991a749a729..634f5dd71fd 100644 --- a/homeassistant/components/mitemp_bt/translations/pt-BR.json +++ b/homeassistant/components/mitemp_bt/translations/pt-BR.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade do Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE foi substitu\u00edda" } } diff --git a/homeassistant/components/openalpr_local/translations/de.json b/homeassistant/components/openalpr_local/translations/de.json new file mode 100644 index 00000000000..d517fe0b37f --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die lokale OpenALPR-Integration wird derzeit aus dem Home Assistant entfernt und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die lokale OpenALPR-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/it.json b/homeassistant/components/openalpr_local/translations/it.json new file mode 100644 index 00000000000..26ce80ee584 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione OpenALPR Local \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "L'integrazione OpenALPR Local verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pl.json b/homeassistant/components/openalpr_local/translations/pl.json new file mode 100644 index 00000000000..ac367d20809 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja OpenALPR Local oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja OpenALPR Local zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pt-BR.json b/homeassistant/components/openalpr_local/translations/pt-BR.json new file mode 100644 index 00000000000..96b2c244b5c --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/zh-Hant.json b/homeassistant/components/openalpr_local/translations/zh-Hant.json new file mode 100644 index 00000000000..8ec55e5a004 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index 9e35beb648a..fef1c64746b 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -21,7 +21,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML \u00e8 stata rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema." + "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Radio Thermostat verr\u00e0 rimossa" } }, "options": { diff --git a/homeassistant/components/senz/translations/de.json b/homeassistant/components/senz/translations/de.json index ffbc7bb458f..fbae91321be 100644 --- a/homeassistant/components/senz/translations/de.json +++ b/homeassistant/components/senz/translations/de.json @@ -16,5 +16,11 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von nVent RAYCHEM SENZ mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die nVent RAYCHEM SENZ YAML Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pt-BR.json b/homeassistant/components/senz/translations/pt-BR.json index 7e3ff2f64a9..02c31d97816 100644 --- a/homeassistant/components/senz/translations/pt-BR.json +++ b/homeassistant/components/senz/translations/pt-BR.json @@ -16,5 +16,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do nVent RAYCHEM SENZ usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o nVent RAYCHEM SENZ YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/zh-Hant.json b/homeassistant/components/senz/translations/zh-Hant.json index 3bf08cf34c7..7094ee18a02 100644 --- a/homeassistant/components/senz/translations/zh-Hant.json +++ b/homeassistant/components/senz/translations/zh-Hant.json @@ -16,5 +16,11 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a nVent RAYCHEM SENZ \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "nVent RAYCHEM SENZ YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/de.json b/homeassistant/components/soundtouch/translations/de.json index 8d28b988834..379516e31be 100644 --- a/homeassistant/components/soundtouch/translations/de.json +++ b/homeassistant/components/soundtouch/translations/de.json @@ -17,5 +17,11 @@ "title": "Best\u00e4tige das Hinzuf\u00fcgen des Bose SoundTouch-Ger\u00e4ts" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Bose SoundTouch mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Bose SoundTouch YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Bose SoundTouch YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/it.json b/homeassistant/components/soundtouch/translations/it.json index a14492bf5c3..f9c6d512b2a 100644 --- a/homeassistant/components/soundtouch/translations/it.json +++ b/homeassistant/components/soundtouch/translations/it.json @@ -17,5 +17,11 @@ "title": "Conferma l'aggiunta del dispositivo Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Bose SoundTouch tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Bose SoundTouch dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Bose SoundTouch verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt-BR.json b/homeassistant/components/soundtouch/translations/pt-BR.json index e707219f342..7446cbc5a06 100644 --- a/homeassistant/components/soundtouch/translations/pt-BR.json +++ b/homeassistant/components/soundtouch/translations/pt-BR.json @@ -17,5 +17,11 @@ "title": "Confirme a adi\u00e7\u00e3o do dispositivo Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Bose SoundTouch usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Bose SoundTouch do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Bose SoundTouch est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json index 08231b8571d..f3d8e8e8560 100644 --- a/homeassistant/components/soundtouch/translations/zh-Hant.json +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -17,5 +17,11 @@ "title": "\u78ba\u8a8d\u65b0\u589e Bose SoundTouch \u88dd\u7f6e" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Bose SoundTouch YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index ce89e224a8c..52773f3e411 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -21,8 +21,8 @@ }, "issues": { "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Spotify YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Spotify YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } }, "system_health": { diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json index e3c725532e9..8b0c932735b 100644 --- a/homeassistant/components/steam_online/translations/zh-Hant.json +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -26,8 +26,8 @@ }, "issues": { "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Steam YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Steam YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } }, "options": { diff --git a/homeassistant/components/uscis/translations/zh-Hant.json b/homeassistant/components/uscis/translations/zh-Hant.json index 4a5882dbd95..ccc72d3d1dd 100644 --- a/homeassistant/components/uscis/translations/zh-Hant.json +++ b/homeassistant/components/uscis/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "issues": { "pending_removal": { - "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u6b63\u8a08\u5283\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u8acb\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u6574\u5408\u6b63\u5728\u79fb\u9664\u4e2d\u3001\u7531\u65bc\u4f7f\u7528\u4e86\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u5c07\u4e0d\u88ab\u5141\u8a31\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "USCIS \u6574\u5408\u6b63\u6e96\u5099\u79fb\u9664" + "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc\u4f7f\u7528\u4e86\u4e0d\u88ab\u5141\u8a31\u7684\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "USCIS \u6574\u5408\u5373\u5c07\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/it.json b/homeassistant/components/xbox/translations/it.json index e60c37c9e5f..6cf5bf15bb9 100644 --- a/homeassistant/components/xbox/translations/it.json +++ b/homeassistant/components/xbox/translations/it.json @@ -13,5 +13,11 @@ "title": "Scegli il metodo di autenticazione" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Xbox in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Xbox verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/pt-BR.json b/homeassistant/components/xbox/translations/pt-BR.json index 7f788c1ebb8..d1bb02e84dd 100644 --- a/homeassistant/components/xbox/translations/pt-BR.json +++ b/homeassistant/components/xbox/translations/pt-BR.json @@ -13,5 +13,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Xbox em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Xbox est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 9b8acdf5b87..b4ad58636a7 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -46,11 +46,13 @@ "title": "Optionen f\u00fcr die Alarmsteuerung" }, "zha_options": { + "always_prefer_xy_color_mode": "Immer den XY-Farbmodus bevorzugen", "consider_unavailable_battery": "Batteriebetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", + "light_transitioning_flag": "Erweiterten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 0b8e90f5ac0..99c519782db 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -46,11 +46,13 @@ "title": "Opcje panelu alarmowego" }, "zha_options": { + "always_prefer_xy_color_mode": "Zawsze preferuj tryb kolor\u00f3w XY", "consider_unavailable_battery": "Uznaj urz\u0105dzenia zasilane bateryjnie za niedost\u0119pne po (sekundach)", "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z gniazdka za niedost\u0119pne po (sekundach)", "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", "enhanced_light_transition": "W\u0142\u0105cz ulepszone przej\u015bcie koloru \u015bwiat\u0142a/temperatury ze stanu wy\u0142\u0105czenia", + "light_transitioning_flag": "W\u0142\u0105cz suwak zwi\u0119kszonej jasno\u015bci podczas przej\u015bcia \u015bwiat\u0142a", "title": "Opcje og\u00f3lne" } }, diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index ba54b4aba87..69b8ced6970 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -46,11 +46,13 @@ "title": "Op\u00e7\u00f5es do painel de controle de alarme" }, "zha_options": { + "always_prefer_xy_color_mode": "Sempre prefira o modo de cor XY", "consider_unavailable_battery": "Considerar dispositivos alimentados por bateria indispon\u00edveis ap\u00f3s (segundos)", "consider_unavailable_mains": "Considerar os dispositivos alimentados pela rede indispon\u00edveis ap\u00f3s (segundos)", "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz padr\u00e3o (segundos)", "enable_identify_on_join": "Ativar o efeito de identifica\u00e7\u00e3o quando os dispositivos ingressarem na rede", "enhanced_light_transition": "Ative a transi\u00e7\u00e3o de cor/temperatura da luz aprimorada de um estado desligado", + "light_transitioning_flag": "Ative o controle deslizante de brilho aprimorado durante a transi\u00e7\u00e3o de luz", "title": "Op\u00e7\u00f5es globais" } }, diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 9505da31e80..546a2f77c31 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -46,11 +46,13 @@ "title": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u9078\u9805" }, "zha_options": { + "always_prefer_xy_color_mode": "\u504f\u597d XY \u8272\u5f69\u6a21\u5f0f", "consider_unavailable_battery": "\u5c07\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "consider_unavailable_mains": "\u5c07\u4e3b\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", "enhanced_light_transition": "\u958b\u555f\u7531\u95dc\u9589\u72c0\u614b\u589e\u5f37\u5149\u8272/\u8272\u6eab\u8f49\u63db", + "light_transitioning_flag": "\u958b\u555f\u71c8\u5149\u8f49\u63db\u589e\u5f37\u4eae\u5ea6\u8abf\u6574\u5217", "title": "Global \u9078\u9805" } }, From 4f25b8d58e00fe6a03c825233fc85dd221147117 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 28 Jul 2022 12:05:56 +0300 Subject: [PATCH 015/903] Add issue to repairs for deprecated Simplepush YAML configuration (#75850) --- homeassistant/components/simplepush/manifest.json | 1 + homeassistant/components/simplepush/notify.py | 12 ++++++++++++ homeassistant/components/simplepush/strings.json | 6 ++++++ .../components/simplepush/translations/en.json | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index 7c37546485a..6b4ee263ba6 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -5,6 +5,7 @@ "requirements": ["simplepush==1.1.4"], "codeowners": ["@engrbm87"], "config_flow": true, + "dependencies": ["repairs"], "iot_class": "cloud_polling", "loggers": ["simplepush"] } diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index e9cd9813175..358d95c770a 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -14,6 +14,8 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.components.notify.const import ATTR_DATA +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_EVENT, CONF_PASSWORD from homeassistant.core import HomeAssistant @@ -41,6 +43,16 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + if discovery_info is None: hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/simplepush/strings.json b/homeassistant/components/simplepush/strings.json index 0031dc32340..77ed05c4b48 100644 --- a/homeassistant/components/simplepush/strings.json +++ b/homeassistant/components/simplepush/strings.json @@ -17,5 +17,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index a36a3b2b273..bf373d8baf0 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } \ No newline at end of file From c16db4c3e1cfdfd0d40c6202e5254ce50bf2510b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 28 Jul 2022 11:41:03 +0200 Subject: [PATCH 016/903] Make Axis utilise forward_entry_setups (#75178) --- homeassistant/components/axis/__init__.py | 26 ++++-- homeassistant/components/axis/config_flow.py | 11 +-- homeassistant/components/axis/device.py | 86 ++++++++------------ tests/components/axis/test_config_flow.py | 4 +- tests/components/axis/test_device.py | 17 ++-- 5 files changed, 62 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 5e211c00028..4af066f4e89 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -4,28 +4,36 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.entity_registry import async_migrate_entries -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice +from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS +from .device import AxisNetworkDevice, get_axis_device +from .errors import AuthenticationRequired, CannotConnect _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Set up the Axis component.""" + """Set up the Axis integration.""" hass.data.setdefault(AXIS_DOMAIN, {}) - device = AxisNetworkDevice(hass, config_entry) - - if not await device.async_setup(): - return False - - hass.data[AXIS_DOMAIN][config_entry.unique_id] = device + try: + api = await get_axis_device(hass, config_entry.data) + except CannotConnect as err: + raise ConfigEntryNotReady from err + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] = AxisNetworkDevice( + hass, config_entry, api + ) await device.async_update_device_registry() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + device.async_setup_events() + config_entry.add_update_listener(device.async_new_address_callback) config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) ) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index f94c27dc2ac..1ce2f08c045 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping from ipaddress import ip_address +from types import MappingProxyType from typing import Any from urllib.parse import urlsplit @@ -32,7 +33,7 @@ from .const import ( DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) -from .device import AxisNetworkDevice, get_device +from .device import AxisNetworkDevice, get_axis_device from .errors import AuthenticationRequired, CannotConnect AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} @@ -66,13 +67,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): if user_input is not None: try: - device = await get_device( - self.hass, - host=user_input[CONF_HOST], - port=user_input[CONF_PORT], - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - ) + device = await get_axis_device(self.hass, MappingProxyType(user_input)) serial = device.vapix.serial_number await self.async_set_unique_id(format_mac(serial)) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index d0d5e230d2f..683991d0f65 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,6 +1,8 @@ """Axis network device abstraction.""" import asyncio +from types import MappingProxyType +from typing import Any import async_timeout import axis @@ -24,7 +26,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -50,15 +51,15 @@ from .errors import AuthenticationRequired, CannotConnect class AxisNetworkDevice: """Manages a Axis device.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry, api): """Initialize the device.""" self.hass = hass self.config_entry = config_entry - self.available = True + self.api = api - self.api = None - self.fw_version = None - self.product_type = None + self.available = True + self.fw_version = api.vapix.firmware_version + self.product_type = api.vapix.product_type @property def host(self): @@ -184,7 +185,7 @@ class AxisNetworkDevice: sw_version=self.fw_version, ) - async def use_mqtt(self, hass: HomeAssistant, component: str) -> None: + async def async_use_mqtt(self, hass: HomeAssistant, component: str) -> None: """Set up to use MQTT.""" try: status = await self.api.vapix.mqtt.get_client_status() @@ -209,50 +210,18 @@ class AxisNetworkDevice: # Setup and teardown methods - async def async_setup(self): - """Set up the device.""" - try: - self.api = await get_device( - self.hass, - host=self.host, - port=self.port, - username=self.username, - password=self.password, + def async_setup_events(self): + """Set up the device events.""" + + if self.option_events: + self.api.stream.connection_status_callback.append( + self.async_connection_status_callback ) + self.api.enable_events(event_callback=self.async_event_callback) + self.api.stream.start() - except CannotConnect as err: - raise ConfigEntryNotReady from err - - except AuthenticationRequired as err: - raise ConfigEntryAuthFailed from err - - self.fw_version = self.api.vapix.firmware_version - self.product_type = self.api.vapix.product_type - - async def start_platforms(): - await asyncio.gather( - *( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - for platform in PLATFORMS - ) - ) - if self.option_events: - self.api.stream.connection_status_callback.append( - self.async_connection_status_callback - ) - self.api.enable_events(event_callback=self.async_event_callback) - self.api.stream.start() - - if self.api.vapix.mqtt: - async_when_setup(self.hass, MQTT_DOMAIN, self.use_mqtt) - - self.hass.async_create_task(start_platforms()) - - self.config_entry.add_update_listener(self.async_new_address_callback) - - return True + if self.api.vapix.mqtt: + async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) @callback def disconnect_from_stream(self): @@ -274,14 +243,21 @@ class AxisNetworkDevice: ) -async def get_device( - hass: HomeAssistant, host: str, port: int, username: str, password: str +async def get_axis_device( + hass: HomeAssistant, + config: MappingProxyType[str, Any], ) -> axis.AxisDevice: """Create a Axis device.""" session = get_async_client(hass, verify_ssl=False) device = axis.AxisDevice( - Configuration(session, host, port=port, username=username, password=password) + Configuration( + session, + config[CONF_HOST], + port=config[CONF_PORT], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + ) ) try: @@ -291,11 +267,13 @@ async def get_device( return device except axis.Unauthorized as err: - LOGGER.warning("Connected to device at %s but not registered", host) + LOGGER.warning( + "Connected to device at %s but not registered", config[CONF_HOST] + ) raise AuthenticationRequired from err except (asyncio.TimeoutError, axis.RequestError) as err: - LOGGER.error("Error connecting to the Axis device at %s", host) + LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) raise CannotConnect from err except axis.AxisException as err: diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 1459ae215d9..2daf350ac93 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -123,7 +123,7 @@ async def test_flow_fails_faulty_credentials(hass): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.AuthenticationRequired, ): result = await hass.config_entries.flow.async_configure( @@ -149,7 +149,7 @@ async def test_flow_fails_cannot_connect(hass): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.CannotConnect, ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 4717e2915c1..ba6df6e2e2d 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -441,7 +441,7 @@ async def test_device_reset(hass): async def test_device_not_accessible(hass): """Failed setup schedules a retry of setup.""" - with patch.object(axis.device, "get_device", side_effect=axis.errors.CannotConnect): + with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -449,7 +449,7 @@ async def test_device_not_accessible(hass): async def test_device_trigger_reauth_flow(hass): """Failed authentication trigger a reauthentication flow.""" with patch.object( - axis.device, "get_device", side_effect=axis.errors.AuthenticationRequired + axis, "get_axis_device", side_effect=axis.errors.AuthenticationRequired ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await setup_axis_integration(hass) mock_flow_init.assert_called_once() @@ -458,7 +458,7 @@ async def test_device_trigger_reauth_flow(hass): async def test_device_unknown_error(hass): """Unknown errors are handled.""" - with patch.object(axis.device, "get_device", side_effect=Exception): + with patch.object(axis, "get_axis_device", side_effect=Exception): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -468,7 +468,7 @@ async def test_new_event_sends_signal(hass): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send: axis_device.async_event_callback(action=OPERATION_INITIALIZED, event_id="event") @@ -484,8 +484,7 @@ async def test_shutdown(): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) - axis_device.api = Mock() + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) await axis_device.shutdown(None) @@ -497,7 +496,7 @@ async def test_get_device_fails(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_device_unavailable(hass): @@ -505,7 +504,7 @@ async def test_get_device_device_unavailable(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_unknown_error(hass): @@ -513,4 +512,4 @@ async def test_get_device_unknown_error(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) From 25d943d272b8a1de671694960e499917e5190563 Mon Sep 17 00:00:00 2001 From: borky Date: Thu, 28 Jul 2022 13:38:04 +0300 Subject: [PATCH 017/903] Add xiaomi air purifier 4 and 4 pro support (#75745) Co-authored-by: Martin Hjelmare --- homeassistant/components/xiaomi_miio/const.py | 14 ++++++ homeassistant/components/xiaomi_miio/fan.py | 11 +++++ .../components/xiaomi_miio/number.py | 5 +++ .../components/xiaomi_miio/sensor.py | 44 +++++++++++++++++++ .../components/xiaomi_miio/switch.py | 32 ++++++++++++++ .../components/xiaomi_miio/vacuum.py | 2 +- 6 files changed, 107 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 3577e7b9907..54289bb8389 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -47,6 +47,8 @@ class SetupException(Exception): # Fan Models +MODEL_AIRPURIFIER_4 = "zhimi.airp.mb5" +MODEL_AIRPURIFIER_4_PRO = "zhimi.airp.vb4" MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2" MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4" @@ -114,6 +116,8 @@ MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_3H, MODEL_AIRPURIFIER_PROH, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, ] MODELS_PURIFIER_MIIO = [ MODEL_AIRPURIFIER_V1, @@ -316,6 +320,7 @@ FEATURE_SET_FAVORITE_RPM = 262144 FEATURE_SET_IONIZER = 524288 FEATURE_SET_DISPLAY = 1048576 FEATURE_SET_PTC = 2097152 +FEATURE_SET_ANION = 4194304 FEATURE_FLAGS_AIRPURIFIER_MIIO = ( FEATURE_SET_BUZZER @@ -335,6 +340,15 @@ FEATURE_FLAGS_AIRPURIFIER_MIOT = ( | FEATURE_SET_LED_BRIGHTNESS ) +FEATURE_FLAGS_AIRPURIFIER_4 = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_FAVORITE_LEVEL + | FEATURE_SET_FAN_LEVEL + | FEATURE_SET_LED_BRIGHTNESS + | FEATURE_SET_ANION +) + FEATURE_FLAGS_AIRPURIFIER_3C = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index aa4b8a8a1bc..39988976564 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -48,6 +48,7 @@ from .const import ( FEATURE_FLAGS_AIRFRESH_T2017, FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, + FEATURE_FLAGS_AIRPURIFIER_4, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -68,6 +69,8 @@ from .const import ( MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V3, @@ -411,6 +414,14 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO self._attr_supported_features = FanEntityFeature.PRESET_MODE self._speed_count = 1 + elif self._model in [MODEL_AIRPURIFIER_4, MODEL_AIRPURIFIER_4_PRO]: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_4 + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT + self._preset_modes = PRESET_MODES_AIRPURIFIER_MIOT + self._attr_supported_features = ( + FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + ) + self._speed_count = 3 elif self._model == MODEL_AIRPURIFIER_PRO_V7: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 577e82e3fd8..364bd59772c 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -24,6 +24,7 @@ from .const import ( FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, + FEATURE_FLAGS_AIRPURIFIER_4, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -55,6 +56,8 @@ from .const import ( MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1, @@ -224,6 +227,8 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRPURIFIER_PRO_V7: FEATURE_FLAGS_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1: FEATURE_FLAGS_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V3: FEATURE_FLAGS_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_4: FEATURE_FLAGS_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C, MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11, MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 235d103b53d..5370357c58c 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -60,6 +60,8 @@ from .const import ( MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRPURIFIER_3C, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V2, @@ -100,6 +102,7 @@ ATTR_DISPLAY_CLOCK = "display_clock" ATTR_FAVORITE_SPEED = "favorite_speed" ATTR_FILTER_LIFE_REMAINING = "filter_life_remaining" ATTR_FILTER_HOURS_USED = "filter_hours_used" +ATTR_FILTER_LEFT_TIME = "filter_left_time" ATTR_DUST_FILTER_LIFE_REMAINING = "dust_filter_life_remaining" ATTR_DUST_FILTER_LIFE_REMAINING_DAYS = "dust_filter_life_remaining_days" ATTR_UPPER_FILTER_LIFE_REMAINING = "upper_filter_life_remaining" @@ -114,6 +117,7 @@ ATTR_MOTOR_SPEED = "motor_speed" ATTR_NIGHT_MODE = "night_mode" ATTR_NIGHT_TIME_BEGIN = "night_time_begin" ATTR_NIGHT_TIME_END = "night_time_end" +ATTR_PM10 = "pm10_density" ATTR_PM25 = "pm25" ATTR_PM25_2 = "pm25_2" ATTR_POWER = "power" @@ -253,6 +257,13 @@ SENSOR_TYPES = { icon="mdi:cloud", state_class=SensorStateClass.MEASUREMENT, ), + ATTR_PM10: XiaomiMiioSensorDescription( + key=ATTR_PM10, + name="PM10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), ATTR_PM25: XiaomiMiioSensorDescription( key=ATTR_AQI, name="PM2.5", @@ -284,6 +295,14 @@ SENSOR_TYPES = { state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), + ATTR_FILTER_LEFT_TIME: XiaomiMiioSensorDescription( + key=ATTR_FILTER_LEFT_TIME, + name="Filter time left", + native_unit_of_measurement=TIME_DAYS, + icon="mdi:clock-outline", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), ATTR_DUST_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_DUST_FILTER_LIFE_REMAINING, name="Dust filter life remaining", @@ -385,6 +404,29 @@ PURIFIER_MIOT_SENSORS = ( ATTR_TEMPERATURE, ATTR_USE_TIME, ) +PURIFIER_4_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_LEFT_TIME, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, + ATTR_USE_TIME, +) +PURIFIER_4_PRO_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_LEFT_TIME, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PM10, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, + ATTR_USE_TIME, +) PURIFIER_3C_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, ATTR_FILTER_USE, @@ -478,6 +520,8 @@ MODEL_TO_SENSORS_MAP: dict[str, tuple[str, ...]] = { MODEL_AIRHUMIDIFIER_CA1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRHUMIDIFIER_CB1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRPURIFIER_3C: PURIFIER_3C_SENSORS, + MODEL_AIRPURIFIER_4: PURIFIER_4_SENSORS, + MODEL_AIRPURIFIER_4_PRO: PURIFIER_4_PRO_SENSORS, MODEL_AIRPURIFIER_PRO: PURIFIER_PRO_SENSORS, MODEL_AIRPURIFIER_PRO_V7: PURIFIER_PRO_V7_SENSORS, MODEL_AIRPURIFIER_V2: PURIFIER_V2_SENSORS, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index f80b6343d09..89cc80ce74f 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -43,6 +43,7 @@ from .const import ( FEATURE_FLAGS_AIRHUMIDIFIER_MJSSQ, FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, + FEATURE_FLAGS_AIRPURIFIER_4, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -55,6 +56,7 @@ from .const import ( FEATURE_FLAGS_FAN_P9, FEATURE_FLAGS_FAN_P10_P11, FEATURE_FLAGS_FAN_ZA5, + FEATURE_SET_ANION, FEATURE_SET_AUTO_DETECT, FEATURE_SET_BUZZER, FEATURE_SET_CHILD_LOCK, @@ -76,6 +78,8 @@ from .const import ( MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1, @@ -128,6 +132,7 @@ ATTR_DRY = "dry" ATTR_LEARN_MODE = "learn_mode" ATTR_LED = "led" ATTR_IONIZER = "ionizer" +ATTR_ANION = "anion" ATTR_LOAD_POWER = "load_power" ATTR_MODEL = "model" ATTR_POWER = "power" @@ -188,6 +193,8 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRPURIFIER_PRO_V7: FEATURE_FLAGS_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1: FEATURE_FLAGS_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V3: FEATURE_FLAGS_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_4: FEATURE_FLAGS_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C, MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11, MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11, @@ -300,6 +307,15 @@ SWITCH_TYPES = ( method_off="async_set_ionizer_off", entity_category=EntityCategory.CONFIG, ), + XiaomiMiioSwitchDescription( + key=ATTR_ANION, + feature=FEATURE_SET_ANION, + name="Ionizer", + icon="mdi:shimmer", + method_on="async_set_anion_on", + method_off="async_set_anion_off", + entity_category=EntityCategory.CONFIG, + ), XiaomiMiioSwitchDescription( key=ATTR_PTC, feature=FEATURE_SET_PTC, @@ -678,6 +694,22 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): False, ) + async def async_set_anion_on(self) -> bool: + """Turn ionizer on.""" + return await self._try_command( + "Turning ionizer of the miio device on failed.", + self._device.set_anion, + True, + ) + + async def async_set_anion_off(self) -> bool: + """Turn ionizer off.""" + return await self._try_command( + "Turning ionizer of the miio device off failed.", + self._device.set_anion, + False, + ) + async def async_set_ptc_on(self) -> bool: """Turn ionizer on.""" return await self._try_command( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index e8f4b334544..7b866d5ff71 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -21,10 +21,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import as_utc from . import VacuumCoordinatorData -from ...helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, From 4f5602849105ae9df7441b9a178ad65c1e181833 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 12:39:10 +0200 Subject: [PATCH 018/903] Fix unit of measurement usage in COSignal (#75856) --- homeassistant/components/co2signal/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index f7514664698..841848621ec 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -103,8 +103,8 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" - if self.entity_description.unit_of_measurement: - return self.entity_description.unit_of_measurement + if self.entity_description.native_unit_of_measurement: + return self.entity_description.native_unit_of_measurement return cast( str, self.coordinator.data["units"].get(self.entity_description.key) ) From 7251445ffbae9544a3fdc39b1662b0f7a647f7a9 Mon Sep 17 00:00:00 2001 From: Chaim Turkel Date: Thu, 28 Jul 2022 17:19:20 +0300 Subject: [PATCH 019/903] Add shabat sensors to jewish_calendar (#57866) * add shabat sensors * add shabat sensors * add shabat sensors * add shabat sensors * add shabat sensors * Remove redundunt classes and combine sensors * Update homeassistant/components/jewish_calendar/binary_sensor.py Co-authored-by: Yuval Aboulafia * Update homeassistant/components/jewish_calendar/binary_sensor.py Co-authored-by: Yuval Aboulafia * updated requirements * call get_zmanim once * add type hint to entity description * fix errors resulted from type hints introduction * fix mypy error * use attr for state * Update homeassistant/components/jewish_calendar/binary_sensor.py Co-authored-by: Teemu R. * Fix typing Co-authored-by: Yuval Aboulafia Co-authored-by: Teemu R. --- .../jewish_calendar/binary_sensor.py | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index f239dfc31b6..3d28e2bb0c0 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -1,9 +1,10 @@ """Support for Jewish Calendar binary sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import datetime as dt from datetime import datetime -from typing import cast import hdate from hdate.zmanim import Zmanim @@ -20,10 +21,38 @@ import homeassistant.util.dt as dt_util from . import DOMAIN -BINARY_SENSORS = BinarySensorEntityDescription( - key="issur_melacha_in_effect", - name="Issur Melacha in Effect", - icon="mdi:power-plug-off", + +@dataclass +class JewishCalendarBinarySensorMixIns(BinarySensorEntityDescription): + """Binary Sensor description mixin class for Jewish Calendar.""" + + is_on: Callable[..., bool] = lambda _: False + + +@dataclass +class JewishCalendarBinarySensorEntityDescription( + JewishCalendarBinarySensorMixIns, BinarySensorEntityDescription +): + """Binary Sensor Entity description for Jewish Calendar.""" + + +BINARY_SENSORS: tuple[JewishCalendarBinarySensorEntityDescription, ...] = ( + JewishCalendarBinarySensorEntityDescription( + key="issur_melacha_in_effect", + name="Issur Melacha in Effect", + icon="mdi:power-plug-off", + is_on=lambda state: bool(state.issur_melacha_in_effect), + ), + JewishCalendarBinarySensorEntityDescription( + key="erev_shabbat_hag", + name="Erev Shabbat/Hag", + is_on=lambda state: bool(state.erev_shabbat_hag), + ), + JewishCalendarBinarySensorEntityDescription( + key="motzei_shabbat_hag", + name="Motzei Shabbat/Hag", + is_on=lambda state: bool(state.motzei_shabbat_hag), + ), ) @@ -37,20 +66,27 @@ async def async_setup_platform( if discovery_info is None: return - async_add_entities([JewishCalendarBinarySensor(hass.data[DOMAIN], BINARY_SENSORS)]) + async_add_entities( + [ + JewishCalendarBinarySensor(hass.data[DOMAIN], description) + for description in BINARY_SENSORS + ] + ) class JewishCalendarBinarySensor(BinarySensorEntity): """Representation of an Jewish Calendar binary sensor.""" _attr_should_poll = False + entity_description: JewishCalendarBinarySensorEntityDescription def __init__( self, data: dict[str, str | bool | int | float], - description: BinarySensorEntityDescription, + description: JewishCalendarBinarySensorEntityDescription, ) -> None: """Initialize the binary sensor.""" + self.entity_description = description self._attr_name = f"{data['name']} {description.name}" self._attr_unique_id = f"{data['prefix']}_{description.key}" self._location = data["location"] @@ -62,7 +98,8 @@ class JewishCalendarBinarySensor(BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - return cast(bool, self._get_zmanim().issur_melacha_in_effect) + zmanim = self._get_zmanim() + return self.entity_description.is_on(zmanim) def _get_zmanim(self) -> Zmanim: """Return the Zmanim object for now().""" From 91180923ae4a50a6b95f5e8a5c54f991139dc71d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 16:43:32 +0200 Subject: [PATCH 020/903] Fix HTTP 404 being logged as a stack trace (#75861) --- homeassistant/components/http/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index c4dc97727a9..6cb1bafdaca 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -33,7 +33,7 @@ def _get_file_path( return None if filepath.is_file(): return filepath - raise HTTPNotFound + raise FileNotFoundError class CachingStaticResource(StaticResource): From 8e2f0497cef2cf61de3da53b99b61dd26b224228 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 28 Jul 2022 11:24:31 -0400 Subject: [PATCH 021/903] ZHA network backup and restore API (#75791) * Implement WS API endpoints for zigpy backups * Implement backup restoration * Display error messages caused by invalid backup JSON * Indicate to the frontend when a backup is incomplete * Perform a coordinator backup before HA performs a backup * Fix `backup.async_post_backup` docstring * Rename `data` to `backup` in restore command * Add unit tests for new websocket APIs * Unit test backup platform * Move code to overwrite EZSP EUI64 into ZHA * Include the radio type in the network settings API response --- homeassistant/components/zha/api.py | 113 ++++++++++++++++++++++- homeassistant/components/zha/backup.py | 21 +++++ tests/components/zha/conftest.py | 12 ++- tests/components/zha/test_api.py | 123 +++++++++++++++++++++++++ tests/components/zha/test_backup.py | 20 ++++ 5 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/zha/backup.py create mode 100644 tests/components/zha/test_backup.py diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 89d360577d4..40996be3248 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -6,6 +6,8 @@ import logging from typing import TYPE_CHECKING, Any, NamedTuple import voluptuous as vol +import zigpy.backups +from zigpy.backups import NetworkBackup from zigpy.config.validators import cv_boolean from zigpy.types.named import EUI64 from zigpy.zcl.clusters.security import IasAce @@ -43,6 +45,7 @@ from .core.const import ( CLUSTER_COMMANDS_SERVER, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, + CONF_RADIO_TYPE, CUSTOM_CONFIGURATION, DATA_ZHA, DATA_ZHA_GATEWAY, @@ -229,6 +232,15 @@ def _cv_cluster_binding(value: dict[str, Any]) -> ClusterBinding: ) +def _cv_zigpy_network_backup(value: dict[str, Any]) -> zigpy.backups.NetworkBackup: + """Transform a zigpy network backup.""" + + try: + return zigpy.backups.NetworkBackup.from_dict(value) + except ValueError as err: + raise vol.Invalid(str(err)) from err + + GROUP_MEMBER_SCHEMA = vol.All( vol.Schema( { @@ -302,7 +314,7 @@ async def websocket_permit_devices( ) else: await zha_gateway.application_controller.permit(time_s=duration, node=ieee) - connection.send_result(msg["id"]) + connection.send_result(msg[ID]) @websocket_api.require_admin @@ -989,7 +1001,7 @@ async def websocket_get_configuration( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA configuration.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] import voluptuous_serialize # pylint: disable=import-outside-toplevel def custom_serializer(schema: Any) -> Any: @@ -1047,6 +1059,99 @@ async def websocket_update_zha_configuration( connection.send_result(msg[ID], status) +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required(TYPE): "zha/network/settings"}) +@websocket_api.async_response +async def websocket_get_network_settings( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Get ZHA network settings.""" + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + application_controller = zha_gateway.application_controller + + # Serialize the current network settings + backup = NetworkBackup( + node_info=application_controller.state.node_info, + network_info=application_controller.state.network_info, + ) + + connection.send_result( + msg[ID], + { + "radio_type": zha_gateway.config_entry.data[CONF_RADIO_TYPE], + "settings": backup.as_dict(), + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required(TYPE): "zha/network/backups/list"}) +@websocket_api.async_response +async def websocket_list_network_backups( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Get ZHA network settings.""" + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + application_controller = zha_gateway.application_controller + + # Serialize known backups + connection.send_result( + msg[ID], [backup.as_dict() for backup in application_controller.backups] + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required(TYPE): "zha/network/backups/create"}) +@websocket_api.async_response +async def websocket_create_network_backup( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Create a ZHA network backup.""" + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + application_controller = zha_gateway.application_controller + + # This can take 5-30s + backup = await application_controller.backups.create_backup(load_devices=True) + connection.send_result( + msg[ID], + { + "backup": backup.as_dict(), + "is_complete": backup.is_complete(), + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/network/backups/restore", + vol.Required("backup"): _cv_zigpy_network_backup, + vol.Optional("ezsp_force_write_eui64", default=False): cv.boolean, + } +) +@websocket_api.async_response +async def websocket_restore_network_backup( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Restore a ZHA network backup.""" + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + application_controller = zha_gateway.application_controller + backup = msg["backup"] + + if msg["ezsp_force_write_eui64"]: + backup.network_info.stack_specific.setdefault("ezsp", {})[ + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" + ] = True + + # This can take 30-40s + try: + await application_controller.backups.restore_backup(backup) + except ValueError as err: + connection.send_error(msg[ID], websocket_api.const.ERR_INVALID_FORMAT, str(err)) + else: + connection.send_result(msg[ID]) + + @callback def async_load_api(hass: HomeAssistant) -> None: """Set up the web socket API.""" @@ -1356,6 +1461,10 @@ def async_load_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_update_topology) websocket_api.async_register_command(hass, websocket_get_configuration) websocket_api.async_register_command(hass, websocket_update_zha_configuration) + websocket_api.async_register_command(hass, websocket_get_network_settings) + websocket_api.async_register_command(hass, websocket_list_network_backups) + websocket_api.async_register_command(hass, websocket_create_network_backup) + websocket_api.async_register_command(hass, websocket_restore_network_backup) @callback diff --git a/homeassistant/components/zha/backup.py b/homeassistant/components/zha/backup.py new file mode 100644 index 00000000000..89d5294e1c4 --- /dev/null +++ b/homeassistant/components/zha/backup.py @@ -0,0 +1,21 @@ +"""Backup platform for the ZHA integration.""" +import logging + +from homeassistant.core import HomeAssistant + +from .core import ZHAGateway +from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY + +_LOGGER = logging.getLogger(__name__) + + +async def async_pre_backup(hass: HomeAssistant) -> None: + """Perform operations before a backup starts.""" + _LOGGER.debug("Performing coordinator backup") + + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + await zha_gateway.application_controller.backups.create_backup(load_devices=True) + + +async def async_post_backup(hass: HomeAssistant) -> None: + """Perform operations after a backup finishes.""" diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b1041a3e2a3..27155e16cc7 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest import zigpy from zigpy.application import ControllerApplication +import zigpy.backups import zigpy.config from zigpy.const import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE import zigpy.device @@ -54,7 +55,16 @@ def zigpy_app_controller(): app.ieee.return_value = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") type(app).nwk = PropertyMock(return_value=zigpy.types.NWK(0x0000)) type(app).devices = PropertyMock(return_value={}) - type(app).state = PropertyMock(return_value=State()) + type(app).backups = zigpy.backups.BackupManager(app) + + state = State() + state.node_info.ieee = app.ieee.return_value + state.network_info.extended_pan_id = app.ieee.return_value + state.network_info.pan_id = 0x1234 + state.network_info.channel = 15 + state.network_info.network_key.key = zigpy.types.KeyData(range(16)) + type(app).state = PropertyMock(return_value=state) + return app diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 08766bc74ac..b25bffebec7 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest import voluptuous as vol +import zigpy.backups import zigpy.profiles.zha import zigpy.types import zigpy.zcl.clusters.general as general @@ -620,3 +621,125 @@ async def test_ws_permit_ha12(app_controller, zha_client, params, duration, node assert app_controller.permit.await_args[1]["time_s"] == duration assert app_controller.permit.await_args[1]["node"] == node assert app_controller.permit_with_key.call_count == 0 + + +async def test_get_network_settings(app_controller, zha_client): + """Test current network settings are returned.""" + + await app_controller.backups.create_backup() + + await zha_client.send_json({ID: 6, TYPE: f"{DOMAIN}/network/settings"}) + msg = await zha_client.receive_json() + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert "radio_type" in msg["result"] + assert "network_info" in msg["result"]["settings"] + + +async def test_list_network_backups(app_controller, zha_client): + """Test backups are serialized.""" + + await app_controller.backups.create_backup() + + await zha_client.send_json({ID: 6, TYPE: f"{DOMAIN}/network/backups/list"}) + msg = await zha_client.receive_json() + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert "network_info" in msg["result"][0] + + +async def test_create_network_backup(app_controller, zha_client): + """Test creating backup.""" + + assert not app_controller.backups.backups + await zha_client.send_json({ID: 6, TYPE: f"{DOMAIN}/network/backups/create"}) + msg = await zha_client.receive_json() + assert len(app_controller.backups.backups) == 1 + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert "backup" in msg["result"] and "is_complete" in msg["result"] + + +async def test_restore_network_backup_success(app_controller, zha_client): + """Test successfully restoring a backup.""" + + backup = zigpy.backups.NetworkBackup() + + with patch.object(app_controller.backups, "restore_backup", new=AsyncMock()) as p: + await zha_client.send_json( + { + ID: 6, + TYPE: f"{DOMAIN}/network/backups/restore", + "backup": backup.as_dict(), + } + ) + msg = await zha_client.receive_json() + + p.assert_called_once_with(backup) + assert "ezsp" not in backup.network_info.stack_specific + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + +async def test_restore_network_backup_force_write_eui64(app_controller, zha_client): + """Test successfully restoring a backup.""" + + backup = zigpy.backups.NetworkBackup() + + with patch.object(app_controller.backups, "restore_backup", new=AsyncMock()) as p: + await zha_client.send_json( + { + ID: 6, + TYPE: f"{DOMAIN}/network/backups/restore", + "backup": backup.as_dict(), + "ezsp_force_write_eui64": True, + } + ) + msg = await zha_client.receive_json() + + # EUI64 will be overwritten + p.assert_called_once_with( + backup.replace( + network_info=backup.network_info.replace( + stack_specific={ + "ezsp": { + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it": True + } + } + ) + ) + ) + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + +@patch("zigpy.backups.NetworkBackup.from_dict", new=lambda v: v) +async def test_restore_network_backup_failure(app_controller, zha_client): + """Test successfully restoring a backup.""" + + with patch.object( + app_controller.backups, + "restore_backup", + new=AsyncMock(side_effect=ValueError("Restore failed")), + ) as p: + await zha_client.send_json( + {ID: 6, TYPE: f"{DOMAIN}/network/backups/restore", "backup": "a backup"} + ) + msg = await zha_client.receive_json() + + p.assert_called_once_with("a backup") + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_INVALID_FORMAT diff --git a/tests/components/zha/test_backup.py b/tests/components/zha/test_backup.py new file mode 100644 index 00000000000..aea50cf0923 --- /dev/null +++ b/tests/components/zha/test_backup.py @@ -0,0 +1,20 @@ +"""Unit tests for ZHA backup platform.""" + +from unittest.mock import AsyncMock, patch + +from homeassistant.components.zha.backup import async_post_backup, async_pre_backup + + +async def test_pre_backup(hass, setup_zha): + """Test backup creation when `async_pre_backup` is called.""" + with patch("zigpy.backups.BackupManager.create_backup", AsyncMock()) as backup_mock: + await setup_zha() + await async_pre_backup(hass) + + backup_mock.assert_called_once_with(load_devices=True) + + +async def test_post_backup(hass, setup_zha): + """Test no-op `async_post_backup`.""" + await setup_zha() + await async_post_backup(hass) From 1012064bb7d7aa6e83dc71ac2d0de260619a4f9f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:13:16 +0200 Subject: [PATCH 022/903] Remove state class from daily net sensors in DSMR Reader (#75864) --- homeassistant/components/dsmr_reader/definitions.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 9f4ee7ed918..ac61837afec 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -225,42 +225,36 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Low tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2", name="High tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_returned", name="Low tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2_returned", name="High tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_merged", name="Power usage total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_returned_merged", name="Power return total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_cost", From 16e75f2a134ade73f38420463a42f960430ad3e6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:15:27 +0200 Subject: [PATCH 023/903] Fix incorrect sensor key in DSMR (#75865) --- homeassistant/components/dsmr/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 7ab1a3bb45b..aa01c798072 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -85,7 +85,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key="electricity_delivery", + key="current_electricity_delivery", name="Power production", obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, device_class=SensorDeviceClass.POWER, From a020482c232d8902088c1123f28562f14ddd5e2a Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 28 Jul 2022 11:20:10 -0500 Subject: [PATCH 024/903] Update frontend to 20220728.0 (#75872) --- 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 2a1fb2d3b37..45331491aa0 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==20220727.0"], + "requirements": ["home-assistant-frontend==20220728.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2554f0185b4..1f86ff4c7c5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index b4b84a84fd1..4b1232999c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8ac231bea1..cf4c54297a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 166e58eaa4f1d1538b47a84a1b3e980712fa8a23 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:20:39 +0200 Subject: [PATCH 025/903] Fix camera token to trigger authentication IP ban (#75870) --- homeassistant/components/camera/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 247f73c89f2..3bf86dedea1 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -715,7 +715,9 @@ class CameraView(HomeAssistantView): ) if not authenticated: - raise web.HTTPUnauthorized() + if request[KEY_AUTHENTICATED]: + raise web.HTTPUnauthorized() + raise web.HTTPForbidden() if not camera.is_on: _LOGGER.debug("Camera is off") From 4ed0463438c86c460e6c425fe5a6efc96723b760 Mon Sep 17 00:00:00 2001 From: Brandon West Date: Thu, 28 Jul 2022 12:27:48 -0400 Subject: [PATCH 026/903] Bump russound_rio to 0.1.8 (#75837) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 4b9b7a2c8d0..e844f478322 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -2,7 +2,7 @@ "domain": "russound_rio", "name": "Russound RIO", "documentation": "https://www.home-assistant.io/integrations/russound_rio", - "requirements": ["russound_rio==0.1.7"], + "requirements": ["russound_rio==0.1.8"], "codeowners": [], "iot_class": "local_push", "loggers": ["russound_rio"] diff --git a/requirements_all.txt b/requirements_all.txt index 4b1232999c2..593bb4e040c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2126,7 +2126,7 @@ rtsp-to-webrtc==0.5.1 russound==0.1.9 # homeassistant.components.russound_rio -russound_rio==0.1.7 +russound_rio==0.1.8 # homeassistant.components.yamaha rxv==0.7.0 From 10356b93795abd8c21539d1b25ebe5ce2e457c09 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 28 Jul 2022 19:10:37 +0100 Subject: [PATCH 027/903] Fix Xiaomi BLE not detecting encryption for some devices (#75851) --- .../components/bluetooth/__init__.py | 30 +++++ .../components/xiaomi_ble/config_flow.py | 44 ++++++- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_init.py | 90 ++++++++++++- tests/components/xiaomi_ble/__init__.py | 12 ++ .../components/xiaomi_ble/test_config_flow.py | 123 ++++++++++++++++++ 8 files changed, 299 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 551e93d5bd9..1853ffa0203 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,6 +1,7 @@ """The bluetooth integration.""" from __future__ import annotations +from asyncio import Future from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta @@ -8,6 +9,7 @@ from enum import Enum import logging from typing import Final, Union +import async_timeout from bleak import BleakError from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData @@ -95,6 +97,9 @@ BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[ [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None ] +ProcessAdvertisementCallback = Callable[ + [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo]], bool +] @hass_callback @@ -159,6 +164,31 @@ def async_register_callback( return manager.async_register_callback(callback, match_dict) +async def async_process_advertisements( + hass: HomeAssistant, + callback: ProcessAdvertisementCallback, + match_dict: BluetoothCallbackMatcher, + timeout: int, +) -> BluetoothServiceInfo: + """Process advertisements until callback returns true or timeout expires.""" + done: Future[BluetoothServiceInfo] = Future() + + @hass_callback + def _async_discovered_device( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + if callback(service_info): + done.set_result(service_info) + + unload = async_register_callback(hass, _async_discovered_device, match_dict) + + try: + async with async_timeout.timeout(timeout): + return await done + finally: + unload() + + @hass_callback def async_track_unavailable( hass: HomeAssistant, diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index e7c4a3e1f8c..f352f43d0bf 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Xiaomi Bluetooth integration.""" from __future__ import annotations +import asyncio import dataclasses from typing import Any @@ -12,6 +13,7 @@ from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothServiceInfo, async_discovered_service_info, + async_process_advertisements, ) from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ADDRESS @@ -19,6 +21,9 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN +# How long to wait for additional advertisement packets if we don't have the right ones +ADDITIONAL_DISCOVERY_TIMEOUT = 5 + @dataclasses.dataclass class Discovery: @@ -44,6 +49,24 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} + async def _async_wait_for_full_advertisement( + self, discovery_info: BluetoothServiceInfo, device: DeviceData + ) -> BluetoothServiceInfo: + """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" + if not device.pending: + return discovery_info + + def _process_more_advertisements(service_info: BluetoothServiceInfo) -> bool: + device.update(service_info) + return not device.pending + + return await async_process_advertisements( + self.hass, + _process_more_advertisements, + {"address": discovery_info.address}, + ADDITIONAL_DISCOVERY_TIMEOUT, + ) + async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo ) -> FlowResult: @@ -53,6 +76,16 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): device = DeviceData() if not device.supported(discovery_info): return self.async_abort(reason="not_supported") + + # Wait until we have received enough information about this device to detect its encryption type + try: + discovery_info = await self._async_wait_for_full_advertisement( + discovery_info, device + ) + except asyncio.TimeoutError: + # If we don't see a valid packet within the timeout then this device is not supported. + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info self._discovered_device = device @@ -161,13 +194,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) discovery = self._discovered_devices[address] + # Wait until we have received enough information about this device to detect its encryption type + try: + self._discovery_info = await self._async_wait_for_full_advertisement( + discovery.discovery_info, discovery.device + ) + except asyncio.TimeoutError: + # If we don't see a valid packet within the timeout then this device is not supported. + return self.async_abort(reason="not_supported") + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: - self._discovery_info = discovery.discovery_info self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_legacy() if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: - self._discovery_info = discovery.discovery_info self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_4_5() diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 2e1a502ca69..41512291749 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.1"], + "requirements": ["xiaomi-ble==0.6.2"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 593bb4e040c..5094f5a3241 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2473,7 +2473,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.1 +xiaomi-ble==0.6.2 # homeassistant.components.knx xknx==0.22.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf4c54297a2..e8b8c5be6b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.1 +xiaomi-ble==0.6.2 # homeassistant.components.knx xknx==0.22.0 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index bb2c5f49cc9..f3f3eda1f27 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" +import asyncio from datetime import timedelta from unittest.mock import MagicMock, patch @@ -12,6 +13,7 @@ from homeassistant.components.bluetooth import ( UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, + async_process_advertisements, async_track_unavailable, models, ) @@ -21,7 +23,7 @@ from homeassistant.components.bluetooth.const import ( ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -791,6 +793,92 @@ async def test_register_callback_by_address( assert service_info.manufacturer_id == 89 +async def test_process_advertisements_bail_on_good_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Test as soon as we see a 'good' advertisement we return it.""" + done = asyncio.Future() + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set_result(None) + return len(service_info.service_data) > 0 + + handle = hass.async_create_task( + async_process_advertisements( + hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + ) + ) + + while not done.done(): + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.name == "wohand" + + +async def test_process_advertisements_ignore_bad_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Check that we ignore bad advertisements.""" + done = asyncio.Event() + return_value = asyncio.Event() + + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""}, + ) + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set() + return return_value.is_set() + + handle = hass.async_create_task( + async_process_advertisements( + hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + ) + ) + + # The goal of this loop is to make sure that async_process_advertisements sees at least one + # callback that returns False + while not done.is_set(): + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + # Set the return value and mutate the advertisement + # Check that scan ends and correct advertisement data is returned + return_value.set() + adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c" + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.service_data["00000d00-0000-1000-8000-00805f9b34fa"] == b"H\x10c" + + +async def test_process_advertisements_timeout( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test we timeout if no advertisements at all.""" + + def _callback(service_info: BluetoothServiceInfo) -> bool: + return False + + with pytest.raises(asyncio.TimeoutError): + await async_process_advertisements(hass, _callback, {}, 0) + + async def test_wrapped_instance_with_filter( hass, mock_bleak_scanner_start, enable_bluetooth ): diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index a6269a02d12..1dd1eeed65a 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -61,6 +61,18 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( source="local", ) +MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfo( + name="LYWSD02MMC", + address="A4:C1:38:56:53:84", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"0X[\x05\x02\x84\x53\x568\xc1\xa4\x08", + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: """Make a dummy advertisement.""" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index d4b4300d2c1..b424228cc6c 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Xiaomi config flow.""" +import asyncio from unittest.mock import patch from homeassistant import config_entries @@ -9,9 +10,11 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( JTYJGD03MI_SERVICE_INFO, LYWSDCGQ_SERVICE_INFO, + MISSING_PAYLOAD_ENCRYPTED, MMC_T201_1_SERVICE_INFO, NOT_SENSOR_PUSH_SERVICE_INFO, YLKG07YL_SERVICE_INFO, + make_advertisement, ) from tests.common import MockConfigEntry @@ -38,6 +41,57 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" +async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): + """Test discovery via bluetooth with a valid device but missing payload.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): + """Test discovering a valid device. Payload is too short, but later we get full one.""" + + async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" + + async def test_async_step_bluetooth_during_onboarding(hass): """Test discovery via bluetooth during onboarding.""" with patch( @@ -287,6 +341,75 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "58:2D:34:35:93:21" +async def test_async_step_user_short_payload(hass): + """Test setup from service info cache with devices found but short payloads.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "not_supported" + + +async def test_async_step_user_short_payload_then_full(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + + async def test_async_step_user_with_found_devices_v4_encryption(hass): """Test setup from service info cache with devices found, with v4 encryption.""" with patch( From 6ba0b9cff21e225fe64b0336a2d32f3a2785b512 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 22:36:11 +0200 Subject: [PATCH 028/903] Fix AdGuard Home rules count sensor (#75879) --- homeassistant/components/adguard/sensor.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 07a483f03c4..86104d15ef2 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -26,7 +26,7 @@ PARALLEL_UPDATES = 4 class AdGuardHomeEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, int | float]]] + value_fn: Callable[[AdGuardHome], Coroutine[Any, Any, int | float]] @dataclass @@ -42,56 +42,56 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( name="DNS queries", icon="mdi:magnify", native_unit_of_measurement="queries", - value_fn=lambda adguard: adguard.stats.dns_queries, + value_fn=lambda adguard: adguard.stats.dns_queries(), ), AdGuardHomeEntityDescription( key="blocked_filtering", name="DNS queries blocked", icon="mdi:magnify-close", native_unit_of_measurement="queries", - value_fn=lambda adguard: adguard.stats.blocked_filtering, + value_fn=lambda adguard: adguard.stats.blocked_filtering(), ), AdGuardHomeEntityDescription( key="blocked_percentage", name="DNS queries blocked ratio", icon="mdi:magnify-close", native_unit_of_measurement=PERCENTAGE, - value_fn=lambda adguard: adguard.stats.blocked_percentage, + value_fn=lambda adguard: adguard.stats.blocked_percentage(), ), AdGuardHomeEntityDescription( key="blocked_parental", name="Parental control blocked", icon="mdi:human-male-girl", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_parental, + value_fn=lambda adguard: adguard.stats.replaced_parental(), ), AdGuardHomeEntityDescription( key="blocked_safebrowsing", name="Safe browsing blocked", icon="mdi:shield-half-full", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_safebrowsing, + value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(), ), AdGuardHomeEntityDescription( key="enforced_safesearch", name="Safe searches enforced", icon="mdi:shield-search", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_safesearch, + value_fn=lambda adguard: adguard.stats.replaced_safesearch(), ), AdGuardHomeEntityDescription( key="average_speed", name="Average processing speed", icon="mdi:speedometer", native_unit_of_measurement=TIME_MILLISECONDS, - value_fn=lambda adguard: adguard.stats.avg_processing_time, + value_fn=lambda adguard: adguard.stats.avg_processing_time(), ), AdGuardHomeEntityDescription( key="rules_count", name="Rules count", icon="mdi:counter", native_unit_of_measurement="rules", - value_fn=lambda adguard: adguard.stats.avg_processing_time, + value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False), entity_registry_enabled_default=False, ), ) @@ -144,7 +144,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - value = await self.entity_description.value_fn(self.adguard)() + value = await self.entity_description.value_fn(self.adguard) self._attr_native_value = value if isinstance(value, float): self._attr_native_value = f"{value:.2f}" From 702cef3fc7bc8290f6d9a72b1016251bea22df19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jul 2022 11:14:13 -1000 Subject: [PATCH 029/903] Add startup timeout to bluetooth (#75848) Co-authored-by: Martin Hjelmare --- .../components/bluetooth/__init__.py | 10 ++++++- tests/components/bluetooth/test_init.py | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 1853ffa0203..eb8e31baef0 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,6 +1,7 @@ """The bluetooth integration.""" from __future__ import annotations +import asyncio from asyncio import Future from collections.abc import Callable from dataclasses import dataclass @@ -45,6 +46,7 @@ _LOGGER = logging.getLogger(__name__) UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 +START_TIMEOUT = 15 SOURCE_LOCAL: Final = "local" @@ -330,7 +332,13 @@ class BluetoothManager: self._device_detected, {} ) try: - await self.scanner.start() + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() + except asyncio.TimeoutError as ex: + self._cancel_device_detected() + raise ConfigEntryNotReady( + f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex except (FileNotFoundError, BleakError) as ex: self._cancel_device_detected() raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index f3f3eda1f27..0664f82dbab 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -97,6 +97,33 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): assert len(bluetooth.async_discovered_service_info(hass)) == 0 +async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): + """Test we fail gracefully when bluetooth/dbus is hanging.""" + mock_bt = [] + + async def _mock_hang(): + await asyncio.sleep(1) + + with patch.object(bluetooth, "START_TIMEOUT", 0), patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=_mock_hang, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "Timed out starting Bluetooth" in caplog.text + + async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): """Test we retry if the adapter is not yet available.""" mock_bt = [] From 003fe9220e1574e3205a9e3cc274069a66753f6b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Jul 2022 00:27:47 +0200 Subject: [PATCH 030/903] Add protocol types for device_tracker `async_see` and `see` (#75891) --- .../components/aprs/device_tracker.py | 13 +++-- .../bluetooth_le_tracker/device_tracker.py | 4 +- .../bluetooth_tracker/device_tracker.py | 7 +-- .../components/demo/device_tracker.py | 6 +-- .../components/device_tracker/__init__.py | 2 + .../components/device_tracker/legacy.py | 50 +++++++++++++++++-- .../components/fleetgo/device_tracker.py | 6 +-- .../components/google_maps/device_tracker.py | 6 +-- .../components/icloud/device_tracker.py | 5 +- .../components/meraki/device_tracker.py | 6 +-- .../components/mqtt_json/device_tracker.py | 4 +- .../components/mysensors/device_tracker.py | 11 ++-- .../components/ping/device_tracker.py | 4 +- .../components/tile/device_tracker.py | 4 +- .../components/traccar/device_tracker.py | 6 +-- .../components/volvooncall/device_tracker.py | 8 ++- pylint/plugins/hass_enforce_type_hints.py | 4 +- tests/pylint/test_enforce_type_hints.py | 4 +- 18 files changed, 101 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 6bae9ce6ebe..b1467a6d2e4 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -1,7 +1,6 @@ """Support for APRS device tracking.""" from __future__ import annotations -from collections.abc import Callable import logging import threading @@ -12,6 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SeeCallback, ) from homeassistant.const import ( ATTR_GPS_ACCURACY, @@ -87,7 +87,7 @@ def gps_accuracy(gps, posambiguity: int) -> int: def setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + see: SeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the APRS tracker.""" @@ -123,8 +123,13 @@ class AprsListenerThread(threading.Thread): """APRS message listener.""" def __init__( - self, callsign: str, password: str, host: str, server_filter: str, see - ): + self, + callsign: str, + password: str, + host: str, + server_filter: str, + see: SeeCallback, + ) -> None: """Initialize the class.""" super().__init__() diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index f85cc2bad0a..a650e65b8f2 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging from uuid import UUID @@ -22,6 +21,7 @@ from homeassistant.components.device_tracker.const import ( ) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, + AsyncSeeCallback, async_load_config, ) from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STOP @@ -57,7 +57,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( async def async_setup_scanner( # noqa: C901 hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Bluetooth LE Scanner.""" diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index b62333c0489..90ae473a0cd 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable from datetime import datetime, timedelta import logging from typing import Final @@ -23,6 +23,7 @@ from homeassistant.components.device_tracker.const import ( ) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, + AsyncSeeCallback, Device, async_load_config, ) @@ -78,7 +79,7 @@ def discover_devices(device_id: int) -> list[tuple[str, str]]: async def see_device( hass: HomeAssistant, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, mac: str, device_name: str, rssi: tuple[int] | None = None, @@ -130,7 +131,7 @@ def lookup_name(mac: str) -> str | None: async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Bluetooth Scanner.""" diff --git a/homeassistant/components/demo/device_tracker.py b/homeassistant/components/demo/device_tracker.py index 74122932337..dacbd95219b 100644 --- a/homeassistant/components/demo/device_tracker.py +++ b/homeassistant/components/demo/device_tracker.py @@ -1,9 +1,9 @@ """Demo platform for the Device tracker component.""" from __future__ import annotations -from collections.abc import Callable import random +from homeassistant.components.device_tracker import SeeCallback from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -13,7 +13,7 @@ from .const import DOMAIN, SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA def setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + see: SeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the demo tracker.""" @@ -42,7 +42,7 @@ def setup_scanner( see( dev_id="demo_home_boy", host_name="Home Boy", - gps=[hass.config.latitude - 0.00002, hass.config.longitude + 0.00002], + gps=(hass.config.latitude - 0.00002, hass.config.longitude + 0.00002), gps_accuracy=20, battery=53, ) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index e9222156b00..5617d56ac3f 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -33,7 +33,9 @@ from .legacy import ( # noqa: F401 SERVICE_SEE, SERVICE_SEE_PAYLOAD_SCHEMA, SOURCE_TYPES, + AsyncSeeCallback, DeviceScanner, + SeeCallback, async_setup_integration as async_setup_legacy_integration, see, ) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 87026fc32ff..d8097a68ad5 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Coroutine, Sequence from datetime import datetime, timedelta import hashlib from types import ModuleType -from typing import Any, Final, final +from typing import Any, Final, Protocol, final import attr import voluptuous as vol @@ -124,6 +124,48 @@ YAML_DEVICES: Final = "known_devices.yaml" EVENT_NEW_DEVICE: Final = "device_tracker_new_device" +class SeeCallback(Protocol): + """Protocol type for DeviceTracker.see callback.""" + + def __call__( + self, + mac: str | None = None, + dev_id: str | None = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, + gps_accuracy: int | None = None, + battery: int | None = None, + attributes: dict[str, Any] | None = None, + source_type: str = SOURCE_TYPE_GPS, + picture: str | None = None, + icon: str | None = None, + consider_home: timedelta | None = None, + ) -> None: + """Define see type.""" + + +class AsyncSeeCallback(Protocol): + """Protocol type for DeviceTracker.async_see callback.""" + + async def __call__( + self, + mac: str | None = None, + dev_id: str | None = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, + gps_accuracy: int | None = None, + battery: int | None = None, + attributes: dict[str, Any] | None = None, + source_type: str = SOURCE_TYPE_GPS, + picture: str | None = None, + icon: str | None = None, + consider_home: timedelta | None = None, + ) -> None: + """Define async_see type.""" + + def see( hass: HomeAssistant, mac: str | None = None, @@ -133,7 +175,7 @@ def see( gps: GPSType | None = None, gps_accuracy: int | None = None, battery: int | None = None, - attributes: dict | None = None, + attributes: dict[str, Any] | None = None, ) -> None: """Call service to notify you see device.""" data: dict[str, Any] = { @@ -447,7 +489,7 @@ class DeviceTracker: gps: GPSType | None = None, gps_accuracy: int | None = None, battery: int | None = None, - attributes: dict | None = None, + attributes: dict[str, Any] | None = None, source_type: str = SOURCE_TYPE_GPS, picture: str | None = None, icon: str | None = None, @@ -480,7 +522,7 @@ class DeviceTracker: gps: GPSType | None = None, gps_accuracy: int | None = None, battery: int | None = None, - attributes: dict | None = None, + attributes: dict[str, Any] | None = None, source_type: str = SOURCE_TYPE_GPS, picture: str | None = None, icon: str | None = None, diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index bfafb5561d7..e80acae8627 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -1,7 +1,6 @@ """Support for FleetGO Platform.""" from __future__ import annotations -from collections.abc import Callable import logging import requests @@ -10,6 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SeeCallback, ) from homeassistant.const import ( CONF_CLIENT_ID, @@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( def setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + see: SeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the DeviceScanner and check if login is valid.""" @@ -53,7 +53,7 @@ def setup_scanner( class FleetGoDeviceScanner: """Define a scanner for the FleetGO platform.""" - def __init__(self, config, see): + def __init__(self, config, see: SeeCallback): """Initialize FleetGoDeviceScanner.""" self._include = config.get(CONF_INCLUDE) self._see = see diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 62627c2e905..8d8be8c0fe1 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -1,7 +1,6 @@ """Support for Google Maps location sharing.""" from __future__ import annotations -from collections.abc import Callable from datetime import timedelta import logging @@ -12,6 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PLATFORM_SCHEMA_BASE, SOURCE_TYPE_GPS, + SeeCallback, ) from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -50,7 +50,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE.extend( def setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + see: SeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Google Maps Location sharing scanner.""" @@ -61,7 +61,7 @@ def setup_scanner( class GoogleMapsScanner: """Representation of an Google Maps location sharing account.""" - def __init__(self, hass, config: ConfigType, see) -> None: + def __init__(self, hass, config: ConfigType, see: SeeCallback) -> None: """Initialize the scanner.""" self.see = see self.username = config[CONF_USERNAME] diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 9c2004f0edb..ec543a9ed30 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,10 +1,9 @@ """Support for tracking for iCloud devices.""" from __future__ import annotations -from collections.abc import Awaitable, Callable from typing import Any -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS, AsyncSeeCallback from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -25,7 +24,7 @@ from .const import ( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Old way of setting up the iCloud tracker.""" diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 86c9ecf95fb..7447d8ce879 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -1,7 +1,6 @@ """Support for the Meraki CMX location service.""" from __future__ import annotations -from collections.abc import Awaitable, Callable from http import HTTPStatus import json import logging @@ -11,6 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER, + AsyncSeeCallback, ) from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback @@ -34,7 +34,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up an endpoint for the Meraki tracker.""" @@ -50,7 +50,7 @@ class MerakiView(HomeAssistantView): name = "api:meraki" requires_auth = False - def __init__(self, config, async_see): + def __init__(self, config: ConfigType, async_see: AsyncSeeCallback) -> None: """Initialize Meraki URL endpoints.""" self.async_see = async_see self.validator = config[CONF_VALIDATOR] diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 1d99e6d7b6f..f0330e39e86 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -1,7 +1,6 @@ """Support for GPS tracking MQTT enabled devices.""" from __future__ import annotations -from collections.abc import Awaitable, Callable import json import logging @@ -10,6 +9,7 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + AsyncSeeCallback, ) from homeassistant.components.mqtt import CONF_QOS from homeassistant.const import ( @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the MQTT JSON tracker.""" diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index d5332ab70bf..3c204776b7d 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -1,10 +1,10 @@ """Support for tracking MySensors devices.""" from __future__ import annotations -from collections.abc import Awaitable, Callable from typing import Any, cast from homeassistant.components import mysensors +from homeassistant.components.device_tracker import AsyncSeeCallback from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -18,7 +18,7 @@ from .helpers import on_unload async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the MySensors device scanner.""" @@ -63,7 +63,12 @@ async def async_setup_scanner( class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, hass: HomeAssistant, async_see: Callable, *args: Any) -> None: + def __init__( + self, + hass: HomeAssistant, + async_see: AsyncSeeCallback, + *args: Any, + ) -> None: """Set up instance.""" super().__init__(*args) self.async_see = async_see diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 0a0e397e6d8..cbce224a373 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable from datetime import timedelta import logging import subprocess @@ -13,6 +12,7 @@ import voluptuous as vol from homeassistant import const, util from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + AsyncSeeCallback, ) from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, @@ -83,7 +83,7 @@ class HostSubProcess: async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Host objects and return the update function.""" diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 61e9a1bdcd9..492c202df08 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -1,11 +1,11 @@ """Support for Tile device trackers.""" from __future__ import annotations -from collections.abc import Awaitable, Callable import logging from pytile.tile import Tile +from homeassistant.components.device_tracker import AsyncSeeCallback from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -52,7 +52,7 @@ async def async_setup_entry( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Detect a legacy configuration and import it.""" diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 970cd20d640..f9676d37aa2 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging @@ -21,6 +20,7 @@ from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SOURCE_TYPE_GPS, + AsyncSeeCallback, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry @@ -174,7 +174,7 @@ async def async_setup_entry( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Validate the configuration and return a Traccar scanner.""" @@ -208,7 +208,7 @@ class TraccarScanner: self, api: ApiClient, hass: HomeAssistant, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, scan_interval: timedelta, max_accuracy: int, skip_accuracy_on: bool, diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index 74a88fb69ab..866634fc5e1 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -1,9 +1,7 @@ """Support for tracking a Volvo.""" from __future__ import annotations -from collections.abc import Awaitable, Callable - -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS, AsyncSeeCallback from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -15,7 +13,7 @@ from . import DATA_KEY, SIGNAL_STATE_UPDATED async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Volvo tracker.""" @@ -26,7 +24,7 @@ async def async_setup_scanner( data = hass.data[DATA_KEY] instrument = data.instrument(vin, component, attr, slug_attr) - async def see_vehicle(): + async def see_vehicle() -> None: """Handle the reporting of the vehicle position.""" host_name = instrument.vehicle_name dev_id = f"volvo_{slugify(host_name)}" diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 680d25414aa..3b50c072eb6 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -293,7 +293,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { arg_types={ 0: "HomeAssistant", 1: "ConfigType", - 2: "Callable[..., None]", + 2: "SeeCallback", 3: "DiscoveryInfoType | None", }, return_type="bool", @@ -303,7 +303,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { arg_types={ 0: "HomeAssistant", 1: "ConfigType", - 2: "Callable[..., Awaitable[None]]", + 2: "AsyncSeeCallback", 3: "DiscoveryInfoType | None", }, return_type="bool", diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index e549d21fe0f..54824e5c0b0 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -201,7 +201,7 @@ def test_invalid_discovery_info( async def async_setup_scanner( #@ hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: dict[str, Any] | None = None, #@ ) -> bool: pass @@ -234,7 +234,7 @@ def test_valid_discovery_info( async def async_setup_scanner( #@ hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: pass From a1d96175a891728dc16dc7cad3d950cef96771d3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 29 Jul 2022 00:25:31 +0000 Subject: [PATCH 031/903] [ci skip] Translation update --- .../components/ambee/translations/ca.json | 6 ++++++ .../components/ambee/translations/it.json | 2 +- .../components/anthemav/translations/ca.json | 6 ++++++ .../components/anthemav/translations/it.json | 4 ++-- .../anthemav/translations/zh-Hant.json | 2 +- .../components/google/translations/ca.json | 10 ++++++++++ .../components/google/translations/it.json | 6 +++--- .../google/translations/zh-Hant.json | 2 +- .../homeassistant_alerts/translations/ca.json | 8 ++++++++ .../lacrosse_view/translations/ca.json | 20 +++++++++++++++++++ .../components/lyric/translations/ca.json | 6 ++++++ .../components/lyric/translations/it.json | 2 +- .../components/miflora/translations/ca.json | 8 ++++++++ .../components/miflora/translations/it.json | 2 +- .../components/mitemp_bt/translations/ca.json | 8 ++++++++ .../components/mitemp_bt/translations/it.json | 2 +- .../openalpr_local/translations/ca.json | 8 ++++++++ .../openalpr_local/translations/it.json | 2 +- .../radiotherm/translations/ca.json | 2 +- .../radiotherm/translations/it.json | 4 ++-- .../radiotherm/translations/zh-Hant.json | 2 +- .../components/senz/translations/ca.json | 6 ++++++ .../components/senz/translations/it.json | 2 +- .../simplepush/translations/ca.json | 6 ++++++ .../simplepush/translations/de.json | 6 ++++++ .../simplepush/translations/en.json | 4 ++-- .../simplepush/translations/it.json | 6 ++++++ .../simplepush/translations/pt-BR.json | 6 ++++++ .../simplepush/translations/zh-Hant.json | 6 ++++++ .../soundtouch/translations/ca.json | 6 ++++++ .../soundtouch/translations/it.json | 4 ++-- .../soundtouch/translations/zh-Hant.json | 2 +- .../components/spotify/translations/ca.json | 6 ++++++ .../components/spotify/translations/it.json | 2 +- .../steam_online/translations/ca.json | 6 ++++++ .../steam_online/translations/it.json | 2 +- .../components/uscis/translations/ca.json | 2 +- .../components/xbox/translations/ca.json | 6 ++++++ .../components/xbox/translations/de.json | 6 ++++++ .../components/xbox/translations/it.json | 4 ++-- .../components/xbox/translations/zh-Hant.json | 6 ++++++ .../xiaomi_ble/translations/it.json | 2 +- .../components/zha/translations/ca.json | 2 ++ .../components/zha/translations/it.json | 1 + 44 files changed, 183 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/ca.json create mode 100644 homeassistant/components/lacrosse_view/translations/ca.json create mode 100644 homeassistant/components/miflora/translations/ca.json create mode 100644 homeassistant/components/mitemp_bt/translations/ca.json create mode 100644 homeassistant/components/openalpr_local/translations/ca.json diff --git a/homeassistant/components/ambee/translations/ca.json b/homeassistant/components/ambee/translations/ca.json index ac48eea1cd6..bb4d49642b5 100644 --- a/homeassistant/components/ambee/translations/ca.json +++ b/homeassistant/components/ambee/translations/ca.json @@ -24,5 +24,11 @@ "description": "Configura la integraci\u00f3 d'Ambee amb Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "La integraci\u00f3 d'Ambee s'eliminar\u00e0 de Home Assistant i deixar\u00e0 d'estar disponible a la versi\u00f3 de Home Assistant 2022.10.\n\nLa integraci\u00f3 s'eliminar\u00e0 perqu\u00e8 Ambee ha eliminat els seus comptes gratu\u00efts (limitats) i no ha donat cap manera per als usuaris normals de registrar-se a un pla de pagament.\n\nElimina la integraci\u00f3 d'Ambee del Home Assistant per solucionar aquest problema.", + "title": "La integraci\u00f3 Ambee est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json index db330a9b239..f2054c8a6ff 100644 --- a/homeassistant/components/ambee/translations/it.json +++ b/homeassistant/components/ambee/translations/it.json @@ -28,7 +28,7 @@ "issues": { "pending_removal": { "description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.", - "title": "L'integrazione Ambee verr\u00e0 rimossa" + "title": "L'integrazione Ambee sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ca.json b/homeassistant/components/anthemav/translations/ca.json index 723883d5c1a..20785a9e67d 100644 --- a/homeassistant/components/anthemav/translations/ca.json +++ b/homeassistant/components/anthemav/translations/ca.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 d'Anthem A/V Receivers mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'Anthem A/V Receivers del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'Anthem A/V Receivers est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json index b8bec832581..847332b57ad 100644 --- a/homeassistant/components/anthemav/translations/it.json +++ b/homeassistant/components/anthemav/translations/it.json @@ -18,8 +18,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Anthem A/V Receivers tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Anthem A/V Receivers verr\u00e0 rimossa" + "description": "La configurazione di Anthem A/V Receivers tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Anthem A/V Receivers sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json index 0751331f82f..acba04f49e6 100644 --- a/homeassistant/components/anthemav/translations/zh-Hant.json +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } diff --git a/homeassistant/components/google/translations/ca.json b/homeassistant/components/google/translations/ca.json index 066630df50d..b7645abe54b 100644 --- a/homeassistant/components/google/translations/ca.json +++ b/homeassistant/components/google/translations/ca.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Google Calentdar mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent de credencials d'aplicaci\u00f3 OAuth i d'acc\u00e9s s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Google Calendar est\u00e0 sent eliminada" + }, + "removed_track_new_yaml": { + "description": "Has desactivat el seguiment d'entitats de Google Calendar a configuration.yaml, que ja no \u00e9s compatible. Per desactivar les entitats descobertes recentment, a partir d'ara, has de canviar manualment les opcions de sistema de la integraci\u00f3 a trav\u00e9s de la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 'track_new' de configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "El seguiment d'entitats de Google Calendar ha canviat" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index 6e24178996f..282c1d06544 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -35,11 +35,11 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Google Calendar verr\u00e0 rimossa" + "description": "La configurazione di Google Calendar in configuration.yaml sar\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Google Calendar sar\u00e0 rimossa" }, "removed_track_new_yaml": { - "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le nuove entit\u00e0 rilevate in futuro. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "Il tracciamento dell'entit\u00e0 di Google Calendar \u00e8 cambiato" } }, diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 0d2031c368f..93e2fa8f7ba 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -35,7 +35,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" }, "removed_track_new_yaml": { diff --git a/homeassistant/components/homeassistant_alerts/translations/ca.json b/homeassistant/components/homeassistant_alerts/translations/ca.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/ca.json b/homeassistant/components/lacrosse_view/translations/ca.json new file mode 100644 index 00000000000..bdf5e41bf54 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_locations": "No s'han trobat ubicacions", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ca.json b/homeassistant/components/lyric/translations/ca.json index 3e301a4bf4b..47f4fdb559b 100644 --- a/homeassistant/components/lyric/translations/ca.json +++ b/homeassistant/components/lyric/translations/ca.json @@ -17,5 +17,11 @@ "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } + }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de Honeywell Lyric mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Honeywell Lyric s'ha eliminat" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json index e0e97ef3246..57c201e952d 100644 --- a/homeassistant/components/lyric/translations/it.json +++ b/homeassistant/components/lyric/translations/it.json @@ -20,7 +20,7 @@ }, "issues": { "removed_yaml": { - "description": "La configurazione di Honeywell Lyric tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "La configurazione di Honeywell Lyric tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Honeywell Lyric \u00e8 stata rimossa" } } diff --git a/homeassistant/components/miflora/translations/ca.json b/homeassistant/components/miflora/translations/ca.json new file mode 100644 index 00000000000..c4dd9209a00 --- /dev/null +++ b/homeassistant/components/miflora/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "La integraci\u00f3 Mi Flora ha deixar de funcionar a Home Assistant 2022.7 i ha estat substitu\u00efda per la integraci\u00f3 Xiaomi BLE a la versi\u00f3 Home Assistant 2022.8.\n\nNo hi ha cap migraci\u00f3 possible, per tant, has d'afegir els teus dispositius Mi Flora manualment utilitzant la nova integraci\u00f3.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML de l'antiga integraci\u00f3. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La integraci\u00f3 de Mi Flora ha estat substitu\u00efda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/it.json b/homeassistant/components/miflora/translations/it.json index cdd2d89ca72..b5893f33d43 100644 --- a/homeassistant/components/miflora/translations/it.json +++ b/homeassistant/components/miflora/translations/it.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "L'integrazione Mi Flora ha smesso di funzionare in Home Assistant 2022.7 e sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Mi Flora utilizzando la nuova integrazione. \n\nLa configurazione YAML di Mi Flora esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "L'integrazione Mi Flora ha smesso di funzionare in Home Assistant 2022.7 e sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Mi Flora utilizzando la nuova integrazione. \n\nLa configurazione YAML di Mi Flora esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione Mi Flora \u00e8 stata sostituita" } } diff --git a/homeassistant/components/mitemp_bt/translations/ca.json b/homeassistant/components/mitemp_bt/translations/ca.json new file mode 100644 index 00000000000..a567bceb950 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE va deixar de funcionar a Home Assistant 2022.7 i ha estat substitu\u00efda per la integraci\u00f3 Xiaomi BLE a la versi\u00f3 Home Assistant 2022.8.\n\nNo hi ha cap migraci\u00f3 possible, per tant, has d'afegir els teus dispositius Xiaomi Mijia BLE manualment utilitzant la nova integraci\u00f3.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML de l'antiga integraci\u00f3. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE ha estat substitu\u00efda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/it.json b/homeassistant/components/mitemp_bt/translations/it.json index cc383e4184c..0fe7ab58919 100644 --- a/homeassistant/components/mitemp_bt/translations/it.json +++ b/homeassistant/components/mitemp_bt/translations/it.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor \u00e8 stata sostituita" } } diff --git a/homeassistant/components/openalpr_local/translations/ca.json b/homeassistant/components/openalpr_local/translations/ca.json new file mode 100644 index 00000000000..3617117ac4a --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3 d'OpenALPR Local s'eliminar\u00e0 de Home Assistant i deixar\u00e0 d'estar disponible a la versi\u00f3 de Home Assistant 2022.10.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", + "title": "La integraci\u00f3 OpenALPR Local est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/it.json b/homeassistant/components/openalpr_local/translations/it.json index 26ce80ee584..d4227ca7e36 100644 --- a/homeassistant/components/openalpr_local/translations/it.json +++ b/homeassistant/components/openalpr_local/translations/it.json @@ -2,7 +2,7 @@ "issues": { "pending_removal": { "description": "L'integrazione OpenALPR Local \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "L'integrazione OpenALPR Local verr\u00e0 rimossa" + "title": "L'integrazione OpenALPR Local sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ca.json b/homeassistant/components/radiotherm/translations/ca.json index 58e8487607f..b362fc467ec 100644 --- a/homeassistant/components/radiotherm/translations/ca.json +++ b/homeassistant/components/radiotherm/translations/ca.json @@ -22,7 +22,7 @@ "issues": { "deprecated_yaml": { "description": "La configuraci\u00f3 de la plataforma 'Radio Thermostat' mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "S'est\u00e0 eliminant la configuraci\u00f3 YAML de Thermostat Radio" + "title": "La configuraci\u00f3 YAML de Thermostat Radio est\u00e0 sent eliminada" } }, "options": { diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index fef1c64746b..58860ab3293 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -21,8 +21,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Radio Thermostat verr\u00e0 rimossa" + "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML sar\u00e0 rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Radio Thermostat sar\u00e0 rimossa" } }, "options": { diff --git a/homeassistant/components/radiotherm/translations/zh-Hant.json b/homeassistant/components/radiotherm/translations/zh-Hant.json index ad1af3bb442..96949adb129 100644 --- a/homeassistant/components/radiotherm/translations/zh-Hant.json +++ b/homeassistant/components/radiotherm/translations/zh-Hant.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Radio \u6eab\u63a7\u5668\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Radio \u6eab\u63a7\u5668\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Radio \u6eab\u63a7\u5668 YAML \u8a2d\u5b9a\u5df2\u79fb\u9664" } }, diff --git a/homeassistant/components/senz/translations/ca.json b/homeassistant/components/senz/translations/ca.json index 20b2ceceddd..964e0319367 100644 --- a/homeassistant/components/senz/translations/ca.json +++ b/homeassistant/components/senz/translations/ca.json @@ -16,5 +16,11 @@ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" } } + }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de nVent RAYCHEM SENZ mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de nVent RAYCHEM SENZ s'ha eliminat" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/it.json b/homeassistant/components/senz/translations/it.json index be6608af499..d3ec2a1568c 100644 --- a/homeassistant/components/senz/translations/it.json +++ b/homeassistant/components/senz/translations/it.json @@ -19,7 +19,7 @@ }, "issues": { "removed_yaml": { - "description": "La configurazione di nVent RAYCHEM SENZ tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "La configurazione di nVent RAYCHEM SENZ tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di nVent RAYCHEM SENZ \u00e8 stata rimossa" } } diff --git a/homeassistant/components/simplepush/translations/ca.json b/homeassistant/components/simplepush/translations/ca.json index 161e1a3c36c..d4e449d8a35 100644 --- a/homeassistant/components/simplepush/translations/ca.json +++ b/homeassistant/components/simplepush/translations/ca.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Simplepush mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Simplepush del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Simplepush est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/de.json b/homeassistant/components/simplepush/translations/de.json index c7f633d312d..523ffda32bf 100644 --- a/homeassistant/components/simplepush/translations/de.json +++ b/homeassistant/components/simplepush/translations/de.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Simplepush mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Simplepush-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Simplepush YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index bf373d8baf0..8674616dda1 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -20,8 +20,8 @@ }, "issues": { "deprecated_yaml": { - "title": "The Simplepush YAML configuration is being removed", - "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Simplepush YAML configuration is being removed" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/it.json b/homeassistant/components/simplepush/translations/it.json index 2ba1bea5d96..b3a9b44f938 100644 --- a/homeassistant/components/simplepush/translations/it.json +++ b/homeassistant/components/simplepush/translations/it.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Simplepush tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Simplepush dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Simplepush sar\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pt-BR.json b/homeassistant/components/simplepush/translations/pt-BR.json index bf933fe94da..f0a330ff1d3 100644 --- a/homeassistant/components/simplepush/translations/pt-BR.json +++ b/homeassistant/components/simplepush/translations/pt-BR.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Simplepush usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Simplepush YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Simplepush YAML est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/zh-Hant.json b/homeassistant/components/simplepush/translations/zh-Hant.json index 891f2242467..15cd0bedb37 100644 --- a/homeassistant/components/simplepush/translations/zh-Hant.json +++ b/homeassistant/components/simplepush/translations/zh-Hant.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Simplepush \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Simplepush YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ca.json b/homeassistant/components/soundtouch/translations/ca.json index baba19644fb..165668e0016 100644 --- a/homeassistant/components/soundtouch/translations/ca.json +++ b/homeassistant/components/soundtouch/translations/ca.json @@ -17,5 +17,11 @@ "title": "Confirma l'addici\u00f3 del dispositiu Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Bose SoundTouch mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Bose SoundTouch del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Bose SoundTouch est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/it.json b/homeassistant/components/soundtouch/translations/it.json index f9c6d512b2a..11aaba04caf 100644 --- a/homeassistant/components/soundtouch/translations/it.json +++ b/homeassistant/components/soundtouch/translations/it.json @@ -20,8 +20,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Bose SoundTouch tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Bose SoundTouch dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Bose SoundTouch verr\u00e0 rimossa" + "description": "La configurazione di Bose SoundTouch tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Bose SoundTouch dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Bose SoundTouch sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json index f3d8e8e8560..80d78ba9d20 100644 --- a/homeassistant/components/soundtouch/translations/zh-Hant.json +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Bose SoundTouch YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } diff --git a/homeassistant/components/spotify/translations/ca.json b/homeassistant/components/spotify/translations/ca.json index fffb248573d..0bde1dc49e3 100644 --- a/homeassistant/components/spotify/translations/ca.json +++ b/homeassistant/components/spotify/translations/ca.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de Spotify mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "S'ha eliminat la configuraci\u00f3 YAML de Spotify" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Endpoint de l'API d'Spotify accessible" diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index 2ca8d323607..2324e80c8ca 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -21,7 +21,7 @@ }, "issues": { "removed_yaml": { - "description": "La configurazione di Spotify tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "La configurazione di Spotify tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Spotify \u00e8 stata rimossa" } }, diff --git a/homeassistant/components/steam_online/translations/ca.json b/homeassistant/components/steam_online/translations/ca.json index a9491e2e502..bd995f8d0e2 100644 --- a/homeassistant/components/steam_online/translations/ca.json +++ b/homeassistant/components/steam_online/translations/ca.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de Steam mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Steam s'ha eliminat" + } + }, "options": { "error": { "unauthorized": "Llista d'amics restringida: consulta la documentaci\u00f3 sobre com veure tots els amics" diff --git a/homeassistant/components/steam_online/translations/it.json b/homeassistant/components/steam_online/translations/it.json index fb962124f2a..5d7712c7072 100644 --- a/homeassistant/components/steam_online/translations/it.json +++ b/homeassistant/components/steam_online/translations/it.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "La configurazione di Steam tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "La configurazione di Steam tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Steam \u00e8 stata rimossa" } }, diff --git a/homeassistant/components/uscis/translations/ca.json b/homeassistant/components/uscis/translations/ca.json index 2072cfb0d92..858214eee34 100644 --- a/homeassistant/components/uscis/translations/ca.json +++ b/homeassistant/components/uscis/translations/ca.json @@ -2,7 +2,7 @@ "issues": { "pending_removal": { "description": "La integraci\u00f3 de Serveis de Ciutadania i Immigraci\u00f3 dels Estats Units (USCIS) est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 es basa en el 'webscraping', que no est\u00e0 adm\u00e8s. \n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", - "title": "S'est\u00e0 eliminant la integraci\u00f3 USCIS" + "title": "La integraci\u00f3 USCIS est\u00e0 sent eliminada" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ca.json b/homeassistant/components/xbox/translations/ca.json index e0fe185973b..56f3a7b3440 100644 --- a/homeassistant/components/xbox/translations/ca.json +++ b/homeassistant/components/xbox/translations/ca.json @@ -13,5 +13,11 @@ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Xbox mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent de credencials d'aplicaci\u00f3 OAuth i d'acc\u00e9s s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Xbox est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/de.json b/homeassistant/components/xbox/translations/de.json index 615c8f8cf2a..e42cf1d37dc 100644 --- a/homeassistant/components/xbox/translations/de.json +++ b/homeassistant/components/xbox/translations/de.json @@ -13,5 +13,11 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration der Xbox in configuration.yaml wird in Home Assistant 2022.9 entfernt.\n\nDeine bestehenden OAuth-Anmeldedaten und Zugriffseinstellungen wurden automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Xbox YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/it.json b/homeassistant/components/xbox/translations/it.json index 6cf5bf15bb9..b1bb503bae0 100644 --- a/homeassistant/components/xbox/translations/it.json +++ b/homeassistant/components/xbox/translations/it.json @@ -16,8 +16,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Xbox in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Xbox verr\u00e0 rimossa" + "description": "La configurazione di Xbox in configuration.yaml sar\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Xbox sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/zh-Hant.json b/homeassistant/components/xbox/translations/zh-Hant.json index 9d348536ec3..4a44e9bc233 100644 --- a/homeassistant/components/xbox/translations/zh-Hant.json +++ b/homeassistant/components/xbox/translations/zh-Hant.json @@ -13,5 +13,11 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Xbox \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Xbox YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/it.json b/homeassistant/components/xiaomi_ble/translations/it.json index 99adacee466..018829bfbd2 100644 --- a/homeassistant/components/xiaomi_ble/translations/it.json +++ b/homeassistant/components/xiaomi_ble/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controllare e riprovare.", + "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controlla e riprova.", "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri.", "no_devices_found": "Nessun dispositivo trovato sulla rete" diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 4167704e4a0..dbb8a20be82 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -46,11 +46,13 @@ "title": "Opcions del panell de control d'alarma" }, "zha_options": { + "always_prefer_xy_color_mode": "Utilitza preferentment el mode de color XY", "consider_unavailable_battery": "Considera els dispositius amb bateria com a no disponibles al cap de (segons)", "consider_unavailable_mains": "Considera els dispositius connectats a la xarxa el\u00e8ctrica com a no disponibles al cap de (segons)", "default_light_transition": "Temps de transici\u00f3 predeterminat (segons)", "enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa", "enhanced_light_transition": "Activa la transici\u00f3 millorada de color/temperatura de llum des de l'estat apagat", + "light_transitioning_flag": "Activa el control lliscant de brillantor millorat durant la transici\u00f3", "title": "Opcions globals" } }, diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 4666ba7e494..a71e96ed308 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -52,6 +52,7 @@ "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", "enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete", "enhanced_light_transition": "Abilita una transizione migliorata del colore/temperatura della luce da uno stato spento", + "light_transitioning_flag": "Abilita il cursore della luminosit\u00e0 avanzata durante la transizione della luce", "title": "Opzioni globali" } }, From bbd7041a73572547be49ead53b183aa1e55a6d75 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Fri, 29 Jul 2022 13:20:05 +1200 Subject: [PATCH 032/903] Refactor and improve anthemav (#75852) --- homeassistant/components/anthemav/__init__.py | 14 ++++- .../components/anthemav/media_player.py | 21 ++++--- tests/components/anthemav/conftest.py | 22 +++++++ tests/components/anthemav/test_config_flow.py | 36 +++++++++-- tests/components/anthemav/test_init.py | 61 +++++++++++-------- 5 files changed, 112 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index 5ad845b52d6..eb1d9b0b560 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -6,8 +6,8 @@ import logging import anthemav from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def async_anthemav_update_callback(message): """Receive notification from transport that new data exists.""" _LOGGER.debug("Received update callback from AVR: %s", message) - async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.data[CONF_NAME]}") + async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.entry_id}") try: avr = await anthemav.Connection.create( @@ -41,6 +41,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + @callback + def close_avr(event: Event) -> None: + avr.close() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_avr) + ) + return True diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 3c3a363a6db..bf8172083e6 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -85,17 +85,17 @@ async def async_setup_entry( ) -> None: """Set up entry.""" name = config_entry.data[CONF_NAME] - macaddress = config_entry.data[CONF_MAC] + mac_address = config_entry.data[CONF_MAC] model = config_entry.data[CONF_MODEL] avr = hass.data[DOMAIN][config_entry.entry_id] - device = AnthemAVR(avr, name, macaddress, model) + entity = AnthemAVR(avr, name, mac_address, model, config_entry.entry_id) - _LOGGER.debug("dump_devicedata: %s", device.dump_avrdata) - _LOGGER.debug("dump_conndata: %s", avr.dump_conndata) + _LOGGER.debug("Device data dump: %s", entity.dump_avrdata) + _LOGGER.debug("Connection data dump: %s", avr.dump_conndata) - async_add_entities([device]) + async_add_entities([entity]) class AnthemAVR(MediaPlayerEntity): @@ -110,14 +110,17 @@ class AnthemAVR(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, avr: Connection, name: str, macaddress: str, model: str) -> None: + def __init__( + self, avr: Connection, name: str, mac_address: str, model: str, entry_id: str + ) -> None: """Initialize entity with transport.""" super().__init__() self.avr = avr + self._entry_id = entry_id self._attr_name = name - self._attr_unique_id = macaddress + self._attr_unique_id = mac_address self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, macaddress)}, + identifiers={(DOMAIN, mac_address)}, name=name, manufacturer=MANUFACTURER, model=model, @@ -131,7 +134,7 @@ class AnthemAVR(MediaPlayerEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{ANTHEMAV_UDATE_SIGNAL}_{self._attr_name}", + f"{ANTHEMAV_UDATE_SIGNAL}_{self._entry_id}", self.async_write_ha_state, ) ) diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py index 8fbdf3145c3..f96696fe308 100644 --- a/tests/components/anthemav/conftest.py +++ b/tests/components/anthemav/conftest.py @@ -3,6 +3,11 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT + +from tests.common import MockConfigEntry + @pytest.fixture def mock_anthemav() -> AsyncMock: @@ -14,6 +19,7 @@ def mock_anthemav() -> AsyncMock: avr.close = MagicMock() avr.protocol.input_list = [] avr.protocol.audio_listening_mode_list = [] + avr.protocol.power = False return avr @@ -26,3 +32,19 @@ def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock: return_value=mock_anthemav, ) as mock: yield mock + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 14999, + CONF_NAME: "Anthem AV", + CONF_MAC: "00:00:00:00:00:01", + CONF_MODEL: "MRX 520", + }, + unique_id="00:00:00:00:00:01", + ) diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py index 1f3dec8d5e1..f8bec435dc6 100644 --- a/tests/components/anthemav/test_config_flow.py +++ b/tests/components/anthemav/test_config_flow.py @@ -2,19 +2,22 @@ from unittest.mock import AsyncMock, patch from anthemav.device_error import DeviceError +import pytest -from homeassistant import config_entries from homeassistant.components.anthemav.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + async def test_form_with_valid_connection( hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["errors"] is None @@ -47,7 +50,7 @@ async def test_form_with_valid_connection( async def test_form_device_info_error(hass: HomeAssistant) -> None: """Test we handle DeviceError from library.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) with patch( @@ -71,7 +74,7 @@ async def test_form_device_info_error(hass: HomeAssistant) -> None: async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) with patch( @@ -102,7 +105,7 @@ async def test_import_configuration( "name": "Anthem Av Import", } result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) assert result["type"] == FlowResultType.CREATE_ENTRY @@ -113,3 +116,26 @@ async def test_import_configuration( "mac": "00:00:00:00:00:01", "model": "MRX 520", } + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_device_already_configured( + hass: HomeAssistant, + mock_connection_create: AsyncMock, + mock_anthemav: AsyncMock, + mock_config_entry: MockConfigEntry, + source: str, +) -> None: + """Test we import existing configuration.""" + config = { + "host": "1.1.1.1", + "port": 14999, + } + + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=config + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 866925f4e46..97dc5be95b0 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -2,28 +2,19 @@ from unittest.mock import ANY, AsyncMock, patch from homeassistant import config_entries -from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry async def test_load_unload_config_entry( - hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock + hass: HomeAssistant, + mock_connection_create: AsyncMock, + mock_anthemav: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test load and unload AnthemAv component.""" - mock_config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 14999, - CONF_NAME: "Anthem AV", - CONF_MAC: "aabbccddeeff", - CONF_MODEL: "MRX 520", - }, - ) - mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -42,18 +33,10 @@ async def test_load_unload_config_entry( mock_anthemav.close.assert_called_once() -async def test_config_entry_not_ready(hass: HomeAssistant) -> None: +async def test_config_entry_not_ready( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test AnthemAV configuration entry not ready.""" - mock_config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 14999, - CONF_NAME: "Anthem AV", - CONF_MAC: "aabbccddeeff", - CONF_MODEL: "MRX 520", - }, - ) with patch( "anthemav.Connection.create", @@ -63,3 +46,31 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY + + +async def test_anthemav_dispatcher_signal( + hass: HomeAssistant, + mock_connection_create: AsyncMock, + mock_anthemav: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test send update signal to dispatcher.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + states = hass.states.get("media_player.anthem_av") + assert states + assert states.state == STATE_OFF + + # change state of the AVR + mock_anthemav.protocol.power = True + + # get the callback function that trigger the signal to update the state + avr_update_callback = mock_connection_create.call_args[1]["update_callback"] + avr_update_callback("power") + + await hass.async_block_till_done() + + states = hass.states.get("media_player.anthem_av") + assert states.state == STATE_ON From 9f16c1468103ed145dfde3e08a2b464e70543a25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jul 2022 17:07:32 -1000 Subject: [PATCH 033/903] Fix incorrect manufacturer_id for govee 5182 model (#75899) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index aa86215da59..270858d04d4 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -18,9 +18,13 @@ { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.3"], + "requirements": ["govee-ble==0.12.4"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 5dde90f1f7a..8d92d6eab4a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -34,6 +34,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" + }, { "domain": "homekit_controller", "manufacturer_id": 76, diff --git a/requirements_all.txt b/requirements_all.txt index 5094f5a3241..b3879d922db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.3 +govee-ble==0.12.4 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8b8c5be6b7..0a81c1cb8d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.3 +govee-ble==0.12.4 # homeassistant.components.gree greeclimate==1.2.0 From 4b2beda4731c6fcbb3025a24e1412f6b460100cc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Jul 2022 22:26:37 -0700 Subject: [PATCH 034/903] Move some bleak imports to be behind TYPE_CHECKING (#75894) --- .../components/bluetooth_le_tracker/device_tracker.py | 5 ++++- homeassistant/components/fjaraskupan/__init__.py | 8 ++++++-- homeassistant/components/switchbot/coordinator.py | 10 ++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index a650e65b8f2..fa55c22f994 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -4,10 +4,10 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta import logging +from typing import TYPE_CHECKING from uuid import UUID from bleak import BleakClient, BleakError -from bleak.backends.device import BLEDevice import voluptuous as vol from homeassistant.components import bluetooth @@ -31,6 +31,9 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + _LOGGER = logging.getLogger(__name__) # Base UUID: 00000000-0000-1000-8000-00805F9B34FB diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 85e95db5513..36608fb026d 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -5,10 +5,9 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging +from typing import TYPE_CHECKING from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry @@ -24,6 +23,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DISPATCH_DETECTION, DOMAIN +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + PLATFORMS = [ Platform.BINARY_SENSOR, Platform.FAN, diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 31f7f2d3992..f461a3e0f4c 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -3,11 +3,9 @@ from __future__ import annotations import asyncio import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast -from bleak.backends.device import BLEDevice import switchbot -from switchbot import parse_advertisement_data from homeassistant.components import bluetooth from homeassistant.components.bluetooth.passive_update_coordinator import ( @@ -15,6 +13,10 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( ) from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + _LOGGER = logging.getLogger(__name__) @@ -52,7 +54,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): """Handle a Bluetooth event.""" super()._async_handle_bluetooth_event(service_info, change) discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) - if adv := parse_advertisement_data( + if adv := switchbot.parse_advertisement_data( discovery_info_bleak.device, discovery_info_bleak.advertisement ): self.data = flatten_sensors_data(adv.data) From aec885a46704baa71537e30d01e4efe52af9faa5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jul 2022 00:53:08 -0700 Subject: [PATCH 035/903] Fix Roon media player being set up before hass.data set up (#75904) --- homeassistant/components/roon/__init__.py | 11 ++++++++++- homeassistant/components/roon/server.py | 9 +-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index 9e5c38f0211..9969b694895 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -1,12 +1,14 @@ """Roon (www.roonlabs.com) component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .const import CONF_ROON_NAME, DOMAIN from .server import RoonServer +PLATFORMS = [Platform.MEDIA_PLAYER] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a roonserver from a config entry.""" @@ -28,10 +30,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manufacturer="Roonlabs", name=f"Roon Core ({name})", ) + + # initialize media_player platform + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + return False + roonserver = hass.data[DOMAIN].pop(entry.entry_id) return await roonserver.async_reset() diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index df9dec3d9af..997db44583d 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -4,7 +4,7 @@ import logging from roonapi import RoonApi, RoonDiscovery -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, Platform +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.dt import utcnow @@ -13,7 +13,6 @@ from .const import CONF_ROON_ID, ROON_APPINFO _LOGGER = logging.getLogger(__name__) INITIAL_SYNC_INTERVAL = 5 FULL_SYNC_INTERVAL = 30 -PLATFORMS = [Platform.MEDIA_PLAYER] class RoonServer: @@ -53,7 +52,6 @@ class RoonServer: (host, port) = get_roon_host() return RoonApi(ROON_APPINFO, token, host, port, blocking_init=True) - hass = self.hass core_id = self.config_entry.data.get(CONF_ROON_ID) self.roonapi = await self.hass.async_add_executor_job(get_roon_api) @@ -67,11 +65,6 @@ class RoonServer: core_id if core_id is not None else self.config_entry.data[CONF_HOST] ) - # initialize media_player platform - await hass.config_entries.async_forward_entry_setups( - self.config_entry, PLATFORMS - ) - # Initialize Roon background polling asyncio.create_task(self.async_do_loop()) From 08b169a7adf5c5500c5aca806652d238265a2aaf Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 29 Jul 2022 11:57:19 +0200 Subject: [PATCH 036/903] Fix broken Yale lock (#75918) Yale fix lock --- homeassistant/components/yale_smart_alarm/lock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index a97a98a2afb..8f9ed6c9ce1 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -46,6 +46,7 @@ class YaleDoorlock(YaleEntity, LockEntity): """Initialize the Yale Lock Device.""" super().__init__(coordinator, data) self._attr_code_format = f"^\\d{code_format}$" + self.lock_name = data["name"] async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" @@ -65,7 +66,7 @@ class YaleDoorlock(YaleEntity, LockEntity): try: get_lock = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.get, self._attr_name + self.coordinator.yale.lock_api.get, self.lock_name ) if command == "locked": lock_state = await self.hass.async_add_executor_job( From ab5dfb3c42426c184afae3c405c58817bb312076 Mon Sep 17 00:00:00 2001 From: Nephiel Date: Fri, 29 Jul 2022 12:08:32 +0200 Subject: [PATCH 037/903] Use climate enums in google_assistant (#75888) --- .../components/google_assistant/trait.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index ee41dd0c678..edc8ed124b3 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -864,13 +864,13 @@ class TemperatureSettingTrait(_Trait): # We do not support "on" as we are unable to know how to restore # the last mode. hvac_to_google = { - climate.HVAC_MODE_HEAT: "heat", - climate.HVAC_MODE_COOL: "cool", - climate.HVAC_MODE_OFF: "off", - climate.HVAC_MODE_AUTO: "auto", - climate.HVAC_MODE_HEAT_COOL: "heatcool", - climate.HVAC_MODE_FAN_ONLY: "fan-only", - climate.HVAC_MODE_DRY: "dry", + climate.HVACMode.HEAT: "heat", + climate.HVACMode.COOL: "cool", + climate.HVACMode.OFF: "off", + climate.HVACMode.AUTO: "auto", + climate.HVACMode.HEAT_COOL: "heatcool", + climate.HVACMode.FAN_ONLY: "fan-only", + climate.HVACMode.DRY: "dry", } google_to_hvac = {value: key for key, value in hvac_to_google.items()} @@ -949,7 +949,7 @@ class TemperatureSettingTrait(_Trait): if current_humidity is not None: response["thermostatHumidityAmbient"] = current_humidity - if operation in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): + if operation in (climate.HVACMode.AUTO, climate.HVACMode.HEAT_COOL): if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: response["thermostatTemperatureSetpointHigh"] = round( temp_util.convert( From 2b1e1365fdb3bfe72feb515fcf2e02331caa4088 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Jul 2022 13:09:03 +0200 Subject: [PATCH 038/903] Add StrEnum for device_tracker `SourceType` (#75892) Add StrEnum for device_tracker SourceType --- .../components/device_tracker/__init__.py | 1 + .../components/device_tracker/config_entry.py | 3 +- .../components/device_tracker/const.py | 16 ++++++++++ .../components/device_tracker/legacy.py | 29 +++++++++---------- .../components/mobile_app/device_tracker.py | 6 ++-- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 5617d56ac3f..9e58c5bbc92 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -26,6 +26,7 @@ from .const import ( # noqa: F401 SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, + SourceType, ) from .legacy import ( # noqa: F401 PLATFORM_SCHEMA, diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index c9b8534c2bc..b587f17d58e 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -30,6 +30,7 @@ from .const import ( CONNECTED_DEVICE_REGISTERED, DOMAIN, LOGGER, + SourceType, ) @@ -187,7 +188,7 @@ class BaseTrackerEntity(Entity): return None @property - def source_type(self) -> str: + def source_type(self) -> SourceType | str: """Return the source type, eg gps or router, of the device.""" raise NotImplementedError diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index c52241ae51f..ad68472d9b0 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -1,8 +1,12 @@ """Device tracker constants.""" +from __future__ import annotations + from datetime import timedelta import logging from typing import Final +from homeassistant.backports.enum import StrEnum + LOGGER: Final = logging.getLogger(__package__) DOMAIN: Final = "device_tracker" @@ -11,11 +15,23 @@ ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" PLATFORM_TYPE_LEGACY: Final = "legacy" PLATFORM_TYPE_ENTITY: Final = "entity_platform" +# SOURCE_TYPE_* below are deprecated as of 2022.9 +# use the SourceType enum instead. SOURCE_TYPE_GPS: Final = "gps" SOURCE_TYPE_ROUTER: Final = "router" SOURCE_TYPE_BLUETOOTH: Final = "bluetooth" SOURCE_TYPE_BLUETOOTH_LE: Final = "bluetooth_le" + +class SourceType(StrEnum): + """Source type for device trackers.""" + + GPS = "gps" + ROUTER = "router" + BLUETOOTH = "bluetooth" + BLUETOOTH_LE = "bluetooth_le" + + CONF_SCAN_INTERVAL: Final = "interval_seconds" SCAN_INTERVAL: Final = timedelta(seconds=12) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index d8097a68ad5..8216c5fba27 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -66,19 +66,16 @@ from .const import ( LOGGER, PLATFORM_TYPE_LEGACY, SCAN_INTERVAL, - SOURCE_TYPE_BLUETOOTH, - SOURCE_TYPE_BLUETOOTH_LE, - SOURCE_TYPE_GPS, - SOURCE_TYPE_ROUTER, + SourceType, ) SERVICE_SEE: Final = "see" SOURCE_TYPES: Final[tuple[str, ...]] = ( - SOURCE_TYPE_GPS, - SOURCE_TYPE_ROUTER, - SOURCE_TYPE_BLUETOOTH, - SOURCE_TYPE_BLUETOOTH_LE, + SourceType.GPS, + SourceType.ROUTER, + SourceType.BLUETOOTH, + SourceType.BLUETOOTH_LE, ) NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( @@ -137,7 +134,7 @@ class SeeCallback(Protocol): gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, picture: str | None = None, icon: str | None = None, consider_home: timedelta | None = None, @@ -158,7 +155,7 @@ class AsyncSeeCallback(Protocol): gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, picture: str | None = None, icon: str | None = None, consider_home: timedelta | None = None, @@ -412,7 +409,7 @@ def async_setup_scanner_platform( kwargs: dict[str, Any] = { "mac": mac, "host_name": host_name, - "source_type": SOURCE_TYPE_ROUTER, + "source_type": SourceType.ROUTER, "attributes": { "scanner": scanner.__class__.__name__, **extra_attributes, @@ -490,7 +487,7 @@ class DeviceTracker: gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, picture: str | None = None, icon: str | None = None, consider_home: timedelta | None = None, @@ -523,7 +520,7 @@ class DeviceTracker: gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, picture: str | None = None, icon: str | None = None, consider_home: timedelta | None = None, @@ -709,7 +706,7 @@ class Device(RestoreEntity): self._icon = icon - self.source_type: str | None = None + self.source_type: SourceType | str | None = None self._attributes: dict[str, Any] = {} @@ -762,7 +759,7 @@ class Device(RestoreEntity): gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, consider_home: timedelta | None = None, ) -> None: """Mark the device as seen.""" @@ -815,7 +812,7 @@ class Device(RestoreEntity): return if self.location_name: self._state = self.location_name - elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: + elif self.gps is not None and self.source_type == SourceType.GPS: zone_state = zone.async_active_zone( self.hass, self.gps[0], self.gps[1], self.gps_accuracy ) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 0f6f0835c3b..d0f1db6caff 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -6,7 +6,7 @@ from homeassistant.components.device_tracker import ( ATTR_LOCATION_NAME, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -103,9 +103,9 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): return self._entry.data[ATTR_DEVICE_NAME] @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def device_info(self): From ca6676a70826c608d903aaa19166df69b558d00c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 29 Jul 2022 13:28:39 +0100 Subject: [PATCH 039/903] Fix xiaomi_ble discovery for devices that don't put the fe95 uuid in service_uuids (#75923) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- homeassistant/generated/bluetooth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 41512291749..0d97dcbedf8 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "bluetooth": [ { - "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], "requirements": ["xiaomi-ble==0.6.2"], diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 8d92d6eab4a..2cbaebb6074 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -80,6 +80,6 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ }, { "domain": "xiaomi_ble", - "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ] From 9bbbee3d8808e2a98dbd37bd878df2ce69953109 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 29 Jul 2022 18:54:49 +0200 Subject: [PATCH 040/903] Update xknx to 0.22.1 (#75932) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 4197cb76209..266eceaacee 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.22.0"], + "requirements": ["xknx==0.22.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index b3879d922db..842376bc696 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2476,7 +2476,7 @@ xboxapi==2.0.1 xiaomi-ble==0.6.2 # homeassistant.components.knx -xknx==0.22.0 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a81c1cb8d3..0d61f2d618a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1668,7 +1668,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.6.2 # homeassistant.components.knx -xknx==0.22.0 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz From 2798a77b5f41fe9c695b7f35f621f67e4d50c462 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Jul 2022 18:56:19 +0200 Subject: [PATCH 041/903] Fix SimplePush repairs issue (#75922) --- homeassistant/components/simplepush/notify.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 358d95c770a..2e58748f323 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -43,17 +43,16 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2022.9.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - if discovery_info is None: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config From 879b41541559d250d5c06d8c3ac2149c1958367d Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Fri, 29 Jul 2022 19:11:53 +0200 Subject: [PATCH 042/903] Fix incorrect check for media source (#75880) * Fix incorrect check for media source * Update homeassistant/components/jellyfin/media_source.py Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- homeassistant/components/jellyfin/media_source.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 879f4a4d4c8..8a09fd8d552 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -363,14 +363,18 @@ class JellyfinSource(MediaSource): def _media_mime_type(media_item: dict[str, Any]) -> str: """Return the mime type of a media item.""" - if not media_item[ITEM_KEY_MEDIA_SOURCES]: + if not media_item.get(ITEM_KEY_MEDIA_SOURCES): raise BrowseError("Unable to determine mime type for item without media source") media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] + + if MEDIA_SOURCE_KEY_PATH not in media_source: + raise BrowseError("Unable to determine mime type for media source without path") + path = media_source[MEDIA_SOURCE_KEY_PATH] mime_type, _ = mimetypes.guess_type(path) - if mime_type is not None: - return mime_type + if mime_type is None: + raise BrowseError(f"Unable to determine mime type for path {path}") - raise BrowseError(f"Unable to determine mime type for path {path}") + return mime_type From 69a0943205b7578388d37e610386f876bd26bfd1 Mon Sep 17 00:00:00 2001 From: Bob van Mierlo <38190383+bobvmierlo@users.noreply.github.com> Date: Fri, 29 Jul 2022 22:46:30 +0200 Subject: [PATCH 043/903] Increase the discovery timeout (#75948) --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index f352f43d0bf..91c7e223f1a 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -22,7 +22,7 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN # How long to wait for additional advertisement packets if we don't have the right ones -ADDITIONAL_DISCOVERY_TIMEOUT = 5 +ADDITIONAL_DISCOVERY_TIMEOUT = 60 @dataclasses.dataclass From c4ad6d46aeeeeecde26d05114b23b9559602b7ad Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 30 Jul 2022 00:22:48 +0000 Subject: [PATCH 044/903] [ci skip] Translation update --- .../components/airzone/translations/sv.json | 7 ++ .../components/ambee/translations/el.json | 6 ++ .../components/ambee/translations/id.json | 6 ++ .../components/ambee/translations/ja.json | 5 ++ .../components/ambee/translations/nl.json | 5 ++ .../components/ambee/translations/sv.json | 6 ++ .../ambient_station/translations/sv.json | 3 + .../components/anthemav/translations/el.json | 6 ++ .../components/anthemav/translations/id.json | 6 ++ .../components/anthemav/translations/ja.json | 5 ++ .../components/anthemav/translations/nl.json | 5 ++ .../components/anthemav/translations/pl.json | 6 ++ .../components/anthemav/translations/sv.json | 25 +++++++ .../components/awair/translations/sv.json | 7 ++ .../components/baf/translations/sv.json | 9 ++- .../components/bluetooth/translations/id.json | 12 +++- .../components/bluetooth/translations/ja.json | 12 +++- .../components/bluetooth/translations/nl.json | 32 +++++++++ .../components/bluetooth/translations/sv.json | 32 +++++++++ .../components/demo/translations/sv.json | 21 ++++++ .../derivative/translations/sv.json | 3 +- .../eight_sleep/translations/sv.json | 6 +- .../components/fan/translations/sv.json | 1 + .../components/fritz/translations/sv.json | 3 + .../components/fritzbox/translations/sv.json | 3 + .../components/generic/translations/sv.json | 28 ++++++++ .../geocaching/translations/sv.json | 4 ++ .../components/google/translations/id.json | 10 +++ .../components/google/translations/ja.json | 8 +++ .../components/google/translations/nl.json | 8 ++- .../components/google/translations/sv.json | 25 ++++++- .../components/govee_ble/translations/nl.json | 21 ++++++ .../components/govee_ble/translations/sv.json | 21 ++++++ .../components/group/translations/sv.json | 17 +++++ .../components/hassio/translations/sv.json | 7 ++ .../here_travel_time/translations/sv.json | 72 ++++++++++++++++++- .../components/hive/translations/sv.json | 7 ++ .../homeassistant/translations/sv.json | 7 ++ .../homeassistant_alerts/translations/el.json | 8 +++ .../homeassistant_alerts/translations/id.json | 8 +++ .../homeassistant_alerts/translations/ja.json | 8 +++ .../homeassistant_alerts/translations/nl.json | 8 +++ .../homeassistant_alerts/translations/ru.json | 8 +++ .../homeassistant_alerts/translations/sv.json | 8 +++ .../components/homekit/translations/id.json | 2 +- .../homekit_controller/translations/ja.json | 2 +- .../components/inkbird/translations/nl.json | 21 ++++++ .../components/inkbird/translations/sv.json | 21 ++++++ .../components/insteon/translations/sv.json | 4 ++ .../integration/translations/sv.json | 22 ++++++ .../intellifire/translations/sv.json | 12 +++- .../components/knx/translations/sv.json | 54 +++++++++++++- .../components/konnected/translations/sv.json | 1 + .../lacrosse_view/translations/el.json | 20 ++++++ .../lacrosse_view/translations/id.json | 20 ++++++ .../lacrosse_view/translations/ja.json | 20 ++++++ .../lacrosse_view/translations/nl.json | 20 ++++++ .../lacrosse_view/translations/pl.json | 20 ++++++ .../lacrosse_view/translations/sv.json | 20 ++++++ .../components/laundrify/translations/sv.json | 13 ++++ .../components/lcn/translations/sv.json | 7 ++ .../lg_soundbar/translations/sv.json | 18 +++++ .../components/life360/translations/sv.json | 25 +++++++ .../components/lifx/translations/nl.json | 19 +++++ .../components/lifx/translations/sv.json | 20 ++++++ .../litterrobot/translations/sensor.sv.json | 28 ++++++++ .../components/lyric/translations/id.json | 6 ++ .../components/lyric/translations/ja.json | 5 ++ .../components/lyric/translations/nl.json | 5 ++ .../components/lyric/translations/ru.json | 6 ++ .../components/lyric/translations/sv.json | 8 +++ .../components/meater/translations/sv.json | 7 +- .../components/miflora/translations/id.json | 8 +++ .../components/miflora/translations/ja.json | 7 ++ .../components/miflora/translations/nl.json | 7 ++ .../components/miflora/translations/ru.json | 8 +++ .../components/miflora/translations/sv.json | 8 +++ .../components/min_max/translations/sv.json | 13 ++++ .../components/mitemp_bt/translations/id.json | 8 +++ .../components/mitemp_bt/translations/nl.json | 7 ++ .../components/mitemp_bt/translations/ru.json | 8 +++ .../components/mitemp_bt/translations/sv.json | 8 +++ .../components/moat/translations/nl.json | 21 ++++++ .../components/moat/translations/sv.json | 21 ++++++ .../components/mqtt/translations/sv.json | 10 +++ .../components/nest/translations/sv.json | 29 ++++++++ .../components/nextdns/translations/sv.json | 29 ++++++++ .../components/nina/translations/sv.json | 24 +++++++ .../components/onewire/translations/sv.json | 10 +++ .../openalpr_local/translations/el.json | 8 +++ .../openalpr_local/translations/id.json | 8 +++ .../openalpr_local/translations/ja.json | 7 ++ .../openalpr_local/translations/nl.json | 7 ++ .../openalpr_local/translations/sv.json | 8 +++ .../components/overkiz/translations/sv.json | 3 + .../components/plex/translations/sv.json | 2 + .../components/plugwise/translations/sv.json | 7 +- .../components/qnap_qsw/translations/sv.json | 6 ++ .../radiotherm/translations/id.json | 6 ++ .../radiotherm/translations/ja.json | 5 ++ .../radiotherm/translations/sv.json | 18 +++++ .../components/recorder/translations/sv.json | 3 + .../components/rhasspy/translations/sv.json | 12 ++++ .../components/roon/translations/sv.json | 13 ++++ .../components/sabnzbd/translations/sv.json | 3 +- .../components/scrape/translations/sv.json | 17 +++++ .../sensibo/translations/sensor.sv.json | 3 +- .../sensorpush/translations/nl.json | 21 ++++++ .../sensorpush/translations/sv.json | 21 ++++++ .../components/senz/translations/id.json | 6 ++ .../components/senz/translations/ja.json | 5 ++ .../components/senz/translations/nl.json | 5 ++ .../components/senz/translations/ru.json | 6 ++ .../components/senz/translations/sv.json | 26 +++++++ .../components/shelly/translations/sv.json | 3 + .../simplepush/translations/el.json | 6 ++ .../simplepush/translations/id.json | 6 ++ .../simplepush/translations/ja.json | 5 ++ .../simplepush/translations/nl.json | 5 ++ .../simplepush/translations/pl.json | 6 ++ .../simplepush/translations/sv.json | 27 +++++++ .../simplisafe/translations/id.json | 7 +- .../simplisafe/translations/ja.json | 5 +- .../simplisafe/translations/sv.json | 8 ++- .../components/siren/translations/sv.json | 3 + .../components/slack/translations/sv.json | 11 ++- .../components/sms/translations/sv.json | 11 +++ .../soundtouch/translations/el.json | 6 ++ .../soundtouch/translations/id.json | 6 ++ .../soundtouch/translations/ja.json | 5 ++ .../soundtouch/translations/nl.json | 5 ++ .../soundtouch/translations/pl.json | 6 ++ .../soundtouch/translations/sv.json | 27 +++++++ .../components/spotify/translations/id.json | 6 ++ .../components/spotify/translations/ja.json | 5 ++ .../components/spotify/translations/nl.json | 5 ++ .../components/spotify/translations/ru.json | 2 +- .../components/spotify/translations/sv.json | 6 ++ .../components/sql/translations/sv.json | 11 +++ .../steam_online/translations/id.json | 6 ++ .../steam_online/translations/ja.json | 5 ++ .../steam_online/translations/nl.json | 5 ++ .../steam_online/translations/ru.json | 2 +- .../steam_online/translations/sv.json | 23 +++++- .../components/switchbot/translations/id.json | 3 +- .../components/switchbot/translations/ja.json | 1 + .../components/switchbot/translations/sv.json | 11 +++ .../tankerkoenig/translations/sv.json | 29 +++++++- .../components/tautulli/translations/sv.json | 13 +++- .../components/tod/translations/sv.json | 3 + .../tomorrowio/translations/sv.json | 3 +- .../totalconnect/translations/sv.json | 11 +++ .../trafikverket_ferry/translations/sv.json | 15 +++- .../trafikverket_train/translations/sv.json | 13 +++- .../transmission/translations/sv.json | 1 + .../ukraine_alarm/translations/sv.json | 17 +++++ .../components/unifi/translations/sv.json | 3 + .../components/uscis/translations/nl.json | 7 ++ .../components/uscis/translations/sv.json | 8 +++ .../components/verisure/translations/sv.json | 15 +++- .../components/vulcan/translations/sv.json | 43 +++++++++++ .../components/withings/translations/sv.json | 4 ++ .../components/ws66i/translations/sv.json | 31 ++++++++ .../components/xbox/translations/el.json | 6 ++ .../components/xbox/translations/id.json | 6 ++ .../components/xbox/translations/ja.json | 5 ++ .../components/xbox/translations/nl.json | 5 ++ .../components/xbox/translations/pl.json | 6 ++ .../components/xbox/translations/sv.json | 8 +++ .../xiaomi_ble/translations/id.json | 15 ++++ .../xiaomi_ble/translations/ja.json | 15 ++++ .../xiaomi_ble/translations/nl.json | 21 ++++++ .../xiaomi_ble/translations/sv.json | 36 ++++++++++ .../components/yolink/translations/sv.json | 1 + .../components/zha/translations/el.json | 2 + .../components/zha/translations/id.json | 2 + .../components/zha/translations/ja.json | 2 + .../components/zha/translations/ru.json | 2 + .../components/zha/translations/sv.json | 7 ++ .../components/zwave_js/translations/sv.json | 7 ++ 180 files changed, 1968 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/airzone/translations/sv.json create mode 100644 homeassistant/components/anthemav/translations/sv.json create mode 100644 homeassistant/components/bluetooth/translations/nl.json create mode 100644 homeassistant/components/bluetooth/translations/sv.json create mode 100644 homeassistant/components/govee_ble/translations/nl.json create mode 100644 homeassistant/components/govee_ble/translations/sv.json create mode 100644 homeassistant/components/hassio/translations/sv.json create mode 100644 homeassistant/components/homeassistant/translations/sv.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/el.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/id.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/ja.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/nl.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/ru.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/sv.json create mode 100644 homeassistant/components/inkbird/translations/nl.json create mode 100644 homeassistant/components/inkbird/translations/sv.json create mode 100644 homeassistant/components/integration/translations/sv.json create mode 100644 homeassistant/components/lacrosse_view/translations/el.json create mode 100644 homeassistant/components/lacrosse_view/translations/id.json create mode 100644 homeassistant/components/lacrosse_view/translations/ja.json create mode 100644 homeassistant/components/lacrosse_view/translations/nl.json create mode 100644 homeassistant/components/lacrosse_view/translations/pl.json create mode 100644 homeassistant/components/lacrosse_view/translations/sv.json create mode 100644 homeassistant/components/lcn/translations/sv.json create mode 100644 homeassistant/components/lg_soundbar/translations/sv.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.sv.json create mode 100644 homeassistant/components/lyric/translations/sv.json create mode 100644 homeassistant/components/miflora/translations/id.json create mode 100644 homeassistant/components/miflora/translations/ja.json create mode 100644 homeassistant/components/miflora/translations/nl.json create mode 100644 homeassistant/components/miflora/translations/ru.json create mode 100644 homeassistant/components/miflora/translations/sv.json create mode 100644 homeassistant/components/mitemp_bt/translations/id.json create mode 100644 homeassistant/components/mitemp_bt/translations/nl.json create mode 100644 homeassistant/components/mitemp_bt/translations/ru.json create mode 100644 homeassistant/components/mitemp_bt/translations/sv.json create mode 100644 homeassistant/components/moat/translations/nl.json create mode 100644 homeassistant/components/moat/translations/sv.json create mode 100644 homeassistant/components/nextdns/translations/sv.json create mode 100644 homeassistant/components/nina/translations/sv.json create mode 100644 homeassistant/components/openalpr_local/translations/el.json create mode 100644 homeassistant/components/openalpr_local/translations/id.json create mode 100644 homeassistant/components/openalpr_local/translations/ja.json create mode 100644 homeassistant/components/openalpr_local/translations/nl.json create mode 100644 homeassistant/components/openalpr_local/translations/sv.json create mode 100644 homeassistant/components/rhasspy/translations/sv.json create mode 100644 homeassistant/components/roon/translations/sv.json create mode 100644 homeassistant/components/sensorpush/translations/nl.json create mode 100644 homeassistant/components/sensorpush/translations/sv.json create mode 100644 homeassistant/components/senz/translations/sv.json create mode 100644 homeassistant/components/simplepush/translations/sv.json create mode 100644 homeassistant/components/siren/translations/sv.json create mode 100644 homeassistant/components/sms/translations/sv.json create mode 100644 homeassistant/components/soundtouch/translations/sv.json create mode 100644 homeassistant/components/switchbot/translations/sv.json create mode 100644 homeassistant/components/tod/translations/sv.json create mode 100644 homeassistant/components/uscis/translations/nl.json create mode 100644 homeassistant/components/uscis/translations/sv.json create mode 100644 homeassistant/components/vulcan/translations/sv.json create mode 100644 homeassistant/components/ws66i/translations/sv.json create mode 100644 homeassistant/components/xbox/translations/sv.json create mode 100644 homeassistant/components/xiaomi_ble/translations/nl.json create mode 100644 homeassistant/components/xiaomi_ble/translations/sv.json diff --git a/homeassistant/components/airzone/translations/sv.json b/homeassistant/components/airzone/translations/sv.json new file mode 100644 index 00000000000..daa1cb38499 --- /dev/null +++ b/homeassistant/components/airzone/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_system_id": "Ogiltigt Airzone System ID" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/el.json b/homeassistant/components/ambee/translations/el.json index 3576b6cd852..99198a39817 100644 --- a/homeassistant/components/ambee/translations/el.json +++ b/homeassistant/components/ambee/translations/el.json @@ -24,5 +24,11 @@ "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Ambee \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Ambee \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10. \n\n \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b7 Ambee \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b4\u03c9\u03c1\u03b5\u03ac\u03bd (\u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2) \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03c3\u03c4\u03bf\u03c5\u03c2 \u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03bf\u03cd\u03bd \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b5\u03c0\u03af \u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03ae. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Ambee \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Ambee \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/id.json b/homeassistant/components/ambee/translations/id.json index a5790d95ecd..686e36fd17b 100644 --- a/homeassistant/components/ambee/translations/id.json +++ b/homeassistant/components/ambee/translations/id.json @@ -24,5 +24,11 @@ "description": "Siapkan Ambee Anda untuk diintegrasikan dengan Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "Integrasi Ambee sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nIntegrasi ini dalam proses penghapusan, karena Ambee telah menghapus akun versi gratis (terbatas) mereka dan tidak menyediakan cara bagi pengguna biasa untuk mendaftar paket berbayar lagi.\n\nHapus entri integrasi Ambee dari instans Anda untuk memperbaiki masalah ini.", + "title": "Integrasi Ambee dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json index e320189e010..e4502068d69 100644 --- a/homeassistant/components/ambee/translations/ja.json +++ b/homeassistant/components/ambee/translations/ja.json @@ -24,5 +24,10 @@ "description": "Ambee \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } + }, + "issues": { + "pending_removal": { + "title": "Ambee\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/nl.json b/homeassistant/components/ambee/translations/nl.json index 5d356037652..957c3547be2 100644 --- a/homeassistant/components/ambee/translations/nl.json +++ b/homeassistant/components/ambee/translations/nl.json @@ -24,5 +24,10 @@ "description": "Stel Ambee in om te integreren met Home Assistant." } } + }, + "issues": { + "pending_removal": { + "title": "De Ambee-integratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sv.json b/homeassistant/components/ambee/translations/sv.json index 5ad5b5b6db4..a8147496b74 100644 --- a/homeassistant/components/ambee/translations/sv.json +++ b/homeassistant/components/ambee/translations/sv.json @@ -12,5 +12,11 @@ } } } + }, + "issues": { + "pending_removal": { + "description": "Ambee-integrationen v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.10. \n\n Integrationen tas bort eftersom Ambee tog bort sina gratis (begr\u00e4nsade) konton och inte l\u00e4ngre ger vanliga anv\u00e4ndare m\u00f6jlighet att registrera sig f\u00f6r en betalplan. \n\n Ta bort Ambee-integreringsposten fr\u00e5n din instans f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Ambee-integrationen tas bort" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/sv.json b/homeassistant/components/ambient_station/translations/sv.json index 7c6be84d594..35ff795627e 100644 --- a/homeassistant/components/ambient_station/translations/sv.json +++ b/homeassistant/components/ambient_station/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, "error": { "invalid_key": "Ogiltigt API-nyckel och/eller applikationsnyckel", "no_devices": "Inga enheter hittades i kontot" diff --git a/homeassistant/components/anthemav/translations/el.json b/homeassistant/components/anthemav/translations/el.json index 983e89155e8..5da08413782 100644 --- a/homeassistant/components/anthemav/translations/el.json +++ b/homeassistant/components/anthemav/translations/el.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03c4\u03ce\u03bd Anthem A/V \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 Anthem A/V Receivers \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 Anthem A/V Receivers \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/id.json b/homeassistant/components/anthemav/translations/id.json index 1eb2ba0b5a1..8c7e40b4c0b 100644 --- a/homeassistant/components/anthemav/translations/id.json +++ b/homeassistant/components/anthemav/translations/id.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Receiver Anthem A/V lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Receiver Anthem A/V dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Anthem A/V Receiver dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ja.json b/homeassistant/components/anthemav/translations/ja.json index 8c87d02e557..b55e8b2b030 100644 --- a/homeassistant/components/anthemav/translations/ja.json +++ b/homeassistant/components/anthemav/translations/ja.json @@ -15,5 +15,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "Anthem A/V Receivers YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/nl.json b/homeassistant/components/anthemav/translations/nl.json index c09dde1bfc3..8754427ea61 100644 --- a/homeassistant/components/anthemav/translations/nl.json +++ b/homeassistant/components/anthemav/translations/nl.json @@ -14,5 +14,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "De Anthem A/V Receivers YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pl.json b/homeassistant/components/anthemav/translations/pl.json index 18eaecb9845..ca40384e6f0 100644 --- a/homeassistant/components/anthemav/translations/pl.json +++ b/homeassistant/components/anthemav/translations/pl.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Anthem A/V Receivers przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Anthem A/V Receivers zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/sv.json b/homeassistant/components/anthemav/translations/sv.json new file mode 100644 index 00000000000..dd3f6f891e2 --- /dev/null +++ b/homeassistant/components/anthemav/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "cannot_receive_deviceinfo": "Det gick inte att h\u00e4mta MAC-adress. Se till att enheten \u00e4r p\u00e5slagen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Anthem A/V-mottagare med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Anthem A/V Receivers YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Anthem A/V-mottagarens YAML-konfiguration tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json index 1fda5b91f5a..a7dd53ffad4 100644 --- a/homeassistant/components/awair/translations/sv.json +++ b/homeassistant/components/awair/translations/sv.json @@ -1,6 +1,13 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "access_token": "\u00c5tkomsttoken", + "email": "Epost" + }, + "description": "Ange din Awair-utvecklar\u00e5tkomsttoken igen." + }, "user": { "data": { "access_token": "\u00c5tkomstnyckel" diff --git a/homeassistant/components/baf/translations/sv.json b/homeassistant/components/baf/translations/sv.json index e3270d4036a..0e346a0f72a 100644 --- a/homeassistant/components/baf/translations/sv.json +++ b/homeassistant/components/baf/translations/sv.json @@ -1,11 +1,18 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "ipv6_not_supported": "IPv6 st\u00f6ds inte." }, "error": { "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} - {model} ( {ip_address} )", + "step": { + "discovery_confirm": { + "description": "Vill du st\u00e4lla in {name} - {model} ( {ip_address} )?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index 5fe99b68c2f..3fc2d6a7623 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "no_adapters": "Tidak ada adaptor Bluetooth yang ditemukan" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Pilih perangkat untuk disiapkan" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Adaptor Bluetooth yang digunakan untuk pemindaian" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index 6257d1c67d4..b3f6b794ee9 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_adapters": "Bluetooth\u30a2\u30c0\u30d7\u30bf\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u30b9\u30ad\u30e3\u30f3\u306b\u4f7f\u7528\u3059\u308bBluetooth\u30a2\u30c0\u30d7\u30bf\u30fc" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/nl.json b/homeassistant/components/bluetooth/translations/nl.json new file mode 100644 index 00000000000..9a0e95df8bc --- /dev/null +++ b/homeassistant/components/bluetooth/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "no_adapters": "Geen Bluetooth-adapters gevonden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "enable_bluetooth": { + "description": "Wilt u Bluetooth instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "De Bluetooth-adapter die gebruikt moet worden voor het scannen." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/sv.json b/homeassistant/components/bluetooth/translations/sv.json new file mode 100644 index 00000000000..fe07d338101 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/sv.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "no_adapters": "Inga Bluetooth-adaptrar hittades" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "enable_bluetooth": { + "description": "Vill du s\u00e4tta upp Bluetooth?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Bluetooth-adaptern som ska anv\u00e4ndas f\u00f6r skanning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/sv.json b/homeassistant/components/demo/translations/sv.json index b577b02e25b..55872e8c81e 100644 --- a/homeassistant/components/demo/translations/sv.json +++ b/homeassistant/components/demo/translations/sv.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tryck p\u00e5 OK n\u00e4r blinkersv\u00e4tska har fyllts p\u00e5", + "title": "Blinkerv\u00e4tska m\u00e5ste fyllas p\u00e5" + } + } + }, + "title": "Blinkersv\u00e4tskan \u00e4r tom och m\u00e5ste fyllas p\u00e5" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier-komponenten \u00e4r nu utfasad p\u00e5 grund av bristen p\u00e5 lokal kontroll tillg\u00e4nglig i det nya API:et", + "title": "Transmogrifier-komponenten \u00e4r utfasad" + }, + "unfixable_problem": { + "description": "Det h\u00e4r problemet kommer aldrig att ge upp.", + "title": "Detta \u00e4r inte ett problem som g\u00e5r att fixa" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/derivative/translations/sv.json b/homeassistant/components/derivative/translations/sv.json index ac5b766d124..0f36236b1ae 100644 --- a/homeassistant/components/derivative/translations/sv.json +++ b/homeassistant/components/derivative/translations/sv.json @@ -28,7 +28,8 @@ "unit_time": "Tidsenhet" }, "data_description": { - "round": "Anger antal decimaler i resultatet." + "round": "Anger antal decimaler i resultatet.", + "unit_prefix": "." } } } diff --git a/homeassistant/components/eight_sleep/translations/sv.json b/homeassistant/components/eight_sleep/translations/sv.json index af5c7e7fe8d..cc8fb8e5149 100644 --- a/homeassistant/components/eight_sleep/translations/sv.json +++ b/homeassistant/components/eight_sleep/translations/sv.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Kan inte ansluta till Eight Sleep-molnet: {error}" + }, + "error": { + "cannot_connect": "Kan inte ansluta till Eight Sleep-molnet: {error}" }, "step": { "user": { diff --git a/homeassistant/components/fan/translations/sv.json b/homeassistant/components/fan/translations/sv.json index dd1aaad4052..31df690b766 100644 --- a/homeassistant/components/fan/translations/sv.json +++ b/homeassistant/components/fan/translations/sv.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "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/fritz/translations/sv.json b/homeassistant/components/fritz/translations/sv.json index 02cd1e39b0e..89c6cb84221 100644 --- a/homeassistant/components/fritz/translations/sv.json +++ b/homeassistant/components/fritz/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." + }, "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/sv.json b/homeassistant/components/fritzbox/translations/sv.json index b611b3a9893..347aeeecc4a 100644 --- a/homeassistant/components/fritzbox/translations/sv.json +++ b/homeassistant/components/fritzbox/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." + }, "step": { "confirm": { "data": { diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 62b30963a50..020b0093092 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -3,21 +3,49 @@ "abort": { "no_devices_found": "Inga enheter hittades i n\u00e4tverket" }, + "error": { + "malformed_url": "Ogiltig URL", + "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tna", + "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information." + }, "step": { + "content_type": { + "data": { + "content_type": "Inneh\u00e5llstyp" + }, + "description": "Ange inneh\u00e5llstypen f\u00f6r str\u00f6mmen." + }, "user": { "data": { "authentication": "Autentiseringen", + "password": "L\u00f6senord", + "rtsp_transport": "RTSP transportprotokoll", "username": "Anv\u00e4ndarnamn" } } } }, "options": { + "error": { + "malformed_url": "Ogiltig URL", + "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tet", + "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information." + }, "step": { + "content_type": { + "data": { + "content_type": "Inneh\u00e5llstyp" + }, + "description": "Ange tyen av inneh\u00e5ll f\u00f6r str\u00f6mmen" + }, "init": { "data": { "authentication": "Autentiseringen", + "use_wallclock_as_timestamps": "Anv\u00e4nd v\u00e4ggklocka som tidsst\u00e4mplar", "username": "Anv\u00e4ndarnamn" + }, + "data_description": { + "use_wallclock_as_timestamps": "Det h\u00e4r alternativet kan korrigera segmenteringsproblem eller kraschproblem som uppst\u00e5r p\u00e5 grund av felaktig implementering av tidsst\u00e4mplar p\u00e5 vissa kameror." } } } diff --git a/homeassistant/components/geocaching/translations/sv.json b/homeassistant/components/geocaching/translations/sv.json index d8622ba37fb..bf674fb43f1 100644 --- a/homeassistant/components/geocaching/translations/sv.json +++ b/homeassistant/components/geocaching/translations/sv.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto har redan konfigurerats", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", "oauth_error": "Mottog ogiltiga tokendata.", @@ -14,6 +17,7 @@ "title": "V\u00e4lj autentiseringsmetod" }, "reauth_confirm": { + "description": "Geocaching-integrationen m\u00e5ste autentisera ditt konto igen", "title": "\u00c5terautenticera integration" } } diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index 20ed21a56be..6de37cee947 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Google Kalender di configuration.yaml dalam proses penghapusan di Home Assistant 2022.9.\n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Google Kalender dalam proses penghapusan" + }, + "removed_track_new_yaml": { + "description": "Anda telah menonaktifkan pelacakan entitas untuk Google Kalender di configuration.yaml, yang kini tidak lagi didukung. Anda harus secara manual mengubah Opsi Sistem integrasi di antarmuka untuk menonaktifkan entitas yang baru ditemukan di masa datang. Hapus pengaturan track_new dari configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Pelacakan entitas Google Kalender telah berubah" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 6e2aac00c5d..9507f32812d 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -33,6 +33,14 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fcyaml\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "removed_track_new_yaml": { + "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/nl.json b/homeassistant/components/google/translations/nl.json index 2f8d67af1e2..572ac985041 100644 --- a/homeassistant/components/google/translations/nl.json +++ b/homeassistant/components/google/translations/nl.json @@ -8,7 +8,8 @@ "invalid_access_token": "Ongeldig toegangstoken", "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", "oauth_error": "Ongeldige tokengegevens ontvangen.", - "reauth_successful": "Herauthenticatie geslaagd" + "reauth_successful": "Herauthenticatie geslaagd", + "timeout_connect": "Time-out bij het maken van verbinding" }, "create_entry": { "default": "Authenticatie geslaagd" @@ -29,6 +30,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "De Google Calendar YAML-configuratie wordt verwijderd" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/sv.json b/homeassistant/components/google/translations/sv.json index 7f1f140af90..d1e92e9b8a0 100644 --- a/homeassistant/components/google/translations/sv.json +++ b/homeassistant/components/google/translations/sv.json @@ -1,8 +1,31 @@ { + "application_credentials": { + "description": "F\u00f6lj [instruktionerna]( {more_info_url} ) f\u00f6r [OAuth-samtyckessk\u00e4rmen]( {oauth_consent_url} ) f\u00f6r att ge Home Assistant \u00e5tkomst till din Google-kalender. Du m\u00e5ste ocks\u00e5 skapa applikationsuppgifter kopplade till din kalender:\n 1. G\u00e5 till [Inloggningsuppgifter]( {oauth_creds_url} ) och klicka p\u00e5 **Skapa inloggningsuppgifter**.\n 1. V\u00e4lj **OAuth-klient-ID** i rullgardinsmenyn.\n 1. V\u00e4lj **TV och begr\u00e4nsade ing\u00e5ngsenheter** f\u00f6r applikationstyp. \n\n" + }, "config": { "abort": { "already_configured": "Konto har redan konfigurerats", - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "timeout_connect": "Timeout vid anslutningsf\u00f6rs\u00f6k" + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Google Kalender i configuration.yaml tas bort i Home Assistant 2022.9. \n\n Dina befintliga OAuth-applikationsuppgifter och \u00e5tkomstinst\u00e4llningar har importerats till anv\u00e4ndargr\u00e4nssnittet automatiskt. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Google Kalender YAML-konfigurationen tas bort" + }, + "removed_track_new_yaml": { + "description": "Du har inaktiverat enhetssp\u00e5rning f\u00f6r Google Kalender i configuration.yaml, som inte l\u00e4ngre st\u00f6ds. Du m\u00e5ste manuellt \u00e4ndra integrationssystemalternativen i anv\u00e4ndargr\u00e4nssnittet f\u00f6r att inaktivera nyuppt\u00e4ckta enheter fram\u00f6ver. Ta bort inst\u00e4llningen track_new fr\u00e5n configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Sp\u00e5rning av enheter i Google Kalender har \u00e4ndrats" + } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant-\u00e5tkomst till Google Kalender" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/nl.json b/homeassistant/components/govee_ble/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/govee_ble/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/sv.json b/homeassistant/components/govee_ble/translations/sv.json new file mode 100644 index 00000000000..911ef1b168f --- /dev/null +++ b/homeassistant/components/govee_ble/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enhet \u00e4r redan konfigurerad", + "already_in_progress": "Konfiguration redan ig\u00e5ng", + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du s\u00e4tta upp {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index c62d9e18ffc..8ba098e02cd 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -12,7 +12,18 @@ "light": { "title": "L\u00e4gg till grupp" }, + "lock": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, + "title": "L\u00e4gg till grupp" + }, "user": { + "menu_options": { + "lock": "L\u00e5sgrupp" + }, "title": "L\u00e4gg till grupp" } } @@ -23,6 +34,12 @@ "data": { "all": "Alla entiteter" } + }, + "lock": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + } } } }, diff --git a/homeassistant/components/hassio/translations/sv.json b/homeassistant/components/hassio/translations/sv.json new file mode 100644 index 00000000000..7d3d7684558 --- /dev/null +++ b/homeassistant/components/hassio/translations/sv.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "agent_version": "Agentversion" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/sv.json b/homeassistant/components/here_travel_time/translations/sv.json index 0757cc44bf1..bb0f36a448f 100644 --- a/homeassistant/components/here_travel_time/translations/sv.json +++ b/homeassistant/components/here_travel_time/translations/sv.json @@ -8,11 +8,81 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "destination_coordinates": { + "data": { + "destination": "Destination som GPS-koordinater" + }, + "title": "V\u00e4lj destination" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Destination med hj\u00e4lp av en enhet" + }, + "title": "V\u00e4lj destination" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Anv\u00e4nda en kartplats", + "destination_entity": "Anv\u00e4nda en enhet" + }, + "title": "V\u00e4lj destination" + }, + "origin_coordinates": { + "data": { + "origin": "Ursprung som GPS-koordinater" + }, + "title": "V\u00e4lj ursprung" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Ursprung med hj\u00e4lp av en enhet" + }, + "title": "V\u00e4lj ursprung" + }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Anv\u00e4nda en kartplats", + "origin_entity": "Anv\u00e4nda en enhet" + }, + "title": "V\u00e4lj ursprung" + }, "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "mode": "Resel\u00e4ge" } } } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Ankomsttid" + }, + "title": "V\u00e4lj ankomsttid" + }, + "departure_time": { + "data": { + "departure_time": "Avg\u00e5ngstid" + }, + "title": "V\u00e4lj avg\u00e5ngstid" + }, + "init": { + "data": { + "route_mode": "Ruttl\u00e4ge", + "traffic_mode": "Trafikl\u00e4ge", + "unit_system": "Enhetssystem" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Konfigurera en ankomsttid", + "departure_time": "Konfigurera en avg\u00e5ngstid", + "no_time": "Konfigurera inte en tid" + }, + "title": "V\u00e4lj tidstyp" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hive/translations/sv.json b/homeassistant/components/hive/translations/sv.json index 6d76a51e90b..60b51beb78b 100644 --- a/homeassistant/components/hive/translations/sv.json +++ b/homeassistant/components/hive/translations/sv.json @@ -4,6 +4,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "configuration": { + "data": { + "device_name": "Enhetsnamn" + }, + "description": "Ange din Hive-konfiguration", + "title": "Hive-konfiguration." + }, "reauth": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/homeassistant/translations/sv.json b/homeassistant/components/homeassistant/translations/sv.json new file mode 100644 index 00000000000..e4778a3a9e0 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/sv.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "config_dir": "Konfigurationskatalog" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/el.json b/homeassistant/components/homeassistant_alerts/translations/el.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/id.json b/homeassistant/components/homeassistant_alerts/translations/id.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/ja.json b/homeassistant/components/homeassistant_alerts/translations/ja.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/ja.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/nl.json b/homeassistant/components/homeassistant_alerts/translations/nl.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/nl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/ru.json b/homeassistant/components/homeassistant_alerts/translations/ru.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/sv.json b/homeassistant/components/homeassistant_alerts/translations/sv.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json index e3889ce031f..eca3dcdd173 100644 --- a/homeassistant/components/homekit/translations/id.json +++ b/homeassistant/components/homekit/translations/id.json @@ -60,7 +60,7 @@ "include_exclude_mode": "Mode Penyertaan", "mode": "Mode HomeKit" }, - "description": "HomeKit dapat dikonfigurasi untuk memaparkakan sebuah bridge atau sebuah aksesori. Dalam mode aksesori, hanya satu entitas yang dapat digunakan. Mode aksesori diperlukan agar pemutar media dengan kelas perangkat TV berfungsi dengan baik. Entitas di \"Domain yang akan disertakan\" akan disertakan ke HomeKit. Anda akan dapat memilih entitas mana yang akan disertakan atau dikecualikan dari daftar ini pada layar berikutnya.", + "description": "HomeKit dapat dikonfigurasi untuk memaparkan sebuah bridge atau sebuah aksesori. Dalam mode aksesori, hanya satu entitas yang dapat digunakan. Mode aksesori diperlukan agar pemutar media dengan kelas perangkat TV berfungsi dengan baik. Entitas di \"Domain yang akan disertakan\" akan disertakan ke HomeKit. Anda akan dapat memilih entitas mana yang akan disertakan atau dikecualikan dari daftar ini pada layar berikutnya.", "title": "Pilih mode dan domain." }, "yaml": { diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index fbac8823897..3d013323fe3 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -11,7 +11,7 @@ "no_devices": "\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" }, "error": { - "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "insecure_setup_code": "\u8981\u6c42\u3055\u308c\u305f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u306f\u3001\u5358\u7d14\u3059\u304e\u308b\u306e\u3067\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u57fa\u672c\u7684\u306a\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u8981\u4ef6\u3092\u6e80\u305f\u3057\u3066\u3044\u307e\u305b\u3093\u3002", "max_peers_error": "\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u7121\u6599\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u30b9\u30c8\u30ec\u30fc\u30b8\u304c\u306a\u3044\u305f\u3081\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u8ffd\u52a0\u3092\u62d2\u5426\u3057\u307e\u3057\u305f\u3002", "pairing_failed": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u4e2d\u306b\u3001\u672a\u51e6\u7406\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u3053\u308c\u306f\u4e00\u6642\u7684\u306a\u969c\u5bb3\u304b\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002", diff --git a/homeassistant/components/inkbird/translations/nl.json b/homeassistant/components/inkbird/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/inkbird/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/sv.json b/homeassistant/components/inkbird/translations/sv.json new file mode 100644 index 00000000000..1b04e08e2f7 --- /dev/null +++ b/homeassistant/components/inkbird/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationen \u00e4r redan ig\u00e5ng", + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du s\u00e4tta upp {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att s\u00e4tta upp" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json index 4992e704f9e..6f3f3666b12 100644 --- a/homeassistant/components/insteon/translations/sv.json +++ b/homeassistant/components/insteon/translations/sv.json @@ -3,7 +3,11 @@ "abort": { "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet" }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Vill du konfigurera {name}?" + }, "hubv2": { "data": { "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/integration/translations/sv.json b/homeassistant/components/integration/translations/sv.json new file mode 100644 index 00000000000..4665a9b8816 --- /dev/null +++ b/homeassistant/components/integration/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data_description": { + "round": "Anger antal decimaler i resultatet.", + "unit_prefix": "Utdata kommer att skalas enligt det valda metriska prefixet.", + "unit_time": "Utg\u00e5ngen kommer att skalas enligt den valda tidsenheten." + } + } + } + }, + "options": { + "step": { + "init": { + "data_description": { + "round": "Anger antal decimaler i resultatet." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/sv.json b/homeassistant/components/intellifire/translations/sv.json index afffc97862b..05685de670b 100644 --- a/homeassistant/components/intellifire/translations/sv.json +++ b/homeassistant/components/intellifire/translations/sv.json @@ -1,16 +1,24 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "Omautentiseringen lyckades" }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "api_error": "Inloggningen misslyckades", + "cannot_connect": "Det gick inte att ansluta.", + "iftapi_connect": "Fel vid anslutning till iftapi.net" }, "step": { "api_config": { "data": { + "password": "L\u00f6senord", "username": "E-postadress" } + }, + "pick_device": { + "description": "F\u00f6ljande IntelliFire-enheter uppt\u00e4cktes. V\u00e4lj vilken du vill konfigurera.", + "title": "Val av enhet" } } } diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index de5def120c9..7c4d6ee8e61 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -1,19 +1,71 @@ { "config": { "error": { + "file_not_found": "Den angivna `.knxkeys`-filen hittades inte i s\u00f6kv\u00e4gen config/.storage/knx/", "invalid_individual_address": "V\u00e4rdet matchar inte m\u00f6nstret f\u00f6r en individuell adress i KNX.\n'area.line.device'", - "invalid_ip_address": "Ogiltig IPv4-adress." + "invalid_ip_address": "Ogiltig IPv4-adress.", + "invalid_signature": "L\u00f6senordet f\u00f6r att dekryptera `.knxkeys`-filen \u00e4r fel." }, "step": { "manual_tunnel": { "data": { "tunneling_type": "KNX tunneltyp" + }, + "data_description": { + "host": "IP-adressen f\u00f6r KNX/IP-tunnelenheten.", + "local_ip": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt.", + "port": "Port p\u00e5 KNX/IP-tunnelenheten." + } + }, + "routing": { + "data_description": { + "individual_address": "KNX-adress som ska anv\u00e4ndas av Home Assistant, t.ex. `0.0.4`", + "local_ip": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt." + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Filnamnet p\u00e5 din `.knxkeys`-fil (inklusive till\u00e4gget)", + "knxkeys_password": "L\u00f6senordet f\u00f6r att dekryptera filen `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Filen f\u00f6rv\u00e4ntas finnas i din config-katalog i `.storage/knx/`.\n I Home Assistant OS skulle detta vara `/config/.storage/knx/`\n Exempel: `my_project.knxkeys`", + "knxkeys_password": "Detta st\u00e4lldes in n\u00e4r filen exporterades fr\u00e5n ETS." + }, + "description": "V\u00e4nligen ange informationen f\u00f6r din `.knxkeys`-fil." + }, + "secure_manual": { + "data": { + "device_authentication": "L\u00f6senord f\u00f6r enhetsautentisering", + "user_id": "Anv\u00e4ndar-ID", + "user_password": "Anv\u00e4ndarl\u00f6senord" + }, + "data_description": { + "device_authentication": "Detta st\u00e4lls in i 'IP'-panelen i gr\u00e4nssnittet i ETS." + }, + "description": "Ange din s\u00e4kra IP-information." + }, + "secure_tunneling": { + "description": "V\u00e4lj hur du vill konfigurera KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Anv\u00e4nd en fil `.knxkeys` som inneh\u00e5ller s\u00e4kra IP-nycklar.", + "secure_manual": "Konfigurera s\u00e4kra IP nycklar manuellt" } } } }, "options": { "step": { + "init": { + "data_description": { + "individual_address": "KNX-adress som ska anv\u00e4ndas av Home Assistant, t.ex. `0.0.4`", + "local_ip": "Anv\u00e4nd `0.0.0.0.0` f\u00f6r automatisk identifiering.", + "multicast_group": "Anv\u00e4nds f\u00f6r routing och uppt\u00e4ckt. Standard: \"224.0.23.12\".", + "multicast_port": "Anv\u00e4nds f\u00f6r routing och uppt\u00e4ckt. Standard: \"3671\".", + "rate_limit": "Maximalt antal utg\u00e5ende telegram per sekund.\n Rekommenderad: 20 till 40", + "state_updater": "St\u00e4ll in som standard f\u00f6r att l\u00e4sa tillst\u00e5nd fr\u00e5n KNX-bussen. N\u00e4r den \u00e4r inaktiverad kommer Home Assistant inte aktivt att h\u00e4mta entitetstillst\u00e5nd fr\u00e5n KNX-bussen. Kan \u00e5sidos\u00e4ttas av entitetsalternativ \"sync_state\"." + } + }, "tunnel": { "data": { "tunneling_type": "KNX tunneltyp" diff --git a/homeassistant/components/konnected/translations/sv.json b/homeassistant/components/konnected/translations/sv.json index 0a0716f87ff..a96f612010a 100644 --- a/homeassistant/components/konnected/translations/sv.json +++ b/homeassistant/components/konnected/translations/sv.json @@ -15,6 +15,7 @@ "title": "Konnected-enheten redo" }, "import_confirm": { + "description": "En ansluten larmpanel med ID {id} har uppt\u00e4ckts i configuration.yaml. Detta fl\u00f6de l\u00e5ter dig importera det till en konfigurationspost.", "title": "Importera Konnected enhet" }, "user": { diff --git a/homeassistant/components/lacrosse_view/translations/el.json b/homeassistant/components/lacrosse_view/translations/el.json new file mode 100644 index 00000000000..1975028e9c9 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/el.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "no_locations": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/id.json b/homeassistant/components/lacrosse_view/translations/id.json new file mode 100644 index 00000000000..d244ba002b8 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "no_locations": "Tidak ada lokasi yang ditemukan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/ja.json b/homeassistant/components/lacrosse_view/translations/ja.json new file mode 100644 index 00000000000..6b058f78cdb --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_locations": "\u5834\u6240\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/nl.json b/homeassistant/components/lacrosse_view/translations/nl.json new file mode 100644 index 00000000000..44c1bc93f79 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "no_locations": "Geen locaties gevonden", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/pl.json b/homeassistant/components/lacrosse_view/translations/pl.json new file mode 100644 index 00000000000..b7fbbe50779 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_locations": "Nie znaleziono lokalizacji", + "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/lacrosse_view/translations/sv.json b/homeassistant/components/lacrosse_view/translations/sv.json new file mode 100644 index 00000000000..241263b2c53 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "no_locations": "Inga platser hittades", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/sv.json b/homeassistant/components/laundrify/translations/sv.json index dd7447e847e..f22a4ea3d3f 100644 --- a/homeassistant/components/laundrify/translations/sv.json +++ b/homeassistant/components/laundrify/translations/sv.json @@ -6,7 +6,20 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "invalid_format": "Ogiltigt format. V\u00e4nligen ange som xxx-xxx.", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "init": { + "data": { + "code": "Auth-kod (xxx-xxx)" + }, + "description": "V\u00e4nligen ange din personliga autentiseringskod som visas i laundrify-appen." + }, + "reauth_confirm": { + "description": "Laundrify-integrationen m\u00e5ste autentiseras p\u00e5 nytt.", + "title": "Autentisera om integration" + } } } } \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/sv.json b/homeassistant/components/lcn/translations/sv.json new file mode 100644 index 00000000000..59b06895f48 --- /dev/null +++ b/homeassistant/components/lcn/translations/sv.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "codelock": "kodl\u00e5skod mottagen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/sv.json b/homeassistant/components/lg_soundbar/translations/sv.json new file mode 100644 index 00000000000..9b8ff6ea1aa --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "existing_instance_updated": "Uppdaterade existerande konfiguration." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index 27a669dfb5c..1a5c7f2e569 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -1,12 +1,23 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, "create_entry": { "default": "F\u00f6r att st\u00e4lla in avancerade alternativ, se [Life360 documentation]({docs_url})." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_username": "Ogiltigt anv\u00e4ndarnmn" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", @@ -16,5 +27,19 @@ "title": "Life360 kontoinformation" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Visa k\u00f6rning som tillst\u00e5nd", + "driving_speed": "K\u00f6rhastighet", + "limit_gps_acc": "Begr\u00e4nsa GPS-noggrannheten", + "max_gps_accuracy": "Maximal GPS-noggrannhet (meter)", + "set_drive_speed": "St\u00e4ll in k\u00f6rhastighetsgr\u00e4ns" + }, + "title": "Konto-alternativ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/nl.json b/homeassistant/components/lifx/translations/nl.json index c8d0ca83dd8..51091fcd365 100644 --- a/homeassistant/components/lifx/translations/nl.json +++ b/homeassistant/components/lifx/translations/nl.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", "no_devices_found": "Geen apparaten gevonden op het netwerk", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Wilt u LIFX instellen?" + }, + "discovery_confirm": { + "description": "Wilt u {label} ({host}) {serial} instellen?" + }, + "pick_device": { + "data": { + "device": "Apparaat" + } + }, + "user": { + "data": { + "host": "Host" + } } } } diff --git a/homeassistant/components/lifx/translations/sv.json b/homeassistant/components/lifx/translations/sv.json index 82a55b48edb..dfd7de02d94 100644 --- a/homeassistant/components/lifx/translations/sv.json +++ b/homeassistant/components/lifx/translations/sv.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "no_devices_found": "Inga LIFX enheter hittas i n\u00e4tverket.", "single_instance_allowed": "Endast en enda konfiguration av LIFX \u00e4r m\u00f6jlig." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Vill du st\u00e4lla in LIFX?" + }, + "discovery_confirm": { + "description": "Vill du st\u00e4lla in {label} ( {host} ) {serial} ?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." } } } diff --git a/homeassistant/components/litterrobot/translations/sensor.sv.json b/homeassistant/components/litterrobot/translations/sensor.sv.json new file mode 100644 index 00000000000..c54c705b8c4 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.sv.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Huven \u00e4r borttagen", + "ccc": "Reningscykel klar", + "ccp": "Reng\u00f6ringscykel p\u00e5g\u00e5r", + "csf": "Kattsensor fel", + "csi": "Kattsensor avbruten", + "cst": "Kattsensor timing", + "df1": "L\u00e5da n\u00e4stan full - 2 cykler kvar", + "df2": "L\u00e5da n\u00e4stan full - 1 cykel kvar", + "dfs": "L\u00e5dan full", + "dhf": "Dump + fel i heml\u00e4get", + "dpf": "Fel i dumpningsl\u00e4get", + "ec": "T\u00f6mningscykel", + "hpf": "Hempositionsfel", + "off": "Avst\u00e4ngd", + "offline": "Offline", + "otf": "Fel vid f\u00f6r h\u00f6gt vridmoment", + "p": "Pausad", + "pd": "Pinch Detect", + "rdy": "Redo", + "scf": "Fel p\u00e5 kattsensorn vid uppstart", + "sdf": "L\u00e5dan full vid uppstart", + "spf": "Pinch Detect vid start" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/id.json b/homeassistant/components/lyric/translations/id.json index a519e2e5f9e..b75093d1718 100644 --- a/homeassistant/components/lyric/translations/id.json +++ b/homeassistant/components/lyric/translations/id.json @@ -17,5 +17,11 @@ "title": "Autentikasi Ulang Integrasi" } } + }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Honeywell Lyric lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Honeywell Lyric telah dihapus" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index 2a5abdcb5b7..5394e978c27 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -17,5 +17,10 @@ "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } + }, + "issues": { + "removed_yaml": { + "title": "Honeywell Lyric YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/nl.json b/homeassistant/components/lyric/translations/nl.json index e820174aa5a..da6f0ed06c1 100644 --- a/homeassistant/components/lyric/translations/nl.json +++ b/homeassistant/components/lyric/translations/nl.json @@ -17,5 +17,10 @@ "title": "Integratie herauthenticeren" } } + }, + "issues": { + "removed_yaml": { + "title": "De Honeywell Lyric YAML-configuratie is verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json index 7aef03ff3c5..536f1a9c0cc 100644 --- a/homeassistant/components/lyric/translations/ru.json +++ b/homeassistant/components/lyric/translations/ru.json @@ -17,5 +17,11 @@ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } + }, + "issues": { + "removed_yaml": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Honeywell Lyric\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Honeywell Lyric \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/sv.json b/homeassistant/components/lyric/translations/sv.json new file mode 100644 index 00000000000..a34370b5ce5 --- /dev/null +++ b/homeassistant/components/lyric/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "removed_yaml": { + "description": "Konfigurering av Honeywell Lyric med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Honeywell Lyric YAML-konfigurationen har tagits bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/sv.json b/homeassistant/components/meater/translations/sv.json index 47a719743f5..c5ecfbc1d84 100644 --- a/homeassistant/components/meater/translations/sv.json +++ b/homeassistant/components/meater/translations/sv.json @@ -2,7 +2,8 @@ "config": { "error": { "invalid_auth": "Ogiltig autentisering", - "service_unavailable_error": "Programmeringsgr\u00e4nssnittet g\u00e5r inte att komma \u00e5t f\u00f6r n\u00e4rvarande. F\u00f6rs\u00f6k igen senare." + "service_unavailable_error": "Programmeringsgr\u00e4nssnittet g\u00e5r inte att komma \u00e5t f\u00f6r n\u00e4rvarande. F\u00f6rs\u00f6k igen senare.", + "unknown_auth_error": "Ov\u00e4ntat fel" }, "step": { "reauth_confirm": { @@ -13,11 +14,13 @@ }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" }, "data_description": { "username": "Meater Cloud anv\u00e4ndarnamn, vanligtvis en e-postadress." - } + }, + "description": "Konfigurera ditt Meater Cloud-konto." } } } diff --git a/homeassistant/components/miflora/translations/id.json b/homeassistant/components/miflora/translations/id.json new file mode 100644 index 00000000000..643d41c9fbd --- /dev/null +++ b/homeassistant/components/miflora/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integrasi Mi Flora berhenti bekerja di Home Assistant 2022.7 dan digantikan oleh integrasi Xiaomi BLE dalam rilis 2022.8.\n\nTidak ada jalur migrasi yang bisa dilakukan, oleh karena itu, Anda harus menambahkan perangkat Mi Flora Anda menggunakan integrasi baru secara manual.\n\nKonfigurasi Mi Flora YAML Anda yang ada tidak lagi digunakan oleh Home Assistant. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi Mi Flora telah diganti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/ja.json b/homeassistant/components/miflora/translations/ja.json new file mode 100644 index 00000000000..30b2730980b --- /dev/null +++ b/homeassistant/components/miflora/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "MiFlora\u306e\u7d71\u5408\u306f\u7f6e\u304d\u63db\u3048\u3089\u308c\u307e\u3057\u305f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/nl.json b/homeassistant/components/miflora/translations/nl.json new file mode 100644 index 00000000000..5189996b75b --- /dev/null +++ b/homeassistant/components/miflora/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "De Mi Flora-integratie is vervangen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/ru.json b/homeassistant/components/miflora/translations/ru.json new file mode 100644 index 00000000000..b5bf7bfd3c1 --- /dev/null +++ b/homeassistant/components/miflora/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0431\u044b\u043b\u0430 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Mi Flora \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/sv.json b/homeassistant/components/miflora/translations/sv.json new file mode 100644 index 00000000000..bff3f7f785e --- /dev/null +++ b/homeassistant/components/miflora/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Mi Flora-integrationen slutade fungera i Home Assistant 2022.7 och ersattes av Xiaomi BLE-integrationen i 2022.8-versionen. \n\n Det finns ingen migreringsv\u00e4g m\u00f6jlig, d\u00e4rf\u00f6r m\u00e5ste du l\u00e4gga till din Mi Flora-enhet med den nya integrationen manuellt. \n\n Din befintliga Mi Flora YAML-konfiguration anv\u00e4nds inte l\u00e4ngre av Home Assistant. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Mi Flora-integrationen har ersatts" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/sv.json b/homeassistant/components/min_max/translations/sv.json index d39c277daff..7a48b0e631b 100644 --- a/homeassistant/components/min_max/translations/sv.json +++ b/homeassistant/components/min_max/translations/sv.json @@ -1,9 +1,22 @@ { + "config": { + "step": { + "user": { + "data_description": { + "round_digits": "Styr antalet decimalsiffror i utg\u00e5ngen n\u00e4r statistikegenskapen \u00e4r medelv\u00e4rde eller median." + }, + "title": "L\u00e4gg till min / max / medelv\u00e4rde / mediansensor" + } + } + }, "options": { "step": { "init": { "data": { "round_digits": "Precision" + }, + "data_description": { + "round_digits": "Styr antalet decimalsiffror i utg\u00e5ngen n\u00e4r statistikegenskapen \u00e4r medelv\u00e4rde eller median." } } } diff --git a/homeassistant/components/mitemp_bt/translations/id.json b/homeassistant/components/mitemp_bt/translations/id.json new file mode 100644 index 00000000000..ae334e0e1ab --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integrasi Sensor Temperatur dan Kelembaban Xiaomi Mijia BLE berhenti bekerja di Home Assistant 2022.7 dan digantikan oleh integrasi Xiaomi BLE dalam rilis 2022.8.\n\nTidak ada jalur migrasi yang bisa dilakukan, oleh karena itu, Anda harus menambahkan perangkat Mi Flora Anda menggunakan integrasi baru secara manual.\n\nKonfigurasi Sensor Temperatur dan Kelembaban Xiaomi Mijia BLE Anda yang ada tidak lagi digunakan oleh Home Assistant. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi Sensor Temperatur dan Kelembaban BLE Xiaomi Mijia telah diganti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/nl.json b/homeassistant/components/mitemp_bt/translations/nl.json new file mode 100644 index 00000000000..e75f4eba261 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "De Xiaomi Mijia BLE Temperature and Humidity Sensor-integratie is vervangen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/ru.json b/homeassistant/components/mitemp_bt/translations/ru.json new file mode 100644 index 00000000000..e25532777b8 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Mijia BLE \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/sv.json b/homeassistant/components/mitemp_bt/translations/sv.json new file mode 100644 index 00000000000..e708454e100 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Xiaomi Mijia BLE temperatur- och fuktsensorintegreringen slutade fungera i Home Assistant 2022.7 och ersattes av Xiaomi BLE-integrationen i 2022.8-versionen. \n\n Det finns ingen migreringsv\u00e4g m\u00f6jlig, d\u00e4rf\u00f6r m\u00e5ste du l\u00e4gga till din Xiaomi Mijia BLE-enhet med den nya integrationen manuellt. \n\n Din befintliga Xiaomi Mijia BLE temperatur- och luftfuktighetssensor YAML-konfiguration anv\u00e4nds inte l\u00e4ngre av Home Assistant. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Xiaomi Mijia BLE temperatur- och fuktsensorintegrering har ersatts" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/nl.json b/homeassistant/components/moat/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/moat/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/sv.json b/homeassistant/components/moat/translations/sv.json new file mode 100644 index 00000000000..8c794885dd2 --- /dev/null +++ b/homeassistant/components/moat/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enhet \u00e4r redan konfigurerad", + "already_in_progress": "Konfiguration redan ig\u00e5ng", + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du s\u00e4tta upp {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att st\u00e4lla in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index b3088ca49a9..1699051db86 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -36,6 +36,16 @@ "button_6": "Sj\u00e4tte knappen", "turn_off": "St\u00e4ng av", "turn_on": "Starta" + }, + "trigger_type": { + "button_double_press": "\"{subtyp}\" dubbelklickad", + "button_long_press": "\" {subtype} \" kontinuerligt nedtryckt", + "button_long_release": "\" {subtype} \" sl\u00e4pptes efter l\u00e5ng tryckning", + "button_quadruple_press": "\"{subtyp}\" fyrdubbelt klickad", + "button_quintuple_press": "\"{subtype}\" kvintubbel klickade", + "button_short_press": "\"{subtyp}\" tryckt", + "button_short_release": "\"{subtyp}\" sl\u00e4pptes", + "button_triple_press": "\" {subtype}\" trippelklickad" } }, "options": { diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 9e91f3ddf94..750a7643ee2 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "F\u00f6lj [instruktionerna]( {more_info_url} ) f\u00f6r att konfigurera Cloud Console: \n\n 1. G\u00e5 till [OAuth-samtyckessk\u00e4rmen]( {oauth_consent_url} ) och konfigurera\n 1. G\u00e5 till [Inloggningsuppgifter]( {oauth_creds_url} ) och klicka p\u00e5 **Skapa inloggningsuppgifter**.\n 1. V\u00e4lj **OAuth-klient-ID** i rullgardinsmenyn.\n 1. V\u00e4lj **Webbapplikation** f\u00f6r applikationstyp.\n 1. L\u00e4gg till ` {redirect_url} ` under *Auktoriserad omdirigerings-URI*." + }, "config": { "abort": { "already_configured": "Konto har redan konfigurerats", @@ -10,6 +13,32 @@ "unknown": "Ok\u00e4nt fel vid validering av kod" }, "step": { + "auth_upgrade": { + "description": "App Auth har fasats ut av Google f\u00f6r att f\u00f6rb\u00e4ttra s\u00e4kerheten, och du m\u00e5ste vidta \u00e5tg\u00e4rder genom att skapa nya applikationsuppgifter. \n\n \u00d6ppna [dokumentationen]( {more_info_url} ) f\u00f6r att f\u00f6lja med eftersom n\u00e4sta steg guidar dig genom stegen du beh\u00f6ver ta f\u00f6r att \u00e5terst\u00e4lla \u00e5tkomsten till dina Nest-enheter.", + "title": "Nest: Utfasning av appautentisering" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Projekt-ID f\u00f6r Google Cloud" + }, + "description": "Ange molnprojekt-ID nedan, t.ex. *example-project-12345*. Se [Google Cloud Console]({cloud_console_url}) eller dokumentationen f\u00f6r [mer information]({more_info_url}).", + "title": "Nest: Ange molnprojekt-ID" + }, + "create_cloud_project": { + "description": "Med Nest-integreringen kan du integrera dina Nest-termostater, kameror och d\u00f6rrklockor med hj\u00e4lp av Smart Device Management API. SDM API **kr\u00e4ver en 5 USD** eng\u00e5ngsavgift f\u00f6r installation. Se dokumentationen f\u00f6r [mer info]( {more_info_url} ). \n\n 1. G\u00e5 till [Google Cloud Console]( {cloud_console_url} ).\n 1. Om detta \u00e4r ditt f\u00f6rsta projekt klickar du p\u00e5 **Skapa projekt** och sedan p\u00e5 **Nytt projekt**.\n 1. Ge ditt molnprojekt ett namn och klicka sedan p\u00e5 **Skapa**.\n 1. Spara Cloud Project ID t.ex. *example-project-12345* som du kommer att beh\u00f6va det senare\n 1. G\u00e5 till API Library f\u00f6r [Smart Device Management API]( {sdm_api_url} ) och klicka p\u00e5 **Aktivera**.\n 1. G\u00e5 till API Library f\u00f6r [Cloud Pub/Sub API]( {pubsub_api_url} ) och klicka p\u00e5 **Aktivera**. \n\n Forts\u00e4tt n\u00e4r ditt molnprojekt har konfigurerats.", + "title": "Nest: Skapa och konfigurera molnprojekt" + }, + "device_project": { + "data": { + "project_id": "Projekt-ID f\u00f6r enhets\u00e5tkomst" + }, + "description": "Skapa ett Nest Device Access-projekt som **kr\u00e4ver en avgift p\u00e5 5 USD** f\u00f6r att konfigurera.\n 1. G\u00e5 till [Device Access Console]( {device_access_console_url} ) och genom betalningsfl\u00f6det.\n 1. Klicka p\u00e5 **Skapa projekt**\n 1. Ge ditt Device Access-projekt ett namn och klicka p\u00e5 **N\u00e4sta**.\n 1. Ange ditt OAuth-klient-ID\n 1. Aktivera h\u00e4ndelser genom att klicka p\u00e5 **Aktivera** och **Skapa projekt**. \n\n Ange ditt Device Access Project ID nedan ([mer info]( {more_info_url} )).\n", + "title": "Nest: Skapa ett projekt f\u00f6r enhets\u00e5tkomst" + }, + "device_project_upgrade": { + "description": "Uppdatera Nest Device Access Project med ditt nya OAuth-klient-ID ([mer info]( {more_info_url} ))\n 1. G\u00e5 till [Device Access Console]( {device_access_console_url} ).\n 1. Klicka p\u00e5 papperskorgen bredvid *OAuth Client ID*.\n 1. Klicka p\u00e5 menyn \"...\" och *L\u00e4gg till klient-ID*.\n 1. Ange ditt nya OAuth-klient-ID och klicka p\u00e5 **L\u00e4gg till**. \n\n Ditt OAuth-klient-ID \u00e4r: ` {client_id} `", + "title": "Nest: Uppdatera Device Access Project" + }, "init": { "data": { "flow_impl": "Leverant\u00f6r" diff --git a/homeassistant/components/nextdns/translations/sv.json b/homeassistant/components/nextdns/translations/sv.json new file mode 100644 index 00000000000..ae0c7c5eeef --- /dev/null +++ b/homeassistant/components/nextdns/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Denna NextDNS-profil \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Misslyckades att ansluta", + "invalid_api_key": "Ogiltig API nyckel", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API nyckel" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 servern" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/sv.json b/homeassistant/components/nina/translations/sv.json new file mode 100644 index 00000000000..11f2e28deac --- /dev/null +++ b/homeassistant/components/nina/translations/sv.json @@ -0,0 +1,24 @@ +{ + "options": { + "error": { + "cannot_connect": "Misslyckades att ansluta", + "no_selection": "V\u00e4lj minst en stad/l\u00e4n", + "unknown": "Ov\u00e4ntatn fel" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Stad/l\u00e4n (AD)", + "_e_to_h": "Stad/l\u00e4n (EH)", + "_i_to_l": "Stad/l\u00e4n (I-L)", + "_m_to_q": "Stad/l\u00e4n (M-Q)", + "_r_to_u": "Stad/l\u00e4n (R-U)", + "_v_to_z": "Stad/l\u00e4n (V-Z)", + "corona_filter": "Ta bort Corona-varningar", + "slots": "Maximala varningar per stad/l\u00e4n" + }, + "title": "Alternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/sv.json b/homeassistant/components/onewire/translations/sv.json index 9b57beabc8f..e4ce4947ca9 100644 --- a/homeassistant/components/onewire/translations/sv.json +++ b/homeassistant/components/onewire/translations/sv.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + }, "options": { "step": { "device_selection": { diff --git a/homeassistant/components/openalpr_local/translations/el.json b/homeassistant/components/openalpr_local/translations/el.json new file mode 100644 index 00000000000..ba56c490298 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 OpenALPR \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 OpenALPR \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/id.json b/homeassistant/components/openalpr_local/translations/id.json new file mode 100644 index 00000000000..1039c96daa1 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasi OpenALPR Local sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nHapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi OpenALPR dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/ja.json b/homeassistant/components/openalpr_local/translations/ja.json new file mode 100644 index 00000000000..dbdd930cd10 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "OpenALPR Local\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/nl.json b/homeassistant/components/openalpr_local/translations/nl.json new file mode 100644 index 00000000000..06bd27e2d56 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "De OpenALPR Local-integratie wordt verwijderd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/sv.json b/homeassistant/components/openalpr_local/translations/sv.json new file mode 100644 index 00000000000..2c02b30458d --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR Local integration v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.10. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "OpenALPR Local integrationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json index c825e3cd616..d88861d15e0 100644 --- a/homeassistant/components/overkiz/translations/sv.json +++ b/homeassistant/components/overkiz/translations/sv.json @@ -4,6 +4,9 @@ "reauth_successful": "\u00c5terautentisering lyckades", "reauth_wrong_account": "Du kan bara \u00e5terautentisera denna post med samma Overkiz-konto och hub" }, + "error": { + "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index 63b12e70e40..4227e45b707 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -36,7 +36,9 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorera nya hanterade/delade anv\u00e4ndare", "ignore_plex_web_clients": "Ignorera Plex Web-klienter", + "monitored_users": "\u00d6vervakade anv\u00e4ndare", "use_episode_art": "Anv\u00e4nd avsnittsbild" }, "description": "Alternativ f\u00f6r Plex-mediaspelare" diff --git a/homeassistant/components/plugwise/translations/sv.json b/homeassistant/components/plugwise/translations/sv.json index affb73f907e..a22c1e882bc 100644 --- a/homeassistant/components/plugwise/translations/sv.json +++ b/homeassistant/components/plugwise/translations/sv.json @@ -1,9 +1,14 @@ { "config": { + "abort": { + "anna_with_adam": "B\u00e5de Anna och Adam uppt\u00e4ckte. L\u00e4gg till din Adam ist\u00e4llet f\u00f6r din Anna" + }, "step": { "user": { "data": { - "port": "Port" + "password": "Smile ID", + "port": "Port", + "username": "Smile Anv\u00e4ndarnamn" } }, "user_gateway": { diff --git a/homeassistant/components/qnap_qsw/translations/sv.json b/homeassistant/components/qnap_qsw/translations/sv.json index ec6c8842dca..f447af18c52 100644 --- a/homeassistant/components/qnap_qsw/translations/sv.json +++ b/homeassistant/components/qnap_qsw/translations/sv.json @@ -8,6 +8,12 @@ "invalid_auth": "Ogiltig autentisering" }, "step": { + "discovered_connection": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, "user": { "data": { "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/radiotherm/translations/id.json b/homeassistant/components/radiotherm/translations/id.json index 1e454cc8cc8..21198e3fad1 100644 --- a/homeassistant/components/radiotherm/translations/id.json +++ b/homeassistant/components/radiotherm/translations/id.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi platform cuaca Radio Thermostat lewat YAML dalam proses penghapusan di Home Assistant 2022.9.\n\nKonfigurasi yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML platform cuaca Radio Thermostat dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Radio Thermostat dalam proses penghapusan" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/ja.json b/homeassistant/components/radiotherm/translations/ja.json index b792d0a1c9b..67f667bacc1 100644 --- a/homeassistant/components/radiotherm/translations/ja.json +++ b/homeassistant/components/radiotherm/translations/ja.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "Radio Thermostat YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/sv.json b/homeassistant/components/radiotherm/translations/sv.json index f341a6314ee..3999a2a90e0 100644 --- a/homeassistant/components/radiotherm/translations/sv.json +++ b/homeassistant/components/radiotherm/translations/sv.json @@ -8,11 +8,29 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "confirm": { + "description": "Vill du konfigurera {name} {model} ( {host} )?" + }, "user": { "data": { "host": "V\u00e4rd" } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av radiotermostatens klimatplattform med YAML tas bort i Home Assistant 2022.9. \n\n Din befintliga konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Radiotermostatens YAML-konfiguration tas bort" + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "St\u00e4ll in en permanent h\u00e5llning n\u00e4r du justerar temperaturen." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/sv.json b/homeassistant/components/recorder/translations/sv.json index bf4d6ccf0a5..0caafb1ee1b 100644 --- a/homeassistant/components/recorder/translations/sv.json +++ b/homeassistant/components/recorder/translations/sv.json @@ -2,6 +2,9 @@ "system_health": { "info": { "current_recorder_run": "Aktuell starttid", + "database_engine": "Databasmotor", + "database_version": "Databasversion", + "estimated_db_size": "Ber\u00e4knad databasstorlek (MiB)", "oldest_recorder_run": "\u00c4ldsta starttid" } } diff --git a/homeassistant/components/rhasspy/translations/sv.json b/homeassistant/components/rhasspy/translations/sv.json new file mode 100644 index 00000000000..9f3e32535be --- /dev/null +++ b/homeassistant/components/rhasspy/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Enbart en konfiguration \u00e4r m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du aktivera Rhasspy-support?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/sv.json b/homeassistant/components/roon/translations/sv.json new file mode 100644 index 00000000000..420e19171ae --- /dev/null +++ b/homeassistant/components/roon/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "fallback": { + "data": { + "host": "V\u00e4rdnamn", + "port": "Port" + }, + "description": "Kunde inte uppt\u00e4cka Roon-servern, ange ditt v\u00e4rdnamn och port." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/sv.json b/homeassistant/components/sabnzbd/translations/sv.json index 61847ff0eb4..d82e85f8c48 100644 --- a/homeassistant/components/sabnzbd/translations/sv.json +++ b/homeassistant/components/sabnzbd/translations/sv.json @@ -9,7 +9,8 @@ "data": { "api_key": "API Nyckel", "name": "Namn", - "path": "S\u00f6kv\u00e4g" + "path": "S\u00f6kv\u00e4g", + "url": "URL" } } } diff --git a/homeassistant/components/scrape/translations/sv.json b/homeassistant/components/scrape/translations/sv.json index c70f08008dc..f1cf81c5aa6 100644 --- a/homeassistant/components/scrape/translations/sv.json +++ b/homeassistant/components/scrape/translations/sv.json @@ -6,9 +6,22 @@ "step": { "user": { "data": { + "attribute": "Attribut", + "authentication": "Autentisering", + "device_class": "Enhetsklass", + "headers": "Headers", + "index": "Index", "password": "L\u00f6senord", + "resource": "Resurs", + "select": "V\u00e4lj", + "state_class": "Tillst\u00e5ndsklass", + "unit_of_measurement": "M\u00e5ttenhet", "username": "Anv\u00e4ndarnamn", + "value_template": "V\u00e4rdemall", "verify_ssl": "Verifiera SSL-certifikat" + }, + "data_description": { + "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen" } } } @@ -17,7 +30,11 @@ "step": { "init": { "data": { + "attribute": "Attribut", + "authentication": "Autentisering", + "index": "Index", "password": "L\u00f6senord", + "select": "V\u00e4lj", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/sensibo/translations/sensor.sv.json b/homeassistant/components/sensibo/translations/sensor.sv.json index ead64d63cd6..b07d40e18fd 100644 --- a/homeassistant/components/sensibo/translations/sensor.sv.json +++ b/homeassistant/components/sensibo/translations/sensor.sv.json @@ -1,7 +1,8 @@ { "state": { "sensibo__sensitivity": { - "n": "Normal" + "n": "Normal", + "s": "K\u00e4nslighet" } } } \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/nl.json b/homeassistant/components/sensorpush/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/sensorpush/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/sv.json b/homeassistant/components/sensorpush/translations/sv.json new file mode 100644 index 00000000000..7606ba7df45 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/id.json b/homeassistant/components/senz/translations/id.json index a2fdf8837bd..6f4eec8f3b9 100644 --- a/homeassistant/components/senz/translations/id.json +++ b/homeassistant/components/senz/translations/id.json @@ -16,5 +16,11 @@ "title": "Pilih Metode Autentikasi" } } + }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi nVent RAYCHEM SENZ lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML nVent RAYCHEM SENZ telah dihapus" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/ja.json b/homeassistant/components/senz/translations/ja.json index b6aa94ef30c..dbb794e2cff 100644 --- a/homeassistant/components/senz/translations/ja.json +++ b/homeassistant/components/senz/translations/ja.json @@ -16,5 +16,10 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" } } + }, + "issues": { + "removed_yaml": { + "title": "nVent RAYCHEM SENZ YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/nl.json b/homeassistant/components/senz/translations/nl.json index 7f1eaccf89c..f12901ee3d5 100644 --- a/homeassistant/components/senz/translations/nl.json +++ b/homeassistant/components/senz/translations/nl.json @@ -16,5 +16,10 @@ "title": "Kies een authenticatie methode" } } + }, + "issues": { + "removed_yaml": { + "title": "De nVent RAYCHEM SENZ YAML-configuratie is verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/ru.json b/homeassistant/components/senz/translations/ru.json index 2f572831b5b..21c6b830841 100644 --- a/homeassistant/components/senz/translations/ru.json +++ b/homeassistant/components/senz/translations/ru.json @@ -16,5 +16,11 @@ "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" } } + }, + "issues": { + "removed_yaml": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"nVent RAYCHEM SENZ\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 nVent RAYCHEM SENZ \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/sv.json b/homeassistant/components/senz/translations/sv.json new file mode 100644 index 00000000000..9955b604e20 --- /dev/null +++ b/homeassistant/components/senz/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Kontot \u00e4r redan konfigurerat", + "already_in_progress": "Konfiguration \u00e4r redan ig\u00e5ng", + "authorize_url_timeout": "Timout under skapandet av autentiseringsURL:en", + "missing_configuration": "Komponenten \u00e4r inte konfigurerad", + "no_url_available": "Ingen URL tillg\u00e4nglig. F\u00f6r mer information om detta felet [kolla i hj\u00e4lpsektionen]({docs_url})", + "oauth_error": "Mottog felaktig token." + }, + "create_entry": { + "default": "Autentiseringen lyckades" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4j autentiseringsmetod" + } + } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av nVent RAYCHEM SENZ med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "nVent RAYCHEM SENZ YAML-konfigurationen har tagits bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index 36b53053594..21a5ba55b58 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support" + }, "step": { "credentials": { "data": { diff --git a/homeassistant/components/simplepush/translations/el.json b/homeassistant/components/simplepush/translations/el.json index bdcc7239acc..8f8c4691045 100644 --- a/homeassistant/components/simplepush/translations/el.json +++ b/homeassistant/components/simplepush/translations/el.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/id.json b/homeassistant/components/simplepush/translations/id.json index 35984af5d5f..715cb3c893b 100644 --- a/homeassistant/components/simplepush/translations/id.json +++ b/homeassistant/components/simplepush/translations/id.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Simplepush lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Simplepush dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index 8e3023e602e..fd22d7dfef5 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index 176318b3f3c..8916c7db473 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -13,5 +13,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "De Simplepush YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pl.json b/homeassistant/components/simplepush/translations/pl.json index fe19feb39a1..10f83b3401c 100644 --- a/homeassistant/components/simplepush/translations/pl.json +++ b/homeassistant/components/simplepush/translations/pl.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Simplepush przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Simplepush zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/sv.json b/homeassistant/components/simplepush/translations/sv.json new file mode 100644 index 00000000000..9ed6908f0c3 --- /dev/null +++ b/homeassistant/components/simplepush/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Misslyckades att ansluta" + }, + "step": { + "user": { + "data": { + "device_key": "Enhetsnyckeln f\u00f6r din enhet", + "event": "H\u00e4ndelsen f\u00f6r h\u00e4ndelserna.", + "name": "Namn", + "password": "L\u00f6senordet f\u00f6r krypteringen som anv\u00e4nds av din enhet", + "salt": "Det salt som anv\u00e4nds av din enhet." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Simplepush med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Simplepush YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Simplepush YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 68da3b7689b..e9919dc734b 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Akun SimpliSafe ini sudah digunakan.", "email_2fa_timed_out": "Tenggang waktu habis ketika menunggu autentikasi dua faktor berbasis email.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "wrong_account": "Kredensial pengguna yang diberikan tidak cocok dengan akun SimpliSafe ini." }, "error": { + "identifier_exists": "Akun sudah terdaftar", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Kode Otorisasi", "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "Masukkan nama pengguna dan kata sandi Anda." + "description": "SimpliSafe mengautentikasi pengguna melalui aplikasi webnya. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan bahwa Anda membaca [dokumentasi] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) sebelum memulai.\n\nJika sudah siap, klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan memasukkan kredensial Anda. Setelah proses selesai, kembali ke sini dan masukkan kode otorisasi dari URL aplikasi web SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index e264ec18aab..4e65fe8a057 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u3053\u306eSimpliSafe account\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "email_2fa_timed_out": "\u96fb\u5b50\u30e1\u30fc\u30eb\u306b\u3088\u308b2\u8981\u7d20\u8a8d\u8a3c\u306e\u5f85\u6a5f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "wrong_account": "\u63d0\u4f9b\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u60c5\u5831\u306f\u3001\u3053\u306eSimpliSafe\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, "error": { + "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -28,6 +30,7 @@ }, "user": { "data": { + "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" }, diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index 2b2d675f2b5..6a3d08b799c 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats." + "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats.", + "email_2fa_timed_out": "Tidsgr\u00e4nsen tog slut i v\u00e4ntan p\u00e5 tv\u00e5faktorsautentisering", + "wrong_account": "De angivna anv\u00e4ndaruppgifterna matchar inte detta SimpliSafe-konto." + }, + "error": { + "identifier_exists": "Kontot \u00e4r redan registrerat" }, "progress": { "email_2fa": "Kontrollera din e-post f\u00f6r en verifieringsl\u00e4nk fr\u00e5n Simplisafe." @@ -15,6 +20,7 @@ }, "user": { "data": { + "auth_code": "Auktoriseringskod", "password": "L\u00f6senord", "username": "E-postadress" } diff --git a/homeassistant/components/siren/translations/sv.json b/homeassistant/components/siren/translations/sv.json new file mode 100644 index 00000000000..549bad8914b --- /dev/null +++ b/homeassistant/components/siren/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Siren" +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/sv.json b/homeassistant/components/slack/translations/sv.json index 34e67b311a2..eb285934e7e 100644 --- a/homeassistant/components/slack/translations/sv.json +++ b/homeassistant/components/slack/translations/sv.json @@ -12,8 +12,17 @@ "user": { "data": { "api_key": "API-nyckel", + "default_channel": "Standardkanal", + "icon": "Ikon", "username": "Anv\u00e4ndarnamn" - } + }, + "data_description": { + "api_key": "Slack API-token som ska anv\u00e4ndas f\u00f6r att skicka Slack-meddelanden.", + "default_channel": "Kanalen att posta till om ingen kanal anges n\u00e4r ett meddelande skickas.", + "icon": "Anv\u00e4nd en av Slack-emojis som en ikon f\u00f6r det angivna anv\u00e4ndarnamnet.", + "username": "Home Assistant kommer att skicka inl\u00e4gg till Slack med det angivna anv\u00e4ndarnamnet." + }, + "description": "Se dokumentationen om hur du skaffar din Slack API-nyckel." } } } diff --git a/homeassistant/components/sms/translations/sv.json b/homeassistant/components/sms/translations/sv.json new file mode 100644 index 00000000000..0020bfcfc37 --- /dev/null +++ b/homeassistant/components/sms/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "baud_speed": "Baud-hastighet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/el.json b/homeassistant/components/soundtouch/translations/el.json index 7346aeca660..b2eea10cb86 100644 --- a/homeassistant/components/soundtouch/translations/el.json +++ b/homeassistant/components/soundtouch/translations/el.json @@ -17,5 +17,11 @@ "title": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Bose SoundTouch \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Bose SoundTouch YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Bose SoundTouch YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json index ce2c8f4e1a3..b5114dcb398 100644 --- a/homeassistant/components/soundtouch/translations/id.json +++ b/homeassistant/components/soundtouch/translations/id.json @@ -17,5 +17,11 @@ "title": "Konfirmasi penambahan perangkat Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Bose SoundTouch lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Bose SoundTouch dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Bose SoundTouch dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ja.json b/homeassistant/components/soundtouch/translations/ja.json index c4a94a23a7e..9bc5e427baf 100644 --- a/homeassistant/components/soundtouch/translations/ja.json +++ b/homeassistant/components/soundtouch/translations/ja.json @@ -17,5 +17,10 @@ "title": "Bose SoundTouch\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" } } + }, + "issues": { + "deprecated_yaml": { + "title": "Bose SoundTouch YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/nl.json b/homeassistant/components/soundtouch/translations/nl.json index 0ccc8057ac8..8328756da76 100644 --- a/homeassistant/components/soundtouch/translations/nl.json +++ b/homeassistant/components/soundtouch/translations/nl.json @@ -13,5 +13,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "De Bose SoundTouch YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pl.json b/homeassistant/components/soundtouch/translations/pl.json index 10a760e1acf..4dad61aa103 100644 --- a/homeassistant/components/soundtouch/translations/pl.json +++ b/homeassistant/components/soundtouch/translations/pl.json @@ -17,5 +17,11 @@ "title": "Potwierd\u017a dodanie urz\u0105dzenia Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Bose SoundTouch przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Bose SoundTouch zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/sv.json b/homeassistant/components/soundtouch/translations/sv.json new file mode 100644 index 00000000000..2415acd1993 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + }, + "zeroconf_confirm": { + "description": "Du h\u00e5ller p\u00e5 att l\u00e4gga till SoundTouch-enheten med namnet ` {name} ` till Home Assistant.", + "title": "Bekr\u00e4fta att du l\u00e4gger till Bose SoundTouch-enhet" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Bose SoundTouch med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Bose SoundTouch YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Bose SoundTouch YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/id.json b/homeassistant/components/spotify/translations/id.json index f75f4159a96..ef201bc638f 100644 --- a/homeassistant/components/spotify/translations/id.json +++ b/homeassistant/components/spotify/translations/id.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Spotify lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Spotify telah dihapus" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Titik akhir API Spotify dapat dijangkau" diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index 4a65f5037dd..ae5f52b68a2 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "removed_yaml": { + "title": "Spotify YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u53ef\u80fd" diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json index 2e478de73ab..710ec3deb7a 100644 --- a/homeassistant/components/spotify/translations/nl.json +++ b/homeassistant/components/spotify/translations/nl.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "removed_yaml": { + "title": "De Spotify YAML-configuratie wordt verwijderd" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API-eindpunt is bereikbaar" diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index 35918b2634f..869d839947c 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -21,7 +21,7 @@ }, "issues": { "removed_yaml": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Spotify \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Spotify \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } }, diff --git a/homeassistant/components/spotify/translations/sv.json b/homeassistant/components/spotify/translations/sv.json index 55f94e6b717..0a64cad7a65 100644 --- a/homeassistant/components/spotify/translations/sv.json +++ b/homeassistant/components/spotify/translations/sv.json @@ -12,5 +12,11 @@ "title": "V\u00e4lj autentiseringsmetod." } } + }, + "issues": { + "removed_yaml": { + "description": "Att konfigurera Spotify med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Spotify YAML-konfigurationen har tagits bort" + } } } \ No newline at end of file diff --git a/homeassistant/components/sql/translations/sv.json b/homeassistant/components/sql/translations/sv.json index 25bd828223f..9009cac8ee3 100644 --- a/homeassistant/components/sql/translations/sv.json +++ b/homeassistant/components/sql/translations/sv.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Konto \u00e4r redan konfigurerat" + }, + "error": { + "db_url_invalid": "Databasens URL \u00e4r ogiltig", + "query_invalid": "SQL fr\u00e5ga \u00e4r ogiltig" + }, "step": { "user": { "data": { + "column": "Kolumn", "name": "Namn" + }, + "data_description": { + "name": "Namn som kommer att anv\u00e4ndas f\u00f6r konfigurationsinmatning och \u00e4ven f\u00f6r sensorn." } } } diff --git a/homeassistant/components/steam_online/translations/id.json b/homeassistant/components/steam_online/translations/id.json index e944662fee1..07130a2a7da 100644 --- a/homeassistant/components/steam_online/translations/id.json +++ b/homeassistant/components/steam_online/translations/id.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Steam lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Steam telah dihapus" + } + }, "options": { "error": { "unauthorized": "Daftar teman dibatasi: Rujuk ke dokumentasi tentang cara melihat semua teman lain" diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index f62fd45e767..46c3eeb7d22 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -24,6 +24,11 @@ } } }, + "issues": { + "removed_yaml": { + "title": "Steam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + }, "options": { "error": { "unauthorized": "\u30d5\u30ec\u30f3\u30c9\u30ea\u30b9\u30c8\u306e\u5236\u9650: \u4ed6\u306e\u3059\u3079\u3066\u306e\u30d5\u30ec\u30f3\u30c9\u3092\u8868\u793a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044" diff --git a/homeassistant/components/steam_online/translations/nl.json b/homeassistant/components/steam_online/translations/nl.json index 5512c18ee2b..85a2b020730 100644 --- a/homeassistant/components/steam_online/translations/nl.json +++ b/homeassistant/components/steam_online/translations/nl.json @@ -24,6 +24,11 @@ } } }, + "issues": { + "removed_yaml": { + "title": "De Steam YAML-configuratie wordt verwijderd" + } + }, "options": { "error": { "unauthorized": "Vriendenlijst beperkt: raadpleeg de documentatie over hoe je alle andere vrienden kunt zien" diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index b69b1a72696..828b66f1dc8 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Steam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } }, diff --git a/homeassistant/components/steam_online/translations/sv.json b/homeassistant/components/steam_online/translations/sv.json index 51c39c12b35..8a087ebd3a7 100644 --- a/homeassistant/components/steam_online/translations/sv.json +++ b/homeassistant/components/steam_online/translations/sv.json @@ -1,18 +1,39 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "Omautentiseringen lyckades" + }, + "error": { + "cannot_connect": "Misslyckades att ansluta", + "invalid_account": "Ogiltigt konto-ID", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { - "description": "Steam-integrationen m\u00e5ste autentiseras p\u00e5 nytt manuellt\n\nDu hittar din nyckel h\u00e4r: {api_key_url}" + "description": "Steam-integrationen m\u00e5ste autentiseras p\u00e5 nytt manuellt\n\nDu hittar din nyckel h\u00e4r: {api_key_url}", + "title": "Om autentisera integration" }, "user": { "data": { + "account": "Steam-konto-ID", "api_key": "API Nyckel" }, "description": "Anv\u00e4nd {account_id_url} f\u00f6r att hitta ditt Steam-konto-ID" } } }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Steam med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Steam YAML-konfigurationen har tagits bort" + } + }, "options": { + "error": { + "unauthorized": "Begr\u00e4nsad v\u00e4nlista: Se dokumentationen om hur du ser alla andra v\u00e4nner" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index f3a9cd169ef..f7baed8c8db 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Jenis Switchbot yang tidak didukung.", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Alamat perangkat", "mac": "Alamat MAC perangkat", "name": "Nama", "password": "Kata Sandi" diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index 91d87431774..3f9425d179a 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "address": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30c9\u30ec\u30b9", "mac": "\u30c7\u30d0\u30a4\u30b9\u306eMAC\u30a2\u30c9\u30ec\u30b9", "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json new file mode 100644 index 00000000000..6b1608c9da6 --- /dev/null +++ b/homeassistant/components/switchbot/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "address": "Enhetsadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/sv.json b/homeassistant/components/tankerkoenig/translations/sv.json index 4b9b566c7d1..f183c4ccb29 100644 --- a/homeassistant/components/tankerkoenig/translations/sv.json +++ b/homeassistant/components/tankerkoenig/translations/sv.json @@ -1,17 +1,44 @@ { "config": { "abort": { + "already_configured": "Plats \u00e4r redan konfigurerad", "reauth_successful": "\u00c5terautentisering lyckades" }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "no_stations": "Det gick inte att hitta n\u00e5gon station inom r\u00e4ckh\u00e5ll." + }, "step": { "reauth_confirm": { "data": { "api_key": "API-nyckel" } }, + "select_station": { + "data": { + "stations": "Stationer" + }, + "description": "hittade {stations_count} stationer i detta omr\u00e5det", + "title": "V\u00e4lj stationer att l\u00e4gga till" + }, "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "fuel_types": "Br\u00e4nsletyper", + "location": "Plats", + "name": "Regionens namn", + "radius": "S\u00f6kradie", + "stations": "Ytterligare bensinstationer" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsintervall", + "stations": "Stationer" } } } diff --git a/homeassistant/components/tautulli/translations/sv.json b/homeassistant/components/tautulli/translations/sv.json index 0058ba541bf..66b23701c73 100644 --- a/homeassistant/components/tautulli/translations/sv.json +++ b/homeassistant/components/tautulli/translations/sv.json @@ -2,12 +2,19 @@ "config": { "step": { "reauth_confirm": { - "description": "F\u00f6r att hitta din API-nyckel, \u00f6ppna Tautullis webbsida och navigera till Inst\u00e4llningar och sedan till webbgr\u00e4nssnitt. API-nyckeln finns l\u00e4ngst ner p\u00e5 sidan." + "data": { + "api_key": "API-nyckel" + }, + "description": "F\u00f6r att hitta din API-nyckel, \u00f6ppna Tautullis webbsida och navigera till Inst\u00e4llningar och sedan till webbgr\u00e4nssnitt. API-nyckeln finns l\u00e4ngst ner p\u00e5 sidan.", + "title": "\u00c5terautenticera Tautulli" }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "url": "URL", + "verify_ssl": "Verifiera SSL certifikat" + }, + "description": "F\u00f6r att hitta din API-nyckel, \u00f6ppna Tautullis webbsida och navigera till Inst\u00e4llningar och sedan till webbgr\u00e4nssnittet. API-nyckeln finns l\u00e4ngst ner p\u00e5 sidan. \n\n Exempel p\u00e5 URL: ```http://192.168.0.10:8181``` med 8181 som standardport." } } } diff --git a/homeassistant/components/tod/translations/sv.json b/homeassistant/components/tod/translations/sv.json new file mode 100644 index 00000000000..79e74cf3935 --- /dev/null +++ b/homeassistant/components/tod/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Tider p\u00e5 dagen sensor" +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sv.json b/homeassistant/components/tomorrowio/translations/sv.json index f4a63bb449d..15498795844 100644 --- a/homeassistant/components/tomorrowio/translations/sv.json +++ b/homeassistant/components/tomorrowio/translations/sv.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "location": "Plats" } } } diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json index bff6edd489e..8504a696619 100644 --- a/homeassistant/components/totalconnect/translations/sv.json +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -11,5 +11,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Automatisk f\u00f6rbikoppling av l\u00e5gt batteri" + }, + "description": "Koppla automatiskt f\u00f6rbi zoner n\u00e4r de rapporterar l\u00e5gt batteri.", + "title": "TotalConnect-alternativ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/sv.json b/homeassistant/components/trafikverket_ferry/translations/sv.json index ad82dbe221d..1abd7dcb6d4 100644 --- a/homeassistant/components/trafikverket_ferry/translations/sv.json +++ b/homeassistant/components/trafikverket_ferry/translations/sv.json @@ -1,7 +1,14 @@ { "config": { + "abort": { + "already_configured": "Konto \u00e4r redan konfigurerat", + "reauth_successful": "Omautentiseringen lyckades" + }, "error": { - "invalid_auth": "Ogiltig autentisering" + "cannot_connect": "Misslyckades att ansluta", + "incorrect_api_key": "Ogiltig API-nyckel f\u00f6r valt konto", + "invalid_auth": "Ogiltig autentisering", + "invalid_route": "Kunde inte hitta rutt med tillhandah\u00e5llen information" }, "step": { "reauth_confirm": { @@ -11,7 +18,11 @@ }, "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "from": "Fr\u00e5n hamn", + "time": "Tid", + "to": "Till hamnen", + "weekday": "Veckodagar" } } } diff --git a/homeassistant/components/trafikverket_train/translations/sv.json b/homeassistant/components/trafikverket_train/translations/sv.json index 5ad5b5b6db4..b6d7ecada04 100644 --- a/homeassistant/components/trafikverket_train/translations/sv.json +++ b/homeassistant/components/trafikverket_train/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "error": { + "incorrect_api_key": "Ogiltig API-nyckel f\u00f6r valt konto", + "invalid_auth": "Ogiltig autentisering", + "invalid_station": "Det gick inte att hitta en station med det angivna namnet", + "invalid_time": "Ogiltig tid har angetts", + "more_stations": "Hittade flera stationer med det angivna namnet" + }, "step": { "reauth_confirm": { "data": { @@ -8,7 +15,11 @@ }, "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "from": "Fr\u00e5n station", + "time": "Tid (valfritt)", + "to": "Till station", + "weekday": "Dagar" } } } diff --git a/homeassistant/components/transmission/translations/sv.json b/homeassistant/components/transmission/translations/sv.json index 0ffbebed9f6..79b40b78ff5 100644 --- a/homeassistant/components/transmission/translations/sv.json +++ b/homeassistant/components/transmission/translations/sv.json @@ -13,6 +13,7 @@ "data": { "password": "L\u00f6senord" }, + "description": "L\u00f6senordet f\u00f6r {username} \u00e4r ogiltigt.", "title": "\u00c5terautenticera integration" }, "user": { diff --git a/homeassistant/components/ukraine_alarm/translations/sv.json b/homeassistant/components/ukraine_alarm/translations/sv.json index 4a9945525c8..cd280080774 100644 --- a/homeassistant/components/ukraine_alarm/translations/sv.json +++ b/homeassistant/components/ukraine_alarm/translations/sv.json @@ -1,8 +1,25 @@ { "config": { "abort": { + "already_configured": "Plats \u00e4r redan konfigurerad", "cannot_connect": "Det gick inte att ansluta.", + "max_regions": "Max 5 regioner kan konfigureras", + "rate_limit": "F\u00f6r mycket f\u00f6rfr\u00e5gningar", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "community": { + "data": { + "region": "Region" + }, + "description": "Om du inte bara vill \u00f6vervaka stat och distrikt, v\u00e4lj dess specifika gemenskap" + }, + "district": { + "description": "Om du inte bara vill \u00f6vervaka staten, v\u00e4lj dess specifika distrikt" + }, + "user": { + "description": "V\u00e4lj tillst\u00e5nd att \u00f6vervaka" + } } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index a84558a6b58..bea79d27094 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -33,10 +33,12 @@ "device_tracker": { "data": { "detection_time": "Tid i sekunder fr\u00e5n senast sett tills den anses borta", + "ssid_filter": "V\u00e4lj SSID att sp\u00e5ra tr\u00e5dl\u00f6sa klienter p\u00e5", "track_clients": "Sp\u00e5ra n\u00e4tverksklienter", "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)", "track_wired_clients": "Inkludera tr\u00e5dbundna n\u00e4tverksklienter" }, + "description": "Konfigurera enhetssp\u00e5rning", "title": "UniFi-inst\u00e4llningar 1/3" }, "init": { @@ -52,6 +54,7 @@ "data": { "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter" }, + "description": "Konfigurera statistiksensorer", "title": "UniFi-inst\u00e4llningar 2/3" } } diff --git a/homeassistant/components/uscis/translations/nl.json b/homeassistant/components/uscis/translations/nl.json new file mode 100644 index 00000000000..b4f2be41ed0 --- /dev/null +++ b/homeassistant/components/uscis/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "De USCIS-integratie wordt verwijderd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/sv.json b/homeassistant/components/uscis/translations/sv.json new file mode 100644 index 00000000000..a139ce02491 --- /dev/null +++ b/homeassistant/components/uscis/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrationen av US Citizenship and Immigration Services (USCIS) v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.10. \n\n Integrationen tas bort eftersom den f\u00f6rlitar sig p\u00e5 webbskrapning, vilket inte \u00e4r till\u00e5tet. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "USCIS-integrationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/sv.json b/homeassistant/components/verisure/translations/sv.json index 3d3dbdb8bda..1113a387339 100644 --- a/homeassistant/components/verisure/translations/sv.json +++ b/homeassistant/components/verisure/translations/sv.json @@ -1,14 +1,27 @@ { "config": { "error": { - "unknown": "Ov\u00e4ntat fel" + "unknown": "Ov\u00e4ntat fel", + "unknown_mfa": "Ett ok\u00e4nt fel har intr\u00e4ffat under konfiguration av MFA" }, "step": { + "mfa": { + "data": { + "code": "Verifieringskod", + "description": "Ditt konto har 2-stegsverifiering aktiverat. Skriv in verifieringskoden Verisure skickar till dig." + } + }, "reauth_confirm": { "data": { "password": "L\u00f6senord" } }, + "reauth_mfa": { + "data": { + "code": "Verifieringskod", + "description": "Ditt konto har 2-stegsverifiering aktiverat. Skriv in verifieringskoden Verisure skickar till dig." + } + }, "user": { "data": { "password": "L\u00f6senord" diff --git a/homeassistant/components/vulcan/translations/sv.json b/homeassistant/components/vulcan/translations/sv.json new file mode 100644 index 00000000000..f62155f1fd3 --- /dev/null +++ b/homeassistant/components/vulcan/translations/sv.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "no_matching_entries": "Inga matchande poster hittades, anv\u00e4nd ett annat konto eller ta bort integration med f\u00f6r\u00e5ldrad student.." + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Anv\u00e4nd sparade autentiseringsuppgifter" + }, + "description": "L\u00e4gg till ytterligare en elev." + }, + "auth": { + "data": { + "pin": "Knappn\u00e5l", + "region": "Symbol", + "token": "Token" + }, + "description": "Logga in p\u00e5 ditt Vulcan-konto med registreringssidan f\u00f6r mobilappen." + }, + "reauth_confirm": { + "data": { + "pin": "Knappn\u00e5l", + "region": "Symbol", + "token": "Token" + }, + "description": "Logga in p\u00e5 ditt Vulcan-konto med registreringssidan f\u00f6r mobilappen." + }, + "select_saved_credentials": { + "data": { + "credentials": "Logga in" + }, + "description": "V\u00e4lj sparade autentiseringsuppgifter." + }, + "select_student": { + "data": { + "student_name": "V\u00e4lj elev" + }, + "description": "V\u00e4lj elev, du kan l\u00e4gga till fler elever genom att l\u00e4gga till integration igen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/sv.json b/homeassistant/components/withings/translations/sv.json index c5b6b0fddab..4adb00bfad5 100644 --- a/homeassistant/components/withings/translations/sv.json +++ b/homeassistant/components/withings/translations/sv.json @@ -17,6 +17,10 @@ }, "description": "Vilken profil valde du p\u00e5 Withings webbplats? Det \u00e4r viktigt att profilerna matchar, annars kommer data att vara felm\u00e4rkta.", "title": "Anv\u00e4ndarprofil." + }, + "reauth_confirm": { + "description": "Profilen \" {profile} \" m\u00e5ste autentiseras p\u00e5 nytt f\u00f6r att kunna forts\u00e4tta att ta emot Withings-data.", + "title": "G\u00f6r om autentiseringen f\u00f6r integrationen" } } } diff --git a/homeassistant/components/ws66i/translations/sv.json b/homeassistant/components/ws66i/translations/sv.json new file mode 100644 index 00000000000..9daf43b9987 --- /dev/null +++ b/homeassistant/components/ws66i/translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "title": "Anslut till enheten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Namn p\u00e5 k\u00e4lla #1", + "source_2": "Namn p\u00e5 k\u00e4lla #2", + "source_3": "Namn p\u00e5 k\u00e4lla #3", + "source_4": "Namn p\u00e5 k\u00e4lla #4", + "source_5": "Namn p\u00e5 k\u00e4lla #5", + "source_6": "Namn p\u00e5 k\u00e4lla #6" + }, + "title": "Konfigurera k\u00e4llor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/el.json b/homeassistant/components/xbox/translations/el.json index 93e620b6b5e..4accbbee1ac 100644 --- a/homeassistant/components/xbox/translations/el.json +++ b/homeassistant/components/xbox/translations/el.json @@ -13,5 +13,11 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xbox \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.9. \n\n \u03a4\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 OAuth \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xbox YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/id.json b/homeassistant/components/xbox/translations/id.json index ed8106b0144..3df7f3ee8f2 100644 --- a/homeassistant/components/xbox/translations/id.json +++ b/homeassistant/components/xbox/translations/id.json @@ -13,5 +13,11 @@ "title": "Pilih Metode Autentikasi" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Xbox di configuration.yaml sedang dihapus di Home Assistant 2022.9. \n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Xbox dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index 2d1e95019b7..f1c31b6c64c 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -13,5 +13,10 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" } } + }, + "issues": { + "deprecated_yaml": { + "title": "Xbox YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/nl.json b/homeassistant/components/xbox/translations/nl.json index 9d68863cc27..3544fe9f90e 100644 --- a/homeassistant/components/xbox/translations/nl.json +++ b/homeassistant/components/xbox/translations/nl.json @@ -13,5 +13,10 @@ "title": "Kies een authenticatie methode" } } + }, + "issues": { + "deprecated_yaml": { + "title": "De Xbox YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/pl.json b/homeassistant/components/xbox/translations/pl.json index b4a5c4d8f38..e035422de62 100644 --- a/homeassistant/components/xbox/translations/pl.json +++ b/homeassistant/components/xbox/translations/pl.json @@ -13,5 +13,11 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Xbox w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2022.9. \n\nTwoje istniej\u0105ce po\u015bwiadczenia aplikacji OAuth i ustawienia dost\u0119pu zosta\u0142y automatycznie zaimportowane do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Xbox zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/sv.json b/homeassistant/components/xbox/translations/sv.json new file mode 100644 index 00000000000..525e91ded0e --- /dev/null +++ b/homeassistant/components/xbox/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Xbox i configuration.yaml tas bort i Home Assistant 2022.9. \n\n Dina befintliga OAuth-applikationsuppgifter och \u00e5tkomstinst\u00e4llningar har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Xbox YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json index 07426a0e290..ea45a7ba9c3 100644 --- a/homeassistant/components/xiaomi_ble/translations/id.json +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", + "decryption_failed": "Bindkey yang disediakan tidak berfungsi, data sensor tidak dapat didekripsi. Silakan periksa dan coba lagi.", + "expected_24_characters": "Diharapkan bindkey berupa 24 karakter heksadesimal.", + "expected_32_characters": "Diharapkan bindkey berupa 32 karakter heksadesimal.", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Ingin menyiapkan {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Data sensor yang disiarkan oleh sensor telah dienkripsi. Untuk mendekripsinya, diperlukan 32 karakter bindkey heksadesimal ." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Data sensor yang disiarkan oleh sensor telah dienkripsi. Untuk mendekripsinya, diperlukan 24 karakter bindkey heksadesimal ." + }, "user": { "data": { "address": "Perangkat" diff --git a/homeassistant/components/xiaomi_ble/translations/ja.json b/homeassistant/components/xiaomi_ble/translations/ja.json index 38f862bd2f6..d15f89bf9d3 100644 --- a/homeassistant/components/xiaomi_ble/translations/ja.json +++ b/homeassistant/components/xiaomi_ble/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "decryption_failed": "\u63d0\u4f9b\u3055\u308c\u305f\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u6a5f\u80fd\u305b\u305a\u3001\u30bb\u30f3\u30b5\u30fc \u30c7\u30fc\u30bf\u3092\u5fa9\u53f7\u5316\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "expected_24_characters": "24\u6587\u5b57\u306716\u9032\u6570\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", + "expected_32_characters": "32\u6587\u5b57\u304b\u3089\u306a\u308b16\u9032\u6570\u306e\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u304b\u3089\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3055\u308c\u308b\u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\u306f\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5fa9\u53f7\u5316\u3059\u308b\u306b\u306f\u300116\u9032\u6570\u306732\u6587\u5b57\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u304b\u3089\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3055\u308c\u308b\u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\u306f\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5fa9\u53f7\u5316\u3059\u308b\u306b\u306f\u300116\u9032\u6570\u306724\u6587\u5b57\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" + }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/xiaomi_ble/translations/nl.json b/homeassistant/components/xiaomi_ble/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/sv.json b/homeassistant/components/xiaomi_ble/translations/sv.json new file mode 100644 index 00000000000..68373aca702 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/sv.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfiguration redan ig\u00e5ng", + "decryption_failed": "Den tillhandah\u00e5llna bindningsnyckeln fungerade inte, sensordata kunde inte dekrypteras. Kontrollera den och f\u00f6rs\u00f6k igen.", + "expected_24_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 24 tecken.", + "expected_32_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 32 tecken.", + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du s\u00e4tta upp {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindningsnyckel" + }, + "description": "De sensordata som s\u00e4nds av sensorn \u00e4r krypterade. F\u00f6r att dekryptera dem beh\u00f6ver vi en hexadecimal bindningsnyckel med 32 tecken." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindningsnyckel" + }, + "description": "De sensordata som s\u00e4nds av sensorn \u00e4r krypterade. F\u00f6r att dekryptera dem beh\u00f6ver vi en hexadecimal bindningsnyckel med 24 tecken." + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att s\u00e4tta upp" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/sv.json b/homeassistant/components/yolink/translations/sv.json index 3c6db089e0e..07392481a29 100644 --- a/homeassistant/components/yolink/translations/sv.json +++ b/homeassistant/components/yolink/translations/sv.json @@ -17,6 +17,7 @@ "title": "V\u00e4lj autentiseringsmetod" }, "reauth_confirm": { + "description": "Yolink-integrationen m\u00e5ste autentisera ditt konto igen", "title": "\u00c5terautenticera integration" } } diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index e0e063df426..85bb3ccce6d 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -46,11 +46,13 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd" }, "zha_options": { + "always_prefer_xy_color_mode": "\u039d\u03b1 \u03c0\u03c1\u03bf\u03c4\u03b9\u03bc\u03ac\u03c4\u03b1\u03b9 \u03c0\u03ac\u03bd\u03c4\u03b1 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 XY", "consider_unavailable_battery": "\u0398\u03b5\u03c9\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03bc\u03b5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c9\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "consider_unavailable_mains": "\u0398\u03b5\u03c9\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03c9\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "default_light_transition": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "enable_identify_on_join": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03c6\u03ad \u03b1\u03bd\u03b1\u03b3\u03bd\u03ce\u03c1\u03b9\u03c3\u03b7\u03c2 \u03cc\u03c4\u03b1\u03bd \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "enhanced_light_transition": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b2\u03b5\u03bb\u03c4\u03b9\u03c9\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2/\u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", + "light_transitioning_flag": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b2\u03b5\u03bb\u03c4\u03b9\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c6\u03c9\u03c4\u03cc\u03c2", "title": "\u039a\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03ad\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" } }, diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 63eee2f376b..ef91a4227c8 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -46,11 +46,13 @@ "title": "Opsi Panel Kontrol Alarm" }, "zha_options": { + "always_prefer_xy_color_mode": "Selalu pilih mode warna XY", "consider_unavailable_battery": "Anggap perangkat bertenaga baterai sebagai tidak tersedia setelah (detik)", "consider_unavailable_mains": "Anggap perangkat bertenaga listrik sebagai tidak tersedia setelah (detik)", "default_light_transition": "Waktu transisi lampu default (detik)", "enable_identify_on_join": "Aktifkan efek identifikasi saat perangkat bergabung dengan jaringan", "enhanced_light_transition": "Aktifkan versi canggih untuk transisi warna/suhu cahaya dari keadaan tidak aktif", + "light_transitioning_flag": "Aktifkan penggeser kecerahan yang lebih canggih pada waktu transisi lampu", "title": "Opsi Global" } }, diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 14ba0b9280f..25d5a5bd9f6 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -46,11 +46,13 @@ "title": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "zha_options": { + "always_prefer_xy_color_mode": "\u5e38\u306bXY\u30ab\u30e9\u30fc\u30e2\u30fc\u30c9\u3092\u512a\u5148", "consider_unavailable_battery": "(\u79d2)\u5f8c\u306b\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", "consider_unavailable_mains": "(\u79d2)\u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", "default_light_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e9\u30a4\u30c8\u9077\u79fb\u6642\u9593(\u79d2)", "enable_identify_on_join": "\u30c7\u30d0\u30a4\u30b9\u304c\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u53c2\u52a0\u3059\u308b\u969b\u306b\u3001\u8b58\u5225\u52b9\u679c\u3092\u6709\u52b9\u306b\u3059\u308b", "enhanced_light_transition": "\u30aa\u30d5\u72b6\u614b\u304b\u3089\u3001\u30a8\u30f3\u30cf\u30f3\u30b9\u30c9\u30e9\u30a4\u30c8\u30ab\u30e9\u30fc/\u8272\u6e29\u5ea6\u3078\u306e\u9077\u79fb\u3092\u6709\u52b9\u306b\u3057\u307e\u3059", + "light_transitioning_flag": "\u5149\u6e90\u79fb\u884c\u6642\u306e\u8f1d\u5ea6\u30b9\u30e9\u30a4\u30c0\u30fc\u306e\u62e1\u5f35\u3092\u6709\u52b9\u306b\u3059\u308b", "title": "\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d7\u30b7\u30e7\u30f3" } }, diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 83ba818087a..6eae6313424 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -46,11 +46,13 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u0435\u0439 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" }, "zha_options": { + "always_prefer_xy_color_mode": "\u0412\u0441\u0435\u0433\u0434\u0430 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0446\u0432\u0435\u0442\u043e\u0432\u043e\u0439 \u0440\u0435\u0436\u0438\u043c XY", "consider_unavailable_battery": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", "enhanced_light_transition": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u0446\u0432\u0435\u0442\u0430/\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0441\u0432\u0435\u0442\u0430 \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "light_transitioning_flag": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u043b\u0437\u0443\u043d\u043e\u043a \u044f\u0440\u043a\u043e\u0441\u0442\u0438 \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0435 \u0441\u0432\u0435\u0442\u0430", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" } }, diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 7bbfe8dcfb1..8917cbe8cd6 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -31,6 +31,13 @@ } } }, + "config_panel": { + "zha_options": { + "always_prefer_xy_color_mode": "F\u00f6redrar alltid XY-f\u00e4rgl\u00e4ge", + "enhanced_light_transition": "Aktivera f\u00f6rb\u00e4ttrad ljusf\u00e4rg/temperatur\u00f6verg\u00e5ng fr\u00e5n ett avst\u00e4ngt l\u00e4ge", + "light_transitioning_flag": "Aktivera f\u00f6rb\u00e4ttrad ljusstyrka vid ljus\u00f6verg\u00e5ng" + } + }, "device_automation": { "action_type": { "squawk": "Kraxa", diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index 907924e08ac..386b306e1b8 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "zeroconf_confirm": { + "title": "Uppt\u00e4ckte Z-Wave JS Server" + } + } + }, "device_automation": { "condition_type": { "value": "Nuvarande v\u00e4rde f\u00f6r ett Z-Wave v\u00e4rde" From 80a9659524a47868070a16cdfa7f654fba7f6bb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jul 2022 14:53:33 -1000 Subject: [PATCH 045/903] Update to bleak 0.15 (#75941) --- .../components/bluetooth/__init__.py | 29 ++++++++------- .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/models.py | 34 ++++++++++++----- .../bluetooth/passive_update_coordinator.py | 8 ++-- .../bluetooth/passive_update_processor.py | 17 +++++---- .../bluetooth/update_coordinator.py | 9 +++-- homeassistant/components/bluetooth/usage.py | 5 ++- .../bluetooth_le_tracker/device_tracker.py | 18 ++++----- .../components/govee_ble/__init__.py | 2 + .../components/govee_ble/config_flow.py | 6 +-- .../homekit_controller/config_flow.py | 7 +++- .../components/homekit_controller/utils.py | 2 +- homeassistant/components/inkbird/__init__.py | 5 +-- .../components/inkbird/config_flow.py | 6 +-- homeassistant/components/moat/__init__.py | 5 +-- homeassistant/components/moat/config_flow.py | 6 +-- .../components/sensorpush/__init__.py | 5 +-- .../components/sensorpush/config_flow.py | 6 +-- .../components/switchbot/config_flow.py | 8 ++-- .../components/switchbot/coordinator.py | 11 +++--- .../components/xiaomi_ble/__init__.py | 5 +-- .../components/xiaomi_ble/config_flow.py | 20 ++++++---- homeassistant/config_entries.py | 4 +- homeassistant/helpers/config_entry_flow.py | 4 +- .../helpers/service_info/bluetooth.py | 1 + homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_init.py | 21 +++++++++-- .../test_passive_update_coordinator.py | 29 ++++++++++----- .../test_passive_update_processor.py | 37 ++++++++++--------- .../test_device_tracker.py | 23 +++++++++--- tests/components/fjaraskupan/conftest.py | 6 +++ tests/components/govee_ble/test_sensor.py | 2 +- tests/components/inkbird/test_sensor.py | 2 +- tests/components/moat/test_sensor.py | 2 +- tests/components/sensorpush/test_sensor.py | 2 +- .../components/xiaomi_ble/test_config_flow.py | 8 +++- tests/components/xiaomi_ble/test_sensor.py | 12 +++--- 39 files changed, 223 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index eb8e31baef0..9adaac84333 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -from typing import Final, Union +from typing import Final import async_timeout from bleak import BleakError @@ -96,12 +96,8 @@ SCANNING_MODE_TO_BLEAK = { BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") -BluetoothCallback = Callable[ - [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None -] -ProcessAdvertisementCallback = Callable[ - [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo]], bool -] +BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] +ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] @hass_callback @@ -157,9 +153,15 @@ def async_register_callback( hass: HomeAssistant, callback: BluetoothCallback, match_dict: BluetoothCallbackMatcher | None, + mode: BluetoothScanningMode, ) -> Callable[[], None]: """Register to receive a callback on bluetooth change. + mode is currently not used as we only support active scanning. + Passive scanning will be available in the future. The flag + is required to be present to avoid a future breaking change + when we support passive scanning. + Returns a callback that can be used to cancel the registration. """ manager: BluetoothManager = hass.data[DOMAIN] @@ -170,19 +172,20 @@ async def async_process_advertisements( hass: HomeAssistant, callback: ProcessAdvertisementCallback, match_dict: BluetoothCallbackMatcher, + mode: BluetoothScanningMode, timeout: int, -) -> BluetoothServiceInfo: +) -> BluetoothServiceInfoBleak: """Process advertisements until callback returns true or timeout expires.""" - done: Future[BluetoothServiceInfo] = Future() + done: Future[BluetoothServiceInfoBleak] = Future() @hass_callback def _async_discovered_device( - service_info: BluetoothServiceInfo, change: BluetoothChange + service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: if callback(service_info): done.set_result(service_info) - unload = async_register_callback(hass, _async_discovered_device, match_dict) + unload = async_register_callback(hass, _async_discovered_device, match_dict, mode) try: async with async_timeout.timeout(timeout): @@ -333,7 +336,7 @@ class BluetoothManager: ) try: async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() + await self.scanner.start() # type: ignore[no-untyped-call] except asyncio.TimeoutError as ex: self._cancel_device_detected() raise ConfigEntryNotReady( @@ -500,7 +503,7 @@ class BluetoothManager: self._cancel_unavailable_tracking = None if self.scanner: try: - await self.scanner.stop() + await self.scanner.stop() # type: ignore[no-untyped-call] except BleakError as ex: # This is not fatal, and they may want to reload # the config entry to restart the scanner if they diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index c6ca8b11400..f215e8fa161 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.2"], + "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.2"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 408e0698879..6f814c7b66b 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import Any, Final, cast +from typing import Any, Final from bleak import BleakScanner from bleak.backends.device import BLEDevice @@ -32,7 +32,7 @@ def _dispatch_callback( """Dispatch the callback.""" if not callback: # Callback destroyed right before being called, ignore - return + return # type: ignore[unreachable] if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( advertisement_data.service_uuids @@ -45,7 +45,7 @@ def _dispatch_callback( _LOGGER.exception("Error in callback: %s", callback) -class HaBleakScanner(BleakScanner): # type: ignore[misc] +class HaBleakScanner(BleakScanner): """BleakScanner that cannot be stopped.""" def __init__( # pylint: disable=super-init-not-called @@ -106,16 +106,29 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] _dispatch_callback(*callback_filters, device, advertisement_data) -class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] +class HaBleakScannerWrapper(BaseBleakScanner): """A wrapper that uses the single instance.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__( + self, + *args: Any, + detection_callback: AdvertisementDataCallback | None = None, + service_uuids: list[str] | None = None, + **kwargs: Any, + ) -> None: """Initialize the BleakScanner.""" self._detection_cancel: CALLBACK_TYPE | None = None self._mapped_filters: dict[str, set[str]] = {} self._adv_data_callback: AdvertisementDataCallback | None = None - self._map_filters(*args, **kwargs) - super().__init__(*args, **kwargs) + remapped_kwargs = { + "detection_callback": detection_callback, + "service_uuids": service_uuids or [], + **kwargs, + } + self._map_filters(*args, **remapped_kwargs) + super().__init__( + detection_callback=detection_callback, service_uuids=service_uuids or [] + ) async def stop(self, *args: Any, **kwargs: Any) -> None: """Stop scanning for devices.""" @@ -153,9 +166,11 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" assert HA_BLEAK_SCANNER is not None - return cast(list[BLEDevice], HA_BLEAK_SCANNER.discovered_devices) + return HA_BLEAK_SCANNER.discovered_devices - def register_detection_callback(self, callback: AdvertisementDataCallback) -> None: + def register_detection_callback( + self, callback: AdvertisementDataCallback | None + ) -> None: """Register a callback that is called when a device is discovered or has a property changed. This method takes the callback and registers it with the long running @@ -171,6 +186,7 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] self._cancel_callback() super().register_detection_callback(self._adv_data_callback) assert HA_BLEAK_SCANNER is not None + assert self._callback is not None self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( self._callback, self._mapped_filters ) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 97e7ddc49ee..31a6b065830 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -6,10 +6,9 @@ import logging from typing import Any from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BluetoothChange +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .update_coordinator import BasePassiveBluetoothCoordinator @@ -25,9 +24,10 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize PassiveBluetoothDataUpdateCoordinator.""" - super().__init__(hass, logger, address) + super().__init__(hass, logger, address, mode) self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} @callback @@ -65,7 +65,7 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 43467701879..1f2047c02cb 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -6,14 +6,12 @@ import dataclasses import logging from typing import Any, Generic, TypeVar -from home_assistant_bluetooth import BluetoothServiceInfo - from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BluetoothChange +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator @@ -62,9 +60,10 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize the coordinator.""" - super().__init__(hass, logger, address) + super().__init__(hass, logger, address, mode) self._processors: list[PassiveBluetoothDataProcessor] = [] @callback @@ -92,7 +91,7 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" @@ -122,7 +121,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): The processor will call the update_method every time the bluetooth device receives a new advertisement data from the coordinator with the following signature: - update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + update_method(service_info: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate As the size of each advertisement is limited, the update_method should return a PassiveBluetoothDataUpdate object that contains only data that @@ -135,7 +134,9 @@ class PassiveBluetoothDataProcessor(Generic[_T]): def __init__( self, - update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + update_method: Callable[ + [BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T] + ], ) -> None: """Initialize the coordinator.""" self.coordinator: PassiveBluetoothProcessorCoordinator @@ -241,7 +242,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): @callback def async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index b1cb2de1453..d0f38ce32c6 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -4,13 +4,13 @@ from __future__ import annotations import logging import time -from home_assistant_bluetooth import BluetoothServiceInfo - from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from . import ( BluetoothCallbackMatcher, BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, async_register_callback, async_track_unavailable, ) @@ -27,6 +27,7 @@ class BasePassiveBluetoothCoordinator: hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize the coordinator.""" self.hass = hass @@ -36,6 +37,7 @@ class BasePassiveBluetoothCoordinator: self._cancel_track_unavailable: CALLBACK_TYPE | None = None self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None self._present = False + self.mode = mode self.last_seen = 0.0 @callback @@ -61,6 +63,7 @@ class BasePassiveBluetoothCoordinator: self.hass, self._async_handle_bluetooth_event, BluetoothCallbackMatcher(address=self.address), + self.mode, ) self._cancel_track_unavailable = async_track_unavailable( self.hass, @@ -86,7 +89,7 @@ class BasePassiveBluetoothCoordinator: @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index da5d062a36f..b3a6783cf30 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -1,4 +1,5 @@ """bluetooth usage utility to handle multiple instances.""" + from __future__ import annotations import bleak @@ -10,9 +11,9 @@ ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" - bleak.BleakScanner = HaBleakScannerWrapper + bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] def uninstall_multiple_bleak_catcher() -> None: """Unwrap the bleak classes.""" - bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER + bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index fa55c22f994..6ba33e506cc 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING from uuid import UUID from bleak import BleakClient, BleakError @@ -31,9 +30,6 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - _LOGGER = logging.getLogger(__name__) # Base UUID: 00000000-0000-1000-8000-00805F9B34FB @@ -139,15 +135,12 @@ async def async_setup_scanner( # noqa: C901 async def _async_see_update_ble_battery( mac: str, now: datetime, - service_info: bluetooth.BluetoothServiceInfo, + service_info: bluetooth.BluetoothServiceInfoBleak, ) -> None: """Lookup Bluetooth LE devices and update status.""" battery = None - ble_device: BLEDevice | str = ( - bluetooth.async_ble_device_from_address(hass, mac) or mac - ) try: - async with BleakClient(ble_device) as client: + async with BleakClient(service_info.device) as client: bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) battery = ord(bat_char) except asyncio.TimeoutError: @@ -168,7 +161,8 @@ async def async_setup_scanner( # noqa: C901 @callback def _async_update_ble( - service_info: bluetooth.BluetoothServiceInfo, change: bluetooth.BluetoothChange + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, ) -> None: """Update from a ble callback.""" mac = service_info.address @@ -202,7 +196,9 @@ async def async_setup_scanner( # noqa: C901 _async_update_ble(service_info, bluetooth.BluetoothChange.ADVERTISEMENT) cancels = [ - bluetooth.async_register_callback(hass, _async_update_ble, None), + bluetooth.async_register_callback( + hass, _async_update_ble, None, bluetooth.BluetoothScanningMode.ACTIVE + ), async_track_time_interval(hass, _async_refresh_ble, interval), ] diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index 3099d401e9b..7a134e43ace 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -27,6 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, address=address, + mode=BluetoothScanningMode.ACTIVE, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index 9f2efac0ce9..1e3a5566bfd 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -7,7 +7,7 @@ from govee_ble import GoveeBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index d8b3fda2d06..31677e37b20 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -20,13 +20,16 @@ from homeassistant.components import zeroconf from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.service_info import bluetooth from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES from .storage import async_get_entity_storage from .utils import async_get_controller +if TYPE_CHECKING: + from homeassistant.components import bluetooth + + HOMEKIT_DIR = ".homekit" HOMEKIT_BRIDGE_DOMAIN = "homekit" @@ -359,7 +362,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form() async def async_step_bluetooth( - self, discovery_info: bluetooth.BluetoothServiceInfo + self, discovery_info: bluetooth.BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED: diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index d7780029331..6e272067b54 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -32,7 +32,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: controller = Controller( async_zeroconf_instance=async_zeroconf_instance, - bleak_scanner_instance=bleak_scanner_instance, + bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type] ) hass.data[CONTROLLER] = controller diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 5553b1c6ded..0272114b83c 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py index 679ff43b19e..21ed85e117e 100644 --- a/homeassistant/components/inkbird/config_flow.py +++ b/homeassistant/components/inkbird/config_flow.py @@ -7,7 +7,7 @@ from inkbird_ble import INKBIRDBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py index 259b6b66709..237948a8ff6 100644 --- a/homeassistant/components/moat/__init__.py +++ b/homeassistant/components/moat/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py index a353f4963ad..6f51b62d110 100644 --- a/homeassistant/components/moat/config_flow.py +++ b/homeassistant/components/moat/config_flow.py @@ -7,7 +7,7 @@ from moat_ble import MoatBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 0a9efcbc752..d4a0872ba3f 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py index 1a8e8b47abe..d10c2f481a6 100644 --- a/homeassistant/components/sensorpush/config_flow.py +++ b/homeassistant/components/sensorpush/config_flow.py @@ -7,7 +7,7 @@ from sensorpush_ble import SensorPushBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 3a34a89d9fd..eaad573d370 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from switchbot import SwitchBotAdvertisement, parse_advertisement_data import voluptuous as vol @@ -15,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES @@ -46,15 +45,14 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_advs: dict[str, SwitchBotAdvertisement] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) await self.async_set_unique_id(format_unique_id(discovery_info.address)) self._abort_if_unique_id_configured() - discovery_info_bleak = cast(BluetoothServiceInfoBleak, discovery_info) parsed = parse_advertisement_data( - discovery_info_bleak.device, discovery_info_bleak.advertisement + discovery_info.device, discovery_info.advertisement ) if not parsed or parsed.data.get("modelName") not in SUPPORTED_MODEL_TYPES: return self.async_abort(reason="not_supported") diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index f461a3e0f4c..43c576249df 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import logging -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import switchbot @@ -39,7 +39,9 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): device: switchbot.SwitchbotDevice, ) -> None: """Initialize global switchbot data updater.""" - super().__init__(hass, logger, ble_device.address) + super().__init__( + hass, logger, ble_device.address, bluetooth.BluetoothScanningMode.ACTIVE + ) self.ble_device = ble_device self.device = device self.data: dict[str, Any] = {} @@ -48,14 +50,13 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: bluetooth.BluetoothServiceInfo, + service_info: bluetooth.BluetoothServiceInfoBleak, change: bluetooth.BluetoothChange, ) -> None: """Handle a Bluetooth event.""" super()._async_handle_bluetooth_event(service_info, change) - discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) if adv := switchbot.parse_advertisement_data( - discovery_info_bleak.device, discovery_info_bleak.advertisement + service_info.device, service_info.advertisement ): self.data = flatten_sensors_data(adv.data) if "modelName" in self.data: diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 4eb20dbd943..791ac1447ad 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 91c7e223f1a..aa1ffc24895 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -11,7 +11,8 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothScanningMode, + BluetoothServiceInfoBleak, async_discovered_service_info, async_process_advertisements, ) @@ -30,11 +31,11 @@ class Discovery: """A discovered bluetooth device.""" title: str - discovery_info: BluetoothServiceInfo + discovery_info: BluetoothServiceInfoBleak device: DeviceData -def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: +def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name @@ -45,18 +46,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} async def _async_wait_for_full_advertisement( - self, discovery_info: BluetoothServiceInfo, device: DeviceData - ) -> BluetoothServiceInfo: + self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData + ) -> BluetoothServiceInfoBleak: """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" if not device.pending: return discovery_info - def _process_more_advertisements(service_info: BluetoothServiceInfo) -> bool: + def _process_more_advertisements( + service_info: BluetoothServiceInfoBleak, + ) -> bool: device.update(service_info) return not device.pending @@ -64,11 +67,12 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self.hass, _process_more_advertisements, {"address": discovery_info.address}, + BluetoothScanningMode.ACTIVE, ADDITIONAL_DISCOVERY_TIMEOUT, ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b5d5804f100..b0c04323005 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -27,12 +27,12 @@ from .util import uuid as uuid_util from .util.decorator import Registry if TYPE_CHECKING: + from .components.bluetooth import BluetoothServiceInfoBleak from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo from .components.ssdp import SsdpServiceInfo from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo - from .helpers.service_info.bluetooth import BluetoothServiceInfo from .helpers.service_info.mqtt import MqttServiceInfo _LOGGER = logging.getLogger(__name__) @@ -1485,7 +1485,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Bluetooth discovery.""" return await self._async_step_discovery_without_unique_id() diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index bf2f95c12c6..c9d6ffe6065 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -15,11 +15,11 @@ from .typing import DiscoveryInfoType if TYPE_CHECKING: import asyncio + from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo - from .service_info.bluetooth import BluetoothServiceInfo from .service_info.mqtt import MqttServiceInfo _R = TypeVar("_R", bound="Awaitable[bool] | bool") @@ -97,7 +97,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle a flow initialized by bluetooth discovery.""" if self._async_in_progress() or self._async_current_entries(): diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index 968d1dde95f..0db3a39b114 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -1,4 +1,5 @@ """The bluetooth integration service info.""" + from home_assistant_bluetooth import BluetoothServiceInfo __all__ = ["BluetoothServiceInfo"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1f86ff4c7c5..c9d424daa33 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 -bleak==0.14.3 +bleak==0.15.0 bluetooth-adapters==0.1.2 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 842376bc696..7fe58a0899b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ bimmer_connected==0.10.1 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.14.3 +bleak==0.15.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d61f2d618a..16919de3c48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ bellows==0.31.2 bimmer_connected==0.10.1 # homeassistant.components.bluetooth -bleak==0.14.3 +bleak==0.15.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 0664f82dbab..a47916506df 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -12,6 +12,7 @@ from homeassistant.components.bluetooth import ( SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, BluetoothServiceInfo, async_process_advertisements, async_track_unavailable, @@ -675,6 +676,7 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo hass, _fake_subscriber, {"service_uuids": {"cba20d00-224d-11e6-9fb8-0002a5d5c51b"}}, + BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 @@ -760,6 +762,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 @@ -799,6 +802,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) cancel() @@ -808,6 +812,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) cancel() @@ -832,7 +837,11 @@ async def test_process_advertisements_bail_on_good_advertisement( handle = hass.async_create_task( async_process_advertisements( - hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, ) ) @@ -873,7 +882,11 @@ async def test_process_advertisements_ignore_bad_advertisement( handle = hass.async_create_task( async_process_advertisements( - hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, ) ) @@ -903,7 +916,9 @@ async def test_process_advertisements_timeout( return False with pytest.raises(asyncio.TimeoutError): - await async_process_advertisements(hass, _callback, {}, 0) + await async_process_advertisements( + hass, _callback, {}, BluetoothScanningMode.ACTIVE, 0 + ) async def test_wrapped_instance_with_filter( diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 4da14fb13d3..31530cd6995 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -10,6 +10,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -42,9 +43,9 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): """An example coordinator that subclasses PassiveBluetoothDataUpdateCoordinator.""" - def __init__(self, hass, logger, device_id) -> None: + def __init__(self, hass, logger, device_id, mode) -> None: """Initialize the coordinator.""" - super().__init__(hass, logger, device_id) + super().__init__(hass, logger, device_id, mode) self.data: dict[str, Any] = {} def _async_handle_bluetooth_event( @@ -60,11 +61,13 @@ class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): async def test_basic_usage(hass, mock_bleak_scanner_start): """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -100,11 +103,13 @@ async def test_context_compatiblity_with_data_update_coordinator( ): """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -149,11 +154,13 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.PASSIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -208,13 +215,15 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start): """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) entity = PassiveBluetoothCoordinatorEntity(coordinator) assert entity.available is False saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index ebb69d7c7d0..6a092746a68 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -16,6 +16,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, ) from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, @@ -90,12 +91,12 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -192,12 +193,12 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -277,12 +278,12 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -336,12 +337,12 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -389,12 +390,12 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -731,12 +732,12 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -841,12 +842,12 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -905,12 +906,12 @@ async def test_passive_bluetooth_entity_with_entity_platform( return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -992,12 +993,12 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index dfe47b38b33..f9f0a51fc0f 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -5,8 +5,9 @@ from datetime import timedelta from unittest.mock import patch from bleak import BleakError +from bleak.backends.scanner import AdvertisementData, BLEDevice -from homeassistant.components.bluetooth import BluetoothServiceInfo +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth_le_tracker import device_tracker from homeassistant.components.bluetooth_le_tracker.device_tracker import ( CONF_TRACK_BATTERY, @@ -79,7 +80,7 @@ async def test_preserve_new_tracked_device_name( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -87,6 +88,8 @@ async def test_preserve_new_tracked_device_name( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -100,7 +103,7 @@ async def test_preserve_new_tracked_device_name( assert result # Seen once here; return without name when seen subsequent times - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=None, address=address, rssi=-19, @@ -108,6 +111,8 @@ async def test_preserve_new_tracked_device_name( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -140,7 +145,7 @@ async def test_tracking_battery_times_out( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -148,6 +153,8 @@ async def test_tracking_battery_times_out( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -202,7 +209,7 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -210,6 +217,8 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -266,7 +275,7 @@ async def test_tracking_battery_successful( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -274,6 +283,8 @@ async def test_tracking_battery_successful( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index d60abcdb9ad..4e06b2ad046 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -17,6 +17,12 @@ def fixture_scanner(hass): class MockScanner(BaseBleakScanner): """Mock Scanner.""" + def __init__(self, *args, **kwargs) -> None: + """Initialize the scanner.""" + super().__init__( + detection_callback=kwargs.pop("detection_callback"), service_uuids=[] + ) + async def start(self): """Start scanning for devices.""" for device in devices: diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index 7a6ecbaed51..75d269ea0ba 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index a851cb92ec3..cafc22911c3 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/moat/test_sensor.py b/tests/components/moat/test_sensor.py index 826bbc72cdb..6424144106b 100644 --- a/tests/components/moat/test_sensor.py +++ b/tests/components/moat/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index 31fbdd8d712..34179985d78 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index b424228cc6c..86fda21aaa0 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -59,7 +59,9 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): """Test discovering a valid device. Payload is too short, but later we get full one.""" - async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): service_info = make_advertisement( "A4:C1:38:56:53:84", b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", @@ -378,7 +380,9 @@ async def test_async_step_user_short_payload_then_full(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): service_info = make_advertisement( "A4:C1:38:56:53:84", b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index dca00e92254..011c6daecae 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -60,7 +60,7 @@ async def test_xiaomi_formaldeyhde(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -107,7 +107,7 @@ async def test_xiaomi_consumable(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -154,7 +154,7 @@ async def test_xiaomi_battery_voltage(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -208,7 +208,7 @@ async def test_xiaomi_HHCCJCY01(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -291,7 +291,7 @@ async def test_xiaomi_CGDK2(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None From bb3e094552cc164f799d990fc904f471c02ad709 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jul 2022 18:33:29 -1000 Subject: [PATCH 046/903] Fix switchbot failing to setup when last_run_success is not saved (#75887) --- homeassistant/components/switchbot/cover.py | 9 ++++++--- homeassistant/components/switchbot/switch.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index dc9ddf4e616..0ae225f55d7 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -80,9 +80,12 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): if not last_state or ATTR_CURRENT_POSITION not in last_state.attributes: return - self._attr_current_cover_position = last_state.attributes[ATTR_CURRENT_POSITION] - self._last_run_success = last_state.attributes["last_run_success"] - self._attr_is_closed = last_state.attributes[ATTR_CURRENT_POSITION] <= 20 + self._attr_current_cover_position = last_state.attributes.get( + ATTR_CURRENT_POSITION + ) + self._last_run_success = last_state.attributes.get("last_run_success") + if self._attr_current_cover_position is not None: + self._attr_is_closed = self._attr_current_cover_position <= 20 async def async_open_cover(self, **kwargs: Any) -> None: """Open the curtain.""" diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 51f15c488d1..e6ba77fa164 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -69,7 +69,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): if not (last_state := await self.async_get_last_state()): return self._attr_is_on = last_state.state == STATE_ON - self._last_run_success = last_state.attributes["last_run_success"] + self._last_run_success = last_state.attributes.get("last_run_success") async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" From 8181da70901c6b848ebc2efb2d39a7a3536599f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 30 Jul 2022 11:04:31 +0200 Subject: [PATCH 047/903] Improve type hints in axis (#75910) --- homeassistant/components/axis/__init__.py | 7 +++--- homeassistant/components/axis/axis_base.py | 12 +++++----- .../components/axis/binary_sensor.py | 17 +++++++++----- homeassistant/components/axis/camera.py | 7 +++--- homeassistant/components/axis/device.py | 14 +++++++----- homeassistant/components/axis/diagnostics.py | 3 ++- homeassistant/components/axis/light.py | 22 +++++++++++-------- homeassistant/components/axis/switch.py | 19 ++++++++++------ 8 files changed, 61 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 4af066f4e89..56ddbc6d8c5 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -26,9 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except AuthenticationRequired as err: raise ConfigEntryAuthFailed from err - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] = AxisNetworkDevice( - hass, config_entry, api - ) + device = AxisNetworkDevice(hass, config_entry, api) + hass.data[AXIS_DOMAIN][config_entry.unique_id] = device await device.async_update_device_registry() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) device.async_setup_events() @@ -43,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Axis device config entry.""" - device = hass.data[AXIS_DOMAIN].pop(config_entry.unique_id) + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN].pop(config_entry.unique_id) return await device.async_reset() diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 3d887478528..2f21d900662 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -1,10 +1,12 @@ """Base classes for Axis entities.""" +from axis.event_stream import AxisEvent from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice class AxisEntityBase(Entity): @@ -12,7 +14,7 @@ class AxisEntityBase(Entity): _attr_has_entity_name = True - def __init__(self, device): + def __init__(self, device: AxisNetworkDevice) -> None: """Initialize the Axis event.""" self.device = device @@ -20,7 +22,7 @@ class AxisEntityBase(Entity): identifiers={(AXIS_DOMAIN, device.unique_id)} ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe device events.""" self.async_on_remove( async_dispatcher_connect( @@ -29,12 +31,12 @@ class AxisEntityBase(Entity): ) @property - def available(self): + def available(self) -> bool: """Return True if device is available.""" return self.device.available @callback - def update_callback(self, no_delay=None): + def update_callback(self, no_delay=None) -> None: """Update the entities state.""" self.async_write_ha_state() @@ -44,7 +46,7 @@ class AxisEventBase(AxisEntityBase): _attr_should_poll = False - def __init__(self, event, device): + def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: """Initialize the Axis event.""" super().__init__(device) self.event = event diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index fba7e8d6248..3e90f5ff2a1 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -1,4 +1,6 @@ """Support for Axis binary sensors.""" +from __future__ import annotations + from datetime import timedelta from axis.event_stream import ( @@ -8,6 +10,8 @@ from axis.event_stream import ( CLASS_OUTPUT, CLASS_PTZ, CLASS_SOUND, + AxisBinaryEvent, + AxisEvent, FenceGuard, LoiteringGuard, MotionGuard, @@ -28,6 +32,7 @@ from homeassistant.util.dt import utcnow from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice DEVICE_CLASS = { CLASS_INPUT: BinarySensorDeviceClass.CONNECTIVITY, @@ -43,12 +48,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis binary sensor.""" - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback def async_add_sensor(event_id): """Add binary sensor from Axis device.""" - event = device.api.event[event_id] + event: AxisEvent = device.api.event[event_id] if event.CLASS not in (CLASS_OUTPUT, CLASS_PTZ) and not ( event.CLASS == CLASS_LIGHT and event.TYPE == "Light" @@ -63,7 +68,9 @@ async def async_setup_entry( class AxisBinarySensor(AxisEventBase, BinarySensorEntity): """Representation of a binary Axis event.""" - def __init__(self, event, device): + event: AxisBinaryEvent + + def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: """Initialize the Axis binary sensor.""" super().__init__(event, device) self.cancel_scheduled_update = None @@ -98,12 +105,12 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): ) @property - def is_on(self): + def is_on(self) -> bool: """Return true if event is active.""" return self.event.is_tripped @property - def name(self): + def name(self) -> str | None: """Return the name of the event.""" if ( self.event.CLASS == CLASS_INPUT diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 4df9a5e2141..992410d9725 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .axis_base import AxisEntityBase from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice async def async_setup_entry( @@ -21,7 +22,7 @@ async def async_setup_entry( """Set up the Axis camera video stream.""" filter_urllib3_logging() - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] if not device.api.vapix.params.image_format: return @@ -34,7 +35,7 @@ class AxisCamera(AxisEntityBase, MjpegCamera): _attr_supported_features = CameraEntityFeature.STREAM - def __init__(self, device): + def __init__(self, device: AxisNetworkDevice) -> None: """Initialize Axis Communications camera component.""" AxisEntityBase.__init__(self, device) @@ -49,7 +50,7 @@ class AxisCamera(AxisEntityBase, MjpegCamera): self._attr_unique_id = f"{device.unique_id}-camera" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe camera events.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 683991d0f65..ea7edcf0483 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -51,7 +51,9 @@ from .errors import AuthenticationRequired, CannotConnect class AxisNetworkDevice: """Manages a Axis device.""" - def __init__(self, hass, config_entry, api): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: axis.AxisDevice + ) -> None: """Initialize the device.""" self.hass = hass self.config_entry = config_entry @@ -167,11 +169,11 @@ class AxisNetworkDevice: This is a static method because a class method (bound method), can not be used with weak references. """ - device = hass.data[AXIS_DOMAIN][entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][entry.unique_id] device.api.config.host = device.host async_dispatcher_send(hass, device.signal_new_address) - async def async_update_device_registry(self): + async def async_update_device_registry(self) -> None: """Update device registry.""" device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( @@ -224,17 +226,17 @@ class AxisNetworkDevice: async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) @callback - def disconnect_from_stream(self): + def disconnect_from_stream(self) -> None: """Stop stream.""" if self.api.stream.state != STATE_STOPPED: self.api.stream.connection_status_callback.clear() self.api.stream.stop() - async def shutdown(self, event): + async def shutdown(self, event) -> None: """Stop the event stream.""" self.disconnect_from_stream() - async def async_reset(self): + async def async_reset(self) -> bool: """Reset this device to default state.""" self.disconnect_from_stream() diff --git a/homeassistant/components/axis/diagnostics.py b/homeassistant/components/axis/diagnostics.py index be3da8daf9e..1c805e8f35b 100644 --- a/homeassistant/components/axis/diagnostics.py +++ b/homeassistant/components/axis/diagnostics.py @@ -9,6 +9,7 @@ from homeassistant.const import CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_US from homeassistant.core import HomeAssistant from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice REDACT_CONFIG = {CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME} REDACT_BASIC_DEVICE_INFO = {"SerialNumber", "SocSerialNumber"} @@ -19,7 +20,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] diag: dict[str, Any] = {} diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG) diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index e34c0d4a2d6..c75d18b1908 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -1,5 +1,7 @@ """Support for Axis lights.""" -from axis.event_stream import CLASS_LIGHT +from typing import Any + +from axis.event_stream import CLASS_LIGHT, AxisBinaryEvent, AxisEvent from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -9,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice async def async_setup_entry( @@ -17,7 +20,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis light.""" - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] if ( device.api.vapix.light_control is None @@ -28,7 +31,7 @@ async def async_setup_entry( @callback def async_add_sensor(event_id): """Add light from Axis device.""" - event = device.api.event[event_id] + event: AxisEvent = device.api.event[event_id] if event.CLASS == CLASS_LIGHT and event.TYPE == "Light": async_add_entities([AxisLight(event, device)]) @@ -42,8 +45,9 @@ class AxisLight(AxisEventBase, LightEntity): """Representation of a light Axis event.""" _attr_should_poll = True + event: AxisBinaryEvent - def __init__(self, event, device): + def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: """Initialize the Axis light.""" super().__init__(event, device) @@ -75,16 +79,16 @@ class AxisLight(AxisEventBase, LightEntity): self.max_intensity = max_intensity["data"]["ranges"][0]["high"] @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" return self.event.is_tripped @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" return int((self.current_intensity / self.max_intensity) * 255) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" if not self.is_on: await self.device.api.vapix.light_control.activate_light(self.light_id) @@ -95,12 +99,12 @@ class AxisLight(AxisEventBase, LightEntity): self.light_id, intensity ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" if self.is_on: await self.device.api.vapix.light_control.deactivate_light(self.light_id) - async def async_update(self): + async def async_update(self) -> None: """Update brightness.""" current_intensity = ( await self.device.api.vapix.light_control.get_current_intensity( diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index 61f16cfc789..6576e9d519e 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -1,5 +1,7 @@ """Support for Axis switches.""" -from axis.event_stream import CLASS_OUTPUT +from typing import Any + +from axis.event_stream import CLASS_OUTPUT, AxisBinaryEvent, AxisEvent from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -9,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice async def async_setup_entry( @@ -17,12 +20,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis switch.""" - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback def async_add_switch(event_id): """Add switch from Axis device.""" - event = device.api.event[event_id] + event: AxisEvent = device.api.event[event_id] if event.CLASS == CLASS_OUTPUT: async_add_entities([AxisSwitch(event, device)]) @@ -35,7 +38,9 @@ async def async_setup_entry( class AxisSwitch(AxisEventBase, SwitchEntity): """Representation of a Axis switch.""" - def __init__(self, event, device): + event: AxisBinaryEvent + + def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: """Initialize the Axis switch.""" super().__init__(event, device) @@ -43,14 +48,14 @@ class AxisSwitch(AxisEventBase, SwitchEntity): self._attr_name = device.api.vapix.ports[event.id].name @property - def is_on(self): + def is_on(self) -> bool: """Return true if event is active.""" return self.event.is_tripped - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" await self.device.api.vapix.ports[self.event.id].close() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" await self.device.api.vapix.ports[self.event.id].open() From ace359b1bd1b86dd8c5f9989c79033496135ccb7 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Sun, 31 Jul 2022 00:04:24 +1200 Subject: [PATCH 048/903] Add multi-zone support to Anthem AV receiver and distribution solution (#74779) * Add multi-zone support to Anthem AV receiver and distribution amplifier * Fix typo in comment * Convert properties to attribute and add test * Migrate entity name * Fix after rebase add strict typing and bump version * fix typing * Simplify test * Small improvement * remove dispatcher send and use callback --- .strict-typing | 1 + homeassistant/components/anthemav/__init__.py | 9 +- .../components/anthemav/config_flow.py | 10 +- homeassistant/components/anthemav/const.py | 1 + .../components/anthemav/manifest.json | 2 +- .../components/anthemav/media_player.py | 123 ++++++++---------- mypy.ini | 11 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/anthemav/conftest.py | 35 ++++- tests/components/anthemav/test_init.py | 37 +++--- .../components/anthemav/test_media_player.py | 71 ++++++++++ 12 files changed, 200 insertions(+), 104 deletions(-) create mode 100644 tests/components/anthemav/test_media_player.py diff --git a/.strict-typing b/.strict-typing index d3102572eb1..ddeaf4f3844 100644 --- a/.strict-typing +++ b/.strict-typing @@ -56,6 +56,7 @@ homeassistant.components.ambee.* homeassistant.components.ambient_station.* homeassistant.components.amcrest.* homeassistant.components.ampio.* +homeassistant.components.anthemav.* homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* homeassistant.components.automation.* diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index eb1d9b0b560..24a83d0aff0 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging import anthemav +from anthemav.device_error import DeviceError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform @@ -11,7 +12,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ANTHEMAV_UDATE_SIGNAL, DOMAIN +from .const import ANTHEMAV_UDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS, DOMAIN PLATFORMS = [Platform.MEDIA_PLAYER] @@ -22,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Anthem A/V Receivers from a config entry.""" @callback - def async_anthemav_update_callback(message): + def async_anthemav_update_callback(message: str) -> None: """Receive notification from transport that new data exists.""" _LOGGER.debug("Received update callback from AVR: %s", message) async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.entry_id}") @@ -34,7 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_callback=async_anthemav_update_callback, ) - except OSError as err: + # Wait for the zones to be initialised based on the model + await avr.protocol.wait_for_device_initialised(DEVICE_TIMEOUT_SECONDS) + except (OSError, DeviceError) as err: raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr diff --git a/homeassistant/components/anthemav/config_flow.py b/homeassistant/components/anthemav/config_flow.py index 55e1fd42e07..0e878bcc913 100644 --- a/homeassistant/components/anthemav/config_flow.py +++ b/homeassistant/components/anthemav/config_flow.py @@ -15,9 +15,13 @@ from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac -from .const import CONF_MODEL, DEFAULT_NAME, DEFAULT_PORT, DOMAIN - -DEVICE_TIMEOUT_SECONDS = 4.0 +from .const import ( + CONF_MODEL, + DEFAULT_NAME, + DEFAULT_PORT, + DEVICE_TIMEOUT_SECONDS, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/anthemav/const.py b/homeassistant/components/anthemav/const.py index c63a477f7f9..02f56aed5c4 100644 --- a/homeassistant/components/anthemav/const.py +++ b/homeassistant/components/anthemav/const.py @@ -5,3 +5,4 @@ DEFAULT_NAME = "Anthem AV" DEFAULT_PORT = 14999 DOMAIN = "anthemav" MANUFACTURER = "Anthem" +DEVICE_TIMEOUT_SECONDS = 4.0 diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 27db9df32a3..2055ec75f27 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -2,7 +2,7 @@ "domain": "anthemav", "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", - "requirements": ["anthemav==1.3.2"], + "requirements": ["anthemav==1.4.1"], "dependencies": ["repairs"], "codeowners": ["@hyralex"], "config_flow": true, diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index bf8172083e6..4754a416d27 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -2,13 +2,14 @@ from __future__ import annotations import logging -from typing import Any from anthemav.connection import Connection +from anthemav.protocol import AVR import voluptuous as vol from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, ) @@ -22,7 +23,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -88,20 +89,28 @@ async def async_setup_entry( mac_address = config_entry.data[CONF_MAC] model = config_entry.data[CONF_MODEL] - avr = hass.data[DOMAIN][config_entry.entry_id] + avr: Connection = hass.data[DOMAIN][config_entry.entry_id] - entity = AnthemAVR(avr, name, mac_address, model, config_entry.entry_id) + entities = [] + for zone_number in avr.protocol.zones: + _LOGGER.debug("Initializing Zone %s", zone_number) + entity = AnthemAVR( + avr.protocol, name, mac_address, model, zone_number, config_entry.entry_id + ) + entities.append(entity) - _LOGGER.debug("Device data dump: %s", entity.dump_avrdata) _LOGGER.debug("Connection data dump: %s", avr.dump_conndata) - async_add_entities([entity]) + async_add_entities(entities) class AnthemAVR(MediaPlayerEntity): """Entity reading values from Anthem AVR protocol.""" + _attr_has_entity_name = True _attr_should_poll = False + _attr_device_class = MediaPlayerDeviceClass.RECEIVER + _attr_icon = "mdi:audio-video" _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -111,23 +120,33 @@ class AnthemAVR(MediaPlayerEntity): ) def __init__( - self, avr: Connection, name: str, mac_address: str, model: str, entry_id: str + self, + avr: AVR, + name: str, + mac_address: str, + model: str, + zone_number: int, + entry_id: str, ) -> None: """Initialize entity with transport.""" super().__init__() self.avr = avr self._entry_id = entry_id - self._attr_name = name - self._attr_unique_id = mac_address + self._zone_number = zone_number + self._zone = avr.zones[zone_number] + if zone_number > 1: + self._attr_name = f"zone {zone_number}" + self._attr_unique_id = f"{mac_address}_{zone_number}" + else: + self._attr_unique_id = mac_address + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mac_address)}, name=name, manufacturer=MANUFACTURER, model=model, ) - - def _lookup(self, propname: str, dval: Any | None = None) -> Any | None: - return getattr(self.avr.protocol, propname, dval) + self.set_states() async def async_added_to_hass(self) -> None: """When entity is added to hass.""" @@ -135,82 +154,42 @@ class AnthemAVR(MediaPlayerEntity): async_dispatcher_connect( self.hass, f"{ANTHEMAV_UDATE_SIGNAL}_{self._entry_id}", - self.async_write_ha_state, + self.update_states, ) ) - @property - def state(self) -> str | None: - """Return state of power on/off.""" - pwrstate = self._lookup("power") + @callback + def update_states(self) -> None: + """Update states for the current zone.""" + self.set_states() + self.async_write_ha_state() - if pwrstate is True: - return STATE_ON - if pwrstate is False: - return STATE_OFF - return None - - @property - def is_volume_muted(self) -> bool | None: - """Return boolean reflecting mute state on device.""" - return self._lookup("mute", False) - - @property - def volume_level(self) -> float | None: - """Return volume level from 0 to 1.""" - return self._lookup("volume_as_percentage", 0.0) - - @property - def media_title(self) -> str | None: - """Return current input name (closest we have to media title).""" - return self._lookup("input_name", "No Source") - - @property - def app_name(self) -> str | None: - """Return details about current video and audio stream.""" - return ( - f"{self._lookup('video_input_resolution_text', '')} " - f"{self._lookup('audio_input_name', '')}" - ) - - @property - def source(self) -> str | None: - """Return currently selected input.""" - return self._lookup("input_name", "Unknown") - - @property - def source_list(self) -> list[str] | None: - """Return all active, configured inputs.""" - return self._lookup("input_list", ["Unknown"]) + def set_states(self) -> None: + """Set all the states from the device to the entity.""" + self._attr_state = STATE_ON if self._zone.power is True else STATE_OFF + self._attr_is_volume_muted = self._zone.mute + self._attr_volume_level = self._zone.volume_as_percentage + self._attr_media_title = self._zone.input_name + self._attr_app_name = self._zone.input_format + self._attr_source = self._zone.input_name + self._attr_source_list = self.avr.input_list async def async_select_source(self, source: str) -> None: """Change AVR to the designated source (by name).""" - self._update_avr("input_name", source) + self._zone.input_name = source async def async_turn_off(self) -> None: """Turn AVR power off.""" - self._update_avr("power", False) + self._zone.power = False async def async_turn_on(self) -> None: """Turn AVR power on.""" - self._update_avr("power", True) + self._zone.power = True async def async_set_volume_level(self, volume: float) -> None: """Set AVR volume (0 to 1).""" - self._update_avr("volume_as_percentage", volume) + self._zone.volume_as_percentage = volume async def async_mute_volume(self, mute: bool) -> None: """Engage AVR mute.""" - self._update_avr("mute", mute) - - def _update_avr(self, propname: str, value: Any | None) -> None: - """Update a property in the AVR.""" - _LOGGER.debug("Sending command to AVR: set %s to %s", propname, str(value)) - setattr(self.avr.protocol, propname, value) - - @property - def dump_avrdata(self): - """Return state of avr object for debugging forensics.""" - attrs = vars(self) - items_string = ", ".join(f"{item}: {item}" for item in attrs.items()) - return f"dump_avrdata: {items_string}" + self._zone.mute = mute diff --git a/mypy.ini b/mypy.ini index af039c74de3..4ef79368f73 100644 --- a/mypy.ini +++ b/mypy.ini @@ -339,6 +339,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.anthemav.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.aseko_pool_live.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 7fe58a0899b..9b5e51affe6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -313,7 +313,7 @@ androidtv[async]==0.0.67 anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anthemav -anthemav==1.3.2 +anthemav==1.4.1 # homeassistant.components.apcupsd apcaccess==0.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16919de3c48..fb09140e435 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ ambiclimate==0.2.1 androidtv[async]==0.0.67 # homeassistant.components.anthemav -anthemav==1.3.2 +anthemav==1.4.1 # homeassistant.components.apprise apprise==0.9.9 diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py index f96696fe308..595c867304b 100644 --- a/tests/components/anthemav/conftest.py +++ b/tests/components/anthemav/conftest.py @@ -1,10 +1,12 @@ """Fixtures for anthemav integration tests.""" +from typing import Callable from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -16,17 +18,25 @@ def mock_anthemav() -> AsyncMock: avr.protocol.macaddress = "000000000001" avr.protocol.model = "MRX 520" avr.reconnect = AsyncMock() + avr.protocol.wait_for_device_initialised = AsyncMock() avr.close = MagicMock() avr.protocol.input_list = [] avr.protocol.audio_listening_mode_list = [] - avr.protocol.power = False + avr.protocol.zones = {1: get_zone(), 2: get_zone()} return avr +def get_zone() -> MagicMock: + """Return a mocked zone.""" + zone = MagicMock() + + zone.power = False + return zone + + @pytest.fixture def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock: """Return the default mocked connection.create.""" - with patch( "anthemav.Connection.create", return_value=mock_anthemav, @@ -34,6 +44,12 @@ def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock: yield mock +@pytest.fixture +def update_callback(mock_connection_create: AsyncMock) -> Callable[[str], None]: + """Return the update_callback used when creating the connection.""" + return mock_connection_create.call_args[1]["update_callback"] + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" @@ -48,3 +64,18 @@ def mock_config_entry() -> MockConfigEntry: }, unique_id="00:00:00:00:00:01", ) + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_connection_create: AsyncMock, +) -> MockConfigEntry: + """Set up the AnthemAv integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 97dc5be95b0..63bd8390958 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -1,6 +1,10 @@ """Test the Anthem A/V Receivers config flow.""" +from typing import Callable from unittest.mock import ANY, AsyncMock, patch +from anthemav.device_error import DeviceError +import pytest + from homeassistant import config_entries from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -12,35 +16,31 @@ async def test_load_unload_config_entry( hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock, - mock_config_entry: MockConfigEntry, + init_integration: MockConfigEntry, ) -> None: """Test load and unload AnthemAv component.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - # assert avr is created mock_connection_create.assert_called_with( host="1.1.1.1", port=14999, update_callback=ANY ) - assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert init_integration.state == config_entries.ConfigEntryState.LOADED # unload - await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.config_entries.async_unload(init_integration.entry_id) await hass.async_block_till_done() # assert unload and avr is closed - assert mock_config_entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert init_integration.state == config_entries.ConfigEntryState.NOT_LOADED mock_anthemav.close.assert_called_once() -async def test_config_entry_not_ready( - hass: HomeAssistant, mock_config_entry: MockConfigEntry +@pytest.mark.parametrize("error", [OSError, DeviceError]) +async def test_config_entry_not_ready_when_oserror( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, error: Exception ) -> None: """Test AnthemAV configuration entry not ready.""" - with patch( "anthemav.Connection.create", - side_effect=OSError, + side_effect=error, ): mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -52,23 +52,18 @@ async def test_anthemav_dispatcher_signal( hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock, - mock_config_entry: MockConfigEntry, + init_integration: MockConfigEntry, + update_callback: Callable[[str], None], ) -> None: """Test send update signal to dispatcher.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - states = hass.states.get("media_player.anthem_av") assert states assert states.state == STATE_OFF # change state of the AVR - mock_anthemav.protocol.power = True + mock_anthemav.protocol.zones[1].power = True - # get the callback function that trigger the signal to update the state - avr_update_callback = mock_connection_create.call_args[1]["update_callback"] - avr_update_callback("power") + update_callback("power") await hass.async_block_till_done() diff --git a/tests/components/anthemav/test_media_player.py b/tests/components/anthemav/test_media_player.py new file mode 100644 index 00000000000..a741c5e3b53 --- /dev/null +++ b/tests/components/anthemav/test_media_player.py @@ -0,0 +1,71 @@ +"""Test the Anthem A/V Receivers config flow.""" +from typing import Callable +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.components.media_player.const import ( + ATTR_APP_NAME, + ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_TITLE, + ATTR_MEDIA_VOLUME_MUTED, +) +from homeassistant.components.siren.const import ATTR_VOLUME_LEVEL +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "entity_id,entity_name", + [ + ("media_player.anthem_av", "Anthem AV"), + ("media_player.anthem_av_zone_2", "Anthem AV zone 2"), + ], +) +async def test_zones_loaded( + hass: HomeAssistant, + init_integration: MockConfigEntry, + entity_id: str, + entity_name: str, +) -> None: + """Test zones are loaded.""" + + states = hass.states.get(entity_id) + + assert states + assert states.state == STATE_OFF + assert states.name == entity_name + + +async def test_update_states_zone1( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_anthemav: AsyncMock, + update_callback: Callable[[str], None], +) -> None: + """Test zone states are updated.""" + + mock_zone = mock_anthemav.protocol.zones[1] + + mock_zone.power = True + mock_zone.mute = True + mock_zone.volume_as_percentage = 42 + mock_zone.input_name = "TEST INPUT" + mock_zone.input_format = "2.0 PCM" + mock_anthemav.protocol.input_list = ["TEST INPUT", "INPUT 2"] + + update_callback("command") + await hass.async_block_till_done() + + states = hass.states.get("media_player.anthem_av") + assert states + assert states.state == STATE_ON + assert states.attributes[ATTR_VOLUME_LEVEL] == 42 + assert states.attributes[ATTR_MEDIA_VOLUME_MUTED] is True + assert states.attributes[ATTR_INPUT_SOURCE] == "TEST INPUT" + assert states.attributes[ATTR_MEDIA_TITLE] == "TEST INPUT" + assert states.attributes[ATTR_APP_NAME] == "2.0 PCM" + assert states.attributes[ATTR_INPUT_SOURCE_LIST] == ["TEST INPUT", "INPUT 2"] From faf25b2235cabea7ceeb94908f94949dc20c1806 Mon Sep 17 00:00:00 2001 From: ildar170975 <71872483+ildar170975@users.noreply.github.com> Date: Sat, 30 Jul 2022 15:06:55 +0300 Subject: [PATCH 049/903] Add telegram disable_web_page_preview (#75898) * Add telegram disable_web_page_preview Adds ability to specify disable_web_page_preview to telegram.notify: ``` - service: notify.telegram data: message: >- HA site data: parse_mode: html disable_web_page_preview: true ``` * Update homeassistant/components/telegram/notify.py Co-authored-by: Martin Hjelmare * Update notify.py * Update notify.py Co-authored-by: Martin Hjelmare --- homeassistant/components/telegram/notify.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index b87ddc670c3..bca48e4b85a 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -13,6 +13,7 @@ from homeassistant.components.notify import ( ) from homeassistant.components.telegram_bot import ( ATTR_DISABLE_NOTIF, + ATTR_DISABLE_WEB_PREV, ATTR_MESSAGE_TAG, ATTR_PARSER, ) @@ -76,6 +77,11 @@ class TelegramNotificationService(BaseNotificationService): parse_mode = data.get(ATTR_PARSER) service_data.update({ATTR_PARSER: parse_mode}) + # Set disable_web_page_preview + if data is not None and ATTR_DISABLE_WEB_PREV in data: + disable_web_page_preview = data[ATTR_DISABLE_WEB_PREV] + service_data.update({ATTR_DISABLE_WEB_PREV: disable_web_page_preview}) + # Get keyboard info if data is not None and ATTR_KEYBOARD in data: keys = data.get(ATTR_KEYBOARD) From b9b916cdcd32e09c37637fd5895a4ef2b09c8a44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Jul 2022 06:15:06 -0700 Subject: [PATCH 050/903] Bump govee-ble to fix H5179 sensors (#75957) Changelog: https://github.com/Bluetooth-Devices/govee-ble/compare/v0.12.4...v0.12.5 --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 270858d04d4..c7909d3e1af 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -24,7 +24,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.4"], + "requirements": ["govee-ble==0.12.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 9b5e51affe6..c602bb56697 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.4 +govee-ble==0.12.5 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb09140e435..0caac1ec257 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.4 +govee-ble==0.12.5 # homeassistant.components.gree greeclimate==1.2.0 From 377f56ff5f6c3031b6abf4de1db630f6177050d4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 31 Jul 2022 00:25:44 +0000 Subject: [PATCH 051/903] [ci skip] Translation update --- .../components/discord/translations/sv.json | 13 ++++++++++ .../components/fibaro/translations/sv.json | 9 +++++++ .../components/filesize/translations/sv.json | 7 ++++++ .../components/group/translations/sv.json | 24 ++++++++++++++++++- .../components/qnap_qsw/translations/sv.json | 2 ++ .../radiotherm/translations/sv.json | 1 + .../components/samsungtv/translations/sv.json | 3 +++ .../components/scrape/translations/sv.json | 12 +++++++++- .../components/senz/translations/ja.json | 1 + .../components/sql/translations/sv.json | 6 ++++- .../components/tautulli/translations/sv.json | 9 +++++++ .../trafikverket_train/translations/sv.json | 5 ++++ 12 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/discord/translations/sv.json create mode 100644 homeassistant/components/filesize/translations/sv.json diff --git a/homeassistant/components/discord/translations/sv.json b/homeassistant/components/discord/translations/sv.json new file mode 100644 index 00000000000..1b52e7816d2 --- /dev/null +++ b/homeassistant/components/discord/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/sv.json b/homeassistant/components/fibaro/translations/sv.json index 23c825f256f..89cfc8f6c3b 100644 --- a/homeassistant/components/fibaro/translations/sv.json +++ b/homeassistant/components/fibaro/translations/sv.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/filesize/translations/sv.json b/homeassistant/components/filesize/translations/sv.json new file mode 100644 index 00000000000..c0b662beebe --- /dev/null +++ b/homeassistant/components/filesize/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index 8ba098e02cd..93cbf568052 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -9,6 +9,12 @@ }, "title": "Ny grupp" }, + "cover": { + "title": "L\u00e4gg till grupp" + }, + "fan": { + "title": "L\u00e4gg till grupp" + }, "light": { "title": "L\u00e4gg till grupp" }, @@ -20,6 +26,15 @@ }, "title": "L\u00e4gg till grupp" }, + "media_player": { + "title": "L\u00e4gg till grupp" + }, + "switch": { + "data": { + "name": "Namn" + }, + "title": "L\u00e4gg till grupp" + }, "user": { "menu_options": { "lock": "L\u00e5sgrupp" @@ -32,7 +47,8 @@ "step": { "binary_sensor": { "data": { - "all": "Alla entiteter" + "all": "Alla entiteter", + "entities": "Medlemmar" } }, "lock": { @@ -40,6 +56,12 @@ "entities": "Medlemmar", "hide_members": "D\u00f6lj medlemmar" } + }, + "switch": { + "data": { + "all": "Alla entiteter", + "entities": "Medlemmar" + } } } }, diff --git a/homeassistant/components/qnap_qsw/translations/sv.json b/homeassistant/components/qnap_qsw/translations/sv.json index f447af18c52..8db64916805 100644 --- a/homeassistant/components/qnap_qsw/translations/sv.json +++ b/homeassistant/components/qnap_qsw/translations/sv.json @@ -16,6 +16,8 @@ }, "user": { "data": { + "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/radiotherm/translations/sv.json b/homeassistant/components/radiotherm/translations/sv.json index 3999a2a90e0..54b0407d29e 100644 --- a/homeassistant/components/radiotherm/translations/sv.json +++ b/homeassistant/components/radiotherm/translations/sv.json @@ -7,6 +7,7 @@ "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "Vill du konfigurera {name} {model} ( {host} )?" diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index 0141800c5c0..feff1c2fc86 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -13,6 +13,9 @@ "confirm": { "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." }, + "pairing": { + "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress", diff --git a/homeassistant/components/scrape/translations/sv.json b/homeassistant/components/scrape/translations/sv.json index f1cf81c5aa6..2c4da93bb6d 100644 --- a/homeassistant/components/scrape/translations/sv.json +++ b/homeassistant/components/scrape/translations/sv.json @@ -32,10 +32,20 @@ "data": { "attribute": "Attribut", "authentication": "Autentisering", + "device_class": "Enhetsklass", + "headers": "Headers", "index": "Index", "password": "L\u00f6senord", + "resource": "Resurs", "select": "V\u00e4lj", - "username": "Anv\u00e4ndarnamn" + "state_class": "Tillst\u00e5ndsklass", + "unit_of_measurement": "M\u00e5ttenhet", + "username": "Anv\u00e4ndarnamn", + "value_template": "V\u00e4rdemall", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "data_description": { + "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen" } } } diff --git a/homeassistant/components/senz/translations/ja.json b/homeassistant/components/senz/translations/ja.json index dbb794e2cff..1d454cf23bc 100644 --- a/homeassistant/components/senz/translations/ja.json +++ b/homeassistant/components/senz/translations/ja.json @@ -19,6 +19,7 @@ }, "issues": { "removed_yaml": { + "description": "nVent RAYCHEM SENZ\u306f\u3001YAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306e\u3001YAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "nVent RAYCHEM SENZ YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/sql/translations/sv.json b/homeassistant/components/sql/translations/sv.json index 9009cac8ee3..dec86c2b66b 100644 --- a/homeassistant/components/sql/translations/sv.json +++ b/homeassistant/components/sql/translations/sv.json @@ -14,12 +14,16 @@ "name": "Namn" }, "data_description": { - "name": "Namn som kommer att anv\u00e4ndas f\u00f6r konfigurationsinmatning och \u00e4ven f\u00f6r sensorn." + "name": "Namn som kommer att anv\u00e4ndas f\u00f6r konfigurationsinmatning och \u00e4ven f\u00f6r sensorn.", + "value_template": "V\u00e4rdemall (valfritt)" } } } }, "options": { + "error": { + "db_url_invalid": "Databasens URL \u00e4r ogiltig" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/tautulli/translations/sv.json b/homeassistant/components/tautulli/translations/sv.json index 66b23701c73..abcbe307998 100644 --- a/homeassistant/components/tautulli/translations/sv.json +++ b/homeassistant/components/tautulli/translations/sv.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/trafikverket_train/translations/sv.json b/homeassistant/components/trafikverket_train/translations/sv.json index b6d7ecada04..e08a6492532 100644 --- a/homeassistant/components/trafikverket_train/translations/sv.json +++ b/homeassistant/components/trafikverket_train/translations/sv.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "incorrect_api_key": "Ogiltig API-nyckel f\u00f6r valt konto", "invalid_auth": "Ogiltig autentisering", "invalid_station": "Det gick inte att hitta en station med det angivna namnet", From abb7495ced8d9ea3cd13a33939d7fd5aa2afbd61 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 31 Jul 2022 12:21:25 +0200 Subject: [PATCH 052/903] Handle failed connection attempts in opentherm_gw (#75961) --- .../components/opentherm_gw/__init__.py | 18 ++++++++++++++++-- .../components/opentherm_gw/config_flow.py | 15 +++++++++++---- homeassistant/components/opentherm_gw/const.py | 2 ++ .../components/opentherm_gw/strings.json | 3 ++- .../opentherm_gw/test_config_flow.py | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 7c27eeceede..cdf360c8795 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,9 +1,11 @@ """Support for OpenTherm Gateway devices.""" +import asyncio from datetime import date, datetime import logging import pyotgw import pyotgw.vars as gw_vars +from serial import SerialException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -23,6 +25,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -37,6 +40,7 @@ from .const import ( CONF_PRECISION, CONF_READ_PRECISION, CONF_SET_PRECISION, + CONNECTION_TIMEOUT, DATA_GATEWAYS, DATA_OPENTHERM_GW, DOMAIN, @@ -107,8 +111,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.add_update_listener(options_updated) - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe()) + try: + await asyncio.wait_for( + gateway.connect_and_subscribe(), + timeout=CONNECTION_TIMEOUT, + ) + except (asyncio.TimeoutError, ConnectionError, SerialException) as ex: + raise ConfigEntryNotReady( + f"Could not connect to gateway at {gateway.device_path}: {ex}" + ) from ex await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -428,6 +439,9 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" self.status = await self.gateway.connect(self.device_path) + if not self.status: + await self.cleanup() + raise ConnectionError version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) self.gw_version = version_string[18:] if version_string else None _LOGGER.debug( diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 3f91496adab..c3a955b2387 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -26,6 +26,7 @@ from .const import ( CONF_READ_PRECISION, CONF_SET_PRECISION, CONF_TEMPORARY_OVRD_MODE, + CONNECTION_TIMEOUT, ) @@ -62,15 +63,21 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): otgw = pyotgw.OpenThermGateway() status = await otgw.connect(device) await otgw.disconnect() + if not status: + raise ConnectionError return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) try: - res = await asyncio.wait_for(test_connection(), timeout=10) - except (asyncio.TimeoutError, SerialException): + await asyncio.wait_for( + test_connection(), + timeout=CONNECTION_TIMEOUT, + ) + except asyncio.TimeoutError: + return self._show_form({"base": "timeout_connect"}) + except (ConnectionError, SerialException): return self._show_form({"base": "cannot_connect"}) - if res: - return self._create_entry(gw_id, name, device) + return self._create_entry(gw_id, name, device) return self._show_form() diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index a5042628529..d72469759f1 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -25,6 +25,8 @@ CONF_READ_PRECISION = "read_precision" CONF_SET_PRECISION = "set_precision" CONF_TEMPORARY_OVRD_MODE = "temporary_override_mode" +CONNECTION_TIMEOUT = 10 + DATA_GATEWAYS = "gateways" DATA_OPENTHERM_GW = "opentherm_gw" diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index f53ffeda6f6..a80a059481d 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -12,7 +12,8 @@ "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "id_exists": "Gateway id already exists", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" } }, "options": { diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 46d53bc54b5..080e9a96d58 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -164,7 +164,7 @@ async def test_form_connection_timeout(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": "timeout_connect"} assert len(mock_connect.mock_calls) == 1 From 1204b4f7000cda093290ecf3d07eaca2d00bdee1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 31 Jul 2022 12:26:16 +0100 Subject: [PATCH 053/903] Add typings to Certificate Expiry integration (#75945) --- .../components/cert_expiry/config_flow.py | 18 +++++++++++++++--- homeassistant/components/cert_expiry/helper.py | 12 ++++++++++-- homeassistant/components/cert_expiry/sensor.py | 10 ++++++++-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 13336c59771..ed294cab981 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -1,12 +1,15 @@ """Config flow for the Cert Expiry platform.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_PORT, DOMAIN from .errors import ( @@ -29,7 +32,10 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._errors: dict[str, str] = {} - async def _test_connection(self, user_input=None): + async def _test_connection( + self, + user_input: Mapping[str, Any], + ): """Test connection to the server and try to get the certificate.""" try: await get_cert_expiry_timestamp( @@ -48,7 +54,10 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return True return False - async def async_step_user(self, user_input=None): + async def async_step_user( + self, + user_input: Mapping[str, Any] | None = None, + ) -> FlowResult: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: @@ -85,7 +94,10 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import( + self, + user_input: Mapping[str, Any] | None = None, + ) -> FlowResult: """Import a config entry. Only host was required in the yaml file all other fields are optional diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py index c00a99c8e86..fcd808a6521 100644 --- a/homeassistant/components/cert_expiry/helper.py +++ b/homeassistant/components/cert_expiry/helper.py @@ -2,6 +2,7 @@ import socket import ssl +from homeassistant.core import HomeAssistant from homeassistant.util import dt from .const import TIMEOUT @@ -13,7 +14,10 @@ from .errors import ( ) -def get_cert(host, port): +def get_cert( + host: str, + port: int, +): """Get the certificate for the host and port combination.""" ctx = ssl.create_default_context() address = (host, port) @@ -23,7 +27,11 @@ def get_cert(host, port): return cert -async def get_cert_expiry_timestamp(hass, hostname, port): +async def get_cert_expiry_timestamp( + hass: HomeAssistant, + hostname: str, + port: int, +): """Return the certificate's expiration timestamp.""" try: cert = await hass.async_add_executor_job(get_cert, hostname, port) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 9ee79e5c449..0c1b6116cdd 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -19,6 +19,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import CertExpiryDataUpdateCoordinator from .const import DEFAULT_PORT, DOMAIN SCAN_INTERVAL = timedelta(hours=12) @@ -57,7 +58,9 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add cert-expiry entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] @@ -88,7 +91,10 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): _attr_device_class = SensorDeviceClass.TIMESTAMP - def __init__(self, coordinator) -> None: + def __init__( + self, + coordinator: CertExpiryDataUpdateCoordinator, + ) -> None: """Initialize a Cert Expiry timestamp sensor.""" super().__init__(coordinator) self._attr_name = f"Cert Expiry Timestamp ({coordinator.name})" From 1e115341af6528a2f002140cd33aecb555e2ac36 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Sun, 31 Jul 2022 13:28:09 +0200 Subject: [PATCH 054/903] Bump enturclient to 0.2.4 (#75928) --- homeassistant/components/entur_public_transport/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index c7f4fbeef53..3bbacb8c3d4 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "entur_public_transport", "name": "Entur", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", - "requirements": ["enturclient==0.2.3"], + "requirements": ["enturclient==0.2.4"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_polling", "loggers": ["enturclient"] diff --git a/requirements_all.txt b/requirements_all.txt index c602bb56697..fc08ba28427 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -606,7 +606,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.entur_public_transport -enturclient==0.2.3 +enturclient==0.2.4 # homeassistant.components.environment_canada env_canada==0.5.22 From ee273daf8d4e833bd5e0d9096122fb1fd703d693 Mon Sep 17 00:00:00 2001 From: MasonCrawford Date: Sun, 31 Jul 2022 19:32:40 +0800 Subject: [PATCH 055/903] Small fixes for LG soundbar (#75938) --- homeassistant/components/lg_soundbar/media_player.py | 3 --- homeassistant/components/lg_soundbar/strings.json | 3 +-- homeassistant/components/lg_soundbar/translations/en.json | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index f8f6fcf26fd..941042d5bce 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -47,7 +47,6 @@ class LGDevice(MediaPlayerEntity): self._port = port self._attr_unique_id = unique_id - self._name = None self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -94,8 +93,6 @@ class LGDevice(MediaPlayerEntity): elif response["msg"] == "SPK_LIST_VIEW_INFO": if "i_vol" in data: self._volume = data["i_vol"] - if "s_user_name" in data: - self._name = data["s_user_name"] if "i_vol_min" in data: self._volume_min = data["i_vol_min"] if "i_vol_max" in data: diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json index ef7bf32a051..52d57eda809 100644 --- a/homeassistant/components/lg_soundbar/strings.json +++ b/homeassistant/components/lg_soundbar/strings.json @@ -11,8 +11,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "existing_instance_updated": "Updated existing configuration.", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json index a646279203f..10441d21536 100644 --- a/homeassistant/components/lg_soundbar/translations/en.json +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is already configured", - "existing_instance_updated": "Updated existing configuration." + "already_configured": "Device is already configured" }, "error": { "cannot_connect": "Failed to connect" From 9e76e8cef824a7356f09a9a3348decd24440fe17 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 31 Jul 2022 04:37:29 -0700 Subject: [PATCH 056/903] Bump grpc requirements to 1.48.0 (#75603) --- homeassistant/package_constraints.txt | 4 ++-- script/gen_requirements_all.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c9d424daa33..d4344ec256c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -54,8 +54,8 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.46.1 -grpcio-status==1.46.1 +grpcio==1.48.0 +grpcio-status==1.48.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5ef794b4eab..3cb35eec147 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,8 +68,8 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.46.1 -grpcio-status==1.46.1 +grpcio==1.48.0 +grpcio-status==1.48.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, From 90458ee200d6d9e6fe7458fec3021d904e365c13 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 13:46:25 +0200 Subject: [PATCH 057/903] Use attributes in zerproc light (#75951) --- homeassistant/components/zerproc/light.py | 76 ++++++++--------------- 1 file changed, 26 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 9bc2e4d29f3..5a32ca23332 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import pyzerproc @@ -14,7 +15,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval @@ -79,26 +80,24 @@ class ZerprocLight(LightEntity): """Representation of an Zerproc Light.""" _attr_color_mode = ColorMode.HS + _attr_icon = "mdi:string-lights" _attr_supported_color_modes = {ColorMode.HS} - def __init__(self, light): + def __init__(self, light) -> None: """Initialize a Zerproc light.""" self._light = light - self._name = None - self._is_on = None - self._hs_color = None - self._brightness = None - self._available = True async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" self.async_on_remove( - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self.async_will_remove_from_hass - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._hass_stop) ) - async def async_will_remove_from_hass(self, *args) -> None: + async def _hass_stop(self, event: Event) -> None: + """Run on EVENT_HOMEASSISTANT_STOP.""" + await self.async_will_remove_from_hass() + + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" try: await self._light.disconnect() @@ -126,64 +125,41 @@ class ZerprocLight(LightEntity): name=self.name, ) - @property - def icon(self) -> str | None: - """Return the icon to use in the frontend.""" - return "mdi:string-lights" - - @property - def brightness(self): - """Return the brightness of the light.""" - return self._brightness - - @property - def hs_color(self): - """Return the hs color.""" - return self._hs_color - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs: - default_hs = (0, 0) if self._hs_color is None else self._hs_color + default_hs = (0, 0) if self.hs_color is None else self.hs_color hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) - default_brightness = 255 if self._brightness is None else self._brightness + default_brightness = 255 if self.brightness is None else self.brightness brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) - rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) + rgb = color_util.color_hsv_to_RGB( + hue_sat[0], hue_sat[1], brightness / 255 * 100 + ) await self._light.set_color(*rgb) else: await self._light.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self._light.turn_off() - async def async_update(self): + async def async_update(self) -> None: """Fetch new state data for this light.""" try: - if not self._available: + if not self.available: await self._light.connect() state = await self._light.get_state() except pyzerproc.ZerprocException: - if self._available: + if self.available: _LOGGER.warning("Unable to connect to %s", self._light.address) - self._available = False + self._attr_available = False return - if self._available is False: + if not self.available: _LOGGER.info("Reconnected to %s", self._light.address) - self._available = True - self._is_on = state.is_on + self._attr_available = True + self._attr_is_on = state.is_on hsv = color_util.color_RGB_to_hsv(*state.color) - self._hs_color = hsv[:2] - self._brightness = int(round((hsv[2] / 100) * 255)) + self._attr_hs_color = hsv[:2] + self._attr_brightness = int(round((hsv[2] / 100) * 255)) From 11a19c2612f9704721c14ea4300c76257a75e468 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 13:50:24 +0200 Subject: [PATCH 058/903] Improve type hints in light [s-z] (#75946) --- homeassistant/components/scsgate/light.py | 5 +-- homeassistant/components/sisyphus/light.py | 11 +++--- homeassistant/components/smartthings/light.py | 7 ++-- homeassistant/components/smarttub/light.py | 6 ++-- homeassistant/components/tellduslive/light.py | 5 +-- homeassistant/components/tikteck/light.py | 5 +-- homeassistant/components/twinkly/light.py | 4 +-- homeassistant/components/unifiled/light.py | 7 ++-- homeassistant/components/upb/light.py | 8 +++-- homeassistant/components/velux/light.py | 6 ++-- homeassistant/components/vera/light.py | 2 +- homeassistant/components/vesync/light.py | 3 +- homeassistant/components/x10/light.py | 7 ++-- .../components/xiaomi_aqara/light.py | 3 +- homeassistant/components/xiaomi_miio/light.py | 35 ++++++++++--------- homeassistant/components/yeelight/light.py | 9 ++--- .../components/yeelightsunflower/light.py | 7 ++-- homeassistant/components/zengge/light.py | 7 ++-- homeassistant/components/zwave_me/light.py | 2 +- 19 files changed, 79 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/scsgate/light.py b/homeassistant/components/scsgate/light.py index 3957aa4cd95..df63bad49d4 100644 --- a/homeassistant/components/scsgate/light.py +++ b/homeassistant/components/scsgate/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from scsgate.tasks import ToggleStatusTask import voluptuous as vol @@ -76,7 +77,7 @@ class SCSGateLight(LightEntity): """Return true if light is on.""" return self._toggled - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._scsgate.append_task(ToggleStatusTask(target=self._scs_id, toggled=True)) @@ -84,7 +85,7 @@ class SCSGateLight(LightEntity): self._toggled = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._scsgate.append_task(ToggleStatusTask(target=self._scs_id, toggled=False)) diff --git a/homeassistant/components/sisyphus/light.py b/homeassistant/components/sisyphus/light.py index 7601ccf7657..d0cd7597e58 100644 --- a/homeassistant/components/sisyphus/light.py +++ b/homeassistant/components/sisyphus/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import aiohttp @@ -47,16 +48,16 @@ class SisyphusLight(LightEntity): self._name = name self._table = table - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add listeners after this object has been initialized.""" self._table.add_listener(self.async_write_ha_state) - async def async_update(self): + async def async_update(self) -> None: """Force update the table state.""" await self._table.refresh() @property - def available(self): + def available(self) -> bool: """Return true if the table is responding to heartbeats.""" return self._table.is_connected @@ -80,12 +81,12 @@ class SisyphusLight(LightEntity): """Return the current brightness of the table's ring light.""" return self._table.brightness * 255 - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Put the table to sleep.""" await self._table.sleep() _LOGGER.debug("Sisyphus table %s: sleep") - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Wake up the table if necessary, optionally changes brightness.""" if not self.is_on: await self._table.wakeup() diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 1b1738a94d4..918e8b4258c 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Sequence +from typing import Any from pysmartthings import Capability @@ -96,7 +97,7 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): return features - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" tasks = [] # Color temperature @@ -121,7 +122,7 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" # Switch/transition if ( @@ -136,7 +137,7 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) - async def async_update(self): + async def async_update(self) -> None: """Update entity attributes when the device status has changed.""" # Brightness and transition if self._supported_features & SUPPORT_BRIGHTNESS: diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py index b8750c50611..f7e229449e0 100644 --- a/homeassistant/components/smarttub/light.py +++ b/homeassistant/components/smarttub/light.py @@ -1,4 +1,6 @@ """Platform for light integration.""" +from typing import Any + from smarttub import SpaLight from homeassistant.components.light import ( @@ -125,7 +127,7 @@ class SmartTubLight(SmartTubEntity, LightEntity): return SpaLight.LightMode[effect.upper()] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" mode = self._effect_to_light_mode(kwargs.get(ATTR_EFFECT, DEFAULT_LIGHT_EFFECT)) @@ -136,7 +138,7 @@ class SmartTubLight(SmartTubEntity, LightEntity): await self.light.set_mode(mode, intensity) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self.light.set_mode(SpaLight.LightMode.OFF, 0) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/tellduslive/light.py b/homeassistant/components/tellduslive/light.py index 7609cc6adee..205fff840d6 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -1,5 +1,6 @@ """Support for Tellstick lights using Tellstick Net.""" import logging +from typing import Any from homeassistant.components import light, tellduslive from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity @@ -58,7 +59,7 @@ class TelldusLiveLight(TelldusLiveEntity, LightEntity): """Return true if light is on.""" return self.device.is_on - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness) if brightness == 0: @@ -70,7 +71,7 @@ class TelldusLiveLight(TelldusLiveEntity, LightEntity): self.device.dim(level=brightness) self.changed() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self.device.turn_off() self.changed() diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 0f810c434fb..28daf4baac1 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import tikteck import voluptuous as vol @@ -111,7 +112,7 @@ class TikteckLight(LightEntity): """Set the bulb state.""" return self._bulb.set_state(red, green, blue, brightness) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" self._state = True @@ -128,7 +129,7 @@ class TikteckLight(LightEntity): self.set_state(rgb[0], rgb[1], rgb[2], self.brightness) self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" self._state = False self.set_state(0, 0, 0, 0) diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 9068c04e01b..ba6ac7cf492 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -140,7 +140,7 @@ class TwinklyLight(LightEntity): return attributes - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(int(kwargs[ATTR_BRIGHTNESS]) / 2.55) @@ -183,7 +183,7 @@ class TwinklyLight(LightEntity): if not self._is_on: await self._client.turn_on() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" await self._client.turn_off() diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index dd5a5b5290a..8ba9fc2b6f9 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from unifiled import unifiled import voluptuous as vol @@ -98,7 +99,7 @@ class UnifiLedLight(LightEntity): """Return true if light is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" self._api.setdevicebrightness( self._unique_id, @@ -106,11 +107,11 @@ class UnifiLedLight(LightEntity): ) self._api.setdeviceoutput(self._unique_id, 1) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._api.setdeviceoutput(self._unique_id, 0) - def update(self): + def update(self) -> None: """Update the light states.""" self._state = self._api.getlightstate(self._unique_id) self._brightness = self._api.convertfrom100to255( diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 98a775c18ab..85cda1f0061 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -1,4 +1,6 @@ """Platform for UPB light integration.""" +from typing import Any + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_FLASH, @@ -85,7 +87,7 @@ class UpbLight(UpbAttachedEntity, LightEntity): """Get the current brightness.""" return self._brightness != 0 - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" if flash := kwargs.get(ATTR_FLASH): await self.async_light_blink(0.5 if flash == "short" else 1.5) @@ -94,7 +96,7 @@ class UpbLight(UpbAttachedEntity, LightEntity): brightness = round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55) self._element.turn_on(brightness, rate) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the device.""" rate = kwargs.get(ATTR_TRANSITION, -1) self._element.turn_off(rate) @@ -114,7 +116,7 @@ class UpbLight(UpbAttachedEntity, LightEntity): blink_rate = int(blink_rate * 60) # Convert seconds to 60 hz pulses self._element.blink(blink_rate) - async def async_update(self): + async def async_update(self) -> None: """Request the device to update its status.""" self._element.update_status() diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index f8a52fc05c1..a600aceedd2 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -1,6 +1,8 @@ """Support for Velux lights.""" from __future__ import annotations +from typing import Any + from pyvlx import Intensity, LighteningDevice from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity @@ -43,7 +45,7 @@ class VeluxLight(VeluxEntity, LightEntity): """Return true if light is on.""" return not self.node.intensity.off and self.node.intensity.known - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if ATTR_BRIGHTNESS in kwargs: intensity_percent = int(100 - kwargs[ATTR_BRIGHTNESS] / 255 * 100) @@ -54,6 +56,6 @@ class VeluxLight(VeluxEntity, LightEntity): else: await self.node.turn_on(wait_for_completion=True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self.node.turn_off(wait_for_completion=True) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index e91492a934f..fa017be475e 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -88,7 +88,7 @@ class VeraLight(VeraDevice[veraApi.VeraDimmer], LightEntity): self._state = True self.schedule_update_ha_state(True) - def turn_off(self, **kwargs: Any): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self.vera_device.switch_off() self._state = False diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index d01319ff0e7..8727a770112 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -1,5 +1,6 @@ """Support for VeSync bulbs and wall dimmers.""" import logging +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -83,7 +84,7 @@ class VeSyncBaseLight(VeSyncDevice, LightEntity): # convert percent brightness to ha expected range return round((max(1, brightness_value) / 100) * 255) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" attribute_adjustment_only = False # set white temperature diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index 42a15a643bd..b7e331e2199 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging from subprocess import STDOUT, CalledProcessError, check_output +from typing import Any import voluptuous as vol @@ -87,7 +88,7 @@ class X10Light(LightEntity): """Return true if light is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if self._is_cm11a: x10_command(f"on {self._id}") @@ -96,7 +97,7 @@ class X10Light(LightEntity): self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) self._state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" if self._is_cm11a: x10_command(f"off {self._id}") @@ -104,7 +105,7 @@ class X10Light(LightEntity): x10_command(f"foff {self._id}") self._state = False - def update(self): + def update(self) -> None: """Fetch update state.""" if self._is_cm11a: self._state = bool(get_unit_status(self._id)) diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 812cd1c96b8..173ffe564fc 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -2,6 +2,7 @@ import binascii import logging import struct +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -113,7 +114,7 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): self._state = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if self._write_to_hub(self._sid, **{self._data_key: 0}): self._state = False diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index fd6af0d9560..2a7fce01442 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -7,6 +7,7 @@ from datetime import timedelta from functools import partial import logging from math import ceil +from typing import Any from miio import ( Ceil, @@ -292,7 +293,7 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): return False - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] @@ -311,11 +312,11 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): else: await self._try_command("Turning the light on failed.", self._device.on) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._try_command("Turning the light off failed.", self._device.off) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -341,7 +342,7 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): self._state_attrs.update({ATTR_SCENE: None, ATTR_DELAYED_TURN_OFF: None}) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -430,7 +431,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): """Return the warmest color_temp that this light supports.""" return 333 - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_COLOR_TEMP in kwargs: color_temp = kwargs[ATTR_COLOR_TEMP] @@ -497,7 +498,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): else: await self._try_command("Turning the light on failed.", self._device.on) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -556,7 +557,7 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): """Return the warmest color_temp that this light supports.""" return 370 - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -602,7 +603,7 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): {ATTR_REMINDER: None, ATTR_NIGHT_LIGHT_MODE: None, ATTR_EYECARE_MODE: None} ) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -714,7 +715,7 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): unique_id = f"{unique_id}-ambient" super().__init__(name, device, entry, unique_id) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] @@ -739,13 +740,13 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): "Turning the ambient light on failed.", self._device.ambient_on ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._try_command( "Turning the ambient light off failed.", self._device.ambient_off ) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -805,7 +806,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): return ColorMode.HS return ColorMode.COLOR_TEMP - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_COLOR_TEMP in kwargs: color_temp = kwargs[ATTR_COLOR_TEMP] @@ -905,7 +906,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): else: await self._try_command("Turning the light on failed.", self._device.on) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -996,7 +997,7 @@ class XiaomiGatewayLight(LightEntity): """Return the hs color value.""" return self._hs - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_HS_COLOR in kwargs: rgb = color.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) @@ -1012,7 +1013,7 @@ class XiaomiGatewayLight(LightEntity): self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._gateway.light.set_rgb(0, self._rgb) self.schedule_update_ha_state() @@ -1071,7 +1072,7 @@ class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity): """Return max cct.""" return self._sub_device.status["cct_max"] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" await self.hass.async_add_executor_job(self._sub_device.on) @@ -1087,6 +1088,6 @@ class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity): self._sub_device.set_brightness, brightness ) - async def async_turn_off(self, **kwargsf): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self.hass.async_add_executor_job(self._sub_device.off) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index a368490e51e..9bb0ed21989 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import logging import math +from typing import Any import voluptuous as vol import yeelight @@ -442,7 +443,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self._async_cancel_pending_state_check() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" self.async_on_remove( async_dispatcher_connect( @@ -585,7 +586,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Return yeelight device.""" return self._device - async def async_update(self): + async def async_update(self) -> None: """Update light properties.""" await self.device.async_update(True) @@ -774,7 +775,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): power_mode=self._turn_on_power_mode, ) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the bulb on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) @@ -836,7 +837,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Turn off with a given transition duration wrapped with _async_cmd.""" await self._bulb.async_turn_off(duration=duration, light_type=self.light_type) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off.""" if not self.is_on: return diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 1b76aef1328..8b51dad40f7 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol import yeelightsunflower @@ -87,7 +88,7 @@ class SunflowerBulb(LightEntity): """Return the color property.""" return color_util.color_RGB_to_hs(*self._rgb_color) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on, optionally set colour/brightness.""" # when no arguments, just turn light on (full brightness) if not kwargs: @@ -104,11 +105,11 @@ class SunflowerBulb(LightEntity): bright = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) self._light.set_brightness(bright) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._light.turn_off() - def update(self): + def update(self) -> None: """Fetch new state data for this light and update local values.""" self._light.update() self._available = self._light.available diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 057b049eefd..6939ecb276b 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol from zengge import zengge @@ -123,7 +124,7 @@ class ZenggeLight(LightEntity): """Set the white state.""" return self._bulb.set_white(white) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" self._state = True self._bulb.on() @@ -153,12 +154,12 @@ class ZenggeLight(LightEntity): ) self._set_rgb(*rgb) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" self._state = False self._bulb.off() - def update(self): + def update(self) -> None: """Synchronise internal state with the actual light state.""" rgb = self._bulb.get_colour() hsv = color_util.color_RGB_to_hsv(*rgb) diff --git a/homeassistant/components/zwave_me/light.py b/homeassistant/components/zwave_me/light.py index fd4488b8dbf..5da0f955059 100644 --- a/homeassistant/components/zwave_me/light.py +++ b/homeassistant/components/zwave_me/light.py @@ -52,7 +52,7 @@ class ZWaveMeRGB(ZWaveMeEntity, LightEntity): """Turn the device on.""" self.controller.zwave_api.send_command(self.device.id, "off") - def turn_on(self, **kwargs: Any): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" color = kwargs.get(ATTR_RGB_COLOR) From 7b1463e03db39b90ac222b0f3143514aa5d7b7f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 13:53:22 +0200 Subject: [PATCH 059/903] Improve type hints in light [i-r] (#75943) --- homeassistant/components/insteon/light.py | 8 +++++--- homeassistant/components/kulersky/light.py | 5 +++-- homeassistant/components/lightwave/light.py | 6 ++++-- homeassistant/components/limitlessled/light.py | 2 +- homeassistant/components/litejet/light.py | 11 ++++++----- homeassistant/components/lutron/light.py | 8 +++++--- homeassistant/components/lutron_caseta/light.py | 5 +++-- homeassistant/components/lw12wifi/light.py | 3 ++- homeassistant/components/mochad/light.py | 5 +++-- homeassistant/components/myq/light.py | 6 ++++-- homeassistant/components/mystrom/light.py | 7 ++++--- homeassistant/components/niko_home_control/light.py | 7 ++++--- homeassistant/components/opple/light.py | 7 ++++--- homeassistant/components/osramlightify/light.py | 7 ++++--- homeassistant/components/philips_js/light.py | 5 +++-- homeassistant/components/pilight/light.py | 4 +++- homeassistant/components/plum_lightpad/light.py | 13 +++++++------ homeassistant/components/rflink/light.py | 11 ++++++----- homeassistant/components/rfxtrx/light.py | 7 ++++--- homeassistant/components/ring/light.py | 5 +++-- 20 files changed, 78 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index bf8b693b103..062a3f54d1b 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -1,4 +1,6 @@ """Support for Insteon lights via PowerLinc Modem.""" +from typing import Any + from pyinsteon.config import ON_LEVEL from homeassistant.components.light import ( @@ -50,11 +52,11 @@ class InsteonDimmerEntity(InsteonEntity, LightEntity): return self._insteon_device_group.value @property - def is_on(self): + def is_on(self) -> bool: """Return the boolean response if the node is on.""" return bool(self.brightness) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) @@ -67,6 +69,6 @@ class InsteonDimmerEntity(InsteonEntity, LightEntity): else: await self._insteon_device.async_on(group=self._insteon_device_group.group) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" await self._insteon_device.async_off(self._insteon_device_group.group) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 0bef2245a7a..c1cc68c9035 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import pykulersky @@ -116,7 +117,7 @@ class KulerskyLight(LightEntity): """Return True if entity is available.""" return self._available - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" default_rgbw = (255,) * 4 if self.rgbw_color is None else self.rgbw_color rgbw = kwargs.get(ATTR_RGBW_COLOR, default_rgbw) @@ -134,7 +135,7 @@ class KulerskyLight(LightEntity): await self._light.set_color(*rgbw_scaled) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self._light.set_color(0, 0, 0, 0) diff --git a/homeassistant/components/lightwave/light.py b/homeassistant/components/lightwave/light.py index 8d46592f439..f89dbb6bf5f 100644 --- a/homeassistant/components/lightwave/light.py +++ b/homeassistant/components/lightwave/light.py @@ -1,6 +1,8 @@ """Support for LightwaveRF lights.""" from __future__ import annotations +from typing import Any + from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -46,7 +48,7 @@ class LWRFLight(LightEntity): self._attr_brightness = MAX_BRIGHTNESS self._lwlink = lwlink - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the LightWave light on.""" self._attr_is_on = True @@ -62,7 +64,7 @@ class LWRFLight(LightEntity): self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the LightWave light off.""" self._attr_is_on = False self._lwlink.turn_off(self._device_id, self._attr_name) diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 41668470dfb..5073e3b4a7d 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -240,7 +240,7 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): self._color = None self._effect = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity about to be added to hass event.""" await super().async_added_to_hass() if last_state := await self.async_get_last_state(): diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index aae28536849..74395117c46 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -1,5 +1,6 @@ """Support for LiteJet lights.""" import logging +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -53,12 +54,12 @@ class LiteJetLight(LightEntity): self._brightness = 0 self._name = name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """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): + async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" self._lj.unsubscribe(self._on_load_changed) @@ -97,7 +98,7 @@ class LiteJetLight(LightEntity): """Return the device state attributes.""" return {ATTR_NUMBER: self._index} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" # If neither attribute is specified then the simple activate load @@ -115,7 +116,7 @@ class LiteJetLight(LightEntity): self._lj.activate_load_at(self._index, brightness, int(transition)) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" if ATTR_TRANSITION in kwargs: self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) @@ -126,6 +127,6 @@ class LiteJetLight(LightEntity): # transition value programmed in the LiteJet system. self._lj.deactivate_load(self._index) - def update(self): + def update(self) -> None: """Retrieve the light's brightness from the LiteJet system.""" self._brightness = int(self._lj.get_load_level(self._index) / 99 * 255) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 52ff2d7843c..15024122338 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -1,6 +1,8 @@ """Support for Lutron lights.""" from __future__ import annotations +from typing import Any + from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,7 +55,7 @@ class LutronLight(LutronDevice, LightEntity): self._prev_brightness = new_brightness return new_brightness - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs and self._lutron_device.is_dimmable: brightness = kwargs[ATTR_BRIGHTNESS] @@ -64,7 +66,7 @@ class LutronLight(LutronDevice, LightEntity): self._prev_brightness = brightness self._lutron_device.level = to_lutron_level(brightness) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._lutron_device.level = 0 @@ -78,7 +80,7 @@ class LutronLight(LutronDevice, LightEntity): """Return true if device is on.""" return self._lutron_device.last_level() > 0 - def update(self): + def update(self) -> None: """Call when forcing a refresh of the device.""" if self._prev_brightness is None: self._prev_brightness = to_hass_level(self._lutron_device.level) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index 9fbb80284f5..cfad8115a20 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -1,5 +1,6 @@ """Support for Lutron Caseta lights.""" from datetime import timedelta +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -69,13 +70,13 @@ class LutronCasetaLight(LutronCasetaDeviceUpdatableEntity, LightEntity): self.device_id, to_lutron_level(brightness), **args ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness = kwargs.pop(ATTR_BRIGHTNESS, 255) await self._set_brightness(brightness, **kwargs) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._set_brightness(0, **kwargs) diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index deb721da29e..de23436a560 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import lw12 import voluptuous as vol @@ -146,7 +147,7 @@ class LW12WiFi(LightEntity): self._light.set_light_option(lw12.LW12_LIGHT.FLASH, transition_speed) self._state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._light.light_off() self._state = False diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 8cc5f4da0a8..3d06a09f479 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pymochad import device from pymochad.exceptions import MochadException @@ -112,7 +113,7 @@ class MochadLight(LightEntity): self.light.send_cmd(f"bright {mochad_brightness}") self._controller.read_data() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Send the command to turn the light on.""" _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) brightness = kwargs.get(ATTR_BRIGHTNESS, 255) @@ -137,7 +138,7 @@ class MochadLight(LightEntity): except (MochadException, OSError) as exc: _LOGGER.error("Error with mochad communication: %s", exc) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Send the command to turn the light on.""" _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) with REQ_LOCK: diff --git a/homeassistant/components/myq/light.py b/homeassistant/components/myq/light.py index 1c001eac7fe..684af64a82e 100644 --- a/homeassistant/components/myq/light.py +++ b/homeassistant/components/myq/light.py @@ -1,4 +1,6 @@ """Support for MyQ-Enabled lights.""" +from typing import Any + from pymyq.errors import MyQError from homeassistant.components.light import ColorMode, LightEntity @@ -43,7 +45,7 @@ class MyQLight(MyQEntity, LightEntity): """Return true if the light is off, else False.""" return MYQ_TO_HASS.get(self._device.state) == STATE_OFF - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Issue on command to light.""" if self.is_on: return @@ -58,7 +60,7 @@ class MyQLight(MyQEntity, LightEntity): # Write new state to HASS self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Issue off command to light.""" if self.is_off: return diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index f6f214301df..26ce5b11567 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pymystrom.bulb import MyStromBulb from pymystrom.exceptions import MyStromConnectionError @@ -120,7 +121,7 @@ class MyStromLight(LightEntity): """Return true if light is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) @@ -147,14 +148,14 @@ class MyStromLight(LightEntity): except MyStromConnectionError: _LOGGER.warning("No route to myStrom bulb") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the bulb.""" try: await self._bulb.set_off() except MyStromConnectionError: _LOGGER.warning("The myStrom bulb not online") - async def async_update(self): + async def async_update(self) -> None: """Fetch new state data for this light.""" try: await self._bulb.get_state() diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index f4d62fd6dc2..4d12591a472 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import nikohomecontrol import voluptuous as vol @@ -77,17 +78,17 @@ class NikoHomeControlLight(LightEntity): """Return true if light is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" _LOGGER.debug("Turn on: %s", self.name) self._light.turn_on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" _LOGGER.debug("Turn off: %s", self.name) self._light.turn_off() - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from NikoHomeControl API.""" await self._data.async_update() self._state = self._data.get_state(self._light.id) diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index a9e24029a13..25bcf821fc9 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyoppleio.OppleLightDevice import OppleLightDevice import voluptuous as vol @@ -107,7 +108,7 @@ class OppleLight(LightEntity): """Return maximum supported color temperature.""" return 333 - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" _LOGGER.debug("Turn on light %s %s", self._device.ip, kwargs) if not self.is_on: @@ -120,12 +121,12 @@ class OppleLight(LightEntity): color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) self._device.color_temperature = color_temp - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._device.power_on = False _LOGGER.debug("Turn off light %s", self._device.ip) - def update(self): + def update(self) -> None: """Synchronize state with light.""" prev_available = self.available self._device.update() diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index a247497550f..6c5c7179006 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import random +from typing import Any from lightify import Lightify import voluptuous as vol @@ -306,7 +307,7 @@ class Luminary(LightEntity): return False - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" transition = int(kwargs.get(ATTR_TRANSITION, 0) * 10) if ATTR_EFFECT in kwargs: @@ -331,7 +332,7 @@ class Luminary(LightEntity): else: self._luminary.set_onoff(True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._is_on = False if ATTR_TRANSITION in kwargs: @@ -374,7 +375,7 @@ class Luminary(LightEntity): if self._supported_features & SUPPORT_COLOR: self._rgb_color = self._luminary.rgb() - def update(self): + def update(self) -> None: """Synchronize state with bridge.""" changed = self.update_func() if changed > self._changed: diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 4fa3b066214..c218c30347e 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any from haphilipsjs import PhilipsTV from haphilipsjs.typing import AmbilightCurrentConfiguration @@ -337,7 +338,7 @@ class PhilipsTVLightEntity( if await self._tv.setAmbilightCurrentConfiguration(config) is False: raise Exception("Failed to set ambilight mode") - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the bulb on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) @@ -378,7 +379,7 @@ class PhilipsTVLightEntity( self._update_from_coordinator() self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn of ambilight.""" if not self._tv.on: diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py index c96644f7ea7..a225489437a 100644 --- a/homeassistant/components/pilight/light.py +++ b/homeassistant/components/pilight/light.py @@ -1,6 +1,8 @@ """Support for switching devices via Pilight to on and off.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.light import ( @@ -63,7 +65,7 @@ class PilightLight(PilightBaseDevice, LightEntity): """Return the brightness.""" return self._brightness - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on by calling pilight.send service with on code.""" # Update brightness only if provided as an argument. # This will allow the switch to keep its previous brightness level. diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 42fdcb538d8..4b7f34f942f 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from plumlightpad import Plum @@ -69,7 +70,7 @@ class PlumLight(LightEntity): self._load = load self._brightness = load.level - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to dimmerchange events.""" self._load.add_event_listener("dimmerchange", self.dimmerchange) @@ -125,14 +126,14 @@ class PlumLight(LightEntity): """Flag supported color modes.""" return {self.color_mode} - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: await self._load.turn_on(kwargs[ATTR_BRIGHTNESS]) else: await self._load.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._load.turn_off() @@ -155,7 +156,7 @@ class GlowRing(LightEntity): self._green = lightpad.glow_color["green"] self._blue = lightpad.glow_color["blue"] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to configchange events.""" self._lightpad.add_event_listener("configchange", self.configchange_event) @@ -222,7 +223,7 @@ class GlowRing(LightEntity): """Return the crop-portrait icon representing the glow ring.""" return "mdi:crop-portrait" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness_pct = kwargs[ATTR_BRIGHTNESS] / 255.0 @@ -234,7 +235,7 @@ class GlowRing(LightEntity): else: await self._lightpad.set_config({"glowEnabled": True}) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if ATTR_BRIGHTNESS in kwargs: brightness_pct = kwargs[ATTR_BRIGHTNESS] / 255.0 diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index e1ebf24c63b..94576155502 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import re +from typing import Any import voluptuous as vol @@ -184,7 +185,7 @@ class DimmableRflinkLight(SwitchableRflinkDevice, LightEntity): _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _brightness = 255 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore RFLink light brightness attribute.""" await super().async_added_to_hass() @@ -196,7 +197,7 @@ class DimmableRflinkLight(SwitchableRflinkDevice, LightEntity): # restore also brightness in dimmables devices self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if ATTR_BRIGHTNESS in kwargs: # rflink only support 16 brightness levels @@ -242,7 +243,7 @@ class HybridRflinkLight(DimmableRflinkLight): Which results in a nice house disco :) """ - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on and set dim level.""" await super().async_turn_on(**kwargs) # if the receiving device does not support dimlevel this @@ -269,10 +270,10 @@ class ToggleRflinkLight(RflinkLight): # if the state is true, it gets set as false self._state = self._state in [None, False] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._async_handle_command("toggle") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._async_handle_command("toggle") diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 198d0ffd3f1..7f32ee0bc83 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import RFXtrx as rfxtrxmod @@ -59,7 +60,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): _attr_brightness: int = 0 _device: rfxtrxmod.LightingDevice - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore RFXtrx device state (ON/OFF).""" await super().async_added_to_hass() @@ -70,7 +71,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): if brightness := old_state.attributes.get(ATTR_BRIGHTNESS): self._attr_brightness = int(brightness) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) self._attr_is_on = True @@ -83,7 +84,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._async_send(self._device.send_off) self._attr_is_on = False diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 5b2cd1c54db..e6b29b94fbf 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,6 +1,7 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" from datetime import timedelta import logging +from typing import Any import requests @@ -93,10 +94,10 @@ class RingLight(RingEntityMixin, LightEntity): self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.async_write_ha_state() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on for 30 seconds.""" self._set_light(ON_STATE) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._set_light(OFF_STATE) From c9ddb10024179dfcbcb6a2867bb95aa282c1832a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Jul 2022 14:01:18 +0200 Subject: [PATCH 060/903] Use device_tracker SourceType enum [s-z] (#75966) --- homeassistant/components/starline/device_tracker.py | 6 +++--- homeassistant/components/tile/device_tracker.py | 6 +++--- homeassistant/components/traccar/device_tracker.py | 6 +++--- homeassistant/components/tractive/device_tracker.py | 11 ++++------- homeassistant/components/unifi/device_tracker.py | 10 +++++----- .../components/volvooncall/device_tracker.py | 4 ++-- homeassistant/components/zha/device_tracker.py | 6 +++--- 7 files changed, 23 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index 796e8ddf08c..35adbd38a4a 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -1,6 +1,6 @@ """StarLine device tracker.""" from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -56,9 +56,9 @@ class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): return self._device.position["y"] @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def icon(self): diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 492c202df08..f749d500b46 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -7,7 +7,7 @@ from pytile.tile import Tile from homeassistant.components.device_tracker import AsyncSeeCallback from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback @@ -122,9 +122,9 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): return self._tile.longitude @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index f9676d37aa2..784e3c70f33 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -19,8 +19,8 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - SOURCE_TYPE_GPS, AsyncSeeCallback, + SourceType, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry @@ -407,9 +407,9 @@ class TraccarEntity(TrackerEntity, RestoreEntity): return {"name": self._name, "identifiers": {(DOMAIN, self._unique_id)}} @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS async def async_added_to_hass(self): """Register state update callback.""" diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 4b08defbf97..19d30aa9856 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -3,10 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import ( - SOURCE_TYPE_BLUETOOTH, - SOURCE_TYPE_GPS, -) +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -56,11 +53,11 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): self._attr_unique_id = item.trackable["_id"] @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" if self._source_type == "PHONE": - return SOURCE_TYPE_BLUETOOTH - return SOURCE_TYPE_GPS + return SourceType.BLUETOOTH + return SourceType.GPS @property def latitude(self) -> float: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index f2c2230b9e0..5484bcf2d5b 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -19,7 +19,7 @@ from aiounifi.events import ( from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -276,9 +276,9 @@ class UniFiClientTracker(UniFiClientBase, ScannerEntity): return self._is_connected @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type of the client.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def unique_id(self) -> str: @@ -406,9 +406,9 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): return self._is_connected @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type of the device.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def name(self) -> str: diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index 866634fc5e1..4f9300fd021 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -1,7 +1,7 @@ """Support for tracking a Volvo.""" from __future__ import annotations -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS, AsyncSeeCallback +from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -31,7 +31,7 @@ async def async_setup_scanner( await async_see( dev_id=dev_id, host_name=host_name, - source_type=SOURCE_TYPE_GPS, + source_type=SourceType.GPS, gps=instrument.state, icon="mdi:car", ) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index cf4a830f4da..eed80a05055 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -4,7 +4,7 @@ from __future__ import annotations import functools import time -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -85,9 +85,9 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): return self._connected @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @callback def async_battery_percentage_remaining_updated(self, attr_id, attr_name, value): From 450b7cd6443a9547c4c3273816adedc9b7232d37 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Jul 2022 15:48:43 +0200 Subject: [PATCH 061/903] Use device_tracker SourceType enum [n-r] (#75965) --- homeassistant/components/netgear/device_tracker.py | 6 +++--- homeassistant/components/nmap_tracker/device_tracker.py | 6 +++--- homeassistant/components/owntracks/device_tracker.py | 6 +++--- homeassistant/components/owntracks/messages.py | 9 +++------ homeassistant/components/person/__init__.py | 4 ++-- homeassistant/components/ping/device_tracker.py | 6 +++--- homeassistant/components/renault/device_tracker.py | 6 +++--- .../components/ruckus_unleashed/device_tracker.py | 6 +++--- 8 files changed, 23 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 0f7f5cffb10..cd648990e05 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -79,9 +79,9 @@ class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity): return self._active @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def ip_address(self) -> str: diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 36a461c4891..afb931b82dd 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -93,9 +93,9 @@ class NmapTrackerEntity(ScannerEntity): return short_hostname(self._device.hostname) @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return tracker source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def should_poll(self) -> bool: diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 92f34617462..5119168e7ae 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -3,7 +3,7 @@ from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import ( ATTR_SOURCE_TYPE, DOMAIN, - SOURCE_TYPE_GPS, + SourceType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -115,9 +115,9 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): return self._data.get("host_name") @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return self._data.get("source_type", SOURCE_TYPE_GPS) + return self._data.get("source_type", SourceType.GPS) @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 4f17c8a5375..cd474054029 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -6,10 +6,7 @@ from nacl.encoding import Base64Encoder from nacl.secret import SecretBox from homeassistant.components import zone as zone_comp -from homeassistant.components.device_tracker import ( - SOURCE_TYPE_BLUETOOTH_LE, - SOURCE_TYPE_GPS, -) +from homeassistant.components.device_tracker import SourceType from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_HOME from homeassistant.util import decorator, slugify @@ -84,9 +81,9 @@ def _parse_see_args(message, subscribe_topic): kwargs["attributes"]["battery_status"] = message["bs"] if "t" in message: if message["t"] in ("c", "u"): - kwargs["source_type"] = SOURCE_TYPE_GPS + kwargs["source_type"] = SourceType.GPS if message["t"] == "b": - kwargs["source_type"] = SOURCE_TYPE_BLUETOOTH_LE + kwargs["source_type"] = SourceType.BLUETOOTH_LE return dev_id, kwargs diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 12ee0da3b82..85a6cf6135e 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -11,7 +11,7 @@ from homeassistant.components import persistent_notification, websocket_api from homeassistant.components.device_tracker import ( ATTR_SOURCE_TYPE, DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_GPS, + SourceType, ) from homeassistant.const import ( ATTR_EDITABLE, @@ -469,7 +469,7 @@ class Person(RestoreEntity): if not state or state.state in IGNORE_STATES: continue - if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS: + if state.attributes.get(ATTR_SOURCE_TYPE) == SourceType.GPS: latest_gps = _get_latest(latest_gps, state) elif state.state == STATE_HOME: latest_non_gps_home = _get_latest(latest_non_gps_home, state) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index cbce224a373..52285350cd4 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -17,7 +17,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, SCAN_INTERVAL, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -114,7 +114,7 @@ async def async_setup_scanner( ) await asyncio.gather( *( - async_see(dev_id=host.dev_id, source_type=SOURCE_TYPE_ROUTER) + async_see(dev_id=host.dev_id, source_type=SourceType.ROUTER) for idx, host in enumerate(hosts) if results[idx] ) @@ -133,7 +133,7 @@ async def async_setup_scanner( _LOGGER.debug("Multiping responses: %s", responses) await asyncio.gather( *( - async_see(dev_id=dev_id, source_type=SOURCE_TYPE_ROUTER) + async_see(dev_id=dev_id, source_type=SourceType.ROUTER) for idx, dev_id in enumerate(ip_to_dev_id.values()) if responses[idx].is_alive ) diff --git a/homeassistant/components/renault/device_tracker.py b/homeassistant/components/renault/device_tracker.py index 3e9a2608f80..da267277d10 100644 --- a/homeassistant/components/renault/device_tracker.py +++ b/homeassistant/components/renault/device_tracker.py @@ -3,7 +3,7 @@ from __future__ import annotations from renault_api.kamereon.models import KamereonVehicleLocationData -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -46,9 +46,9 @@ class RenaultDeviceTracker( return self.coordinator.data.gpsLongitude if self.coordinator.data else None @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS DEVICE_TRACKER_TYPES: tuple[RenaultDataEntityDescription, ...] = ( diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index 67c86f3bb51..8c3e78ae479 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -1,7 +1,7 @@ """Support for Ruckus Unleashed devices.""" from __future__ import annotations -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -111,6 +111,6 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): return self._mac in self.coordinator.data[API_CLIENTS] @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER From f068fc8eb71ee51b7fe87ddbc0227bf745aab1c5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Jul 2022 15:49:57 +0200 Subject: [PATCH 062/903] Use device_tracker SourceType enum [h-m] (#75964) --- homeassistant/components/huawei_lte/device_tracker.py | 8 ++++---- homeassistant/components/icloud/device_tracker.py | 6 +++--- homeassistant/components/keenetic_ndms2/device_tracker.py | 6 +++--- homeassistant/components/life360/device_tracker.py | 6 +++--- homeassistant/components/locative/device_tracker.py | 6 +++--- homeassistant/components/mazda/device_tracker.py | 6 +++--- homeassistant/components/meraki/device_tracker.py | 4 ++-- homeassistant/components/mikrotik/device_tracker.py | 6 +++--- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 179587d72d7..18d797aefc6 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -11,7 +11,7 @@ from stringcase import snakecase from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -194,9 +194,9 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): return self.mac_address @property - def source_type(self) -> str: - """Return SOURCE_TYPE_ROUTER.""" - return SOURCE_TYPE_ROUTER + def source_type(self) -> SourceType: + """Return SourceType.ROUTER.""" + return SourceType.ROUTER @property def ip_address(self) -> str | None: diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index ec543a9ed30..eaebb0b7717 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS, AsyncSeeCallback +from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -105,9 +105,9 @@ class IcloudTrackerEntity(TrackerEntity): return self._device.battery_level @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def icon(self) -> str: diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index b625cabbbc1..397d1888a9e 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -7,7 +7,7 @@ from ndms2_client import Device from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry @@ -109,9 +109,9 @@ class KeeneticTracker(ScannerEntity): ) @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type of the client.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def name(self) -> str: diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 5a18422487e..ebb179beba2 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any, cast -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING @@ -192,9 +192,9 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): return self._data.battery_level @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def location_accuracy(self) -> int: diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index 4ededae6b01..cd927aace38 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -1,5 +1,5 @@ """Support for the Locative platform.""" -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -60,9 +60,9 @@ class LocativeEntity(TrackerEntity): return self._name @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS async def async_added_to_hass(self): """Register state update callback.""" diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py index 79a9ef90b9b..946cff72f5b 100644 --- a/homeassistant/components/mazda/device_tracker.py +++ b/homeassistant/components/mazda/device_tracker.py @@ -1,5 +1,5 @@ """Platform for Mazda device tracker integration.""" -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -40,9 +40,9 @@ class MazdaDeviceTracker(MazdaEntity, TrackerEntity): self._attr_unique_id = self.vin @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def latitude(self): diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 7447d8ce879..dae98734ae7 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -9,8 +9,8 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - SOURCE_TYPE_ROUTER, AsyncSeeCallback, + SourceType, ) from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback @@ -127,7 +127,7 @@ class MerakiView(HomeAssistantView): self.async_see( gps=gps_location, mac=mac, - source_type=SOURCE_TYPE_ROUTER, + source_type=SourceType.ROUTER, gps_accuracy=accuracy, attributes=attrs, ) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 158d95dd683..d02bf69b5ab 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -101,9 +101,9 @@ class MikrotikDataUpdateCoordinatorTracker( return False @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type of the client.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def hostname(self) -> str: From 1a8ccfeb56095b16d6de471e99c3a82471a83716 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Jul 2022 15:51:04 +0200 Subject: [PATCH 063/903] Use device_tracker SourceType enum [a-g] (#75963) --- homeassistant/components/asuswrt/device_tracker.py | 6 +++--- .../components/bluetooth_le_tracker/device_tracker.py | 4 ++-- .../components/bluetooth_tracker/device_tracker.py | 4 ++-- .../components/bmw_connected_drive/device_tracker.py | 7 +++---- .../components/devolo_home_network/device_tracker.py | 6 +++--- homeassistant/components/dhcp/__init__.py | 4 ++-- homeassistant/components/freebox/device_tracker.py | 6 +++--- homeassistant/components/fritz/device_tracker.py | 6 +++--- homeassistant/components/geofency/device_tracker.py | 6 +++--- homeassistant/components/google_maps/device_tracker.py | 4 ++-- homeassistant/components/gpslogger/device_tracker.py | 6 +++--- 11 files changed, 29 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index af43294c954..fc2d4ede26d 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,7 +1,7 @@ """Support for ASUSWRT routers.""" from __future__ import annotations -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -69,9 +69,9 @@ class AsusWrtDevice(ScannerEntity): return self._device.is_connected @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def hostname(self) -> str | None: diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 6ba33e506cc..b416fe3e070 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -16,7 +16,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker.const import ( CONF_TRACK_NEW, SCAN_INTERVAL, - SOURCE_TYPE_BLUETOOTH_LE, + SourceType, ) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, @@ -106,7 +106,7 @@ async def async_setup_scanner( # noqa: C901 await async_see( mac=BLE_PREFIX + address, host_name=name, - source_type=SOURCE_TYPE_BLUETOOTH_LE, + source_type=SourceType.BLUETOOTH_LE, battery=battery, ) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 90ae473a0cd..d266ba5d542 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -19,7 +19,7 @@ from homeassistant.components.device_tracker.const import ( CONF_TRACK_NEW, DEFAULT_TRACK_NEW, SCAN_INTERVAL, - SOURCE_TYPE_BLUETOOTH, + SourceType, ) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, @@ -93,7 +93,7 @@ async def see_device( mac=f"{BT_PREFIX}{mac}", host_name=device_name, attributes=attributes, - source_type=SOURCE_TYPE_BLUETOOTH, + source_type=SourceType.BLUETOOTH, ) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index dc71100455d..c06ecdaa9bb 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -2,11 +2,10 @@ from __future__ import annotations import logging -from typing import Literal from bimmer_connected.vehicle import MyBMWVehicle -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -80,6 +79,6 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): ) @property - def source_type(self) -> Literal["gps"]: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py index 9dffeef7db9..0e3e47d9320 100644 --- a/homeassistant/components/devolo_home_network/device_tracker.py +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -7,7 +7,7 @@ from devolo_plc_api.device import Device from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry @@ -149,9 +149,9 @@ class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): return self._mac @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return tracker source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def unique_id(self) -> str: diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 8581a5f2241..be9cbd7426d 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -31,7 +31,7 @@ from homeassistant.components.device_tracker.const import ( ATTR_SOURCE_TYPE, CONNECTED_DEVICE_REGISTERED, DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, @@ -318,7 +318,7 @@ class DeviceTrackerWatcher(WatcherBase): attributes = state.attributes - if attributes.get(ATTR_SOURCE_TYPE) != SOURCE_TYPE_ROUTER: + if attributes.get(ATTR_SOURCE_TYPE) != SourceType.ROUTER: return ip_address = attributes.get(ATTR_IP) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 0fe04fe7eb9..1fd7a35e975 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime from typing import Any -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -98,9 +98,9 @@ class FreeboxDevice(ScannerEntity): return self._active @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def icon(self) -> str: diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 34c3f64e1e5..9591fec156b 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -4,7 +4,7 @@ from __future__ import annotations import datetime import logging -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -118,6 +118,6 @@ class FritzBoxTracker(FritzDeviceBase, ScannerEntity): return attrs @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return tracker source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index bd4b2852019..61deb9ede7d 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,5 +1,5 @@ """Support for the Geofency device tracker platform.""" -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE @@ -96,9 +96,9 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): return DeviceInfo(identifiers={(GF_DOMAIN, self._unique_id)}, name=self._name) @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS async def async_added_to_hass(self): """Register state update callback.""" diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 8d8be8c0fe1..8f15278e214 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -10,8 +10,8 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PLATFORM_SCHEMA_BASE, - SOURCE_TYPE_GPS, SeeCallback, + SourceType, ) from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -129,7 +129,7 @@ class GoogleMapsScanner: dev_id=dev_id, gps=(person.latitude, person.longitude), picture=person.picture_url, - source_type=SOURCE_TYPE_GPS, + source_type=SourceType.GPS, gps_accuracy=person.accuracy, attributes=attrs, ) diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index ea648ed7495..22e9529706f 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,5 +1,5 @@ """Support for the GPSLogger device tracking.""" -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -118,9 +118,9 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): return DeviceInfo(identifiers={(GPL_DOMAIN, self._unique_id)}, name=self._name) @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS async def async_added_to_hass(self): """Register state update callback.""" From c79559751156f009c22e1b8f1f0d61e2cb4f7082 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jul 2022 18:00:42 +0200 Subject: [PATCH 064/903] Improve authentication handling for camera view (#75979) --- homeassistant/components/camera/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 3bf86dedea1..77bd0b57f1c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -14,7 +14,7 @@ import os from random import SystemRandom from typing import Final, Optional, cast, final -from aiohttp import web +from aiohttp import hdrs, web import async_timeout import attr import voluptuous as vol @@ -715,8 +715,11 @@ class CameraView(HomeAssistantView): ) if not authenticated: - if request[KEY_AUTHENTICATED]: + # Attempt with invalid bearer token, raise unauthorized + # so ban middleware can handle it. + if hdrs.AUTHORIZATION in request.headers: raise web.HTTPUnauthorized() + # Invalid sigAuth or camera access token raise web.HTTPForbidden() if not camera.is_on: From 20fec104e2a11b1a5164d7fe779eb0d894e098cf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 20:46:13 +0200 Subject: [PATCH 065/903] Improve type hints in light [a-i] (#75936) * Improve type hints in ads light * Improve type hints in avea light * Improve type hints in avion light * Improve type hints in broadlink light * More type hints * One more --- homeassistant/components/ads/light.py | 8 +++++--- homeassistant/components/avea/light.py | 8 +++++--- homeassistant/components/avion/light.py | 5 +++-- homeassistant/components/broadlink/light.py | 5 +++-- homeassistant/components/control4/light.py | 5 +++-- homeassistant/components/decora_wifi/light.py | 11 ++++++----- homeassistant/components/dynalite/light.py | 6 ++++-- homeassistant/components/enocean/light.py | 5 +++-- homeassistant/components/eufy/light.py | 14 ++++++++------ homeassistant/components/firmata/light.py | 5 +++-- homeassistant/components/fjaraskupan/light.py | 6 ++++-- homeassistant/components/futurenow/light.py | 12 +++++++----- homeassistant/components/greenwave/light.py | 7 ++++--- homeassistant/components/homematic/light.py | 10 ++++++---- .../components/homematicip_cloud/light.py | 12 ++++++------ homeassistant/components/homeworks/light.py | 7 ++++--- homeassistant/components/iaqualink/light.py | 8 +++++--- homeassistant/components/iglo/light.py | 9 +++++---- homeassistant/components/ihc/light.py | 6 ++++-- 19 files changed, 88 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index a4ec970ee15..8dd55775b7a 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -1,6 +1,8 @@ """Support for ADS light sources.""" from __future__ import annotations +from typing import Any + import pyads import voluptuous as vol @@ -66,7 +68,7 @@ class AdsLight(AdsEntity, LightEntity): self._attr_color_mode = ColorMode.ONOFF self._attr_supported_color_modes = {ColorMode.ONOFF} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register device notification.""" await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL) @@ -87,7 +89,7 @@ class AdsLight(AdsEntity, LightEntity): """Return True if the entity is on.""" return self._state_dict[STATE_KEY_STATE] - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on or set a specific dimmer value.""" brightness = kwargs.get(ATTR_BRIGHTNESS) self._ads_hub.write_by_name(self._ads_var, True, pyads.PLCTYPE_BOOL) @@ -97,6 +99,6 @@ class AdsLight(AdsEntity, LightEntity): self._ads_var_brightness, brightness, pyads.PLCTYPE_UINT ) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._ads_hub.write_by_name(self._ads_var, False, pyads.PLCTYPE_BOOL) diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index 31054e2e287..5b306b058d3 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -1,6 +1,8 @@ """Support for the Elgato Avea lights.""" from __future__ import annotations +from typing import Any + import avea # pylint: disable=import-error from homeassistant.components.light import ( @@ -46,7 +48,7 @@ class AveaLight(LightEntity): self._attr_name = light.name self._attr_brightness = light.brightness - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if not kwargs: self._light.set_brightness(4095) @@ -58,11 +60,11 @@ class AveaLight(LightEntity): rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._light.set_rgb(rgb[0], rgb[1], rgb[2]) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._light.set_brightness(0) - def update(self): + def update(self) -> None: """Fetch new state data for this light. This is the only method that should fetch new data for Home Assistant. diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index 7df17c2e74e..54d06d50a60 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib import time +from typing import Any import voluptuous as vol @@ -103,7 +104,7 @@ class AvionLight(LightEntity): self._switch.connect() return True - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: self._attr_brightness = brightness @@ -111,7 +112,7 @@ class AvionLight(LightEntity): self.set_state(self.brightness) self._attr_is_on = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified or all lights off.""" self.set_state(0) self._attr_is_on = False diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py index 4b655e4bd42..c4dd48b7dc0 100644 --- a/homeassistant/components/broadlink/light.py +++ b/homeassistant/components/broadlink/light.py @@ -1,5 +1,6 @@ """Support for Broadlink lights.""" import logging +from typing import Any from broadlink.exceptions import BroadlinkException @@ -88,7 +89,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): # Scenes are not yet supported. self._attr_color_mode = ColorMode.UNKNOWN - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" state = {"pwr": 1} @@ -122,7 +123,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): await self._async_set_state(state) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" await self._async_set_state({"pwr": 0}) diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index ded72944af7..539547a79f1 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging +from typing import Any from pyControl4.error_handling import C4Exception from pyControl4.light import C4Light @@ -197,7 +198,7 @@ class Control4Light(Control4Entity, LightEntity): return LightEntityFeature.TRANSITION return 0 - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" c4_light = self.create_api_object() if self._is_dimmer: @@ -220,7 +221,7 @@ class Control4Light(Control4Entity, LightEntity): await asyncio.sleep(delay_time) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" c4_light = self.create_api_object() if self._is_dimmer: diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 7d3b1e353e2..3c43e816097 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any # pylint: disable=import-error from decora_wifi import DecoraWiFiSession @@ -111,7 +112,7 @@ class DecoraWifiLight(LightEntity): return {self.color_mode} @property - def supported_features(self): + def supported_features(self) -> int: """Return supported features.""" if self._switch.canSetLevel: return LightEntityFeature.TRANSITION @@ -137,9 +138,9 @@ class DecoraWifiLight(LightEntity): """Return true if switch is on.""" return self._switch.power == "ON" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the switch to turn on & adjust brightness.""" - attribs = {"power": "ON"} + attribs: dict[str, Any] = {"power": "ON"} if ATTR_BRIGHTNESS in kwargs: min_level = self._switch.data.get("minLevel", 0) @@ -157,7 +158,7 @@ class DecoraWifiLight(LightEntity): except ValueError: _LOGGER.error("Failed to turn on myLeviton switch") - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the switch to turn off.""" attribs = {"power": "OFF"} try: @@ -165,7 +166,7 @@ class DecoraWifiLight(LightEntity): except ValueError: _LOGGER.error("Failed to turn off myLeviton switch") - def update(self): + def update(self) -> None: """Fetch new state data for this switch.""" try: self._switch.refresh() diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index 826506fa9b7..27cd6f8cae8 100644 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -1,5 +1,7 @@ """Support for Dynalite channels as lights.""" +from typing import Any + from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -35,10 +37,10 @@ class DynaliteLight(DynaliteBase, LightEntity): """Return true if device is on.""" return self._device.is_on - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" await self._device.async_turn_on(**kwargs) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.async_turn_off(**kwargs) diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index 8ecc3f63831..479723bd051 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import Any from enocean.utils import combine_hex import voluptuous as vol @@ -80,7 +81,7 @@ class EnOceanLight(EnOceanEntity, LightEntity): """If light is on.""" return self._on_state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light source on or sets a specific dimmer value.""" if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: self._brightness = brightness @@ -94,7 +95,7 @@ class EnOceanLight(EnOceanEntity, LightEntity): self.send_command(command, [], 0x01) self._on_state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light source off.""" command = [0xA5, 0x02, 0x00, 0x01, 0x09] command.extend(self._sender_id) diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 57d54146137..f3d5fe58e7d 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,6 +1,8 @@ """Support for Eufy lights.""" from __future__ import annotations +from typing import Any + import lakeside from homeassistant.components.light import ( @@ -59,7 +61,7 @@ class EufyLight(LightEntity): self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} self._bulb.connect() - def update(self): + def update(self) -> None: """Synchronise state from the bulb.""" self._bulb.update() if self._bulb.power: @@ -93,12 +95,12 @@ class EufyLight(LightEntity): return int(self._brightness * 255 / 100) @property - def min_mireds(self): + def min_mireds(self) -> int: """Return minimum supported color temperature.""" return kelvin_to_mired(EUFY_MAX_KELVIN) @property - def max_mireds(self): + def max_mireds(self) -> int: """Return maximum supported color temperature.""" return kelvin_to_mired(EUFY_MIN_KELVIN) @@ -116,7 +118,7 @@ class EufyLight(LightEntity): return self._hs @property - def color_mode(self) -> str | None: + def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self._type == "T1011": return ColorMode.BRIGHTNESS @@ -127,7 +129,7 @@ class EufyLight(LightEntity): return ColorMode.COLOR_TEMP return ColorMode.HS - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) @@ -169,7 +171,7 @@ class EufyLight(LightEntity): power=True, brightness=brightness, temperature=temp, colors=rgb ) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" try: self._bulb.set_state(power=False) diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py index 8f562b12d30..59be2484d66 100644 --- a/homeassistant/components/firmata/light.py +++ b/homeassistant/components/firmata/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -82,14 +83,14 @@ class FirmataLight(FirmataPinEntity, LightEntity): """Return the brightness of the light.""" return self._api.state - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" level = kwargs.get(ATTR_BRIGHTNESS, self._last_on_level) await self._api.set_level(level) self.async_write_ha_state() self._last_on_level = level - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" await self._api.set_level(0) self.async_write_ha_state() diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index f492f628a3a..c42943710de 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -1,6 +1,8 @@ """Support for lights.""" from __future__ import annotations +from typing import Any + from fjaraskupan import COMMAND_LIGHT_ON_OFF, Device from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity @@ -45,7 +47,7 @@ class Light(CoordinatorEntity[Coordinator], LightEntity): self._attr_unique_id = device.address self._attr_device_info = device_info - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: await self._device.send_dim(int(kwargs[ATTR_BRIGHTNESS] * (100.0 / 255.0))) @@ -54,7 +56,7 @@ class Light(CoordinatorEntity[Coordinator], LightEntity): await self._device.send_command(COMMAND_LIGHT_ON_OFF) self.coordinator.async_set_updated_data(self._device.state) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if self.is_on: await self._device.send_command(COMMAND_LIGHT_ON_OFF) diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index e27436966fc..d070d832052 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -1,6 +1,8 @@ """Support for FutureNow Ethernet unit outputs as Lights.""" from __future__ import annotations +from typing import Any + import pyfnip import voluptuous as vol @@ -106,18 +108,18 @@ class FutureNowLight(LightEntity): return self._brightness @property - def color_mode(self) -> str: + def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self._dimmable: return ColorMode.BRIGHTNESS return ColorMode.ONOFF @property - def supported_color_modes(self) -> set[str] | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" return {self.color_mode} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if self._dimmable: level = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness) @@ -125,13 +127,13 @@ class FutureNowLight(LightEntity): level = 255 self._light.turn_on(to_futurenow_level(level)) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._light.turn_off() if self._brightness: self._last_brightness = self._brightness - def update(self): + def update(self) -> None: """Fetch new state data for this light.""" state = int(self._light.is_on()) self._state = bool(state) diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index a6b018f33ff..1e991feee90 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging import os +from typing import Any import greenwavereality as greenwave import voluptuous as vol @@ -99,17 +100,17 @@ class GreenwaveLight(LightEntity): """Return true if light is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100) greenwave.set_brightness(self._host, self._did, temp_brightness, self._token) greenwave.turn_on(self._host, self._did, self._token) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" greenwave.turn_off(self._host, self._did, self._token) - def update(self): + def update(self) -> None: """Fetch new state data for this light.""" self._gatewaydata.update() bulbs = self._gatewaydata.greenwave diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 4087f83dd2a..38a75266a17 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -1,6 +1,8 @@ """Support for Homematic lights.""" from __future__ import annotations +from typing import Any + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -80,9 +82,9 @@ class HMLight(HMDevice, LightEntity): return color_modes @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" - features = LightEntityFeature.TRANSITION + features: int = LightEntityFeature.TRANSITION if "PROGRAM" in self._hmdevice.WRITENODE: features |= LightEntityFeature.EFFECT return features @@ -117,7 +119,7 @@ class HMLight(HMDevice, LightEntity): return None return self._hmdevice.get_effect() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on and/or change color or color effect settings.""" if ATTR_TRANSITION in kwargs: self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION], self._channel) @@ -146,7 +148,7 @@ class HMLight(HMDevice, LightEntity): if ATTR_EFFECT in kwargs: self._hmdevice.set_effect(kwargs[ATTR_EFFECT]) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if ATTR_TRANSITION in kwargs: self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION], self._channel) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 0cf02d88471..c43ac510023 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -81,11 +81,11 @@ class HomematicipLight(HomematicipGenericEntity, LightEntity): """Return true if light is on.""" return self._device.on - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.turn_off() @@ -125,7 +125,7 @@ class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): (self._device.functionalChannels[self._channel].dimLevel or 0.0) * 255 ) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the dimmer on.""" if ATTR_BRIGHTNESS in kwargs: await self._device.set_dim_level( @@ -134,7 +134,7 @@ class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): else: await self._device.set_dim_level(1, self._channel) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the dimmer off.""" await self._device.set_dim_level(0, self._channel) @@ -213,7 +213,7 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): """Return a unique ID.""" return f"{self.__class__.__name__}_{self._post}_{self._device.id}" - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" # Use hs_color from kwargs, # if not applicable use current hs_color. @@ -241,7 +241,7 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): rampTime=transition, ) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" simple_rgb_color = self._func_channel.simpleRGBColorState transition = kwargs.get(ATTR_TRANSITION, 0.5) diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 0a8d7f2fb28..35a9e5665d1 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED @@ -50,7 +51,7 @@ class HomeworksLight(HomeworksDevice, LightEntity): self._level = 0 self._prev_level = 0 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" signal = f"homeworks_entity_{self._addr}" _LOGGER.debug("connecting %s", signal) @@ -59,7 +60,7 @@ class HomeworksLight(HomeworksDevice, LightEntity): ) self._controller.request_dimmer_level(self._addr) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" if ATTR_BRIGHTNESS in kwargs: new_level = kwargs[ATTR_BRIGHTNESS] @@ -69,7 +70,7 @@ class HomeworksLight(HomeworksDevice, LightEntity): new_level = self._prev_level self._set_brightness(new_level) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" self._set_brightness(0) diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 505ad8da0cc..3a8b1e0fb4a 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -1,6 +1,8 @@ """Support for Aqualink pool lights.""" from __future__ import annotations +from typing import Any + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, @@ -46,7 +48,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): return self.dev.is_on @refresh_system - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light. This handles brightness and light effects for lights that do support @@ -63,7 +65,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): await await_or_reraise(self.dev.turn_on()) @refresh_system - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" await await_or_reraise(self.dev.turn_off()) @@ -93,7 +95,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): return ColorMode.ONOFF @property - def supported_color_modes(self) -> set[str] | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" return {self.color_mode} diff --git a/homeassistant/components/iglo/light.py b/homeassistant/components/iglo/light.py index 39e1a5986ab..5de24625ea3 100644 --- a/homeassistant/components/iglo/light.py +++ b/homeassistant/components/iglo/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import Any from iglo import Lamp from iglo.lamp import MODE_WHITE @@ -86,14 +87,14 @@ class IGloLamp(LightEntity): return color_util.color_temperature_kelvin_to_mired(self._lamp.state()["white"]) @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" return math.ceil( color_util.color_temperature_kelvin_to_mired(self._lamp.max_kelvin) ) @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" return math.ceil( color_util.color_temperature_kelvin_to_mired(self._lamp.min_kelvin) @@ -119,7 +120,7 @@ class IGloLamp(LightEntity): """Return true if light is on.""" return self._lamp.state()["on"] - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if not self.is_on: self._lamp.switch(True) @@ -145,6 +146,6 @@ class IGloLamp(LightEntity): self._lamp.effect(effect) return - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._lamp.switch(False) diff --git a/homeassistant/components/ihc/light.py b/homeassistant/components/ihc/light.py index caceaf9737c..089317ca7d7 100644 --- a/homeassistant/components/ihc/light.py +++ b/homeassistant/components/ihc/light.py @@ -1,6 +1,8 @@ """Support for IHC lights.""" from __future__ import annotations +from typing import Any + from ihcsdk.ihccontroller import IHCController from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity @@ -90,7 +92,7 @@ class IhcLight(IHCDevice, LightEntity): """Return true if light is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] @@ -108,7 +110,7 @@ class IhcLight(IHCDevice, LightEntity): else: await async_set_bool(self.hass, self.ihc_controller, self.ihc_id, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if self._dimmable: await async_set_int(self.hass, self.ihc_controller, self.ihc_id, 0) From 84d91d2b3ad04102169824a78ca306494c4ecd44 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 31 Jul 2022 21:14:03 +0200 Subject: [PATCH 066/903] Bump pyotgw to 2.0.2 (#75980) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 0bc69387d0b..02b1604ea11 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.1"], + "requirements": ["pyotgw==2.0.2"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index fc08ba28427..a5d214626ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1728,7 +1728,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0caac1ec257..abf62aaccdc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1193,7 +1193,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 97b30bec8b3f6727183bafebc2fb2696d46d966e Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Sun, 31 Jul 2022 22:25:16 +0300 Subject: [PATCH 067/903] Added a configuration_url for the ukraine_alarm service (#75988) --- homeassistant/components/ukraine_alarm/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ukraine_alarm/binary_sensor.py b/homeassistant/components/ukraine_alarm/binary_sensor.py index b98add95e03..10d66e0cb64 100644 --- a/homeassistant/components/ukraine_alarm/binary_sensor.py +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -98,6 +98,7 @@ class UkraineAlarmSensor( identifiers={(DOMAIN, unique_id)}, manufacturer=MANUFACTURER, name=name, + configuration_url="https://siren.pp.ua/", ) @property From 98293f2179fdb6d0d6dc559f7d80d601efeb96e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 21:29:54 +0200 Subject: [PATCH 068/903] Use climate enums in alexa (#75911) --- homeassistant/components/alexa/capabilities.py | 2 +- homeassistant/components/alexa/const.py | 16 ++++++++-------- homeassistant/components/alexa/entities.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 3963372fec1..1456221e20e 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -379,7 +379,7 @@ class AlexaPowerController(AlexaCapability): raise UnsupportedProperty(name) if self.entity.domain == climate.DOMAIN: - is_on = self.entity.state != climate.HVAC_MODE_OFF + is_on = self.entity.state != climate.HVACMode.OFF elif self.entity.domain == fan.DOMAIN: is_on = self.entity.state == fan.STATE_ON elif self.entity.domain == vacuum.DOMAIN: diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 6b509d9b3c6..c6ac3071d94 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -68,16 +68,16 @@ API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"} # back to HA state. API_THERMOSTAT_MODES = OrderedDict( [ - (climate.HVAC_MODE_HEAT, "HEAT"), - (climate.HVAC_MODE_COOL, "COOL"), - (climate.HVAC_MODE_HEAT_COOL, "AUTO"), - (climate.HVAC_MODE_AUTO, "AUTO"), - (climate.HVAC_MODE_OFF, "OFF"), - (climate.HVAC_MODE_FAN_ONLY, "OFF"), - (climate.HVAC_MODE_DRY, "CUSTOM"), + (climate.HVACMode.HEAT, "HEAT"), + (climate.HVACMode.COOL, "COOL"), + (climate.HVACMode.HEAT_COOL, "AUTO"), + (climate.HVACMode.AUTO, "AUTO"), + (climate.HVACMode.OFF, "OFF"), + (climate.HVACMode.FAN_ONLY, "OFF"), + (climate.HVACMode.DRY, "CUSTOM"), ] ) -API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} +API_THERMOSTAT_MODES_CUSTOM = {climate.HVACMode.DRY: "DEHUMIDIFY"} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} # AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f380f990449..ac78dbeed5e 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -460,7 +460,7 @@ class ClimateCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" # If we support two modes, one being off, we allow turning on too. - if climate.HVAC_MODE_OFF in self.entity.attributes.get( + if climate.HVACMode.OFF in self.entity.attributes.get( climate.ATTR_HVAC_MODES, [] ): yield AlexaPowerController(self.entity) From a3bffdf523ef962d10395cc94346a12dd48e8d2c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 31 Jul 2022 14:10:29 -0600 Subject: [PATCH 069/903] Appropriately mark Guardian entities as `unavailable` during reboot (#75234) --- homeassistant/components/guardian/__init__.py | 13 ++++++- .../components/guardian/binary_sensor.py | 2 +- homeassistant/components/guardian/button.py | 3 ++ homeassistant/components/guardian/sensor.py | 4 +- homeassistant/components/guardian/util.py | 38 ++++++++++++++++++- 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index da22066d7aa..58d70667cdf 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -137,6 +137,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # so we use a lock to ensure that only one API request is reaching it at a time: api_lock = asyncio.Lock() + async def async_init_coordinator( + coordinator: GuardianDataUpdateCoordinator, + ) -> None: + """Initialize a GuardianDataUpdateCoordinator.""" + await coordinator.async_initialize() + await coordinator.async_config_entry_first_refresh() + # Set up GuardianDataUpdateCoordinators for the valve controller: valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] @@ -151,13 +158,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api ] = GuardianDataUpdateCoordinator( hass, + entry=entry, client=client, api_name=api, api_coro=api_coro, api_lock=api_lock, valve_controller_uid=entry.data[CONF_UID], ) - init_valve_controller_tasks.append(coordinator.async_refresh()) + init_valve_controller_tasks.append(async_init_coordinator(coordinator)) await asyncio.gather(*init_valve_controller_tasks) @@ -352,6 +360,7 @@ class PairedSensorManager: coordinator = self.coordinators[uid] = GuardianDataUpdateCoordinator( self._hass, + entry=self._entry, client=self._client, api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}", api_coro=lambda: cast( @@ -422,7 +431,7 @@ class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity. + """Update the entity's underlying data. This should be extended by Guardian platforms. """ diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index eb6d49c3ec1..766e5d961e8 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -137,7 +137,7 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: self._attr_is_on = self.coordinator.data["wet"] elif self.entity_description.key == SENSOR_KIND_MOVED: diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index 01efb7deba4..740cce43c62 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -15,6 +15,7 @@ from homeassistant.components.button import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -111,3 +112,5 @@ class GuardianButton(ValveControllerEntity, ButtonEntity): raise HomeAssistantError( f'Error while pressing button "{self.entity_id}": {err}' ) from err + + async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested) diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 5b4c621cce6..05de437b10a 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -128,7 +128,7 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_BATTERY: self._attr_native_value = self.coordinator.data["battery"] elif self.entity_description.key == SENSOR_KIND_TEMPERATURE: @@ -142,7 +142,7 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_TEMPERATURE: self._attr_native_value = self.coordinator.data["temperature"] elif self.entity_description.key == SENSOR_KIND_UPTIME: diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 2cedcf9c1e4..c88d6762e51 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -9,21 +9,28 @@ from typing import Any, cast from aioguardian import Client from aioguardian.errors import GuardianError -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) +SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}" + class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): """Define an extended DataUpdateCoordinator with some Guardian goodies.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, *, + entry: ConfigEntry, client: Client, api_name: str, api_coro: Callable[..., Awaitable], @@ -41,6 +48,12 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): self._api_coro = api_coro self._api_lock = api_lock self._client = client + self._signal_handler_unsubs: list[Callable[..., None]] = [] + + self.config_entry = entry + self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( + self.config_entry.entry_id + ) async def _async_update_data(self) -> dict[str, Any]: """Execute a "locked" API request against the valve controller.""" @@ -50,3 +63,26 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): except GuardianError as err: raise UpdateFailed(err) from err return cast(dict[str, Any], resp["data"]) + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + + @callback + def async_reboot_requested() -> None: + """Respond to a reboot request.""" + self.last_update_success = False + self.async_update_listeners() + + self._signal_handler_unsubs.append( + async_dispatcher_connect( + self.hass, self.signal_reboot_requested, async_reboot_requested + ) + ) + + @callback + def async_teardown() -> None: + """Tear the coordinator down appropriately.""" + for unsub in self._signal_handler_unsubs: + unsub() + + self.config_entry.async_on_unload(async_teardown) From a95851c9c23124ff5d46e8a815d8e3ea5c33b845 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jul 2022 13:13:05 -0700 Subject: [PATCH 070/903] Bump pySwitchbot to 0.16.0 to fix compat with bleak 0.15 (#75991) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index d6acb69431e..dcb33c03882 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.15.2"], + "requirements": ["PySwitchbot==0.16.0"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], diff --git a/requirements_all.txt b/requirements_all.txt index a5d214626ba..1d14d244148 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.2 +PySwitchbot==0.16.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abf62aaccdc..3b44079a48d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.2 +PySwitchbot==0.16.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 4af95ecf787b72ec2a23e4354f441985a338aeef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jul 2022 22:14:09 +0200 Subject: [PATCH 071/903] Fix Home Connect services not being set up (#75997) --- .../components/home_connect/__init__.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index a2876dd86f5..6e664ad07e4 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -117,24 +117,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Home Connect component.""" hass.data[DOMAIN] = {} - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of Home Connect integration in YAML is deprecated and " - "will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) + if DOMAIN in config: + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential( + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + ), + ) + _LOGGER.warning( + "Configuration of Home Connect integration in YAML is deprecated and " + "will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) async def _async_service_program(call, method): """Execute calls to services taking a program.""" From 003ee853a3ce7dce7f7890d39c2c5d3586ea354a Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 31 Jul 2022 16:14:30 -0400 Subject: [PATCH 072/903] Bump AIOAladdinConnect to 0.1.33 (#75986) Bump aladdin_connect 0.1.33 --- homeassistant/components/aladdin_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/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index d551b91bce9..a142e838f3e 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.31"], + "requirements": ["AIOAladdinConnect==0.1.33"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 1d14d244148..e3dfbab9e19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.31 +AIOAladdinConnect==0.1.33 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b44079a48d..e6653d8f599 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.31 +AIOAladdinConnect==0.1.33 # homeassistant.components.adax Adax-local==0.1.4 From 0167875789cac40a15b53d52b3b14165caaccf06 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 31 Jul 2022 21:30:29 +0100 Subject: [PATCH 073/903] Add physical controls lock to homekit_controller (#75993) --- homeassistant/components/homekit_controller/switch.py | 6 ++++++ .../homekit_controller/specific_devices/test_eve_energy.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 53b3958ecf6..be6c3b8bfe0 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -50,6 +50,12 @@ SWITCH_ENTITIES: dict[str, DeclarativeSwitchEntityDescription] = { icon="mdi:lock-open", entity_category=EntityCategory.CONFIG, ), + CharacteristicsTypes.LOCK_PHYSICAL_CONTROLS: DeclarativeSwitchEntityDescription( + key=CharacteristicsTypes.LOCK_PHYSICAL_CONTROLS, + name="Lock Physical Controls", + icon="mdi:lock-open", + entity_category=EntityCategory.CONFIG, + ), } diff --git a/tests/components/homekit_controller/specific_devices/test_eve_energy.py b/tests/components/homekit_controller/specific_devices/test_eve_energy.py index 0ba9b0bee25..70ae4a1db23 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_energy.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_energy.py @@ -74,6 +74,13 @@ async def test_eve_degree_setup(hass): unit_of_measurement=ENERGY_KILO_WATT_HOUR, state="0.28999999165535", ), + EntityTestInfo( + entity_id="switch.eve_energy_50ff_lock_physical_controls", + unique_id="homekit-AA00A0A00000-aid:1-sid:28-cid:36", + friendly_name="Eve Energy 50FF Lock Physical Controls", + entity_category=EntityCategory.CONFIG, + state="off", + ), EntityTestInfo( entity_id="button.eve_energy_50ff_identify", unique_id="homekit-AA00A0A00000-aid:1-sid:1-cid:3", From 89729b2c495ff3310e255c3c975ba9211f6b1bff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Aug 2022 00:39:38 +0200 Subject: [PATCH 074/903] Improve Registry typing in Alexa handlers (#75921) --- homeassistant/components/alexa/handlers.py | 440 ++++++++++++++++----- 1 file changed, 342 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index f9162026fe8..ba3892a62f2 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1,6 +1,10 @@ """Alexa message handlers.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine import logging import math +from typing import Any from homeassistant import core as ha from homeassistant.components import ( @@ -51,6 +55,7 @@ from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util from homeassistant.util.temperature import convert as convert_temperature +from .config import AbstractConfig from .const import ( API_TEMP_UNITS, API_THERMOSTAT_MODES, @@ -70,14 +75,27 @@ from .errors import ( AlexaUnsupportedThermostatModeError, AlexaVideoActionNotPermittedForContentError, ) +from .messages import AlexaDirective, AlexaResponse from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) -HANDLERS = Registry() # type: ignore[var-annotated] +DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive" +HANDLERS: Registry[ + tuple[str, str], + Callable[ + [ha.HomeAssistant, AbstractConfig, AlexaDirective, ha.Context], + Coroutine[Any, Any, AlexaResponse], + ], +] = Registry() @HANDLERS.register(("Alexa.Discovery", "Discover")) -async def async_api_discovery(hass, config, directive, context): +async def async_api_discovery( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Create a API formatted discovery response. Async friendly. @@ -96,7 +114,12 @@ async def async_api_discovery(hass, config, directive, context): @HANDLERS.register(("Alexa.Authorization", "AcceptGrant")) -async def async_api_accept_grant(hass, config, directive, context): +async def async_api_accept_grant( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Create a API formatted AcceptGrant response. Async friendly. @@ -116,7 +139,12 @@ async def async_api_accept_grant(hass, config, directive, context): @HANDLERS.register(("Alexa.PowerController", "TurnOn")) -async def async_api_turn_on(hass, config, directive, context): +async def async_api_turn_on( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a turn on request.""" entity = directive.entity if (domain := entity.domain) == group.DOMAIN: @@ -157,7 +185,12 @@ async def async_api_turn_on(hass, config, directive, context): @HANDLERS.register(("Alexa.PowerController", "TurnOff")) -async def async_api_turn_off(hass, config, directive, context): +async def async_api_turn_off( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a turn off request.""" entity = directive.entity domain = entity.domain @@ -199,7 +232,12 @@ async def async_api_turn_off(hass, config, directive, context): @HANDLERS.register(("Alexa.BrightnessController", "SetBrightness")) -async def async_api_set_brightness(hass, config, directive, context): +async def async_api_set_brightness( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set brightness request.""" entity = directive.entity brightness = int(directive.payload["brightness"]) @@ -216,7 +254,12 @@ async def async_api_set_brightness(hass, config, directive, context): @HANDLERS.register(("Alexa.BrightnessController", "AdjustBrightness")) -async def async_api_adjust_brightness(hass, config, directive, context): +async def async_api_adjust_brightness( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust brightness request.""" entity = directive.entity brightness_delta = int(directive.payload["brightnessDelta"]) @@ -237,7 +280,12 @@ async def async_api_adjust_brightness(hass, config, directive, context): @HANDLERS.register(("Alexa.ColorController", "SetColor")) -async def async_api_set_color(hass, config, directive, context): +async def async_api_set_color( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set color request.""" entity = directive.entity rgb = color_util.color_hsb_to_RGB( @@ -258,7 +306,12 @@ async def async_api_set_color(hass, config, directive, context): @HANDLERS.register(("Alexa.ColorTemperatureController", "SetColorTemperature")) -async def async_api_set_color_temperature(hass, config, directive, context): +async def async_api_set_color_temperature( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set color temperature request.""" entity = directive.entity kelvin = int(directive.payload["colorTemperatureInKelvin"]) @@ -275,7 +328,12 @@ async def async_api_set_color_temperature(hass, config, directive, context): @HANDLERS.register(("Alexa.ColorTemperatureController", "DecreaseColorTemperature")) -async def async_api_decrease_color_temp(hass, config, directive, context): +async def async_api_decrease_color_temp( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a decrease color temperature request.""" entity = directive.entity current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) @@ -294,7 +352,12 @@ async def async_api_decrease_color_temp(hass, config, directive, context): @HANDLERS.register(("Alexa.ColorTemperatureController", "IncreaseColorTemperature")) -async def async_api_increase_color_temp(hass, config, directive, context): +async def async_api_increase_color_temp( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an increase color temperature request.""" entity = directive.entity current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) @@ -313,7 +376,12 @@ async def async_api_increase_color_temp(hass, config, directive, context): @HANDLERS.register(("Alexa.SceneController", "Activate")) -async def async_api_activate(hass, config, directive, context): +async def async_api_activate( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an activate request.""" entity = directive.entity domain = entity.domain @@ -343,7 +411,12 @@ async def async_api_activate(hass, config, directive, context): @HANDLERS.register(("Alexa.SceneController", "Deactivate")) -async def async_api_deactivate(hass, config, directive, context): +async def async_api_deactivate( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a deactivate request.""" entity = directive.entity domain = entity.domain @@ -367,16 +440,24 @@ async def async_api_deactivate(hass, config, directive, context): @HANDLERS.register(("Alexa.PercentageController", "SetPercentage")) -async def async_api_set_percentage(hass, config, directive, context): +async def async_api_set_percentage( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set percentage request.""" entity = directive.entity - service = None - data = {ATTR_ENTITY_ID: entity.entity_id} - if entity.domain == fan.DOMAIN: - service = fan.SERVICE_SET_PERCENTAGE - percentage = int(directive.payload["percentage"]) - data[fan.ATTR_PERCENTAGE] = percentage + if entity.domain != fan.DOMAIN: + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) + + percentage = int(directive.payload["percentage"]) + service = fan.SERVICE_SET_PERCENTAGE + data = { + ATTR_ENTITY_ID: entity.entity_id, + fan.ATTR_PERCENTAGE: percentage, + } await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -386,20 +467,27 @@ async def async_api_set_percentage(hass, config, directive, context): @HANDLERS.register(("Alexa.PercentageController", "AdjustPercentage")) -async def async_api_adjust_percentage(hass, config, directive, context): +async def async_api_adjust_percentage( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust percentage request.""" entity = directive.entity + + if entity.domain != fan.DOMAIN: + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) + percentage_delta = int(directive.payload["percentageDelta"]) - service = None - data = {ATTR_ENTITY_ID: entity.entity_id} - - if entity.domain == fan.DOMAIN: - service = fan.SERVICE_SET_PERCENTAGE - current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 - - # set percentage - percentage = min(100, max(0, percentage_delta + current)) - data[fan.ATTR_PERCENTAGE] = percentage + current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 + # set percentage + percentage = min(100, max(0, percentage_delta + current)) + service = fan.SERVICE_SET_PERCENTAGE + data = { + ATTR_ENTITY_ID: entity.entity_id, + fan.ATTR_PERCENTAGE: percentage, + } await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -409,7 +497,12 @@ async def async_api_adjust_percentage(hass, config, directive, context): @HANDLERS.register(("Alexa.LockController", "Lock")) -async def async_api_lock(hass, config, directive, context): +async def async_api_lock( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a lock request.""" entity = directive.entity await hass.services.async_call( @@ -428,7 +521,12 @@ async def async_api_lock(hass, config, directive, context): @HANDLERS.register(("Alexa.LockController", "Unlock")) -async def async_api_unlock(hass, config, directive, context): +async def async_api_unlock( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an unlock request.""" if config.locale not in {"de-DE", "en-US", "ja-JP"}: msg = f"The unlock directive is not supported for the following locales: {config.locale}" @@ -452,7 +550,12 @@ async def async_api_unlock(hass, config, directive, context): @HANDLERS.register(("Alexa.Speaker", "SetVolume")) -async def async_api_set_volume(hass, config, directive, context): +async def async_api_set_volume( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set volume request.""" volume = round(float(directive.payload["volume"] / 100), 2) entity = directive.entity @@ -470,7 +573,12 @@ async def async_api_set_volume(hass, config, directive, context): @HANDLERS.register(("Alexa.InputController", "SelectInput")) -async def async_api_select_input(hass, config, directive, context): +async def async_api_select_input( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set input request.""" media_input = directive.payload["input"] entity = directive.entity @@ -514,7 +622,12 @@ async def async_api_select_input(hass, config, directive, context): @HANDLERS.register(("Alexa.Speaker", "AdjustVolume")) -async def async_api_adjust_volume(hass, config, directive, context): +async def async_api_adjust_volume( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust volume request.""" volume_delta = int(directive.payload["volume"]) @@ -542,7 +655,12 @@ async def async_api_adjust_volume(hass, config, directive, context): @HANDLERS.register(("Alexa.StepSpeaker", "AdjustVolume")) -async def async_api_adjust_volume_step(hass, config, directive, context): +async def async_api_adjust_volume_step( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust volume step request.""" # media_player volume up/down service does not support specifying steps # each component handles it differently e.g. via config. @@ -575,7 +693,12 @@ async def async_api_adjust_volume_step(hass, config, directive, context): @HANDLERS.register(("Alexa.StepSpeaker", "SetMute")) @HANDLERS.register(("Alexa.Speaker", "SetMute")) -async def async_api_set_mute(hass, config, directive, context): +async def async_api_set_mute( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set mute request.""" mute = bool(directive.payload["mute"]) entity = directive.entity @@ -592,7 +715,12 @@ async def async_api_set_mute(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Play")) -async def async_api_play(hass, config, directive, context): +async def async_api_play( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a play request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -605,7 +733,12 @@ async def async_api_play(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Pause")) -async def async_api_pause(hass, config, directive, context): +async def async_api_pause( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a pause request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -618,7 +751,12 @@ async def async_api_pause(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Stop")) -async def async_api_stop(hass, config, directive, context): +async def async_api_stop( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a stop request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -631,7 +769,12 @@ async def async_api_stop(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Next")) -async def async_api_next(hass, config, directive, context): +async def async_api_next( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a next request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -644,7 +787,12 @@ async def async_api_next(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Previous")) -async def async_api_previous(hass, config, directive, context): +async def async_api_previous( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a previous request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -676,7 +824,12 @@ def temperature_from_object(hass, temp_obj, interval=False): @HANDLERS.register(("Alexa.ThermostatController", "SetTargetTemperature")) -async def async_api_set_target_temp(hass, config, directive, context): +async def async_api_set_target_temp( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set target temperature request.""" entity = directive.entity min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) @@ -736,7 +889,12 @@ async def async_api_set_target_temp(hass, config, directive, context): @HANDLERS.register(("Alexa.ThermostatController", "AdjustTargetTemperature")) -async def async_api_adjust_target_temp(hass, config, directive, context): +async def async_api_adjust_target_temp( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust target temperature request.""" entity = directive.entity min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) @@ -773,7 +931,12 @@ async def async_api_adjust_target_temp(hass, config, directive, context): @HANDLERS.register(("Alexa.ThermostatController", "SetThermostatMode")) -async def async_api_set_thermostat_mode(hass, config, directive, context): +async def async_api_set_thermostat_mode( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set thermostat mode request.""" entity = directive.entity mode = directive.payload["thermostatMode"] @@ -836,13 +999,23 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): @HANDLERS.register(("Alexa", "ReportState")) -async def async_api_reportstate(hass, config, directive, context): +async def async_api_reportstate( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a ReportState request.""" return directive.response(name="StateReport") @HANDLERS.register(("Alexa.SecurityPanelController", "Arm")) -async def async_api_arm(hass, config, directive, context): +async def async_api_arm( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a Security Panel Arm request.""" entity = directive.entity service = None @@ -859,6 +1032,8 @@ async def async_api_arm(hass, config, directive, context): service = SERVICE_ALARM_ARM_NIGHT elif arm_state == "ARMED_STAY": service = SERVICE_ALARM_ARM_HOME + else: + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -883,7 +1058,12 @@ async def async_api_arm(hass, config, directive, context): @HANDLERS.register(("Alexa.SecurityPanelController", "Disarm")) -async def async_api_disarm(hass, config, directive, context): +async def async_api_disarm( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a Security Panel Disarm request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -916,7 +1096,12 @@ async def async_api_disarm(hass, config, directive, context): @HANDLERS.register(("Alexa.ModeController", "SetMode")) -async def async_api_set_mode(hass, config, directive, context): +async def async_api_set_mode( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a SetMode directive.""" entity = directive.entity instance = directive.instance @@ -955,9 +1140,8 @@ async def async_api_set_mode(hass, config, directive, context): elif position == "custom": service = cover.SERVICE_STOP_COVER - else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + if not service: + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -977,7 +1161,12 @@ async def async_api_set_mode(hass, config, directive, context): @HANDLERS.register(("Alexa.ModeController", "AdjustMode")) -async def async_api_adjust_mode(hass, config, directive, context): +async def async_api_adjust_mode( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a AdjustMode request. Requires capabilityResources supportedModes to be ordered. @@ -985,26 +1174,30 @@ async def async_api_adjust_mode(hass, config, directive, context): """ # Currently no supportedModes are configured with ordered=True to support this request. - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) @HANDLERS.register(("Alexa.ToggleController", "TurnOn")) -async def async_api_toggle_on(hass, config, directive, context): +async def async_api_toggle_on( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a toggle on request.""" entity = directive.entity instance = directive.instance domain = entity.domain - service = None - data = {ATTR_ENTITY_ID: entity.entity_id} # Fan Oscillating - if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": - service = fan.SERVICE_OSCILLATE - data[fan.ATTR_OSCILLATING] = True - else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + if instance != f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) + + service = fan.SERVICE_OSCILLATE + data = { + ATTR_ENTITY_ID: entity.entity_id, + fan.ATTR_OSCILLATING: True, + } await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -1024,21 +1217,26 @@ async def async_api_toggle_on(hass, config, directive, context): @HANDLERS.register(("Alexa.ToggleController", "TurnOff")) -async def async_api_toggle_off(hass, config, directive, context): +async def async_api_toggle_off( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a toggle off request.""" entity = directive.entity instance = directive.instance domain = entity.domain - service = None - data = {ATTR_ENTITY_ID: entity.entity_id} # Fan Oscillating - if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": - service = fan.SERVICE_OSCILLATE - data[fan.ATTR_OSCILLATING] = False - else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + if instance != f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) + + service = fan.SERVICE_OSCILLATE + data = { + ATTR_ENTITY_ID: entity.entity_id, + fan.ATTR_OSCILLATING: False, + } await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -1058,7 +1256,12 @@ async def async_api_toggle_off(hass, config, directive, context): @HANDLERS.register(("Alexa.RangeController", "SetRangeValue")) -async def async_api_set_range(hass, config, directive, context): +async def async_api_set_range( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a next request.""" entity = directive.entity instance = directive.instance @@ -1125,8 +1328,7 @@ async def async_api_set_range(hass, config, directive, context): data[vacuum.ATTR_FAN_SPEED] = speed else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -1146,16 +1348,21 @@ async def async_api_set_range(hass, config, directive, context): @HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue")) -async def async_api_adjust_range(hass, config, directive, context): +async def async_api_adjust_range( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a next request.""" entity = directive.entity instance = directive.instance domain = entity.domain service = None - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} range_delta = directive.payload["rangeValueDelta"] range_delta_default = bool(directive.payload["rangeValueDeltaDefault"]) - response_value = 0 + response_value: int | None = 0 # Cover Position if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": @@ -1232,12 +1439,10 @@ async def async_api_adjust_range(hass, config, directive, context): speed = next( (v for i, v in enumerate(speed_list) if i == new_speed_index), None ) - data[vacuum.ATTR_FAN_SPEED] = response_value = speed else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -1257,7 +1462,12 @@ async def async_api_adjust_range(hass, config, directive, context): @HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) -async def async_api_changechannel(hass, config, directive, context): +async def async_api_changechannel( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a change channel request.""" channel = "0" entity = directive.entity @@ -1309,7 +1519,12 @@ async def async_api_changechannel(hass, config, directive, context): @HANDLERS.register(("Alexa.ChannelController", "SkipChannels")) -async def async_api_skipchannel(hass, config, directive, context): +async def async_api_skipchannel( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a skipchannel request.""" channel = int(directive.payload["channelCount"]) entity = directive.entity @@ -1340,7 +1555,12 @@ async def async_api_skipchannel(hass, config, directive, context): @HANDLERS.register(("Alexa.SeekController", "AdjustSeekPosition")) -async def async_api_seek(hass, config, directive, context): +async def async_api_seek( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a seek request.""" entity = directive.entity position_delta = int(directive.payload["deltaPositionMilliseconds"]) @@ -1379,7 +1599,12 @@ async def async_api_seek(hass, config, directive, context): @HANDLERS.register(("Alexa.EqualizerController", "SetMode")) -async def async_api_set_eq_mode(hass, config, directive, context): +async def async_api_set_eq_mode( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a SetMode request for EqualizerController.""" mode = directive.payload["mode"] entity = directive.entity @@ -1406,18 +1631,27 @@ async def async_api_set_eq_mode(hass, config, directive, context): @HANDLERS.register(("Alexa.EqualizerController", "AdjustBands")) @HANDLERS.register(("Alexa.EqualizerController", "ResetBands")) @HANDLERS.register(("Alexa.EqualizerController", "SetBands")) -async def async_api_bands_directive(hass, config, directive, context): +async def async_api_bands_directive( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Handle an AdjustBands, ResetBands, SetBands request. Only mode directives are currently supported for the EqualizerController. """ # Currently bands directives are not supported. - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) @HANDLERS.register(("Alexa.TimeHoldController", "Hold")) -async def async_api_hold(hass, config, directive, context): +async def async_api_hold( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a TimeHoldController Hold request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -1429,8 +1663,7 @@ async def async_api_hold(hass, config, directive, context): service = vacuum.SERVICE_START_PAUSE else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -1440,7 +1673,12 @@ async def async_api_hold(hass, config, directive, context): @HANDLERS.register(("Alexa.TimeHoldController", "Resume")) -async def async_api_resume(hass, config, directive, context): +async def async_api_resume( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a TimeHoldController Resume request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -1452,8 +1690,7 @@ async def async_api_resume(hass, config, directive, context): service = vacuum.SERVICE_START_PAUSE else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -1463,11 +1700,18 @@ async def async_api_resume(hass, config, directive, context): @HANDLERS.register(("Alexa.CameraStreamController", "InitializeCameraStreams")) -async def async_api_initialize_camera_stream(hass, config, directive, context): +async def async_api_initialize_camera_stream( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a InitializeCameraStreams request.""" entity = directive.entity stream_source = await camera.async_request_stream(hass, entity.entity_id, fmt="hls") - camera_image = hass.states.get(entity.entity_id).attributes[ATTR_ENTITY_PICTURE] + state = hass.states.get(entity.entity_id) + assert state + camera_image = state.attributes[ATTR_ENTITY_PICTURE] try: external_url = network.get_url( From 5bb59206971261e138eb89837068aa4f4a49b78e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 1 Aug 2022 00:28:32 +0000 Subject: [PATCH 075/903] [ci skip] Translation update --- .../components/ambee/translations/fr.json | 5 +++++ .../components/bluetooth/translations/hu.json | 12 ++++++++++- .../homeassistant_alerts/translations/fr.json | 8 ++++++++ .../lacrosse_view/translations/fr.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/hu.json | 20 +++++++++++++++++++ .../lg_soundbar/translations/ca.json | 2 +- .../lg_soundbar/translations/de.json | 2 +- .../lg_soundbar/translations/en.json | 3 ++- .../lg_soundbar/translations/fr.json | 2 +- .../lg_soundbar/translations/pt-BR.json | 2 +- .../opentherm_gw/translations/ca.json | 3 ++- .../opentherm_gw/translations/de.json | 3 ++- .../opentherm_gw/translations/en.json | 3 ++- .../opentherm_gw/translations/fr.json | 3 ++- .../opentherm_gw/translations/hu.json | 3 ++- .../opentherm_gw/translations/it.json | 3 ++- .../opentherm_gw/translations/pl.json | 3 ++- .../opentherm_gw/translations/pt-BR.json | 3 ++- .../opentherm_gw/translations/zh-Hant.json | 3 ++- .../radiotherm/translations/hu.json | 6 ++++++ .../simplepush/translations/fr.json | 5 +++++ .../soundtouch/translations/fr.json | 5 +++++ .../soundtouch/translations/hu.json | 5 +++++ .../components/spotify/translations/hu.json | 6 ++++++ .../steam_online/translations/hu.json | 6 ++++++ .../components/xbox/translations/fr.json | 5 +++++ .../components/xbox/translations/hu.json | 5 +++++ 27 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/fr.json create mode 100644 homeassistant/components/lacrosse_view/translations/fr.json create mode 100644 homeassistant/components/lacrosse_view/translations/hu.json diff --git a/homeassistant/components/ambee/translations/fr.json b/homeassistant/components/ambee/translations/fr.json index cfa3e0b9946..da3932962a6 100644 --- a/homeassistant/components/ambee/translations/fr.json +++ b/homeassistant/components/ambee/translations/fr.json @@ -24,5 +24,10 @@ "description": "Configurer Ambee pour l'int\u00e9grer \u00e0 Home Assistant." } } + }, + "issues": { + "pending_removal": { + "title": "L'int\u00e9gration Ambee est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json index 5cdc199476c..8b51191e938 100644 --- a/homeassistant/components/bluetooth/translations/hu.json +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "no_adapters": "Nem tal\u00e1lhat\u00f3 Bluetooth adapter" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "A szkennel\u00e9shez haszn\u00e1lhat\u00f3 Bluetooth-adapter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/fr.json b/homeassistant/components/homeassistant_alerts/translations/fr.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/fr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/fr.json b/homeassistant/components/lacrosse_view/translations/fr.json new file mode 100644 index 00000000000..689d3cf7cf6 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification non valide", + "no_locations": "Aucun emplacement trouv\u00e9", + "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/lacrosse_view/translations/hu.json b/homeassistant/components/lacrosse_view/translations/hu.json new file mode 100644 index 00000000000..a040bd96b91 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_locations": "Nem tal\u00e1lhat\u00f3k helyek", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ca.json b/homeassistant/components/lg_soundbar/translations/ca.json index 8c445361eb8..3d1b6f3bc98 100644 --- a/homeassistant/components/lg_soundbar/translations/ca.json +++ b/homeassistant/components/lg_soundbar/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat", + "already_configured": "El dispositiu ja est\u00e0 configurat", "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/de.json b/homeassistant/components/lg_soundbar/translations/de.json index a840fb04abe..b8458a653aa 100644 --- a/homeassistant/components/lg_soundbar/translations/de.json +++ b/homeassistant/components/lg_soundbar/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json index 10441d21536..cbf35dc2976 100644 --- a/homeassistant/components/lg_soundbar/translations/en.json +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "existing_instance_updated": "Updated existing configuration." }, "error": { "cannot_connect": "Failed to connect" diff --git a/homeassistant/components/lg_soundbar/translations/fr.json b/homeassistant/components/lg_soundbar/translations/fr.json index b13f3d0d595..5f6977ed3ab 100644 --- a/homeassistant/components/lg_soundbar/translations/fr.json +++ b/homeassistant/components/lg_soundbar/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/pt-BR.json b/homeassistant/components/lg_soundbar/translations/pt-BR.json index dfbff8cddc8..8a2b69e069d 100644 --- a/homeassistant/components/lg_soundbar/translations/pt-BR.json +++ b/homeassistant/components/lg_soundbar/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." }, "error": { diff --git a/homeassistant/components/opentherm_gw/translations/ca.json b/homeassistant/components/opentherm_gw/translations/ca.json index 6427c3c7633..fcfc8186b18 100644 --- a/homeassistant/components/opentherm_gw/translations/ca.json +++ b/homeassistant/components/opentherm_gw/translations/ca.json @@ -3,7 +3,8 @@ "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", - "id_exists": "L'identificador de passarel\u00b7la ja existeix" + "id_exists": "L'identificador de passarel\u00b7la ja existeix", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/de.json b/homeassistant/components/opentherm_gw/translations/de.json index 1217839eaed..322ac29aec4 100644 --- a/homeassistant/components/opentherm_gw/translations/de.json +++ b/homeassistant/components/opentherm_gw/translations/de.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", - "id_exists": "Gateway-ID ist bereits vorhanden" + "id_exists": "Gateway-ID ist bereits vorhanden", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/en.json b/homeassistant/components/opentherm_gw/translations/en.json index c84e9dcf0e2..a44e121063d 100644 --- a/homeassistant/components/opentherm_gw/translations/en.json +++ b/homeassistant/components/opentherm_gw/translations/en.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Device is already configured", "cannot_connect": "Failed to connect", - "id_exists": "Gateway id already exists" + "id_exists": "Gateway id already exists", + "timeout_connect": "Timeout establishing connection" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/fr.json b/homeassistant/components/opentherm_gw/translations/fr.json index d0a058eacfa..4a9cee29f1f 100644 --- a/homeassistant/components/opentherm_gw/translations/fr.json +++ b/homeassistant/components/opentherm_gw/translations/fr.json @@ -3,7 +3,8 @@ "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "id_exists": "L'identifiant de la passerelle existe d\u00e9j\u00e0" + "id_exists": "L'identifiant de la passerelle existe d\u00e9j\u00e0", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index 9e2b6478bec..262a42aaaa5 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "id_exists": "Az \u00e1tj\u00e1r\u00f3 azonos\u00edt\u00f3ja m\u00e1r l\u00e9tezik" + "id_exists": "Az \u00e1tj\u00e1r\u00f3 azonos\u00edt\u00f3ja m\u00e1r l\u00e9tezik", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/it.json b/homeassistant/components/opentherm_gw/translations/it.json index 4d1feac60dd..16f7a54bf90 100644 --- a/homeassistant/components/opentherm_gw/translations/it.json +++ b/homeassistant/components/opentherm_gw/translations/it.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "cannot_connect": "Impossibile connettersi", - "id_exists": "ID del gateway esiste gi\u00e0" + "id_exists": "ID del gateway esiste gi\u00e0", + "timeout_connect": "Tempo scaduto per stabile la connessione." }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/pl.json b/homeassistant/components/opentherm_gw/translations/pl.json index fafc4fdc0d7..b2c15052b69 100644 --- a/homeassistant/components/opentherm_gw/translations/pl.json +++ b/homeassistant/components/opentherm_gw/translations/pl.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "id_exists": "Identyfikator bramki ju\u017c istnieje" + "id_exists": "Identyfikator bramki ju\u017c istnieje", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/pt-BR.json b/homeassistant/components/opentherm_gw/translations/pt-BR.json index 018aed87dc3..3d2649aad08 100644 --- a/homeassistant/components/opentherm_gw/translations/pt-BR.json +++ b/homeassistant/components/opentherm_gw/translations/pt-BR.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha ao conectar", - "id_exists": "ID do gateway j\u00e1 existe" + "id_exists": "ID do gateway j\u00e1 existe", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index d5b9d02de42..8227c53e1e9 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -3,7 +3,8 @@ "error": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728" + "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" }, "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/hu.json b/homeassistant/components/radiotherm/translations/hu.json index f55ea666f5f..05e1ece66ec 100644 --- a/homeassistant/components/radiotherm/translations/hu.json +++ b/homeassistant/components/radiotherm/translations/hu.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A Radio Thermostat kl\u00edmaplatform YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa a Home Assistant 2022.9-ben megsz\u0171nik. \n\nMegl\u00e9v\u0151 konfigur\u00e1ci\u00f3ja automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre. T\u00e1vol\u00edtsa el a YAML-konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st a probl\u00e9ma megold\u00e1s\u00e1hoz.", + "title": "A r\u00e1di\u00f3s termoszt\u00e1t YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplepush/translations/fr.json b/homeassistant/components/simplepush/translations/fr.json index 546d03bb131..49356553426 100644 --- a/homeassistant/components/simplepush/translations/fr.json +++ b/homeassistant/components/simplepush/translations/fr.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Simplepush est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/fr.json b/homeassistant/components/soundtouch/translations/fr.json index e0e187756af..94cbd1ed69e 100644 --- a/homeassistant/components/soundtouch/translations/fr.json +++ b/homeassistant/components/soundtouch/translations/fr.json @@ -17,5 +17,10 @@ "title": "Confirmer l'ajout de l'appareil Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Bose SoundTouch est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/hu.json b/homeassistant/components/soundtouch/translations/hu.json index a0f5c364af6..2e4b4c36d8a 100644 --- a/homeassistant/components/soundtouch/translations/hu.json +++ b/homeassistant/components/soundtouch/translations/hu.json @@ -17,5 +17,10 @@ "title": "Bose SoundTouch eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1nak meger\u0151s\u00edt\u00e9se" } } + }, + "issues": { + "deprecated_yaml": { + "title": "A Bose SoundTouch YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index 735c4942fb2..846ddaf5ce3 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A Spotify YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Spotify YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + } + }, "system_health": { "info": { "api_endpoint_reachable": "A Spotify API v\u00e9gpont el\u00e9rhet\u0151" diff --git a/homeassistant/components/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json index 87e78abedb4..5dcc97464c8 100644 --- a/homeassistant/components/steam_online/translations/hu.json +++ b/homeassistant/components/steam_online/translations/hu.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A Steam YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML-konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Steam YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + } + }, "options": { "error": { "unauthorized": "A bar\u00e1t-lista korl\u00e1tozva van: K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t arr\u00f3l, hogyan l\u00e1thatja az \u00f6sszes t\u00f6bbi bar\u00e1tj\u00e1t." diff --git a/homeassistant/components/xbox/translations/fr.json b/homeassistant/components/xbox/translations/fr.json index 5b37704cec7..ee3af75d401 100644 --- a/homeassistant/components/xbox/translations/fr.json +++ b/homeassistant/components/xbox/translations/fr.json @@ -13,5 +13,10 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour XBox est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json index fc8428b3101..b7c32ad8008 100644 --- a/homeassistant/components/xbox/translations/hu.json +++ b/homeassistant/components/xbox/translations/hu.json @@ -13,5 +13,10 @@ "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } + }, + "issues": { + "deprecated_yaml": { + "title": "Az Xbox YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file From 0738f082156def8db0445fefd719d1b167497bb7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 1 Aug 2022 09:52:51 +0200 Subject: [PATCH 076/903] Add missing sensors for Shelly Plus H&T (#76001) * Add missing sensors for Shelly Plus H&T * Cleanup * Fix * Add voltage to battery sensor * Apply review comments --- homeassistant/components/shelly/entity.py | 6 ++-- homeassistant/components/shelly/sensor.py | 37 ++++++++++++++++++++++- homeassistant/components/shelly/utils.py | 17 +++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 8cba4d2804e..a38bef54bea 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -190,9 +190,9 @@ def async_setup_entry_rpc( ] and not description.supported(wrapper.device.status[key]): continue - # Filter and remove entities that according to settings should not create an entity + # Filter and remove entities that according to settings/status should not create an entity if description.removal_condition and description.removal_condition( - wrapper.device.config, key + wrapper.device.config, wrapper.device.status, key ): domain = sensor_class.__module__.split(".")[-1] unique_id = f"{wrapper.mac}-{key}-{sensor_id}" @@ -268,7 +268,7 @@ class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin): value: Callable[[Any, Any], Any] | None = None available: Callable[[dict], bool] | None = None - removal_condition: Callable[[dict, str], bool] | None = None + removal_condition: Callable[[dict, dict, str], bool] | None = None extra_state_attributes: Callable[[dict, dict], dict | None] | None = None use_polling_wrapper: bool = False supported: Callable = lambda _: False diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 92c19734414..5a36b5e99bf 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -46,7 +46,12 @@ from .entity import ( async_setup_entry_rest, async_setup_entry_rpc, ) -from .utils import get_device_entry_gen, get_device_uptime, temperature_unit +from .utils import ( + get_device_entry_gen, + get_device_uptime, + is_rpc_device_externally_powered, + temperature_unit, +) @dataclass @@ -352,6 +357,15 @@ RPC_SENSORS: Final = { entity_category=EntityCategory.DIAGNOSTIC, use_polling_wrapper=True, ), + "temperature_0": RpcSensorDescription( + key="temperature:0", + sub_key="tC", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + ), "rssi": RpcSensorDescription( key="wifi", sub_key="rssi", @@ -373,6 +387,27 @@ RPC_SENSORS: Final = { entity_category=EntityCategory.DIAGNOSTIC, use_polling_wrapper=True, ), + "humidity_0": RpcSensorDescription( + key="humidity:0", + sub_key="rh", + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + ), + "battery": RpcSensorDescription( + key="devicepower:0", + sub_key="battery", + name="Battery", + native_unit_of_measurement=PERCENTAGE, + value=lambda status, _: status["percent"], + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + removal_condition=is_rpc_device_externally_powered, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + ), } diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 6dfc2fb3be8..7eeb93f2918 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -271,7 +271,9 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str: entity_name = device.config[key].get("name", device_name) if entity_name is None: - return f"{device_name} {key.replace(':', '_')}" + if [k for k in key if k.startswith(("input", "switch"))]: + return f"{device_name} {key.replace(':', '_')}" + return device_name return entity_name @@ -325,7 +327,9 @@ def get_rpc_key_ids(keys_dict: dict[str, Any], key: str) -> list[int]: return key_ids -def is_rpc_momentary_input(config: dict[str, Any], key: str) -> bool: +def is_rpc_momentary_input( + config: dict[str, Any], status: dict[str, Any], key: str +) -> bool: """Return true if rpc input button settings is set to a momentary type.""" return cast(bool, config[key]["type"] == "button") @@ -342,6 +346,13 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool: return con_types is not None and con_types[channel].lower().startswith("light") +def is_rpc_device_externally_powered( + config: dict[str, Any], status: dict[str, Any], key: str +) -> bool: + """Return true if device has external power instead of battery.""" + return cast(bool, status[key]["external"]["present"]) + + def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]: """Return list of input triggers for RPC device.""" triggers = [] @@ -350,7 +361,7 @@ def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]: for id_ in key_ids: key = f"input:{id_}" - if not is_rpc_momentary_input(device.config, key): + if not is_rpc_momentary_input(device.config, device.status, key): continue for trigger_type in RPC_INPUTS_EVENTS_TYPES: From 81d1786a16e0c5153239be9f4da2d2b2d93a4ce0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Aug 2022 10:13:05 +0200 Subject: [PATCH 077/903] Remove unused logging args parameter (#75619) --- homeassistant/util/logging.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 6c4a55f51f2..76df4bb17b6 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -118,22 +118,20 @@ def log_exception(format_err: Callable[..., Any], *args: Any) -> None: @overload def catch_log_exception( - func: Callable[..., Coroutine[Any, Any, Any]], - format_err: Callable[..., Any], - *args: Any, + func: Callable[..., Coroutine[Any, Any, Any]], format_err: Callable[..., Any] ) -> Callable[..., Coroutine[Any, Any, None]]: """Overload for Callables that return a Coroutine.""" @overload def catch_log_exception( - func: Callable[..., Any], format_err: Callable[..., Any], *args: Any + func: Callable[..., Any], format_err: Callable[..., Any] ) -> Callable[..., None | Coroutine[Any, Any, None]]: """Overload for Callables that return Any.""" def catch_log_exception( - func: Callable[..., Any], format_err: Callable[..., Any], *args: Any + func: Callable[..., Any], format_err: Callable[..., Any] ) -> Callable[..., None | Coroutine[Any, Any, None]]: """Decorate a callback to catch and log exceptions.""" From 826de707e47c5277e6e0b6d6127cbf8b8617ca33 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 1 Aug 2022 11:35:31 +0200 Subject: [PATCH 078/903] Add strict typing to openexchangerates (#76004) --- .strict-typing | 1 + .../components/openexchangerates/sensor.py | 47 +++++++++---------- mypy.ini | 11 +++++ 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/.strict-typing b/.strict-typing index ddeaf4f3844..37dcdd7623f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -186,6 +186,7 @@ homeassistant.components.nut.* homeassistant.components.oncue.* homeassistant.components.onewire.* homeassistant.components.open_meteo.* +homeassistant.components.openexchangerates.* homeassistant.components.openuv.* homeassistant.components.peco.* homeassistant.components.overkiz.* diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 37879551ee4..318cae4ae0e 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -4,18 +4,13 @@ from __future__ import annotations from datetime import timedelta from http import HTTPStatus import logging +from typing import Any import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_BASE, - CONF_NAME, - CONF_QUOTE, -) +from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -49,10 +44,10 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Open Exchange Rates sensor.""" - name = config.get(CONF_NAME) - api_key = config.get(CONF_API_KEY) - base = config.get(CONF_BASE) - quote = config.get(CONF_QUOTE) + name: str = config[CONF_NAME] + api_key: str = config[CONF_API_KEY] + base: str = config[CONF_BASE] + quote: str = config[CONF_QUOTE] parameters = {"base": base, "app_id": api_key} @@ -70,50 +65,55 @@ def setup_platform( class OpenexchangeratesSensor(SensorEntity): """Representation of an Open Exchange Rates sensor.""" - def __init__(self, rest, name, quote): + _attr_attribution = ATTRIBUTION + + def __init__(self, rest: OpenexchangeratesData, name: str, quote: str) -> None: """Initialize the sensor.""" self.rest = rest self._name = name self._quote = quote - self._state = None + self._state: float | None = None @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def native_value(self): + def native_value(self) -> float | None: """Return the state of the sensor.""" return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return other attributes of the sensor.""" attr = self.rest.data - attr[ATTR_ATTRIBUTION] = ATTRIBUTION return attr - def update(self): + def update(self) -> None: """Update current conditions.""" self.rest.update() - value = self.rest.data - self._state = round(value[str(self._quote)], 4) + if (value := self.rest.data) is None: + self._attr_available = False + return + + self._attr_available = True + self._state = round(value[self._quote], 4) class OpenexchangeratesData: """Get data from Openexchangerates.org.""" - def __init__(self, resource, parameters, quote): + def __init__(self, resource: str, parameters: dict[str, str], quote: str) -> None: """Initialize the data object.""" self._resource = resource self._parameters = parameters self._quote = quote - self.data = None + self.data: dict[str, Any] | None = None @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the latest data from openexchangerates.org.""" try: result = requests.get(self._resource, params=self._parameters, timeout=10) @@ -121,4 +121,3 @@ class OpenexchangeratesData: except requests.exceptions.HTTPError: _LOGGER.error("Check the Openexchangerates API key") self.data = None - return False diff --git a/mypy.ini b/mypy.ini index 4ef79368f73..8c85b550f71 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1769,6 +1769,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.openexchangerates.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.openuv.*] check_untyped_defs = true disallow_incomplete_defs = true From 1a40d400dca69bb55f2450db24a118faccbafe10 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Aug 2022 14:09:47 +0200 Subject: [PATCH 079/903] Add function/property name to pylint message (#75913) --- pylint/plugins/hass_enforce_type_hints.py | 14 +++++---- tests/pylint/test_enforce_type_hints.py | 35 ++++++++++++----------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 3b50c072eb6..ac21a9bf686 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1577,12 +1577,12 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] priority = -1 msgs = { "W7431": ( - "Argument %s should be of type %s", + "Argument %s should be of type %s in %s", "hass-argument-type", "Used when method argument type is incorrect", ), "W7432": ( - "Return type should be %s", + "Return type should be %s in %s", "hass-return-type", "Used when method return type is incorrect", ), @@ -1669,7 +1669,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self.add_message( "hass-argument-type", node=node.args.args[key], - args=(key + 1, expected_type), + args=(key + 1, expected_type, node.name), ) # Check that all keyword arguments are correctly annotated. @@ -1680,7 +1680,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self.add_message( "hass-argument-type", node=arg_node, - args=(arg_name, expected_type), + args=(arg_name, expected_type, node.name), ) # Check that kwargs is correctly annotated. @@ -1690,13 +1690,15 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self.add_message( "hass-argument-type", node=node, - args=(node.args.kwarg, match.kwargs_type), + args=(node.args.kwarg, match.kwargs_type, node.name), ) # Check the return type. if not _is_valid_return_type(match, node.returns): self.add_message( - "hass-return-type", node=node, args=match.return_type or "None" + "hass-return-type", + node=node, + args=(match.return_type or "None", node.name), ) diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 54824e5c0b0..53c17880716 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -215,7 +215,7 @@ def test_invalid_discovery_info( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=discovery_info_node, - args=(4, "DiscoveryInfoType | None"), + args=(4, "DiscoveryInfoType | None", "async_setup_scanner"), line=6, col_offset=4, end_line=6, @@ -268,7 +268,10 @@ def test_invalid_list_dict_str_any( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args=["list[dict[str, str]]", "list[dict[str, Any]]"], + args=( + ["list[dict[str, str]]", "list[dict[str, Any]]"], + "async_get_triggers", + ), line=2, col_offset=0, end_line=2, @@ -325,7 +328,7 @@ def test_invalid_config_flow_step( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=arg_node, - args=(2, "ZeroconfServiceInfo"), + args=(2, "ZeroconfServiceInfo", "async_step_zeroconf"), line=10, col_offset=8, end_line=10, @@ -334,7 +337,7 @@ def test_invalid_config_flow_step( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args="FlowResult", + args=("FlowResult", "async_step_zeroconf"), line=8, col_offset=4, end_line=8, @@ -399,7 +402,7 @@ def test_invalid_config_flow_async_get_options_flow( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=arg_node, - args=(1, "ConfigEntry"), + args=(1, "ConfigEntry", "async_get_options_flow"), line=12, col_offset=8, end_line=12, @@ -408,7 +411,7 @@ def test_invalid_config_flow_async_get_options_flow( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args="OptionsFlow", + args=("OptionsFlow", "async_get_options_flow"), line=11, col_offset=4, end_line=11, @@ -491,7 +494,7 @@ def test_invalid_entity_properties( pylint.testutils.MessageTest( msg_id="hass-return-type", node=prop_node, - args=["str", None], + args=(["str", None], "changed_by"), line=9, col_offset=4, end_line=9, @@ -500,7 +503,7 @@ def test_invalid_entity_properties( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=func_node, - args=("kwargs", "Any"), + args=("kwargs", "Any", "async_lock"), line=14, col_offset=4, end_line=14, @@ -509,7 +512,7 @@ def test_invalid_entity_properties( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args="None", + args=("None", "async_lock"), line=14, col_offset=4, end_line=14, @@ -587,7 +590,7 @@ def test_named_arguments( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=percentage_node, - args=("percentage", "int | None"), + args=("percentage", "int | None", "async_turn_on"), line=10, col_offset=8, end_line=10, @@ -596,7 +599,7 @@ def test_named_arguments( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=preset_mode_node, - args=("preset_mode", "str | None"), + args=("preset_mode", "str | None", "async_turn_on"), line=12, col_offset=8, end_line=12, @@ -605,7 +608,7 @@ def test_named_arguments( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=func_node, - args=("kwargs", "Any"), + args=("kwargs", "Any", "async_turn_on"), line=8, col_offset=4, end_line=8, @@ -614,7 +617,7 @@ def test_named_arguments( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args="None", + args=("None", "async_turn_on"), line=8, col_offset=4, end_line=8, @@ -670,7 +673,7 @@ def test_invalid_mapping_return_type( pylint.testutils.MessageTest( msg_id="hass-return-type", node=property_node, - args=["Mapping[str, Any]", None], + args=(["Mapping[str, Any]", None], "capability_attributes"), line=15, col_offset=4, end_line=15, @@ -809,7 +812,7 @@ def test_invalid_long_tuple( pylint.testutils.MessageTest( msg_id="hass-return-type", node=rgbw_node, - args=["tuple[int, int, int, int]", None], + args=(["tuple[int, int, int, int]", None], "rgbw_color"), line=15, col_offset=4, end_line=15, @@ -818,7 +821,7 @@ def test_invalid_long_tuple( pylint.testutils.MessageTest( msg_id="hass-return-type", node=rgbww_node, - args=["tuple[int, int, int, int, int]", None], + args=(["tuple[int, int, int, int, int]", None], "rgbww_color"), line=21, col_offset=4, end_line=21, From 687ac91947d2a17b730c6b520e378a5815ba8e98 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 1 Aug 2022 14:22:24 +0200 Subject: [PATCH 080/903] Support MWh for gas consumption sensors (#76016) --- homeassistant/components/energy/validate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 02549ddfe96..9d6b3bd53c7 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -40,7 +40,11 @@ GAS_USAGE_DEVICE_CLASSES = ( sensor.SensorDeviceClass.GAS, ) GAS_USAGE_UNITS = { - sensor.SensorDeviceClass.ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), + sensor.SensorDeviceClass.ENERGY: ( + ENERGY_WATT_HOUR, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ), sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET), } GAS_PRICE_UNITS = tuple( From f2da46d99b778592630b3efc2564a5784332ff7c Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 1 Aug 2022 10:07:17 -0400 Subject: [PATCH 081/903] Remove aiohttp close from aladdin connect config_flow (#76029) Remove aiohttp close from config_flow - throwing error found in #75933 --- homeassistant/components/aladdin_connect/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 44d3f01a9ec..4f03d7cdb3b 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -44,7 +44,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: CLIENT_ID, ) login = await acc.login() - await acc.close() if not login: raise InvalidAuth From 2dd62b14b65bef4037a2fbcfa86029633433290c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 1 Aug 2022 16:56:08 +0200 Subject: [PATCH 082/903] =?UTF-8?q?Convert=20fj=C3=A4r=C3=A5skupan=20to=20?= =?UTF-8?q?built=20in=20bluetooth=20(#75380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add bluetooth discovery * Use home assistant standard api * Fixup manufacture data * Adjust config flow to use standard features * Fixup tests * Mock bluetooth * Simplify device check * Fix missing typing Co-authored-by: Martin Hjelmare --- .../components/fjaraskupan/__init__.py | 87 +++++++++---------- .../components/fjaraskupan/config_flow.py | 29 ++----- .../components/fjaraskupan/manifest.json | 9 +- homeassistant/generated/bluetooth.py | 12 +++ tests/components/fjaraskupan/__init__.py | 10 +++ tests/components/fjaraskupan/conftest.py | 46 +--------- .../fjaraskupan/test_config_flow.py | 48 +++++----- 7 files changed, 109 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 36608fb026d..fbd2f13d2b4 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -5,14 +5,20 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging -from typing import TYPE_CHECKING -from bleak import BleakScanner -from fjaraskupan import Device, State, device_filter +from fjaraskupan import Device, State +from homeassistant.components.bluetooth import ( + BluetoothCallbackMatcher, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_address_present, + async_register_callback, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -23,11 +29,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DISPATCH_DETECTION, DOMAIN -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - from bleak.backends.scanner import AdvertisementData - - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.FAN, @@ -70,16 +71,18 @@ class Coordinator(DataUpdateCoordinator[State]): async def _async_update_data(self) -> State: """Handle an explicit update request.""" if self._refresh_was_scheduled: - raise UpdateFailed("No data received within schedule.") + if async_address_present(self.hass, self.device.address): + return self.device.state + raise UpdateFailed( + "No data received within schedule, and device is no longer present" + ) await self.device.update() return self.device.state - def detection_callback( - self, ble_device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: + def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None: """Handle a new announcement of data.""" - self.device.detection_callback(ble_device, advertisement_data) + self.device.detection_callback(service_info.device, service_info.advertisement) self.async_set_updated_data(self.device.state) @@ -87,59 +90,52 @@ class Coordinator(DataUpdateCoordinator[State]): class EntryState: """Store state of config entry.""" - scanner: BleakScanner coordinators: dict[str, Coordinator] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"DuplicateData": True}) - - state = EntryState(scanner, {}) + state = EntryState({}) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = state - async def detection_callback( - ble_device: BLEDevice, advertisement_data: AdvertisementData + def detection_callback( + service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: - if data := state.coordinators.get(ble_device.address): - _LOGGER.debug( - "Update: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - data.detection_callback(ble_device, advertisement_data) + if change != BluetoothChange.ADVERTISEMENT: + return + if data := state.coordinators.get(service_info.address): + _LOGGER.debug("Update: %s", service_info) + data.detection_callback(service_info) else: - if not device_filter(ble_device, advertisement_data): - return + _LOGGER.debug("Detected: %s", service_info) - _LOGGER.debug( - "Detected: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - device = Device(ble_device) + device = Device(service_info.device) device_info = DeviceInfo( - identifiers={(DOMAIN, ble_device.address)}, + identifiers={(DOMAIN, service_info.address)}, manufacturer="Fjäråskupan", name="Fjäråskupan", ) coordinator: Coordinator = Coordinator(hass, device, device_info) - coordinator.detection_callback(ble_device, advertisement_data) + coordinator.detection_callback(service_info) - state.coordinators[ble_device.address] = coordinator + state.coordinators[service_info.address] = coordinator async_dispatcher_send( hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator ) - scanner.register_detection_callback(detection_callback) - await scanner.start() - - async def on_hass_stop(event: Event) -> None: - await scanner.stop() - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + async_register_callback( + hass, + detection_callback, + BluetoothCallbackMatcher( + manufacturer_id=20296, + manufacturer_data_start=[79, 68, 70, 74, 65, 82], + ), + BluetoothScanningMode.ACTIVE, + ) ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -177,7 +173,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - entry_state: EntryState = hass.data[DOMAIN].pop(entry.entry_id) - await entry_state.scanner.stop() + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index ffac366500b..dd1dc03d3ad 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -1,42 +1,25 @@ """Config flow for Fjäråskupan integration.""" from __future__ import annotations -import asyncio - -import async_timeout -from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from fjaraskupan import device_filter +from homeassistant.components.bluetooth import async_discovered_service_info from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow from .const import DOMAIN -CONST_WAIT_TIME = 5.0 - async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" - event = asyncio.Event() + service_infos = async_discovered_service_info(hass) - def detection(device: BLEDevice, advertisement_data: AdvertisementData): - if device_filter(device, advertisement_data): - event.set() + for service_info in service_infos: + if device_filter(service_info.device, service_info.advertisement): + return True - async with BleakScanner( - detection_callback=detection, - filters={"DuplicateData": True}, - ): - try: - async with async_timeout.timeout(CONST_WAIT_TIME): - await event.wait() - except asyncio.TimeoutError: - return False - - return True + return False register_discovery_flow(DOMAIN, "Fjäråskupan", _async_has_devices) diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index 3ff6e599a6b..bf7956d297d 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -6,5 +6,12 @@ "requirements": ["fjaraskupan==1.0.2"], "codeowners": ["@elupus"], "iot_class": "local_polling", - "loggers": ["bleak", "fjaraskupan"] + "loggers": ["bleak", "fjaraskupan"], + "dependencies": ["bluetooth"], + "bluetooth": [ + { + "manufacturer_id": 20296, + "manufacturer_data_start": [79, 68, 70, 74, 65, 82] + } + ] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 2cbaebb6074..ef8193dad28 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,18 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, str | int | list[int]]] = [ + { + "domain": "fjaraskupan", + "manufacturer_id": 20296, + "manufacturer_data_start": [ + 79, + 68, + 70, + 74, + 65, + 82 + ] + }, { "domain": "govee_ble", "local_name": "Govee*" diff --git a/tests/components/fjaraskupan/__init__.py b/tests/components/fjaraskupan/__init__.py index 26a5ecd6605..35c69f98d65 100644 --- a/tests/components/fjaraskupan/__init__.py +++ b/tests/components/fjaraskupan/__init__.py @@ -1 +1,11 @@ """Tests for the Fjäråskupan integration.""" + + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import SOURCE_LOCAL, BluetoothServiceInfoBleak + +COOKER_SERVICE_INFO = BluetoothServiceInfoBleak.from_advertisement( + BLEDevice("1.1.1.1", "COOKERHOOD_FJAR"), AdvertisementData(), source=SOURCE_LOCAL +) diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index 4e06b2ad046..46ff5ae167a 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -1,47 +1,9 @@ """Standard fixtures for the Fjäråskupan integration.""" from __future__ import annotations -from unittest.mock import patch - -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData, BaseBleakScanner -from pytest import fixture +import pytest -@fixture(name="scanner", autouse=True) -def fixture_scanner(hass): - """Fixture for scanner.""" - - devices = [BLEDevice("1.1.1.1", "COOKERHOOD_FJAR")] - - class MockScanner(BaseBleakScanner): - """Mock Scanner.""" - - def __init__(self, *args, **kwargs) -> None: - """Initialize the scanner.""" - super().__init__( - detection_callback=kwargs.pop("detection_callback"), service_uuids=[] - ) - - async def start(self): - """Start scanning for devices.""" - for device in devices: - self._callback(device, AdvertisementData()) - - async def stop(self): - """Stop scanning for devices.""" - - @property - def discovered_devices(self) -> list[BLEDevice]: - """Return discovered devices.""" - return devices - - def set_scanning_filter(self, **kwargs): - """Set the scanning filter.""" - - with patch( - "homeassistant.components.fjaraskupan.config_flow.BleakScanner", new=MockScanner - ), patch( - "homeassistant.components.fjaraskupan.config_flow.CONST_WAIT_TIME", new=0.01 - ): - yield devices +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index a51b8c6f9fa..bef53e18073 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations from unittest.mock import patch -from bleak.backends.device import BLEDevice from pytest import fixture from homeassistant import config_entries @@ -11,6 +10,8 @@ from homeassistant.components.fjaraskupan.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import COOKER_SERVICE_INFO + @fixture(name="mock_setup_entry", autouse=True) async def fixture_mock_setup_entry(hass): @@ -24,31 +25,38 @@ async def fixture_mock_setup_entry(hass): async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[COOKER_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Fjäråskupan" - assert result["data"] == {} + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Fjäråskupan" + assert result["data"] == {} - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 -async def test_scan_no_devices(hass: HomeAssistant, scanner: list[BLEDevice]) -> None: +async def test_scan_no_devices(hass: HomeAssistant) -> None: """Test we get the form.""" - scanner.clear() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "no_devices_found" + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" From 1eb0983fba0116eeb2d18707daadad59b29422a0 Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Mon, 1 Aug 2022 08:06:28 -0700 Subject: [PATCH 083/903] Enable air quality sensor for Core300s (#75695) --- homeassistant/components/vesync/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index 45018c79ca0..c5a2e7182a9 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -73,8 +73,8 @@ def ha_dev_type(device): FILTER_LIFE_SUPPORTED = ["LV-PUR131S", "Core200S", "Core300S", "Core400S", "Core600S"] -AIR_QUALITY_SUPPORTED = ["LV-PUR131S", "Core400S", "Core600S"] -PM25_SUPPORTED = ["Core400S", "Core600S"] +AIR_QUALITY_SUPPORTED = ["LV-PUR131S", "Core300S", "Core400S", "Core600S"] +PM25_SUPPORTED = ["Core300S", "Core400S", "Core600S"] SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( VeSyncSensorEntityDescription( From 91384e07d0320b2252dee64d768e971da1425816 Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Mon, 1 Aug 2022 10:15:51 -0500 Subject: [PATCH 084/903] Add unique id for todoist calendar entity (#75674) Co-authored-by: Martin Hjelmare --- homeassistant/components/todoist/calendar.py | 1 + tests/components/todoist/test_calendar.py | 67 +++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index b1e15e42221..f1240f33b1b 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -296,6 +296,7 @@ class TodoistProjectEntity(CalendarEntity): ) self._cal_data = {} self._name = data[CONF_NAME] + self._attr_unique_id = data.get(CONF_ID) @property def event(self) -> CalendarEvent: diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 2f0832fe739..3458a7bf5d3 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -1,8 +1,15 @@ """Unit tests for the Todoist calendar platform.""" from datetime import datetime +from typing import Any +from unittest.mock import Mock, patch -from homeassistant.components.todoist.calendar import _parse_due_date +import pytest + +from homeassistant import setup +from homeassistant.components.todoist.calendar import DOMAIN, _parse_due_date from homeassistant.components.todoist.types import DueDate +from homeassistant.const import CONF_TOKEN +from homeassistant.helpers import entity_registry from homeassistant.util import dt @@ -42,3 +49,61 @@ def test_parse_due_date_without_timezone_uses_offset(): } actual = _parse_due_date(data, timezone_offset=-8) assert datetime(2022, 2, 2, 22, 0, 0, tzinfo=dt.UTC) == actual + + +@pytest.fixture(name="state") +def mock_state() -> dict[str, Any]: + """Mock the api state.""" + return { + "collaborators": [], + "labels": [{"name": "label1", "id": 1}], + "projects": [{"id": 12345, "name": "Name"}], + } + + +@patch("homeassistant.components.todoist.calendar.TodoistAPI") +async def test_calendar_entity_unique_id(todoist_api, hass, state): + """Test unique id is set to project id.""" + api = Mock(state=state) + todoist_api.return_value = api + assert await setup.async_setup_component( + hass, + "calendar", + { + "calendar": { + "platform": DOMAIN, + CONF_TOKEN: "token", + } + }, + ) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entity = registry.async_get("calendar.name") + assert 12345 == entity.unique_id + + +@patch("homeassistant.components.todoist.calendar.TodoistAPI") +async def test_calendar_custom_project_unique_id(todoist_api, hass, state): + """Test unique id is None for any custom projects.""" + api = Mock(state=state) + todoist_api.return_value = api + assert await setup.async_setup_component( + hass, + "calendar", + { + "calendar": { + "platform": DOMAIN, + CONF_TOKEN: "token", + "custom_projects": [{"name": "All projects"}], + } + }, + ) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entity = registry.async_get("calendar.all_projects") + assert entity is None + + state = hass.states.get("calendar.all_projects") + assert state.state == "off" From 7141c36f8b9d8249348d3573680c1825854c79e7 Mon Sep 17 00:00:00 2001 From: rhadamantys <46837767+rhadamantys@users.noreply.github.com> Date: Mon, 1 Aug 2022 17:42:47 +0200 Subject: [PATCH 085/903] Fix invalid enocean unique_id (#74508) Co-authored-by: Martin Hjelmare --- homeassistant/components/enocean/switch.py | 43 +++++++++++-- tests/components/enocean/test_switch.py | 73 ++++++++++++++++++++++ 2 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 tests/components/enocean/test_switch.py diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index a53f691df19..5edd2bb6155 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -5,12 +5,14 @@ from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_ID, CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import DOMAIN, LOGGER from .device import EnOceanEntity CONF_CHANNEL = "channel" @@ -25,10 +27,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +def generate_unique_id(dev_id: list[int], channel: int) -> str: + """Generate a valid unique id.""" + return f"{combine_hex(dev_id)}-{channel}" + + +def _migrate_to_new_unique_id(hass: HomeAssistant, dev_id, channel) -> None: + """Migrate old unique ids to new unique ids.""" + old_unique_id = f"{combine_hex(dev_id)}" + + ent_reg = entity_registry.async_get(hass) + entity_id = ent_reg.async_get_entity_id(Platform.SWITCH, DOMAIN, old_unique_id) + + if entity_id is not None: + new_unique_id = generate_unique_id(dev_id, channel) + try: + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + except ValueError: + LOGGER.warning( + "Skip migration of id [%s] to [%s] because it already exists", + old_unique_id, + new_unique_id, + ) + else: + LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) + + +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the EnOcean switch platform.""" @@ -36,7 +68,8 @@ def setup_platform( dev_id = config.get(CONF_ID) dev_name = config.get(CONF_NAME) - add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) + _migrate_to_new_unique_id(hass, dev_id, channel) + async_add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) class EnOceanSwitch(EnOceanEntity, SwitchEntity): @@ -49,7 +82,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): self._on_state = False self._on_state2 = False self.channel = channel - self._attr_unique_id = f"{combine_hex(dev_id)}" + self._attr_unique_id = generate_unique_id(dev_id, channel) @property def is_on(self): diff --git a/tests/components/enocean/test_switch.py b/tests/components/enocean/test_switch.py new file mode 100644 index 00000000000..a7aafa6fc73 --- /dev/null +++ b/tests/components/enocean/test_switch.py @@ -0,0 +1,73 @@ +"""Tests for the EnOcean switch platform.""" + +from enocean.utils import combine_hex + +from homeassistant.components.enocean import DOMAIN as ENOCEAN_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, assert_setup_component + +SWITCH_CONFIG = { + "switch": [ + { + "platform": ENOCEAN_DOMAIN, + "id": [0xDE, 0xAD, 0xBE, 0xEF], + "channel": 1, + "name": "room0", + }, + ] +} + + +async def test_unique_id_migration(hass: HomeAssistant) -> None: + """Test EnOcean switch ID migration.""" + + entity_name = SWITCH_CONFIG["switch"][0]["name"] + switch_entity_id = f"{SWITCH_DOMAIN}.{entity_name}" + dev_id = SWITCH_CONFIG["switch"][0]["id"] + channel = SWITCH_CONFIG["switch"][0]["channel"] + + ent_reg = er.async_get(hass) + + old_unique_id = f"{combine_hex(dev_id)}" + + entry = MockConfigEntry(domain=ENOCEAN_DOMAIN, data={"device": "/dev/null"}) + + entry.add_to_hass(hass) + + # Add a switch with an old unique_id to the entity registry + entity_entry = ent_reg.async_get_or_create( + SWITCH_DOMAIN, + ENOCEAN_DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=entry, + original_name=entity_name, + ) + + assert entity_entry.entity_id == switch_entity_id + assert entity_entry.unique_id == old_unique_id + + # Now add the sensor to check, whether the old unique_id is migrated + + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + SWITCH_CONFIG, + ) + + await hass.async_block_till_done() + + # Check that new entry has a new unique_id + entity_entry = ent_reg.async_get(switch_entity_id) + new_unique_id = f"{combine_hex(dev_id)}-{channel}" + + assert entity_entry.unique_id == new_unique_id + assert ( + ent_reg.async_get_entity_id(SWITCH_DOMAIN, ENOCEAN_DOMAIN, old_unique_id) + is None + ) From bd3de4452bb673c19f1e555b4566c5db032a598f Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 1 Aug 2022 11:43:07 -0400 Subject: [PATCH 086/903] Enhance logging for ZHA device trigger validation (#76036) * Enhance logging for ZHA device trigger validation * use IntegrationError --- homeassistant/components/zha/core/gateway.py | 4 +++- homeassistant/components/zha/core/helpers.py | 18 ++++++++++++++++-- homeassistant/components/zha/device_trigger.py | 4 ++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index f2fd226249b..14fbf2cf701 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -142,6 +142,7 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + self.initialized: bool = False async def async_initialize(self) -> None: """Initialize controller and connect radio.""" @@ -183,6 +184,7 @@ class ZHAGateway: self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) self.async_load_devices() self.async_load_groups() + self.initialized = True @callback def async_load_devices(self) -> None: @@ -217,7 +219,7 @@ class ZHAGateway: async def async_initialize_devices_and_entities(self) -> None: """Initialize devices and load entities.""" - _LOGGER.debug("Loading all devices") + _LOGGER.debug("Initializing all devices from Zigpy cache") await asyncio.gather( *(dev.async_initialize(from_cache=True) for dev in self.devices.values()) ) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b60f61b1e8e..7fd789ac3f5 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -26,6 +26,7 @@ import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback +from homeassistant.exceptions import IntegrationError from homeassistant.helpers import device_registry as dr from .const import ( @@ -42,6 +43,7 @@ if TYPE_CHECKING: from .gateway import ZHAGateway _T = TypeVar("_T") +_LOGGER = logging.getLogger(__name__) @dataclass @@ -170,10 +172,22 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) if not registry_device: + _LOGGER.error("Device id `%s` not found in registry", device_id) raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = zigpy.types.EUI64.convert(ieee_address) + if not zha_gateway.initialized: + _LOGGER.error("Attempting to get a ZHA device when ZHA is not initialized") + raise IntegrationError("ZHA is not initialized yet") + try: + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = zigpy.types.EUI64.convert(ieee_address) + except (IndexError, ValueError) as ex: + _LOGGER.error( + "Unable to determine device IEEE for device with device id `%s`", device_id + ) + raise KeyError( + f"Unable to determine device IEEE for device with device id `{device_id}`." + ) from ex return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 44682aaa559..cdd98110f83 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -13,7 +13,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, IntegrationError from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -39,7 +39,7 @@ async def async_validate_trigger_config( trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError) as err: + except (KeyError, AttributeError, IntegrationError) as err: raise InvalidDeviceAutomationConfig from err if ( zha_device.device_automation_triggers is None From fc399f21e9d81e3f1251d9b3887576564478f437 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Aug 2022 17:54:06 +0200 Subject: [PATCH 087/903] Guard imports for type hinting in Bluetooth (#75984) --- homeassistant/components/bluetooth/__init__.py | 12 ++++++++---- homeassistant/components/bluetooth/config_flow.py | 6 ++++-- homeassistant/components/bluetooth/match.py | 12 ++++++++---- homeassistant/components/bluetooth/models.py | 7 +++++-- .../bluetooth/passive_update_coordinator.py | 11 +++++++---- .../components/bluetooth/passive_update_processor.py | 12 ++++++++---- 6 files changed, 40 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 9adaac84333..d42f6b4f230 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,12 +8,10 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -from typing import Final +from typing import TYPE_CHECKING, Final import async_timeout from bleak import BleakError -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -27,7 +25,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo -from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_bluetooth from . import models @@ -42,6 +39,13 @@ from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .util import async_get_bluetooth_adapters +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + from homeassistant.helpers.typing import ConfigType + + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index bbba5f411b2..1a0be8706bf 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,18 +1,20 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol from homeassistant.components import onboarding from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN from .util import async_get_bluetooth_adapters +if TYPE_CHECKING: + from homeassistant.data_entry_flow import FlowResult + class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Bluetooth.""" diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 000f39eefd4..2cd4f62ae5e 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -1,17 +1,21 @@ """The bluetooth integration matchers.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass import fnmatch -from typing import Final, TypedDict +from typing import TYPE_CHECKING, Final, TypedDict -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from lru import LRU # pylint: disable=no-name-in-module from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional +if TYPE_CHECKING: + from collections.abc import Mapping + + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + MAX_REMEMBER_ADDRESSES: Final = 2048 diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 6f814c7b66b..51704a2f530 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -4,10 +4,9 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import Any, Final +from typing import TYPE_CHECKING, Any, Final from bleak import BleakScanner -from bleak.backends.device import BLEDevice from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, @@ -16,6 +15,10 @@ from bleak.backends.scanner import ( from homeassistant.core import CALLBACK_TYPE, callback as hass_callback +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + _LOGGER = logging.getLogger(__name__) FILTER_UUIDS: Final = "UUIDs" diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 31a6b065830..5c6b5b79509 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,16 +1,19 @@ """Passive update coordinator for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Generator -import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .update_coordinator import BasePassiveBluetoothCoordinator +if TYPE_CHECKING: + from collections.abc import Callable, Generator + import logging + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): """Class to manage passive bluetooth advertisements. diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 1f2047c02cb..78966d9b7ab 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -1,20 +1,24 @@ """Passive update processors for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Mapping import dataclasses import logging -from typing import Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator +if TYPE_CHECKING: + from collections.abc import Callable, Mapping + + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + @dataclasses.dataclass(frozen=True) class PassiveBluetoothEntityKey: From 567f181a21e60bf2d13ae08640be7adf3c05259c Mon Sep 17 00:00:00 2001 From: krazos Date: Mon, 1 Aug 2022 12:45:18 -0400 Subject: [PATCH 088/903] Fix capitalization of Sonos "Status light" entity name (#76035) Tweak capitalization of "Status light" entity name Tweak capitalization of "Status light" entity name for consistency with blog post guidance, which states that entity names should start with a capital letter, with the rest of the words lower case --- homeassistant/components/sonos/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 8dcf47fd0a2..a348b40cb0f 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -72,7 +72,7 @@ FRIENDLY_NAMES = { ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround music full volume", ATTR_NIGHT_SOUND: "Night sound", ATTR_SPEECH_ENHANCEMENT: "Speech enhancement", - ATTR_STATUS_LIGHT: "Status Light", + ATTR_STATUS_LIGHT: "Status light", ATTR_SUB_ENABLED: "Subwoofer enabled", ATTR_SURROUND_ENABLED: "Surround enabled", ATTR_TOUCH_CONTROLS: "Touch controls", From deff0ad61e8ed01a9ffc2e5c4e224af1fb444649 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Aug 2022 19:18:29 +0200 Subject: [PATCH 089/903] Implement generic in Deconz base device (#76015) * Make DevonzBase a generic * Adjust alarm_control_panel * Adjust binary_sensor * Adjust climate * More platforms * Adjust light * Ignore type-var * Add space * Implement recommendation * Use type: ignore[union-attr] * Revert "Use type: ignore[union-attr]" This reverts commit 983443062aab0a9c599b2750d823d0c5148c05ce. * Adjust assert * Adjust lock * Rename type variables * type: ignore[union-attr] * Formatting Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/deconz/alarm_control_panel.py | 3 +- .../components/deconz/binary_sensor.py | 3 +- homeassistant/components/deconz/climate.py | 3 +- homeassistant/components/deconz/cover.py | 3 +- .../components/deconz/deconz_device.py | 41 ++++++++++++------- homeassistant/components/deconz/fan.py | 3 +- homeassistant/components/deconz/light.py | 14 ++----- homeassistant/components/deconz/lock.py | 5 +-- homeassistant/components/deconz/number.py | 3 +- homeassistant/components/deconz/sensor.py | 3 +- homeassistant/components/deconz/siren.py | 3 +- homeassistant/components/deconz/switch.py | 3 +- 12 files changed, 41 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index bf0f39b75d0..e1fb0757b12 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -80,11 +80,10 @@ async def async_setup_entry( ) -class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): +class DeconzAlarmControlPanel(DeconzDevice[AncillaryControl], AlarmControlPanelEntity): """Representation of a deCONZ alarm control panel.""" TYPE = DOMAIN - _device: AncillaryControl _attr_code_format = CodeFormat.NUMBER _attr_supported_features = ( diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 814dec443e0..a7dbc2eacff 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -233,11 +233,10 @@ async def async_setup_entry( ) -class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): +class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): """Representation of a deCONZ binary sensor.""" TYPE = DOMAIN - _device: SensorResources entity_description: DeconzBinarySensorDescription def __init__( diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 75b37db2c13..e49918e14f2 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -92,11 +92,10 @@ async def async_setup_entry( ) -class DeconzThermostat(DeconzDevice, ClimateEntity): +class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): """Representation of a deCONZ thermostat.""" TYPE = DOMAIN - _device: Thermostat _attr_temperature_unit = TEMP_CELSIUS diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 3e56882f15a..8df974cf146 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -49,11 +49,10 @@ async def async_setup_entry( ) -class DeconzCover(DeconzDevice, CoverEntity): +class DeconzCover(DeconzDevice[Cover], CoverEntity): """Representation of a deCONZ cover.""" TYPE = DOMAIN - _device: Cover def __init__(self, cover_id: str, gateway: DeconzGateway) -> None: """Set up cover device.""" diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index e9be4658fb5..0ac7acf5b49 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -2,10 +2,13 @@ from __future__ import annotations -from pydeconz.models.group import Group as DeconzGroup -from pydeconz.models.light import LightBase as DeconzLight +from typing import Generic, TypeVar, Union + +from pydeconz.models.deconz_device import DeconzDevice as PydeconzDevice +from pydeconz.models.group import Group as PydeconzGroup +from pydeconz.models.light import LightBase as PydeconzLightBase from pydeconz.models.scene import Scene as PydeconzScene -from pydeconz.models.sensor import SensorBase as DeconzSensor +from pydeconz.models.sensor import SensorBase as PydeconzSensorBase from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -15,29 +18,39 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as DECONZ_DOMAIN from .gateway import DeconzGateway +_DeviceT = TypeVar( + "_DeviceT", + bound=Union[ + PydeconzGroup, + PydeconzLightBase, + PydeconzSensorBase, + PydeconzScene, + ], +) -class DeconzBase: + +class DeconzBase(Generic[_DeviceT]): """Common base for deconz entities and events.""" def __init__( self, - device: DeconzGroup | DeconzLight | DeconzSensor | PydeconzScene, + device: _DeviceT, gateway: DeconzGateway, ) -> None: """Set up device and add update callback to get data from websocket.""" - self._device = device + self._device: _DeviceT = device self.gateway = gateway @property def unique_id(self) -> str: """Return a unique identifier for this device.""" - assert not isinstance(self._device, PydeconzScene) + assert isinstance(self._device, PydeconzDevice) return self._device.unique_id @property def serial(self) -> str | None: """Return a serial number for this device.""" - assert not isinstance(self._device, PydeconzScene) + assert isinstance(self._device, PydeconzDevice) if not self._device.unique_id or self._device.unique_id.count(":") != 7: return None return self._device.unique_id.split("-", 1)[0] @@ -45,7 +58,7 @@ class DeconzBase: @property def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" - assert not isinstance(self._device, PydeconzScene) + assert isinstance(self._device, PydeconzDevice) if self.serial is None: return None @@ -60,7 +73,7 @@ class DeconzBase: ) -class DeconzDevice(DeconzBase, Entity): +class DeconzDevice(DeconzBase[_DeviceT], Entity): """Representation of a deCONZ device.""" _attr_should_poll = False @@ -69,7 +82,7 @@ class DeconzDevice(DeconzBase, Entity): def __init__( self, - device: DeconzGroup | DeconzLight | DeconzSensor | PydeconzScene, + device: _DeviceT, gateway: DeconzGateway, ) -> None: """Set up device and add update callback to get data from websocket.""" @@ -114,16 +127,14 @@ class DeconzDevice(DeconzBase, Entity): """Return True if device is available.""" if isinstance(self._device, PydeconzScene): return self.gateway.available - return self.gateway.available and self._device.reachable + return self.gateway.available and self._device.reachable # type: ignore[union-attr] -class DeconzSceneMixin(DeconzDevice): +class DeconzSceneMixin(DeconzDevice[PydeconzScene]): """Representation of a deCONZ scene.""" _attr_has_entity_name = True - _device: PydeconzScene - def __init__( self, device: PydeconzScene, diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 6002e61d326..a0d62126b92 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -49,11 +49,10 @@ async def async_setup_entry( ) -class DeconzFan(DeconzDevice, FanEntity): +class DeconzFan(DeconzDevice[Light], FanEntity): """Representation of a deCONZ fan.""" TYPE = DOMAIN - _device: Light _default_on_speed = LightFanSpeed.PERCENT_50 _attr_supported_features = FanEntityFeature.SET_SPEED diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 7c6f1a0e362..590c0795e65 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -1,7 +1,7 @@ """Support for deCONZ lights.""" from __future__ import annotations -from typing import Any, Generic, TypedDict, TypeVar +from typing import Any, TypedDict, TypeVar, Union from pydeconz.interfaces.groups import GroupHandler from pydeconz.interfaces.lights import LightHandler @@ -47,7 +47,7 @@ DECONZ_TO_COLOR_MODE = { LightColorMode.XY: ColorMode.XY, } -_L = TypeVar("_L", Group, Light) +_LightDeviceT = TypeVar("_LightDeviceT", bound=Union[Group, Light]) class SetStateAttributes(TypedDict, total=False): @@ -121,14 +121,12 @@ async def async_setup_entry( ) -class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): +class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity): """Representation of a deCONZ light.""" TYPE = DOMAIN - _device: _L - - def __init__(self, device: _L, gateway: DeconzGateway) -> None: + def __init__(self, device: _LightDeviceT, gateway: DeconzGateway) -> None: """Set up light.""" super().__init__(device, gateway) @@ -261,8 +259,6 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): class DeconzLight(DeconzBaseLight[Light]): """Representation of a deCONZ light.""" - _device: Light - @property def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" @@ -289,8 +285,6 @@ class DeconzGroup(DeconzBaseLight[Group]): _attr_has_entity_name = True - _device: Group - def __init__(self, device: Group, gateway: DeconzGateway) -> None: """Set up group and create an unique id.""" self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}" diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index d6f9d670c01..9c4c5b43dbe 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, Union from pydeconz.models.event import EventType from pydeconz.models.light.lock import Lock @@ -50,11 +50,10 @@ async def async_setup_entry( ) -class DeconzLock(DeconzDevice, LockEntity): +class DeconzLock(DeconzDevice[Union[DoorLock, Lock]], LockEntity): """Representation of a deCONZ lock.""" TYPE = DOMAIN - _device: DoorLock | Lock @property def is_locked(self) -> bool: diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 4c0959f950d..636711d609d 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -81,11 +81,10 @@ async def async_setup_entry( ) -class DeconzNumber(DeconzDevice, NumberEntity): +class DeconzNumber(DeconzDevice[Presence], NumberEntity): """Representation of a deCONZ number entity.""" TYPE = DOMAIN - _device: Presence def __init__( self, diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 15af1b3dd8f..941729ac8c2 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -273,11 +273,10 @@ async def async_setup_entry( ) -class DeconzSensor(DeconzDevice, SensorEntity): +class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): """Representation of a deCONZ sensor.""" TYPE = DOMAIN - _device: SensorResources entity_description: DeconzSensorDescription def __init__( diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index d44bce01aad..45c81c9e31c 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -41,7 +41,7 @@ async def async_setup_entry( ) -class DeconzSiren(DeconzDevice, SirenEntity): +class DeconzSiren(DeconzDevice[Siren], SirenEntity): """Representation of a deCONZ siren.""" TYPE = DOMAIN @@ -50,7 +50,6 @@ class DeconzSiren(DeconzDevice, SirenEntity): | SirenEntityFeature.TURN_OFF | SirenEntityFeature.DURATION ) - _device: Siren @property def is_on(self) -> bool: diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index b21ec929909..990de24dffc 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -43,11 +43,10 @@ async def async_setup_entry( ) -class DeconzPowerPlug(DeconzDevice, SwitchEntity): +class DeconzPowerPlug(DeconzDevice[Light], SwitchEntity): """Representation of a deCONZ power plug.""" TYPE = DOMAIN - _device: Light @property def is_on(self) -> bool: From 27ed3d324f35b0c04b7d604667bc3ccfc1373a8b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Aug 2022 19:34:06 +0200 Subject: [PATCH 090/903] Replace object with enum for pylint sentinel (#76030) * Replace object with enum for pylint sentinel * Use standard enum --- pylint/plugins/hass_enforce_type_hints.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index ac21a9bf686..551db458b1d 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from enum import Enum import re from astroid import nodes @@ -10,8 +11,13 @@ from pylint.lint import PyLinter from homeassistant.const import Platform -DEVICE_CLASS = object() -UNDEFINED = object() + +class _Special(Enum): + """Sentinel values""" + + UNDEFINED = 1 + DEVICE_CLASS = 2 + _PLATFORMS: set[str] = {platform.value for platform in Platform} @@ -21,7 +27,7 @@ class TypeHintMatch: """Class for pattern matching.""" function_name: str - return_type: list[str] | str | None | object + return_type: list[str | _Special | None] | str | _Special | None arg_types: dict[int, str] | None = None """arg_types is for positional arguments""" named_arg_types: dict[str, str] | None = None @@ -361,7 +367,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", 1: "ConfigEntry", }, - return_type=UNDEFINED, + return_type=_Special.UNDEFINED, ), TypeHintMatch( function_name="async_get_device_diagnostics", @@ -370,7 +376,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigEntry", 2: "DeviceEntry", }, - return_type=UNDEFINED, + return_type=_Special.UNDEFINED, ), ], } @@ -499,7 +505,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ ), TypeHintMatch( function_name="device_class", - return_type=[DEVICE_CLASS, "str", None], + return_type=[_Special.DEVICE_CLASS, "str", None], ), TypeHintMatch( function_name="unit_of_measurement", @@ -1407,11 +1413,11 @@ def _is_valid_type( in_return: bool = False, ) -> bool: """Check the argument node against the expected type.""" - if expected_type is UNDEFINED: + if expected_type is _Special.UNDEFINED: return True # Special case for device_class - if expected_type == DEVICE_CLASS and in_return: + if expected_type is _Special.DEVICE_CLASS and in_return: return ( isinstance(node, nodes.Name) and node.name.endswith("DeviceClass") From 652a8e9e8af6b6c2a39f89f51c64f8ca0e64db3a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 1 Aug 2022 22:04:16 +0100 Subject: [PATCH 091/903] Add reauth flow to xiaomi_ble, fixes problem adding LYWSD03MMC (#76028) --- .../components/xiaomi_ble/config_flow.py | 136 +++++--- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 45 ++- .../components/xiaomi_ble/strings.json | 4 + .../xiaomi_ble/translations/en.json | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/xiaomi_ble/test_config_flow.py | 293 +++++++++++++++++- 8 files changed, 439 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index aa1ffc24895..092c60e9713 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import dataclasses from typing import Any @@ -12,7 +13,7 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothScanningMode, - BluetoothServiceInfoBleak, + BluetoothServiceInfo, async_discovered_service_info, async_process_advertisements, ) @@ -31,11 +32,11 @@ class Discovery: """A discovered bluetooth device.""" title: str - discovery_info: BluetoothServiceInfoBleak + discovery_info: BluetoothServiceInfo device: DeviceData -def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str: +def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name @@ -46,19 +47,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovery_info: BluetoothServiceInfo | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} async def _async_wait_for_full_advertisement( - self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData - ) -> BluetoothServiceInfoBleak: + self, discovery_info: BluetoothServiceInfo, device: DeviceData + ) -> BluetoothServiceInfo: """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" if not device.pending: return discovery_info def _process_more_advertisements( - service_info: BluetoothServiceInfoBleak, + service_info: BluetoothServiceInfo, ) -> bool: device.update(service_info) return not device.pending @@ -72,7 +73,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfoBleak + self, discovery_info: BluetoothServiceInfo ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) @@ -81,20 +82,21 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if not device.supported(discovery_info): return self.async_abort(reason="not_supported") + title = _title(discovery_info, device) + self.context["title_placeholders"] = {"name": title} + + self._discovered_device = device + # Wait until we have received enough information about this device to detect its encryption type try: - discovery_info = await self._async_wait_for_full_advertisement( + self._discovery_info = await self._async_wait_for_full_advertisement( discovery_info, device ) except asyncio.TimeoutError: - # If we don't see a valid packet within the timeout then this device is not supported. - return self.async_abort(reason="not_supported") - - self._discovery_info = discovery_info - self._discovered_device = device - - title = _title(discovery_info, device) - self.context["title_placeholders"] = {"name": title} + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: return await self.async_step_get_encryption_key_legacy() @@ -107,6 +109,8 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Enter a legacy bindkey for a v2/v3 MiBeacon device.""" assert self._discovery_info + assert self._discovered_device + errors = {} if user_input is not None: @@ -115,18 +119,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if len(bindkey) != 24: errors["bindkey"] = "expected_24_characters" else: - device = DeviceData(bindkey=bytes.fromhex(bindkey)) + self._discovered_device.bindkey = bytes.fromhex(bindkey) # If we got this far we already know supported will # return true so we don't bother checking that again # We just want to retry the decryption - device.supported(self._discovery_info) + self._discovered_device.supported(self._discovery_info) - if device.bindkey_verified: - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={"bindkey": bindkey}, - ) + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) errors["bindkey"] = "decryption_failed" @@ -142,6 +143,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Enter a bindkey for a v4/v5 MiBeacon device.""" assert self._discovery_info + assert self._discovered_device errors = {} @@ -151,18 +153,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if len(bindkey) != 32: errors["bindkey"] = "expected_32_characters" else: - device = DeviceData(bindkey=bytes.fromhex(bindkey)) + self._discovered_device.bindkey = bytes.fromhex(bindkey) # If we got this far we already know supported will # return true so we don't bother checking that again # We just want to retry the decryption - device.supported(self._discovery_info) + self._discovered_device.supported(self._discovery_info) - if device.bindkey_verified: - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={"bindkey": bindkey}, - ) + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) errors["bindkey"] = "decryption_failed" @@ -178,10 +177,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={}, - ) + return self._async_get_or_create_entry() self._set_confirm_only() return self.async_show_form( @@ -189,6 +185,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders=self.context["title_placeholders"], ) + async def async_step_confirm_slow( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ack that device is slow.""" + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + return self._async_get_or_create_entry() + + self._set_confirm_only() + return self.async_show_form( + step_id="confirm_slow", + description_placeholders=self.context["title_placeholders"], + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -198,24 +207,28 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) discovery = self._discovered_devices[address] + self.context["title_placeholders"] = {"name": discovery.title} + # Wait until we have received enough information about this device to detect its encryption type try: self._discovery_info = await self._async_wait_for_full_advertisement( discovery.discovery_info, discovery.device ) except asyncio.TimeoutError: - # If we don't see a valid packet within the timeout then this device is not supported. - return self.async_abort(reason="not_supported") + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() + + self._discovered_device = discovery.device if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: - self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_legacy() if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: - self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_4_5() - return self.async_create_entry(title=discovery.title, data={}) + return self._async_get_or_create_entry() current_addresses = self._async_current_ids() for discovery_info in async_discovered_service_info(self.hass): @@ -241,3 +254,46 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a flow initialized by a reauth event.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry is not None + + device: DeviceData = self.context["device"] + self._discovered_device = device + + self._discovery_info = device.last_service_info + + if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + return await self.async_step_get_encryption_key_legacy() + + if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + return await self.async_step_get_encryption_key_4_5() + + # Otherwise there wasn't actually encryption so abort + return self.async_abort(reason="reauth_successful") + + def _async_get_or_create_entry(self, bindkey=None): + data = {} + + if bindkey: + data["bindkey"] = bindkey + + if entry_id := self.context.get("entry_id"): + entry = self.hass.config_entries.async_get_entry(entry_id) + assert entry is not None + + self.hass.config_entries.async_update_entry(entry, data=data) + + # Reload the config entry to notify of updated config + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data=data, + ) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 0d97dcbedf8..a901439b2c9 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.2"], + "requirements": ["xiaomi-ble==0.6.4"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index a96722620a9..b3cd5126967 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -11,8 +11,10 @@ from xiaomi_ble import ( Units, XiaomiBluetoothDeviceData, ) +from xiaomi_ble.parser import EncryptionScheme from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, @@ -163,6 +165,45 @@ def sensor_update_to_bluetooth_data_update( ) +def process_service_info( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + data: XiaomiBluetoothDeviceData, + service_info: BluetoothServiceInfoBleak, +) -> PassiveBluetoothDataUpdate: + """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + update = data.update(service_info) + + # If device isn't pending we know it has seen at least one broadcast with a payload + # If that payload was encrypted and the bindkey was not verified then we need to reauth + if ( + not data.pending + and data.encryption_scheme != EncryptionScheme.NONE + and not data.bindkey_verified + ): + flow_context = { + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + "device": data, + } + + for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): + if flow["context"] == flow_context: + break + else: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context=flow_context, + data=entry.data, + ) + ) + + return sensor_update_to_bluetooth_data_update(update) + + async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, @@ -177,9 +218,7 @@ async def async_setup_entry( kwargs["bindkey"] = bytes.fromhex(bindkey) data = XiaomiBluetoothDeviceData(**kwargs) processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) + lambda service_info: process_service_info(hass, entry, data, service_info) ) entry.async_on_unload( processor.async_add_entities_listener( diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index e12a15a0671..48d5c3a87f7 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -11,6 +11,9 @@ "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + }, "get_encryption_key_legacy": { "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", "data": { @@ -25,6 +28,7 @@ } }, "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index b9f4e024f92..836ccc51637 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -6,7 +6,8 @@ "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", - "no_devices_found": "No devices found on the network" + "no_devices_found": "No devices found on the network", + "reauth_successful": "Re-authentication was successful" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + }, "user": { "data": { "address": "Device" diff --git a/requirements_all.txt b/requirements_all.txt index e3dfbab9e19..5a56eb1b3b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2473,7 +2473,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.2 +xiaomi-ble==0.6.4 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6653d8f599..110f0446b0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.2 +xiaomi-ble==0.6.4 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 86fda21aaa0..0d123f0cd54 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -3,7 +3,10 @@ import asyncio from unittest.mock import patch +from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData + from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.data_entry_flow import FlowResultType @@ -52,8 +55,19 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): context={"source": config_entries.SOURCE_BLUETOOTH}, data=MISSING_PAYLOAD_ENCRYPTED, ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "not_supported" + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): @@ -318,6 +332,24 @@ async def test_async_step_user_no_devices_found(hass): assert result["reason"] == "no_devices_found" +async def test_async_step_user_no_devices_found_2(hass): + """ + Test setup from service info cache with no devices found. + + This variant tests with a non-Xiaomi device known to us. + """ + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[NOT_SENSOR_PUSH_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + async def test_async_step_user_with_found_devices(hass): """Test setup from service info cache with devices found.""" with patch( @@ -363,8 +395,19 @@ async def test_async_step_user_short_payload(hass): result["flow_id"], user_input={"address": "A4:C1:38:56:53:84"}, ) - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "not_supported" + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "LYWSD02MMC" + assert result3["data"] == {} + assert result3["result"].unique_id == "A4:C1:38:56:53:84" async def test_async_step_user_short_payload_then_full(hass): @@ -755,3 +798,245 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): # Verify the original one was aborted assert not hass.config_entries.flow.async_progress(DOMAIN) + + +async def test_async_step_reauth_legacy(hass): + """Test reauth with a legacy key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_legacy_wrong_key(hass): + """Test reauth with a bad legacy key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b85307515a487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4(hass): + """Test reauth with a v4 key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4_wrong_key(hass): + """Test reauth for v4 with a bad key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_abort_early(hass): + """ + Test we can abort the reauth if there is no encryption. + + (This can't currently happen in practice). + """ + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + + device = DeviceData() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + "device": device, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" From 3eafe13085444a5f29b57c3a43740eff5be36f35 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Aug 2022 00:03:52 +0200 Subject: [PATCH 092/903] Improve UI in pylint plugin (#74157) * Adjust FlowResult result type * Adjust tests * Adjust return_type * Use StrEnum for base device_class * Add test for device_class * Add and use SentinelValues.DEVICE_CLASS * Remove duplicate device_class * Cleanup return-type * Drop inheritance check from device_class * Add caching for class methods * Improve tests * Adjust duplicate checks * Adjust tests * Fix rebase --- pylint/plugins/hass_enforce_type_hints.py | 29 ++--- tests/pylint/test_enforce_type_hints.py | 131 +++++++++++++++++----- 2 files changed, 116 insertions(+), 44 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 551db458b1d..d0d20cedd7c 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -16,7 +16,6 @@ class _Special(Enum): """Sentinel values""" UNDEFINED = 1 - DEVICE_CLASS = 2 _PLATFORMS: set[str] = {platform.value for platform in Platform} @@ -466,6 +465,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { } # Overriding properties and functions are normally checked by mypy, and will only # be checked by pylint when --ignore-missing-annotations is False + _ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="should_poll", @@ -505,7 +505,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ ), TypeHintMatch( function_name="device_class", - return_type=[_Special.DEVICE_CLASS, "str", None], + return_type=["str", None], ), TypeHintMatch( function_name="unit_of_measurement", @@ -1416,15 +1416,6 @@ def _is_valid_type( if expected_type is _Special.UNDEFINED: return True - # Special case for device_class - if expected_type is _Special.DEVICE_CLASS and in_return: - return ( - isinstance(node, nodes.Name) - and node.name.endswith("DeviceClass") - or isinstance(node, nodes.Attribute) - and node.attrname.endswith("DeviceClass") - ) - if isinstance(expected_type, list): for expected_type_item in expected_type: if _is_valid_type(expected_type_item, node, in_return): @@ -1636,18 +1627,28 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] def visit_classdef(self, node: nodes.ClassDef) -> None: """Called when a ClassDef node is visited.""" ancestor: nodes.ClassDef + checked_class_methods: set[str] = set() for ancestor in node.ancestors(): for class_matches in self._class_matchers: if ancestor.name == class_matches.base_class: - self._visit_class_functions(node, class_matches.matches) + self._visit_class_functions( + node, class_matches.matches, checked_class_methods + ) def _visit_class_functions( - self, node: nodes.ClassDef, matches: list[TypeHintMatch] + self, + node: nodes.ClassDef, + matches: list[TypeHintMatch], + checked_class_methods: set[str], ) -> None: + cached_methods: list[nodes.FunctionDef] = list(node.mymethods()) for match in matches: - for function_node in node.mymethods(): + for function_node in cached_methods: + if function_node.name in checked_class_methods: + continue if match.need_to_check_function(function_node): self._check_function(function_node, match) + checked_class_methods.add(function_node.name) def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Called when a FunctionDef node is visited.""" diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 53c17880716..d9edde9fdee 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -307,7 +307,10 @@ def test_invalid_config_flow_step( """Ensure invalid hints are rejected for ConfigFlow step.""" class_node, func_node, arg_node = astroid.extract_node( """ - class ConfigFlow(): + class FlowHandler(): + pass + + class ConfigFlow(FlowHandler): pass class AxisFlowHandler( #@ @@ -329,18 +332,18 @@ def test_invalid_config_flow_step( msg_id="hass-argument-type", node=arg_node, args=(2, "ZeroconfServiceInfo", "async_step_zeroconf"), - line=10, + line=13, col_offset=8, - end_line=10, + end_line=13, end_col_offset=27, ), pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, args=("FlowResult", "async_step_zeroconf"), - line=8, + line=11, col_offset=4, - end_line=8, + end_line=11, end_col_offset=33, ), ): @@ -353,7 +356,10 @@ def test_valid_config_flow_step( """Ensure valid hints are accepted for ConfigFlow step.""" class_node = astroid.extract_node( """ - class ConfigFlow(): + class FlowHandler(): + pass + + class ConfigFlow(FlowHandler): pass class AxisFlowHandler( #@ @@ -377,9 +383,16 @@ def test_invalid_config_flow_async_get_options_flow( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Ensure invalid hints are rejected for ConfigFlow async_get_options_flow.""" + # AxisOptionsFlow doesn't inherit OptionsFlow, and therefore should fail class_node, func_node, arg_node = astroid.extract_node( """ - class ConfigFlow(): + class FlowHandler(): + pass + + class ConfigFlow(FlowHandler): + pass + + class OptionsFlow(FlowHandler): pass class AxisOptionsFlow(): @@ -403,18 +416,18 @@ def test_invalid_config_flow_async_get_options_flow( msg_id="hass-argument-type", node=arg_node, args=(1, "ConfigEntry", "async_get_options_flow"), - line=12, + line=18, col_offset=8, - end_line=12, + end_line=18, end_col_offset=20, ), pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, args=("OptionsFlow", "async_get_options_flow"), - line=11, + line=17, col_offset=4, - end_line=11, + end_line=17, end_col_offset=30, ), ): @@ -427,10 +440,13 @@ def test_valid_config_flow_async_get_options_flow( """Ensure valid hints are accepted for ConfigFlow async_get_options_flow.""" class_node = astroid.extract_node( """ - class ConfigFlow(): + class FlowHandler(): pass - class OptionsFlow(): + class ConfigFlow(FlowHandler): + pass + + class OptionsFlow(FlowHandler): pass class AxisOptionsFlow(OptionsFlow): @@ -467,7 +483,10 @@ def test_invalid_entity_properties( class_node, prop_node, func_node = astroid.extract_node( """ - class LockEntity(): + class Entity(): + pass + + class LockEntity(Entity): pass class DoorLock( #@ @@ -495,27 +514,27 @@ def test_invalid_entity_properties( msg_id="hass-return-type", node=prop_node, args=(["str", None], "changed_by"), - line=9, + line=12, col_offset=4, - end_line=9, + end_line=12, end_col_offset=18, ), pylint.testutils.MessageTest( msg_id="hass-argument-type", node=func_node, args=("kwargs", "Any", "async_lock"), - line=14, + line=17, col_offset=4, - end_line=14, + end_line=17, end_col_offset=24, ), pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, args=("None", "async_lock"), - line=14, + line=17, col_offset=4, - end_line=14, + end_line=17, end_col_offset=24, ), ): @@ -531,7 +550,10 @@ def test_ignore_invalid_entity_properties( class_node = astroid.extract_node( """ - class LockEntity(): + class Entity(): + pass + + class LockEntity(Entity): pass class DoorLock( #@ @@ -566,7 +588,13 @@ def test_named_arguments( class_node, func_node, percentage_node, preset_mode_node = astroid.extract_node( """ - class FanEntity(): + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class FanEntity(ToggleEntity): pass class MyFan( #@ @@ -591,36 +619,36 @@ def test_named_arguments( msg_id="hass-argument-type", node=percentage_node, args=("percentage", "int | None", "async_turn_on"), - line=10, + line=16, col_offset=8, - end_line=10, + end_line=16, end_col_offset=18, ), pylint.testutils.MessageTest( msg_id="hass-argument-type", node=preset_mode_node, args=("preset_mode", "str | None", "async_turn_on"), - line=12, + line=18, col_offset=8, - end_line=12, + end_line=18, end_col_offset=24, ), pylint.testutils.MessageTest( msg_id="hass-argument-type", node=func_node, args=("kwargs", "Any", "async_turn_on"), - line=8, + line=14, col_offset=4, - end_line=8, + end_line=14, end_col_offset=27, ), pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, args=("None", "async_turn_on"), - line=8, + line=14, col_offset=4, - end_line=8, + end_line=14, end_col_offset=27, ), ): @@ -829,3 +857,46 @@ def test_invalid_long_tuple( ), ): type_hint_checker.visit_classdef(class_node) + + +def test_invalid_device_class( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure invalid hints are rejected for entity device_class.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, prop_node = astroid.extract_node( + """ + class Entity(): + pass + + class CoverEntity(Entity): + pass + + class MyCover( #@ + CoverEntity + ): + @property + def device_class( #@ + self + ): + pass + """, + "homeassistant.components.pylint_test.cover", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=prop_node, + args=(["CoverDeviceClass", "str", None], "device_class"), + line=12, + col_offset=4, + end_line=12, + end_col_offset=20, + ), + ): + type_hint_checker.visit_classdef(class_node) From 81e3ef03f7e980e7540bc29e9edbecb635e85ad9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 2 Aug 2022 00:27:42 +0000 Subject: [PATCH 093/903] [ci skip] Translation update --- .../components/abode/translations/sv.json | 16 +++- .../accuweather/translations/sensor.sv.json | 3 +- .../accuweather/translations/sv.json | 29 ++++++- .../components/acmeda/translations/sv.json | 12 +++ .../components/adax/translations/sv.json | 10 +++ .../components/adguard/translations/sv.json | 6 ++ .../components/aemet/translations/sv.json | 21 +++++- .../components/agent_dvr/translations/sv.json | 3 +- .../components/airnow/translations/sv.json | 7 ++ .../components/airthings/translations/sv.json | 21 ++++++ .../components/airvisual/translations/sv.json | 22 ++++++ .../alarm_control_panel/translations/sv.json | 8 ++ .../alarmdecoder/translations/sv.json | 50 ++++++++++++- .../components/almond/translations/sv.json | 3 +- .../components/ambee/translations/hu.json | 6 ++ .../components/ambee/translations/no.json | 6 ++ .../ambee/translations/sensor.sv.json | 9 +++ .../components/ambee/translations/sv.json | 6 +- .../components/anthemav/translations/hu.json | 6 ++ .../components/anthemav/translations/no.json | 6 ++ .../components/arcam_fmj/translations/sv.json | 24 ++++++ .../components/asuswrt/translations/sv.json | 2 + .../components/atag/translations/sv.json | 4 + .../components/august/translations/sv.json | 1 + .../components/aurora/translations/sv.json | 17 ++++- .../components/awair/translations/sv.json | 19 ++++- .../components/axis/translations/sv.json | 14 +++- .../azure_devops/translations/sv.json | 23 +++++- .../components/baf/translations/sv.json | 5 ++ .../binary_sensor/translations/sv.json | 3 + .../components/blebox/translations/sv.json | 14 +++- .../components/blink/translations/sv.json | 29 ++++++- .../components/bluetooth/translations/no.json | 32 ++++++++ .../components/bond/translations/sv.json | 16 +++- .../components/bosch_shc/translations/sv.json | 37 +++++++++ .../components/braviatv/translations/sv.json | 18 ++++- .../components/broadlink/translations/sv.json | 23 +++++- .../components/brother/translations/sv.json | 1 + .../components/bsblan/translations/sv.json | 7 ++ .../buienradar/translations/sv.json | 29 +++++++ .../components/cast/translations/sv.json | 13 ++++ .../cert_expiry/translations/sv.json | 3 +- .../components/co2signal/translations/sv.json | 29 +++++++ .../components/coinbase/translations/sv.json | 24 +++++- .../components/control4/translations/sv.json | 17 +++++ .../coolmaster/translations/sv.json | 1 + .../coronavirus/translations/sv.json | 3 +- .../components/cover/translations/sv.json | 7 +- .../crownstone/translations/sv.json | 75 +++++++++++++++++++ .../components/daikin/translations/sv.json | 7 +- .../components/deconz/translations/sv.json | 8 ++ .../components/demo/translations/no.json | 21 ++++++ .../components/demo/translations/sv.json | 1 + .../components/denonavr/translations/sv.json | 37 +++++++++ .../device_tracker/translations/sv.json | 4 + .../devolo_home_control/translations/sv.json | 3 + .../components/dexcom/translations/sv.json | 4 + .../dialogflow/translations/sv.json | 1 + .../components/directv/translations/sv.json | 2 + .../components/doorbird/translations/sv.json | 21 +++++- .../components/dsmr/translations/sv.json | 17 +++++ .../components/dunehd/translations/sv.json | 20 +++++ .../components/elgato/translations/sv.json | 6 +- .../components/elkm1/translations/sv.json | 10 +++ .../components/emonitor/translations/sv.json | 12 +++ .../emulated_roku/translations/sv.json | 3 + .../components/enocean/translations/sv.json | 25 +++++++ .../enphase_envoy/translations/sv.json | 6 +- .../components/epson/translations/sv.json | 8 ++ .../components/firmata/translations/sv.json | 7 ++ .../flick_electric/translations/sv.json | 13 +++- .../components/flipr/translations/sv.json | 29 +++++++ .../components/flo/translations/sv.json | 6 ++ .../components/flume/translations/sv.json | 10 ++- .../flunearyou/translations/sv.json | 4 +- .../forecast_solar/translations/sv.json | 19 ++++- .../forked_daapd/translations/sv.json | 24 +++++- .../components/foscam/translations/sv.json | 11 +++ .../components/freebox/translations/sv.json | 3 + .../freedompro/translations/sv.json | 9 ++- .../components/fritz/translations/sv.json | 16 +++- .../components/fritzbox/translations/sv.json | 11 ++- .../fritzbox_callmonitor/translations/sv.json | 30 ++++++++ .../components/generic/translations/cs.json | 3 +- .../components/generic/translations/no.json | 1 + .../components/generic/translations/sv.json | 34 +++++++-- .../components/geofency/translations/sv.json | 4 +- .../components/gios/translations/sv.json | 5 ++ .../components/gogogate2/translations/sv.json | 13 +++- .../components/google/translations/hu.json | 10 +++ .../components/google/translations/no.json | 13 +++- .../google_travel_time/translations/sv.json | 17 ++++- .../components/govee_ble/translations/no.json | 15 ++++ .../components/gpslogger/translations/sv.json | 4 + .../components/gree/translations/sv.json | 13 ++++ .../growatt_server/translations/sv.json | 21 +++++- .../components/guardian/translations/sv.json | 21 ++++++ .../components/habitica/translations/sv.json | 12 ++- .../components/harmony/translations/sv.json | 14 +++- .../components/hassio/translations/sv.json | 14 +++- .../here_travel_time/translations/sv.json | 3 +- .../components/hive/translations/hu.json | 2 +- .../components/hive/translations/sv.json | 36 ++++++++- .../components/hlk_sw16/translations/sv.json | 12 ++- .../home_connect/translations/sv.json | 16 ++++ .../home_plus_control/translations/sv.json | 11 +++ .../homeassistant/translations/sv.json | 4 +- .../homeassistant_alerts/translations/hu.json | 8 ++ .../components/homekit/translations/sv.json | 19 +++++ .../homekit_controller/translations/hu.json | 4 +- .../homekit_controller/translations/sv.json | 17 +++++ .../huawei_lte/translations/sv.json | 9 ++- .../components/hue/translations/sv.json | 15 +++- .../humidifier/translations/sv.json | 20 ++++- .../hvv_departures/translations/sv.json | 7 +- .../components/hyperion/translations/sv.json | 53 +++++++++++++ .../components/ialarm/translations/sv.json | 19 +++++ .../components/icloud/translations/sv.json | 3 +- .../components/ifttt/translations/sv.json | 4 + .../components/inkbird/translations/no.json | 20 +++++ .../components/insteon/translations/sv.json | 41 ++++++++++ .../integration/translations/sv.json | 3 +- .../components/iotawatt/translations/sv.json | 4 + .../components/ipma/translations/sv.json | 5 ++ .../components/ipp/translations/sv.json | 5 +- .../islamic_prayer_times/translations/sv.json | 7 ++ .../components/isy994/translations/sv.json | 32 +++++++- .../keenetic_ndms2/translations/sv.json | 33 ++++++++ .../components/kmtronic/translations/sv.json | 9 +++ .../components/knx/translations/sv.json | 4 +- .../components/konnected/translations/sv.json | 6 +- .../components/kulersky/translations/sv.json | 13 ++++ .../lacrosse_view/translations/no.json | 20 +++++ .../components/life360/translations/hu.json | 2 +- .../components/life360/translations/sv.json | 5 +- .../components/lifx/translations/no.json | 20 +++++ .../components/light/translations/sv.json | 1 + .../components/locative/translations/sv.json | 4 + .../components/lovelace/translations/sv.json | 3 +- .../lutron_caseta/translations/sv.json | 36 +++++++++ .../components/lyric/translations/hu.json | 6 ++ .../components/lyric/translations/no.json | 6 ++ .../components/lyric/translations/sv.json | 19 +++++ .../components/mailgun/translations/sv.json | 4 + .../components/mazda/translations/sv.json | 12 +++ .../media_player/translations/cs.json | 1 + .../media_player/translations/sv.json | 4 +- .../met_eireann/translations/sv.json | 3 + .../meteo_france/translations/sv.json | 15 +++- .../components/miflora/translations/hu.json | 8 ++ .../components/miflora/translations/no.json | 8 ++ .../components/mill/translations/sv.json | 3 + .../components/mitemp_bt/translations/hu.json | 8 ++ .../components/mitemp_bt/translations/no.json | 8 ++ .../components/mjpeg/translations/cs.json | 1 + .../components/moat/translations/no.json | 10 +++ .../mobile_app/translations/sv.json | 3 +- .../modem_callerid/translations/sv.json | 22 ++++++ .../components/monoprice/translations/sv.json | 23 +++++- .../motion_blinds/translations/sv.json | 12 ++- .../components/motioneye/translations/sv.json | 10 +++ .../components/mqtt/translations/sv.json | 23 +++++- .../components/mutesync/translations/sv.json | 16 ++++ .../components/myq/translations/sv.json | 10 ++- .../components/mysensors/translations/sv.json | 32 +++++++- .../components/nanoleaf/translations/sv.json | 23 +++++- .../components/neato/translations/sv.json | 14 +++- .../components/nest/translations/sv.json | 11 ++- .../components/netatmo/translations/sv.json | 36 +++++++++ .../components/nexia/translations/sv.json | 3 + .../nfandroidtv/translations/sv.json | 19 +++++ .../nightscout/translations/sv.json | 18 +++++ .../nmap_tracker/translations/sv.json | 12 +++ .../components/notion/translations/sv.json | 13 +++- .../components/nuheat/translations/sv.json | 6 +- .../components/nuki/translations/sv.json | 12 +++ .../components/nut/translations/sv.json | 3 +- .../components/nws/translations/sv.json | 7 +- .../components/nzbget/translations/sv.json | 26 ++++++- .../components/omnilogic/translations/sv.json | 9 +++ .../components/onvif/translations/sv.json | 41 +++++++++- .../openalpr_local/translations/hu.json | 8 ++ .../opengarage/translations/sv.json | 13 +++- .../opentherm_gw/translations/el.json | 3 +- .../opentherm_gw/translations/sv.json | 5 +- .../components/openuv/translations/sv.json | 14 ++++ .../openweathermap/translations/sv.json | 8 +- .../ovo_energy/translations/sv.json | 5 ++ .../components/owntracks/translations/sv.json | 3 + .../panasonic_viera/translations/sv.json | 4 + .../philips_js/translations/sv.json | 18 ++++- .../components/pi_hole/translations/sv.json | 13 +++- .../components/picnic/translations/sv.json | 7 ++ .../components/plaato/translations/sv.json | 10 +++ .../components/plex/translations/sv.json | 10 ++- .../components/plugwise/translations/sv.json | 29 ++++++- .../components/powerwall/translations/sv.json | 7 +- .../progettihwsw/translations/sv.json | 22 ++++++ .../pvpc_hourly_pricing/translations/sv.json | 15 ++++ .../components/qnap_qsw/translations/sv.json | 1 + .../components/rachio/translations/sv.json | 10 +++ .../radiotherm/translations/hu.json | 2 +- .../rainforest_eagle/translations/sv.json | 11 +++ .../rainmachine/translations/sv.json | 3 + .../recollect_waste/translations/sv.json | 28 +++++++ .../components/remote/translations/sv.json | 1 + .../components/renault/translations/sv.json | 5 +- .../components/rfxtrx/translations/sv.json | 10 +++ .../components/rhasspy/translations/no.json | 5 ++ .../components/risco/translations/sv.json | 15 ++++ .../translations/sv.json | 21 ++++++ .../components/roku/translations/sv.json | 8 +- .../components/roomba/translations/sv.json | 24 ++++++ .../components/roon/translations/sv.json | 11 +++ .../components/rpi_power/translations/sv.json | 4 +- .../components/scrape/translations/sv.json | 24 +++++- .../screenlogic/translations/sv.json | 7 ++ .../components/sense/translations/sv.json | 3 +- .../components/sensor/translations/sv.json | 7 ++ .../sensorpush/translations/no.json | 21 ++++++ .../components/sentry/translations/sv.json | 26 +++++++ .../components/senz/translations/hu.json | 6 ++ .../components/senz/translations/no.json | 6 ++ .../components/sharkiq/translations/sv.json | 9 +++ .../components/shelly/translations/sv.json | 4 +- .../components/sia/translations/sv.json | 36 +++++++++ .../simplepush/translations/hu.json | 6 ++ .../simplepush/translations/no.json | 5 ++ .../simplisafe/translations/hu.json | 4 +- .../simplisafe/translations/sv.json | 21 +++++- .../components/sma/translations/sv.json | 27 +++++++ .../components/smappee/translations/sv.json | 35 +++++++++ .../smart_meter_texas/translations/sv.json | 9 +++ .../smartthings/translations/sv.json | 11 +++ .../components/smarttub/translations/sv.json | 21 ++++++ .../components/sms/translations/sv.json | 14 +++- .../somfy_mylink/translations/sv.json | 20 +++++ .../components/sonarr/translations/sv.json | 8 ++ .../components/sonos/translations/sv.json | 1 + .../soundtouch/translations/hu.json | 1 + .../speedtestdotnet/translations/sv.json | 12 +++ .../components/spider/translations/sv.json | 6 ++ .../components/spotify/translations/hu.json | 4 +- .../components/spotify/translations/sv.json | 8 +- .../components/sql/translations/sv.json | 27 ++++++- .../squeezebox/translations/sv.json | 17 ++++- .../srp_energy/translations/sv.json | 3 + .../steam_online/translations/hu.json | 4 +- .../components/subaru/translations/sv.json | 28 +++++++ .../components/switchbot/translations/hu.json | 2 +- .../components/syncthru/translations/sv.json | 22 +++++- .../synology_dsm/translations/sv.json | 26 ++++++- .../components/tado/translations/sv.json | 1 + .../tankerkoenig/translations/sv.json | 4 +- .../components/tasmota/translations/sv.json | 20 +++++ .../tellduslive/translations/sv.json | 3 + .../components/tibber/translations/sv.json | 11 ++- .../components/tile/translations/sv.json | 17 ++++- .../components/toon/translations/sv.json | 18 ++++- .../totalconnect/translations/sv.json | 3 + .../components/tplink/translations/sv.json | 15 ++++ .../components/traccar/translations/sv.json | 4 + .../components/tractive/translations/sv.json | 21 ++++++ .../components/twilio/translations/sv.json | 4 + .../components/twinkly/translations/sv.json | 17 +++++ .../ukraine_alarm/translations/sv.json | 6 ++ .../components/unifi/translations/sv.json | 15 +++- .../components/upb/translations/sv.json | 5 ++ .../components/upcloud/translations/sv.json | 14 ++++ .../components/upnp/translations/sv.json | 7 ++ .../uptimerobot/translations/sv.json | 19 ++++- .../components/vera/translations/sv.json | 28 +++++++ .../components/vesync/translations/sv.json | 3 + .../components/vizio/translations/sv.json | 10 +++ .../components/volumio/translations/sv.json | 24 ++++++ .../components/vulcan/translations/sv.json | 12 ++- .../components/wallbox/translations/sv.json | 10 +++ .../water_heater/translations/sv.json | 9 +++ .../components/watttime/translations/sv.json | 10 ++- .../waze_travel_time/translations/sv.json | 1 + .../components/wiffi/translations/sv.json | 9 +++ .../components/withings/translations/sv.json | 12 ++- .../components/wled/translations/sv.json | 13 ++++ .../wolflink/translations/sensor.sv.json | 47 +++++++++++- .../components/wolflink/translations/sv.json | 17 ++++- .../components/ws66i/translations/sv.json | 3 + .../components/xbox/translations/hu.json | 1 + .../components/xbox/translations/sv.json | 14 ++++ .../xiaomi_aqara/translations/sv.json | 41 ++++++++++ .../xiaomi_miio/translations/select.sv.json | 9 +++ .../xiaomi_miio/translations/sv.json | 40 +++++++++- .../yamaha_musiccast/translations/sv.json | 23 ++++++ .../components/yeelight/translations/sv.json | 26 +++++++ .../components/zha/translations/hu.json | 2 + .../components/zha/translations/sv.json | 29 ++++++- .../zoneminder/translations/sv.json | 9 ++- .../components/zwave_js/translations/sv.json | 58 +++++++++++++- 297 files changed, 3758 insertions(+), 167 deletions(-) create mode 100644 homeassistant/components/acmeda/translations/sv.json create mode 100644 homeassistant/components/adax/translations/sv.json create mode 100644 homeassistant/components/airthings/translations/sv.json create mode 100644 homeassistant/components/ambee/translations/sensor.sv.json create mode 100644 homeassistant/components/arcam_fmj/translations/sv.json create mode 100644 homeassistant/components/bluetooth/translations/no.json create mode 100644 homeassistant/components/bosch_shc/translations/sv.json create mode 100644 homeassistant/components/buienradar/translations/sv.json create mode 100644 homeassistant/components/co2signal/translations/sv.json create mode 100644 homeassistant/components/crownstone/translations/sv.json create mode 100644 homeassistant/components/denonavr/translations/sv.json create mode 100644 homeassistant/components/dsmr/translations/sv.json create mode 100644 homeassistant/components/dunehd/translations/sv.json create mode 100644 homeassistant/components/enocean/translations/sv.json create mode 100644 homeassistant/components/firmata/translations/sv.json create mode 100644 homeassistant/components/flipr/translations/sv.json create mode 100644 homeassistant/components/govee_ble/translations/no.json create mode 100644 homeassistant/components/gree/translations/sv.json create mode 100644 homeassistant/components/guardian/translations/sv.json create mode 100644 homeassistant/components/home_connect/translations/sv.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/hu.json create mode 100644 homeassistant/components/hyperion/translations/sv.json create mode 100644 homeassistant/components/ialarm/translations/sv.json create mode 100644 homeassistant/components/inkbird/translations/no.json create mode 100644 homeassistant/components/islamic_prayer_times/translations/sv.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/sv.json create mode 100644 homeassistant/components/kulersky/translations/sv.json create mode 100644 homeassistant/components/lacrosse_view/translations/no.json create mode 100644 homeassistant/components/lutron_caseta/translations/sv.json create mode 100644 homeassistant/components/mazda/translations/sv.json create mode 100644 homeassistant/components/miflora/translations/hu.json create mode 100644 homeassistant/components/miflora/translations/no.json create mode 100644 homeassistant/components/mitemp_bt/translations/hu.json create mode 100644 homeassistant/components/mitemp_bt/translations/no.json create mode 100644 homeassistant/components/moat/translations/no.json create mode 100644 homeassistant/components/modem_callerid/translations/sv.json create mode 100644 homeassistant/components/mutesync/translations/sv.json create mode 100644 homeassistant/components/nfandroidtv/translations/sv.json create mode 100644 homeassistant/components/nightscout/translations/sv.json create mode 100644 homeassistant/components/nmap_tracker/translations/sv.json create mode 100644 homeassistant/components/openalpr_local/translations/hu.json create mode 100644 homeassistant/components/progettihwsw/translations/sv.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/translations/sv.json create mode 100644 homeassistant/components/rainforest_eagle/translations/sv.json create mode 100644 homeassistant/components/recollect_waste/translations/sv.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/sv.json create mode 100644 homeassistant/components/sensorpush/translations/no.json create mode 100644 homeassistant/components/sma/translations/sv.json create mode 100644 homeassistant/components/smappee/translations/sv.json create mode 100644 homeassistant/components/smarttub/translations/sv.json create mode 100644 homeassistant/components/somfy_mylink/translations/sv.json create mode 100644 homeassistant/components/tasmota/translations/sv.json create mode 100644 homeassistant/components/tractive/translations/sv.json create mode 100644 homeassistant/components/twinkly/translations/sv.json create mode 100644 homeassistant/components/vera/translations/sv.json create mode 100644 homeassistant/components/volumio/translations/sv.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/sv.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.sv.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/sv.json diff --git a/homeassistant/components/abode/translations/sv.json b/homeassistant/components/abode/translations/sv.json index ef61917ad43..df8937dc092 100644 --- a/homeassistant/components/abode/translations/sv.json +++ b/homeassistant/components/abode/translations/sv.json @@ -1,13 +1,27 @@ { "config": { "abort": { + "reauth_successful": "\u00c5terautentisering lyckades", "single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten." }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_mfa_code": "Ogiltig MFA-kod" + }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA-kod (6 siffror)" + }, + "title": "Ange din MFA-kod f\u00f6r Abode" + }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "title": "Fyll i din Abode-inloggningsinformation" }, "user": { "data": { diff --git a/homeassistant/components/accuweather/translations/sensor.sv.json b/homeassistant/components/accuweather/translations/sensor.sv.json index cc940f75b17..33020a4c601 100644 --- a/homeassistant/components/accuweather/translations/sensor.sv.json +++ b/homeassistant/components/accuweather/translations/sensor.sv.json @@ -2,7 +2,8 @@ "state": { "accuweather__pressure_tendency": { "falling": "Fallande", - "rising": "Stigande" + "rising": "Stigande", + "steady": "Stadig" } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sv.json b/homeassistant/components/accuweather/translations/sv.json index f4a63bb449d..a87b6736271 100644 --- a/homeassistant/components/accuweather/translations/sv.json +++ b/homeassistant/components/accuweather/translations/sv.json @@ -1,11 +1,38 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel", + "requests_exceeded": "Det till\u00e5tna antalet f\u00f6rfr\u00e5gningar till Accuweather API har \u00f6verskridits. Du m\u00e5ste v\u00e4nta eller \u00e4ndra API-nyckel." + }, "step": { "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" } } } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "V\u00e4derprognos" + }, + "description": "P\u00e5 grund av begr\u00e4nsningarna f\u00f6r den kostnadsfria versionen av AccuWeather API-nyckeln, n\u00e4r du aktiverar v\u00e4derprognos, kommer datauppdateringar att utf\u00f6ras var 80:e minut ist\u00e4llet f\u00f6r var 40:e minut." + } + } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 AccuWeather-servern", + "remaining_requests": "\u00c5terst\u00e5ende till\u00e5tna f\u00f6rfr\u00e5gningar" + } } } \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/sv.json b/homeassistant/components/acmeda/translations/sv.json new file mode 100644 index 00000000000..487295ecade --- /dev/null +++ b/homeassistant/components/acmeda/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "id": "V\u00e4rd-ID" + }, + "title": "V\u00e4lj en hubb att l\u00e4gga till" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/sv.json b/homeassistant/components/adax/translations/sv.json new file mode 100644 index 00000000000..be36fec5fe3 --- /dev/null +++ b/homeassistant/components/adax/translations/sv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/sv.json b/homeassistant/components/adguard/translations/sv.json index 0b58d9dcc97..155d2e9afd1 100644 --- a/homeassistant/components/adguard/translations/sv.json +++ b/homeassistant/components/adguard/translations/sv.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "existing_instance_updated": "Uppdaterade existerande konfiguration." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "hassio_confirm": { "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Supervisor Add-on: {addon}?", @@ -10,7 +14,9 @@ }, "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord", + "port": "Port", "ssl": "AdGuard Home anv\u00e4nder ett SSL-certifikat", "username": "Anv\u00e4ndarnamn", "verify_ssl": "AdGuard Home anv\u00e4nder ett korrekt certifikat" diff --git a/homeassistant/components/aemet/translations/sv.json b/homeassistant/components/aemet/translations/sv.json index f4a63bb449d..7b88d6d5ed2 100644 --- a/homeassistant/components/aemet/translations/sv.json +++ b/homeassistant/components/aemet/translations/sv.json @@ -1,9 +1,28 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, + "error": { + "invalid_api_key": "Ogiltig API-nyckel" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Integrationens namn" + }, + "description": "F\u00f6r att generera API-nyckel g\u00e5 till https://opendata.aemet.es/centrodedescargas/altaUsuario" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Samla data fr\u00e5n AEMET v\u00e4derstationer" } } } diff --git a/homeassistant/components/agent_dvr/translations/sv.json b/homeassistant/components/agent_dvr/translations/sv.json index cf600b98e96..fa0abfb767d 100644 --- a/homeassistant/components/agent_dvr/translations/sv.json +++ b/homeassistant/components/agent_dvr/translations/sv.json @@ -4,7 +4,8 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { - "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan." + "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan.", + "cannot_connect": "Det gick inte att ansluta." }, "step": { "user": { diff --git a/homeassistant/components/airnow/translations/sv.json b/homeassistant/components/airnow/translations/sv.json index f4a63bb449d..138764f44d9 100644 --- a/homeassistant/components/airnow/translations/sv.json +++ b/homeassistant/components/airnow/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/airthings/translations/sv.json b/homeassistant/components/airthings/translations/sv.json new file mode 100644 index 00000000000..aa2cb03973e --- /dev/null +++ b/homeassistant/components/airthings/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "description": "Logga in p\u00e5 {url} f\u00f6r att hitta dina autentiseringsuppgifter", + "id": "ID", + "secret": "Hemlighet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index 5273668aa12..d3559f89aa0 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad eller Node/Pro ID \u00e4r redan regristrerat.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade.", "invalid_api_key": "Ogiltig API-nyckel" }, @@ -16,7 +21,24 @@ "password": "Enhetsl\u00f6senord" } }, + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + }, + "title": "Autentisera AirVisual igen" + }, "user": { + "description": "V\u00e4lj typ av AirVisual data att \u00f6vervaka.", + "title": "Konfigurera AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Visa \u00f6vervakad geografi p\u00e5 kartan" + }, "title": "Konfigurera AirVisual" } } diff --git a/homeassistant/components/alarm_control_panel/translations/sv.json b/homeassistant/components/alarm_control_panel/translations/sv.json index cff9e0cbc52..cd737ef3ebe 100644 --- a/homeassistant/components/alarm_control_panel/translations/sv.json +++ b/homeassistant/components/alarm_control_panel/translations/sv.json @@ -4,16 +4,23 @@ "arm_away": "Larma {entity_name} borta", "arm_home": "Larma {entity_name} hemma", "arm_night": "Larma {entity_name} natt", + "arm_vacation": "Larma semesterl\u00e4ge {entity_name}", "disarm": "Avlarma {entity_name}", "trigger": "Utl\u00f6sare {entity_name}" }, "condition_type": { + "is_armed_away": "{entity_name} \u00e4r bortalarmat", + "is_armed_home": "{entity_name} \u00e4r hemmalarmat", + "is_armed_night": "{entity_name} \u00e4r nattlarmat", + "is_armed_vacation": "{entity_name} \u00e4r larmad i semesterl\u00e4ge", + "is_disarmed": "{entity_name} \u00e4r bortkopplad", "is_triggered": "har utl\u00f6sts" }, "trigger_type": { "armed_away": "{entity_name} larmad borta", "armed_home": "{entity_name} larmad hemma", "armed_night": "{entity_name} larmad natt", + "armed_vacation": "{entity_name} larmad i semesterl\u00e4ge", "disarmed": "{entity_name} bortkopplad", "triggered": "{entity_name} utl\u00f6st" } @@ -25,6 +32,7 @@ "armed_custom_bypass": "Larm f\u00f6rbikopplat", "armed_home": "Hemmalarmat", "armed_night": "Nattlarmat", + "armed_vacation": "Larmad semesterl\u00e4ge", "arming": "Tillkopplar", "disarmed": "Avlarmat", "disarming": "Fr\u00e5nkopplar", diff --git a/homeassistant/components/alarmdecoder/translations/sv.json b/homeassistant/components/alarmdecoder/translations/sv.json index 6c9f0dbcb43..0e8f0208b6c 100644 --- a/homeassistant/components/alarmdecoder/translations/sv.json +++ b/homeassistant/components/alarmdecoder/translations/sv.json @@ -1,26 +1,70 @@ { "config": { + "create_entry": { + "default": "Ansluten till AlarmDecoder." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "protocol": { "data": { - "device_path": "Enhetsv\u00e4g" + "device_baudrate": "Enhetens Baud Rate", + "device_path": "Enhetsv\u00e4g", + "host": "V\u00e4rd", + "port": "Port" }, "title": "Konfigurera anslutningsinst\u00e4llningar" }, "user": { "data": { "protocol": "Protokoll" - } + }, + "title": "V\u00e4lj AlarmDecoder Protocol" } } }, "options": { + "error": { + "int": "F\u00e4ltet nedan m\u00e5ste vara ett heltal.", + "loop_range": "RF Loop m\u00e5ste vara ett heltal mellan 1 och 4.", + "loop_rfid": "RF Loop kan inte anv\u00e4ndas utan RF Serial.", + "relay_inclusive": "Rel\u00e4adress och rel\u00e4kanal \u00e4r beroende av varandra och m\u00e5ste inkluderas tillsammans." + }, "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternativt nattl\u00e4ge", + "auto_bypass": "Automatisk f\u00f6rbikoppling p\u00e5 arm", + "code_arm_required": "Kod kr\u00e4vs f\u00f6r tillkoppling" + }, + "title": "Konfigurera AlarmDecoder" + }, "init": { "data": { "edit_select": "Redigera" }, - "description": "Vad vill du redigera?" + "description": "Vad vill du redigera?", + "title": "Konfigurera AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF loop", + "zone_name": "Zonnamn", + "zone_relayaddr": "Rel\u00e4adress", + "zone_relaychan": "Rel\u00e4kanal", + "zone_rfid": "RF seriell", + "zone_type": "Zontyp" + }, + "description": "Ange detaljer f\u00f6r zon {zone_number} . F\u00f6r att ta bort zon {zone_number} l\u00e4mnar du Zonnamn tomt.", + "title": "Konfigurera AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Zonnummer" + }, + "description": "Ange zonnumret du vill l\u00e4gga till, redigera eller ta bort.", + "title": "Konfigurera AlarmDecoder" } } } diff --git a/homeassistant/components/almond/translations/sv.json b/homeassistant/components/almond/translations/sv.json index 8b20512df9b..6cccf60b2c2 100644 --- a/homeassistant/components/almond/translations/sv.json +++ b/homeassistant/components/almond/translations/sv.json @@ -2,7 +2,8 @@ "config": { "abort": { "cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.", - "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond." + "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json index 80b14ac7470..98e9fbdabea 100644 --- a/homeassistant/components/ambee/translations/hu.json +++ b/homeassistant/components/ambee/translations/hu.json @@ -24,5 +24,11 @@ "description": "Integr\u00e1lja \u00f6ssze Ambeet Home Assistanttal." } } + }, + "issues": { + "pending_removal": { + "description": "Az Ambee integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a 2022.10-es Home Assistant-t\u00f3l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sa az\u00e9rt t\u00f6rt\u00e9nik, mert az Ambee elt\u00e1vol\u00edtotta az ingyenes (korl\u00e1tozott) fi\u00f3kjait, \u00e9s a rendszeres felhaszn\u00e1l\u00f3k sz\u00e1m\u00e1ra m\u00e1r nem biztos\u00edt lehet\u0151s\u00e9get arra, hogy fizet\u0151s csomagra regisztr\u00e1ljanak.\n\nA hiba\u00fczenet elrejt\u00e9s\u00e9hez t\u00e1vol\u00edtsa el az Ambee integr\u00e1ci\u00f3s bejegyz\u00e9st a rendszerb\u0151l.", + "title": "Az Ambee integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/no.json b/homeassistant/components/ambee/translations/no.json index b735ee91509..2c10f596722 100644 --- a/homeassistant/components/ambee/translations/no.json +++ b/homeassistant/components/ambee/translations/no.json @@ -24,5 +24,11 @@ "description": "Sett opp Ambee for \u00e5 integrere med Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "Ambee-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra Home Assistant 2022.10. \n\n Integrasjonen blir fjernet, fordi Ambee fjernet deres gratis (begrensede) kontoer og ikke gir vanlige brukere mulighet til \u00e5 registrere seg for en betalt plan lenger. \n\n Fjern Ambee-integrasjonsoppf\u00f8ringen fra forekomsten din for \u00e5 fikse dette problemet.", + "title": "Ambee-integrasjonen blir fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.sv.json b/homeassistant/components/ambee/translations/sensor.sv.json new file mode 100644 index 00000000000..a7c17b93906 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "ambee__risk": { + "low": "L\u00e5g", + "moderate": "M\u00e5ttlig", + "very high": "V\u00e4ldigt h\u00f6gt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sv.json b/homeassistant/components/ambee/translations/sv.json index a8147496b74..77149dc7889 100644 --- a/homeassistant/components/ambee/translations/sv.json +++ b/homeassistant/components/ambee/translations/sv.json @@ -1,9 +1,13 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + }, "step": { "reauth_confirm": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "description": "Autentisera p\u00e5 nytt med ditt Ambee-konto." } }, "user": { diff --git a/homeassistant/components/anthemav/translations/hu.json b/homeassistant/components/anthemav/translations/hu.json index f13544fff61..af7d008356c 100644 --- a/homeassistant/components/anthemav/translations/hu.json +++ b/homeassistant/components/anthemav/translations/hu.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Az Anthem A/V egys\u00e9gek YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el az Anthem A/V egys\u00e9gek YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az Anthem A/V Receivers YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/no.json b/homeassistant/components/anthemav/translations/no.json index e7b3f66ae8d..ae6b59cc89c 100644 --- a/homeassistant/components/anthemav/translations/no.json +++ b/homeassistant/components/anthemav/translations/no.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Anthem A/V-mottakere ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Anthem A/V Receivers YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Anthem A/V-mottakernes YAML-konfigurasjon blir fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/sv.json b/homeassistant/components/arcam_fmj/translations/sv.json new file mode 100644 index 00000000000..42d58bfc929 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{host}", + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "description": "Ange v\u00e4rdnamnet eller IP-adressen f\u00f6r enheten." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} har ombetts att sl\u00e5 p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/sv.json b/homeassistant/components/asuswrt/translations/sv.json index 4e7196b1b21..ec6717b75f1 100644 --- a/homeassistant/components/asuswrt/translations/sv.json +++ b/homeassistant/components/asuswrt/translations/sv.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Kunde inte ansluta", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", "pwd_and_ssh": "Ange endast l\u00f6senord eller SSH-nyckelfil", "pwd_or_ssh": "V\u00e4nligen ange l\u00f6senord eller SSH-nyckelfil", "ssh_not_file": "SSH-nyckelfil hittades inte", @@ -14,6 +15,7 @@ "step": { "user": { "data": { + "host": "V\u00e4rd", "mode": "L\u00e4ge", "name": "Namn", "password": "L\u00f6senord", diff --git a/homeassistant/components/atag/translations/sv.json b/homeassistant/components/atag/translations/sv.json index ae07cfa6221..480da89cb4a 100644 --- a/homeassistant/components/atag/translations/sv.json +++ b/homeassistant/components/atag/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unauthorized": "Parning nekad, kontrollera enheten f\u00f6r autentiseringsbeg\u00e4ran" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index 762d5fd7640..b4d4e8835fa 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -24,6 +24,7 @@ "data": { "code": "Verifieringskod" }, + "description": "Kontrollera din {login_method} ( {username} ) och ange verifieringskoden nedan", "title": "Tv\u00e5faktorsautentisering" } } diff --git a/homeassistant/components/aurora/translations/sv.json b/homeassistant/components/aurora/translations/sv.json index 7e16a2c036e..eabf6a41d0b 100644 --- a/homeassistant/components/aurora/translations/sv.json +++ b/homeassistant/components/aurora/translations/sv.json @@ -1,4 +1,18 @@ { + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + } + } + } + }, "options": { "step": { "init": { @@ -7,5 +21,6 @@ } } } - } + }, + "title": "NOAA Aurora Sensor" } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json index a7dd53ffad4..4823ac2df6a 100644 --- a/homeassistant/components/awair/translations/sv.json +++ b/homeassistant/components/awair/translations/sv.json @@ -1,6 +1,22 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "reauth": { + "data": { + "access_token": "\u00c5tkomstnyckel", + "email": "E-post" + }, + "description": "Ange din Awair-utvecklar\u00e5tkomsttoken igen." + }, "reauth_confirm": { "data": { "access_token": "\u00c5tkomsttoken", @@ -10,7 +26,8 @@ }, "user": { "data": { - "access_token": "\u00c5tkomstnyckel" + "access_token": "\u00c5tkomstnyckel", + "email": "E-post" } } } diff --git a/homeassistant/components/axis/translations/sv.json b/homeassistant/components/axis/translations/sv.json index e04267cb5d7..73de4193654 100644 --- a/homeassistant/components/axis/translations/sv.json +++ b/homeassistant/components/axis/translations/sv.json @@ -7,7 +7,9 @@ }, "error": { "already_configured": "Enheten \u00e4r redan konfigurerad", - "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r enheten p\u00e5g\u00e5r redan." + "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r enheten p\u00e5g\u00e5r redan.", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, "flow_title": "Axisenhet: {name} ({host})", "step": { @@ -21,5 +23,15 @@ "title": "Konfigurera Axis-enhet" } } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "V\u00e4lj streamprofil att anv\u00e4nda" + }, + "title": "Konfigurera enhetens videostr\u00f6mningsalternativ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/sv.json b/homeassistant/components/azure_devops/translations/sv.json index e87d9570334..0728a279d21 100644 --- a/homeassistant/components/azure_devops/translations/sv.json +++ b/homeassistant/components/azure_devops/translations/sv.json @@ -1,10 +1,31 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "project_error": "Kunde inte h\u00e4mta projektinformation" + }, + "flow_title": "{project_url}", "step": { + "reauth": { + "data": { + "personal_access_token": "Personlig \u00e5tkomsttoken (PAT)" + }, + "description": "Autentisering misslyckades f\u00f6r {project_url} . Ange dina nuvarande uppgifter.", + "title": "\u00c5terautentisering" + }, "user": { "data": { + "organization": "Organisation", + "personal_access_token": "Personlig \u00e5tkomsttoken (PAT)", "project": "Projekt" - } + }, + "description": "Konfigurera en Azure DevOps-instans f\u00f6r att komma \u00e5t ditt projekt. En personlig \u00e5tkomsttoken kr\u00e4vs endast f\u00f6r ett privat projekt.", + "title": "L\u00e4gg till Azure DevOps Project" } } } diff --git a/homeassistant/components/baf/translations/sv.json b/homeassistant/components/baf/translations/sv.json index 0e346a0f72a..b0126b30f97 100644 --- a/homeassistant/components/baf/translations/sv.json +++ b/homeassistant/components/baf/translations/sv.json @@ -12,6 +12,11 @@ "step": { "discovery_confirm": { "description": "Vill du st\u00e4lla in {name} - {model} ( {ip_address} )?" + }, + "user": { + "data": { + "ip_address": "IP-adress" + } } } } diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index 904ecd8fddc..8b05d4b024e 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -104,6 +104,9 @@ "off": "Normal", "on": "L\u00e5g" }, + "battery_charging": { + "off": "Laddar inte" + }, "cold": { "off": "Normal", "on": "Kallt" diff --git a/homeassistant/components/blebox/translations/sv.json b/homeassistant/components/blebox/translations/sv.json index 892b8b2cd91..e521f4c6f4a 100644 --- a/homeassistant/components/blebox/translations/sv.json +++ b/homeassistant/components/blebox/translations/sv.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "address_already_configured": "En BleBox-enhet \u00e4r redan konfigurerad p\u00e5 {address} .", + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel", + "unsupported_version": "BleBox-enheten har f\u00f6r\u00e5ldrad firmware. Uppgradera den f\u00f6rst." + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "port": "Port" - } + }, + "description": "St\u00e4ll in din BleBox f\u00f6r att integrera med Home Assistant.", + "title": "Konfigurera din BleBox-enhet" } } } diff --git a/homeassistant/components/blink/translations/sv.json b/homeassistant/components/blink/translations/sv.json index 23c825f256f..282dc374e67 100644 --- a/homeassistant/components/blink/translations/sv.json +++ b/homeassistant/components/blink/translations/sv.json @@ -1,10 +1,37 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "2fa": { + "data": { + "2fa": "Tv\u00e5faktorkod" + }, + "description": "Ange PIN-koden som skickades till din e-post", + "title": "Tv\u00e5faktorautentisering" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Logga in med Blink-konto" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Skanningsintervall (sekunder)" + }, + "description": "Konfigurera Blink integrationen", + "title": "Blink alternativ" } } } diff --git a/homeassistant/components/bluetooth/translations/no.json b/homeassistant/components/bluetooth/translations/no.json new file mode 100644 index 00000000000..fbc59772d6f --- /dev/null +++ b/homeassistant/components/bluetooth/translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "no_adapters": "Finner ingen Bluetooth-adaptere" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "enable_bluetooth": { + "description": "Vil du konfigurere Bluetooth?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Bluetooth-adapteren som skal brukes til skanning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/sv.json b/homeassistant/components/bond/translations/sv.json index 1fda5b91f5a..e745283a84a 100644 --- a/homeassistant/components/bond/translations/sv.json +++ b/homeassistant/components/bond/translations/sv.json @@ -1,9 +1,23 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "old_firmware": "Gamla firmware som inte st\u00f6ds p\u00e5 Bond-enheten - uppgradera innan du forts\u00e4tter." + }, + "flow_title": "{name} ({host})", "step": { - "user": { + "confirm": { "data": { "access_token": "\u00c5tkomstnyckel" + }, + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "access_token": "\u00c5tkomstnyckel", + "host": "V\u00e4rd" } } } diff --git a/homeassistant/components/bosch_shc/translations/sv.json b/homeassistant/components/bosch_shc/translations/sv.json new file mode 100644 index 00000000000..280064d278c --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "pairing_failed": "Kopplingen misslyckades; kontrollera att Bosch Smart Home Controller \u00e4r i kopplingsl\u00e4ge (lysdioden blinkar) och att ditt l\u00f6senord \u00e4r korrekt.", + "session_error": "Sessionsfel: API returnerar resultat som inte \u00e4r OK.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Tryck p\u00e5 knappen p\u00e5 framsidan av Bosch Smart Home Controller tills lysdioden b\u00f6rjar blinka.\nRedo att forts\u00e4tta att st\u00e4lla in {modell} @ {host} med Home Assistant?" + }, + "credentials": { + "data": { + "password": "L\u00f6senord f\u00f6r Smart Home Controller" + } + }, + "reauth_confirm": { + "description": "Bosch_shc-integrationen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "St\u00e4ll in din Bosch Smart Home Controller f\u00f6r att till\u00e5ta \u00f6vervakning och kontroll med Home Assistant.", + "title": "SHC-autentiseringsparametrar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index e3b98b4e76e..f545bdcabfb 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -1,20 +1,34 @@ { "config": { "abort": { - "already_configured": "Den h\u00e4r TV:n \u00e4r redan konfigurerad" + "already_configured": "Den h\u00e4r TV:n \u00e4r redan konfigurerad", + "no_ip_control": "IP-kontroll \u00e4r inaktiverat p\u00e5 din TV eller s\u00e5 st\u00f6ds inte TV:n." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress.", "unsupported_model": "Den h\u00e4r tv modellen st\u00f6ds inte." }, "step": { "authorize": { + "description": "Ange PIN-koden som visas p\u00e5 Sony Bravia TV. \n\n Om PIN-koden inte visas m\u00e5ste du avregistrera Home Assistant p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Inst\u00e4llningar f\u00f6r fj\u00e4rrenhet - > Avregistrera fj\u00e4rrenhet.", "title": "Auktorisera Sony Bravia TV" }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress f\u00f6r TV" - } + }, + "description": "Se till att din TV \u00e4r p\u00e5slagen innan du f\u00f6rs\u00f6ker st\u00e4lla in den." + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Lista \u00f6ver ignorerade k\u00e4llor" + }, + "title": "Alternativ f\u00f6r Sony Bravia TV" } } } diff --git a/homeassistant/components/broadlink/translations/sv.json b/homeassistant/components/broadlink/translations/sv.json index 38d02e42d90..bc621dfaa6a 100644 --- a/homeassistant/components/broadlink/translations/sv.json +++ b/homeassistant/components/broadlink/translations/sv.json @@ -1,7 +1,28 @@ { "config": { "abort": { - "not_supported": "Enheten st\u00f6ds inte" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "not_supported": "Enheten st\u00f6ds inte", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({model} p\u00e5 {host})", + "step": { + "auth": { + "title": "Autentisera till enheten" + }, + "finish": { + "data": { + "name": "Namn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/brother/translations/sv.json b/homeassistant/components/brother/translations/sv.json index 00d29aa3a0a..9a4fd5948d9 100644 --- a/homeassistant/components/brother/translations/sv.json +++ b/homeassistant/components/brother/translations/sv.json @@ -5,6 +5,7 @@ "unsupported_model": "Den h\u00e4r skrivarmodellen st\u00f6ds inte." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "snmp_error": "SNMP-servern har st\u00e4ngts av eller s\u00e5 st\u00f6ds inte skrivaren.", "wrong_host": "Ogiltigt v\u00e4rdnamn eller IP-adress." }, diff --git a/homeassistant/components/bsblan/translations/sv.json b/homeassistant/components/bsblan/translations/sv.json index 6dad0946ee5..f96cbe0051f 100644 --- a/homeassistant/components/bsblan/translations/sv.json +++ b/homeassistant/components/bsblan/translations/sv.json @@ -1,11 +1,18 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "cannot_connect": "Det gick inte att ansluta." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", "step": { "user": { "data": { + "host": "V\u00e4rd", + "passkey": "Nyckelstr\u00e4ng", "port": "Port", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/buienradar/translations/sv.json b/homeassistant/components/buienradar/translations/sv.json new file mode 100644 index 00000000000..f0a63c7d5ed --- /dev/null +++ b/homeassistant/components/buienradar/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, + "error": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Landskod f\u00f6r landet f\u00f6r att visa kamerabilder.", + "delta": "Tidsintervall i sekunder mellan kamerabilduppdateringar", + "timeframe": "Minuter att se fram\u00e5t f\u00f6r nederb\u00f6rdsprognos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/sv.json b/homeassistant/components/cast/translations/sv.json index 0d5fb586cc8..49ec796088a 100644 --- a/homeassistant/components/cast/translations/sv.json +++ b/homeassistant/components/cast/translations/sv.json @@ -3,13 +3,26 @@ "abort": { "single_instance_allowed": "Endast en enda konfiguration av Google Cast \u00e4r n\u00f6dv\u00e4ndig." }, + "error": { + "invalid_known_hosts": "K\u00e4nda v\u00e4rdar m\u00e5sta vara en kommaseparerad lista av v\u00e4rdnamn." + }, "step": { + "config": { + "data": { + "known_hosts": "K\u00e4nad v\u00e4rdar" + }, + "description": "K\u00e4nda v\u00e4rdar - En kommaseparerad lista \u00f6ver v\u00e4rdnamn eller IP-adresser f\u00f6r cast-enheter, anv\u00e4nd om mDNS-uppt\u00e4ckt inte fungerar.", + "title": "Google Cast-konfiguration" + }, "confirm": { "description": "Vill du konfigurera Google Cast?" } } }, "options": { + "error": { + "invalid_known_hosts": "K\u00e4nda v\u00e4rdar m\u00e5sta vara en kommaseparerad lista av v\u00e4rdnamn." + }, "step": { "advanced_options": { "data": { diff --git a/homeassistant/components/cert_expiry/translations/sv.json b/homeassistant/components/cert_expiry/translations/sv.json index f00fc236d09..aefa1c45444 100644 --- a/homeassistant/components/cert_expiry/translations/sv.json +++ b/homeassistant/components/cert_expiry/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tj\u00e4nsten har redan konfigurerats" + "already_configured": "Tj\u00e4nsten har redan konfigurerats", + "import_failed": "Importering av konfiguration misslyckades" }, "error": { "connection_refused": "Anslutningen blev tillbakavisad under anslutning till v\u00e4rd.", diff --git a/homeassistant/components/co2signal/translations/sv.json b/homeassistant/components/co2signal/translations/sv.json new file mode 100644 index 00000000000..0abdae46923 --- /dev/null +++ b/homeassistant/components/co2signal/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "api_ratelimit": "API-hastighetsgr\u00e4nsen har \u00f6verskridits", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + }, + "country": { + "data": { + "country_code": "Landskod" + } + }, + "user": { + "data": { + "api_key": "\u00c5tkomstnyckel", + "location": "H\u00e4mta uppgifter f\u00f6r" + }, + "description": "Bes\u00f6k https://co2signal.com/ f\u00f6r att beg\u00e4ra en token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/sv.json b/homeassistant/components/coinbase/translations/sv.json index 1ab71904a13..7d6a4a507aa 100644 --- a/homeassistant/components/coinbase/translations/sv.json +++ b/homeassistant/components/coinbase/translations/sv.json @@ -1,17 +1,35 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "api_token": "API-hemlighet" + }, + "title": "Coinbase API-nyckeldetaljer" } } }, "options": { "error": { "currency_unavailable": "En eller flera av de beg\u00e4rda valutasaldona tillhandah\u00e5lls inte av ditt Coinbase API.", - "exchange_rate_unavailable": "En eller flera av de beg\u00e4rda v\u00e4xelkurserna tillhandah\u00e5lls inte av Coinbase." + "exchange_rate_unavailable": "En eller flera av de beg\u00e4rda v\u00e4xelkurserna tillhandah\u00e5lls inte av Coinbase.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Balans i pl\u00e5nboken att rapportera.", + "exchange_rate_currencies": "Valutakurser att rapportera.", + "exchnage_rate_precision": "Antal decimaler f\u00f6r v\u00e4xelkurser." + }, + "description": "Justera Coinbase-alternativ" + } } } } \ No newline at end of file diff --git a/homeassistant/components/control4/translations/sv.json b/homeassistant/components/control4/translations/sv.json index e1ecf8798c1..1ee1c5f34ae 100644 --- a/homeassistant/components/control4/translations/sv.json +++ b/homeassistant/components/control4/translations/sv.json @@ -1,12 +1,29 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "host": "IP-adress", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" + }, + "description": "V\u00e4nligen ange dina Control4-kontouppgifter och IP-adressen till din lokala controller." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellan uppdateringarna" } } } diff --git a/homeassistant/components/coolmaster/translations/sv.json b/homeassistant/components/coolmaster/translations/sv.json index 366295c0966..0d3c7278a7f 100644 --- a/homeassistant/components/coolmaster/translations/sv.json +++ b/homeassistant/components/coolmaster/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Det gick inte att ansluta.", "no_units": "Det gick inte att hitta n\u00e5gra HVAC-enheter i CoolMasterNet-v\u00e4rden." }, "step": { diff --git a/homeassistant/components/coronavirus/translations/sv.json b/homeassistant/components/coronavirus/translations/sv.json index 7e6686c2a04..28a3cbee34f 100644 --- a/homeassistant/components/coronavirus/translations/sv.json +++ b/homeassistant/components/coronavirus/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Detta land \u00e4r redan konfigurerat." + "already_configured": "Detta land \u00e4r redan konfigurerat.", + "cannot_connect": "Det gick inte att ansluta." }, "step": { "user": { diff --git a/homeassistant/components/cover/translations/sv.json b/homeassistant/components/cover/translations/sv.json index 624b5102d82..f2ef369ae4f 100644 --- a/homeassistant/components/cover/translations/sv.json +++ b/homeassistant/components/cover/translations/sv.json @@ -2,7 +2,12 @@ "device_automation": { "action_type": { "close": "St\u00e4ng {entity_name}", - "open": "\u00d6ppna {entity_name}" + "close_tilt": "St\u00e4ng {entity_name} lutning", + "open": "\u00d6ppna {entity_name}", + "open_tilt": "\u00d6ppna {entity_name} lutning", + "set_position": "S\u00e4tt {entity_name} position", + "set_tilt_position": "S\u00e4tt {entity_name} lutningsposition", + "stop": "Stoppa {entity_name}" }, "condition_type": { "is_closed": "{entity_name} \u00e4r st\u00e4ngd", diff --git a/homeassistant/components/crownstone/translations/sv.json b/homeassistant/components/crownstone/translations/sv.json new file mode 100644 index 00000000000..69519c18dc0 --- /dev/null +++ b/homeassistant/components/crownstone/translations/sv.json @@ -0,0 +1,75 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "usb_setup_complete": "Crownstone USB-installation klar.", + "usb_setup_unsuccessful": "Crownstone USB-installationen misslyckades." + }, + "error": { + "account_not_verified": "Kontot \u00e4r inte verifierat. V\u00e4nligen aktivera ditt konto via aktiveringsmailet fr\u00e5n Crownstone.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "V\u00e4lj den seriella porten p\u00e5 Crownstone USB-dongeln, eller v\u00e4lj \"Anv\u00e4nd inte USB\" om du inte vill st\u00e4lla in en USB-dongel. \n\n Leta efter en enhet med VID 10C4 och PID EA60.", + "title": "Crownstone USB-dongelkonfiguration" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "Ange s\u00f6kv\u00e4gen f\u00f6r en Crownstone USB-dongel manuellt.", + "title": "Crownstone USB-dongel manuell v\u00e4g" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "V\u00e4lj en Crownstone Sphere d\u00e4r USB-enheten finns.", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "title": "Crownstone konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere d\u00e4r USB-enheten finns", + "use_usb_option": "Anv\u00e4nd en Crownstone USB-dongel f\u00f6r lokal data\u00f6verf\u00f6ring" + } + }, + "usb_config": { + "data": { + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "V\u00e4lj serieporten p\u00e5 Crownstone USB-dongeln. \n\n Leta efter en enhet med VID 10C4 och PID EA60.", + "title": "Crownstone USB-dongelkonfiguration" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "Ange s\u00f6kv\u00e4gen f\u00f6r en Crownstone USB-dongel manuellt.", + "title": "Crownstone USB-dongel manuell v\u00e4g" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "V\u00e4lj en Crownstone Sphere d\u00e4r USB-enheten finns.", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/sv.json b/homeassistant/components/daikin/translations/sv.json index 03b7358ee1b..1ea73051e9b 100644 --- a/homeassistant/components/daikin/translations/sv.json +++ b/homeassistant/components/daikin/translations/sv.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, "step": { "user": { diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index fd0968a941a..774144c5ba3 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -25,12 +25,18 @@ "host": "V\u00e4rd", "port": "Port" } + }, + "user": { + "data": { + "host": "V\u00e4lj uppt\u00e4ckt deCONZ-gateway" + } } } }, "device_automation": { "trigger_subtype": { "both_buttons": "B\u00e5da knapparna", + "bottom_buttons": "Bottenknappar", "button_1": "F\u00f6rsta knappen", "button_2": "Andra knappen", "button_3": "Tredje knappen", @@ -51,6 +57,7 @@ "side_4": "Sida 4", "side_5": "Sida 5", "side_6": "Sida 6", + "top_buttons": "Toppknappar", "turn_off": "St\u00e4ng av", "turn_on": "Starta" }, @@ -62,6 +69,7 @@ "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt", "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt", "remote_button_rotated": "Knappen roterade \"{subtype}\"", + "remote_button_rotated_fast": "Knappen roterades snabbt \" {subtype} \"", "remote_button_rotation_stopped": "Knapprotationen \"{subtype}\" stoppades", "remote_button_short_press": "\"{subtype}\"-knappen trycktes in", "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", diff --git a/homeassistant/components/demo/translations/no.json b/homeassistant/components/demo/translations/no.json index 48da80fd629..7cbf50d76bb 100644 --- a/homeassistant/components/demo/translations/no.json +++ b/homeassistant/components/demo/translations/no.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Trykk OK n\u00e5r blinklysv\u00e6ske er fylt p\u00e5 igjen", + "title": "Blinkerv\u00e6ske m\u00e5 etterfylles" + } + } + }, + "title": "Blinklysv\u00e6sken er tom og m\u00e5 etterfylles" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier-komponenten er n\u00e5 avviklet p\u00e5 grunn av mangelen p\u00e5 lokal kontroll tilgjengelig i det nye API-et", + "title": "Transmogrifier-komponenten er utdatert" + }, + "unfixable_problem": { + "description": "Denne saken kommer aldri til \u00e5 gi opp.", + "title": "Dette er ikke et problem som kan fikses" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/sv.json b/homeassistant/components/demo/translations/sv.json index 55872e8c81e..d88f06a3f99 100644 --- a/homeassistant/components/demo/translations/sv.json +++ b/homeassistant/components/demo/translations/sv.json @@ -31,6 +31,7 @@ "options_1": { "data": { "bool": "Valfritt boolesk", + "constant": "Konstant", "int": "Numerisk inmatning" } }, diff --git a/homeassistant/components/denonavr/translations/sv.json b/homeassistant/components/denonavr/translations/sv.json new file mode 100644 index 00000000000..f27ff32b05c --- /dev/null +++ b/homeassistant/components/denonavr/translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen, att koppla bort n\u00e4t- och ethernetkablar och \u00e5teransluta dem kan hj\u00e4lpa" + }, + "step": { + "confirm": { + "description": "Bekr\u00e4fta att du l\u00e4gger till receivern" + }, + "select": { + "data": { + "select_host": "Receiverns IP-adress" + }, + "description": "K\u00f6r installationen igen om du vill ansluta ytterligare mottagare", + "title": "V\u00e4lj receivern du vill ansluta till" + }, + "user": { + "data": { + "host": "IP-adress" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "Visa alla k\u00e4llor", + "update_audyssey": "Uppdatera Audyssey-inst\u00e4llningarna", + "zone2": "St\u00e4ll in zon 2", + "zone3": "St\u00e4ll in zon 3" + }, + "description": "Ange valfria inst\u00e4llningar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/sv.json b/homeassistant/components/device_tracker/translations/sv.json index 7ef1cc7b2f8..cff85228a59 100644 --- a/homeassistant/components/device_tracker/translations/sv.json +++ b/homeassistant/components/device_tracker/translations/sv.json @@ -3,6 +3,10 @@ "condition_type": { "is_home": "{entity_name} \u00e4r hemma", "is_not_home": "{entity_name} \u00e4r inte hemma" + }, + "trigger_type": { + "enters": "{entity_name} g\u00e5r in i en zon", + "leaves": "{entity_name} l\u00e4mnar en zon" } }, "state": { diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json index 13e09780b40..6727e5c5107 100644 --- a/homeassistant/components/devolo_home_control/translations/sv.json +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/dexcom/translations/sv.json b/homeassistant/components/dexcom/translations/sv.json index 23c825f256f..d82f098eb83 100644 --- a/homeassistant/components/dexcom/translations/sv.json +++ b/homeassistant/components/dexcom/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/dialogflow/translations/sv.json b/homeassistant/components/dialogflow/translations/sv.json index ebae7e612d0..daf0f4f3ea6 100644 --- a/homeassistant/components/dialogflow/translations/sv.json +++ b/homeassistant/components/dialogflow/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant instans m\u00e5ste kunna n\u00e5s fr\u00e5n Internet f\u00f6r att ta emot webhook meddelanden" }, "create_entry": { diff --git a/homeassistant/components/directv/translations/sv.json b/homeassistant/components/directv/translations/sv.json index c42c03d9944..91275d8c75a 100644 --- a/homeassistant/components/directv/translations/sv.json +++ b/homeassistant/components/directv/translations/sv.json @@ -1,11 +1,13 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "unknown": "Ov\u00e4ntat fel" }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" }, + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Do vill du konfigurera {name}?" diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json index 56c44dee6fb..d91cc55a3be 100644 --- a/homeassistant/components/doorbird/translations/sv.json +++ b/homeassistant/components/doorbird/translations/sv.json @@ -1,8 +1,16 @@ { "config": { - "error": { - "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "link_local_address": "Lokala l\u00e4nkadresser st\u00f6ds inte", + "not_doorbird_device": "Den h\u00e4r enheten \u00e4r inte en DoorBird" }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { @@ -13,5 +21,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Kommaseparerad lista \u00f6ver h\u00e4ndelser." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/sv.json b/homeassistant/components/dsmr/translations/sv.json new file mode 100644 index 00000000000..38b86e964b6 --- /dev/null +++ b/homeassistant/components/dsmr/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Minsta tid mellan enhetsuppdateringar [s]" + }, + "title": "DSMR-alternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/sv.json b/homeassistant/components/dunehd/translations/sv.json new file mode 100644 index 00000000000..b4d134814a2 --- /dev/null +++ b/homeassistant/components/dunehd/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Se till att din spelare \u00e4r p\u00e5slagen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/sv.json b/homeassistant/components/elgato/translations/sv.json index 4ed0161b0e3..57fd302ab90 100644 --- a/homeassistant/components/elgato/translations/sv.json +++ b/homeassistant/components/elgato/translations/sv.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Den h\u00e4r Elgato Key Light-enheten \u00e4r redan konfigurerad." + "already_configured": "Den h\u00e4r Elgato Key Light-enheten \u00e4r redan konfigurerad.", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." }, "flow_title": "Elgato Key Light: {serial_number}", "step": { diff --git a/homeassistant/components/elkm1/translations/sv.json b/homeassistant/components/elkm1/translations/sv.json index 8765d95baf6..305329feff4 100644 --- a/homeassistant/components/elkm1/translations/sv.json +++ b/homeassistant/components/elkm1/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "address_already_configured": "En ElkM1 med denna adress \u00e4r redan konfigurerad", + "already_configured": "En ElkM1 med detta prefix \u00e4r redan konfigurerad", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "invalid_auth": "Ogiltig autentisering", @@ -16,6 +22,10 @@ "protocol": "Protokoll", "username": "Anv\u00e4ndarnamn" } + }, + "user": { + "description": "V\u00e4lj ett uppt\u00e4ckt system eller \"Manuell inmatning\" om inga enheter har uppt\u00e4ckts.", + "title": "Anslut till Elk-M1 Control" } } } diff --git a/homeassistant/components/emonitor/translations/sv.json b/homeassistant/components/emonitor/translations/sv.json index c5ad71d784d..d7081bafe9c 100644 --- a/homeassistant/components/emonitor/translations/sv.json +++ b/homeassistant/components/emonitor/translations/sv.json @@ -6,6 +6,18 @@ "error": { "cannot_connect": "Kunde inte ansluta", "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vill du konfigurera {name} ({host})?", + "title": "St\u00e4ll in SiteSage Emonitor" + }, + "user": { + "data": { + "host": "V\u00e4rd" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/sv.json b/homeassistant/components/emulated_roku/translations/sv.json index ddf62b50df1..f186cfe0f76 100644 --- a/homeassistant/components/emulated_roku/translations/sv.json +++ b/homeassistant/components/emulated_roku/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/enocean/translations/sv.json b/homeassistant/components/enocean/translations/sv.json new file mode 100644 index 00000000000..8a881b2a9a2 --- /dev/null +++ b/homeassistant/components/enocean/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Ogiltig s\u00f6kv\u00e4g f\u00f6r dongle", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "invalid_dongle_path": "Ingen giltig dongel hittades f\u00f6r denna s\u00f6kv\u00e4g" + }, + "step": { + "detect": { + "data": { + "path": "USB-dongle-s\u00f6kv\u00e4g" + }, + "title": "V\u00e4lj s\u00f6kv\u00e4g till din ENOcean dongle" + }, + "manual": { + "data": { + "path": "USB-dongle-s\u00f6kv\u00e4g" + }, + "title": "Ange s\u00f6kv\u00e4gen till din ENOcean-dongel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/sv.json b/homeassistant/components/enphase_envoy/translations/sv.json index ecc6740fc9d..3889eae836b 100644 --- a/homeassistant/components/enphase_envoy/translations/sv.json +++ b/homeassistant/components/enphase_envoy/translations/sv.json @@ -1,15 +1,19 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Kunde inte ansluta", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{serial} ({host})", "step": { "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/epson/translations/sv.json b/homeassistant/components/epson/translations/sv.json index e7ec27624a5..45224016263 100644 --- a/homeassistant/components/epson/translations/sv.json +++ b/homeassistant/components/epson/translations/sv.json @@ -2,6 +2,14 @@ "config": { "error": { "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/sv.json b/homeassistant/components/firmata/translations/sv.json new file mode 100644 index 00000000000..46631acc69a --- /dev/null +++ b/homeassistant/components/firmata/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Det gick inte att ansluta." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/sv.json b/homeassistant/components/flick_electric/translations/sv.json index 2957bed953a..9c6855a6c81 100644 --- a/homeassistant/components/flick_electric/translations/sv.json +++ b/homeassistant/components/flick_electric/translations/sv.json @@ -1,13 +1,22 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "error": { - "invalid_auth": "Ogiltig autentisering" + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "client_id": "Klient ID (valfritt)", + "client_secret": "Klient Nyckel (valfritt)", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Flick Autentiseringsuppgifter" } } } diff --git a/homeassistant/components/flipr/translations/sv.json b/homeassistant/components/flipr/translations/sv.json new file mode 100644 index 00000000000..ec835585137 --- /dev/null +++ b/homeassistant/components/flipr/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "no_flipr_id_found": "Inget flipr-ID kopplat till ditt konto f\u00f6r tillf\u00e4llet. Du b\u00f6r f\u00f6rst verifiera att den fungerar med Fliprs mobilapp.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "V\u00e4lj ditt Flipr-ID i listan", + "title": "V\u00e4lj din Flipr" + }, + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "description": "Anslut med ditt Flipr-konto." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/sv.json b/homeassistant/components/flo/translations/sv.json index 78879942876..a07a2c509bc 100644 --- a/homeassistant/components/flo/translations/sv.json +++ b/homeassistant/components/flo/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flume/translations/sv.json b/homeassistant/components/flume/translations/sv.json index f4fdb861fc7..768f499b2ac 100644 --- a/homeassistant/components/flume/translations/sv.json +++ b/homeassistant/components/flume/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Det h\u00e4r kontot har redan konfigurerats." + "already_configured": "Det h\u00e4r kontot har redan konfigurerats.", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", @@ -9,6 +10,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "L\u00f6senordet f\u00f6r {username} \u00e4r inte l\u00e4ngre giltigt.", + "title": "Autentisera ditt Flume-konto igen" + }, "user": { "data": { "client_id": "Klient ID", diff --git a/homeassistant/components/flunearyou/translations/sv.json b/homeassistant/components/flunearyou/translations/sv.json index e39e45a3ec0..eb41d4ff78f 100644 --- a/homeassistant/components/flunearyou/translations/sv.json +++ b/homeassistant/components/flunearyou/translations/sv.json @@ -8,7 +8,9 @@ "data": { "latitude": "Latitud", "longitude": "Longitud" - } + }, + "description": "\u00d6vervaka anv\u00e4ndarbaserade och CDC-rapporter f\u00f6r ett par koordinater.", + "title": "Konfigurera influensa i n\u00e4rheten av dig" } } } diff --git a/homeassistant/components/forecast_solar/translations/sv.json b/homeassistant/components/forecast_solar/translations/sv.json index fceb441190b..8a1e0911c8d 100644 --- a/homeassistant/components/forecast_solar/translations/sv.json +++ b/homeassistant/components/forecast_solar/translations/sv.json @@ -3,7 +3,24 @@ "step": { "user": { "data": { - "modules power": "Total maxeffekt (Watt) p\u00e5 dina solpaneler" + "azimuth": "Azimuth (360 grader, 0 = norr, 90 = \u00f6st, 180 = s\u00f6der, 270 = v\u00e4ster)", + "declination": "Deklination (0 = horisontell, 90 = vertikal)", + "latitude": "Latitud", + "longitude": "Longitud", + "modules power": "Total maxeffekt (Watt) p\u00e5 dina solpaneler", + "name": "Namn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "API-nyckel f\u00f6r Forecast.Solar (valfritt)", + "azimuth": "Azimuth (360 grader, 0 = norr, 90 = \u00f6st, 180 = s\u00f6der, 270 = v\u00e4ster)", + "damping": "D\u00e4mpningsfaktor: justerar resultaten p\u00e5 morgonen och kv\u00e4llen", + "declination": "Deklination (0 = horisontell, 90 = vertikal)" } } } diff --git a/homeassistant/components/forked_daapd/translations/sv.json b/homeassistant/components/forked_daapd/translations/sv.json index 80f45e2d887..3e36427c525 100644 --- a/homeassistant/components/forked_daapd/translations/sv.json +++ b/homeassistant/components/forked_daapd/translations/sv.json @@ -1,10 +1,32 @@ { "config": { + "error": { + "wrong_server_type": "Forked-daapd-integrationen kr\u00e4ver en forked-daapd-server med version > = 27.0." + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { + "host": "V\u00e4rd", + "name": "Eget namn", + "password": "API-l\u00f6senord (l\u00e4mna tomt om inget l\u00f6senord)", "port": "API port" - } + }, + "title": "Konfigurera forked-daapd-enhet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Port f\u00f6r librespot-java pipe control (om s\u00e5dan anv\u00e4nds)", + "max_playlists": "Max antal spellistor som anv\u00e4nds som k\u00e4llor", + "tts_pause_time": "Sekunder att pausa f\u00f6re och efter TTS", + "tts_volume": "TTS-volym (flytande inom intervallet [0,1])" + }, + "description": "St\u00e4ll in olika alternativ f\u00f6r forked-daapd-integreringen.", + "title": "Konfigurera alternativ f\u00f6r forked-daapd" } } } diff --git a/homeassistant/components/foscam/translations/sv.json b/homeassistant/components/foscam/translations/sv.json index 78879942876..9de4793eabe 100644 --- a/homeassistant/components/foscam/translations/sv.json +++ b/homeassistant/components/foscam/translations/sv.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord", + "port": "Port", + "stream": "Str\u00f6m", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/freebox/translations/sv.json b/homeassistant/components/freebox/translations/sv.json index aa43ee66032..ed8ebe67ae9 100644 --- a/homeassistant/components/freebox/translations/sv.json +++ b/homeassistant/components/freebox/translations/sv.json @@ -9,6 +9,9 @@ "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare" }, "step": { + "link": { + "title": "L\u00e4nka Freebox-router" + }, "user": { "data": { "host": "V\u00e4rd", diff --git a/homeassistant/components/freedompro/translations/sv.json b/homeassistant/components/freedompro/translations/sv.json index 9feab4808f7..83f1c6b3dac 100644 --- a/homeassistant/components/freedompro/translations/sv.json +++ b/homeassistant/components/freedompro/translations/sv.json @@ -1,13 +1,18 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, "step": { "user": { "data": { "api_key": "API-nyckel" - } + }, + "title": "Freedompro API-nyckel" } } } diff --git a/homeassistant/components/fritz/translations/sv.json b/homeassistant/components/fritz/translations/sv.json index 89c6cb84221..5afcb156965 100644 --- a/homeassistant/components/fritz/translations/sv.json +++ b/homeassistant/components/fritz/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "confirm": { "data": { @@ -12,13 +15,24 @@ "reauth_confirm": { "data": { "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Uppdaterar FRITZ!Box Tools - referenser" }, "user": { "data": { + "host": "V\u00e4rd", "username": "Anv\u00e4ndarnamn" } } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Sekunder att \u00f6verv\u00e4ga en enhet hemma" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/sv.json b/homeassistant/components/fritzbox/translations/sv.json index 347aeeecc4a..a028ec5f217 100644 --- a/homeassistant/components/fritzbox/translations/sv.json +++ b/homeassistant/components/fritzbox/translations/sv.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." + "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" }, "step": { "confirm": { @@ -13,8 +18,10 @@ }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Uppdatera din inloggningsinformation f\u00f6r {name} ." }, "user": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/sv.json b/homeassistant/components/fritzbox_callmonitor/translations/sv.json index 23c825f256f..7a0fbc4f3f6 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/sv.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/sv.json @@ -1,11 +1,41 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "insufficient_permissions": "Anv\u00e4ndaren har otillr\u00e4ckliga beh\u00f6righeter f\u00f6r att komma \u00e5t AVM FRITZ!Box-inst\u00e4llningarna och dess telefonb\u00f6cker.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "flow_title": "{name}", "step": { + "phonebook": { + "data": { + "phonebook": "Telefonbok" + } + }, "user": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" } } } + }, + "options": { + "error": { + "malformed_prefixes": "Prefix \u00e4r felaktiga, kontrollera deras format." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefix (kommaseparerad lista)" + }, + "title": "Konfigurera prefix" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/generic/translations/cs.json b/homeassistant/components/generic/translations/cs.json index 8ef4333b872..73c3e470129 100644 --- a/homeassistant/components/generic/translations/cs.json +++ b/homeassistant/components/generic/translations/cs.json @@ -13,7 +13,8 @@ "data": { "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - } + }, + "description": "Zadejte nastaven\u00ed pro p\u0159ipojen\u00ed ke kame\u0159e." } } }, diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index 5c718dc0f47..23319f0a938 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -54,6 +54,7 @@ "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", + "relative_url": "Relative URL-adresser ikke tillatt", "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", "stream_io_error": "Inn-/utdatafeil under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 020b0093092..9a4067bbcfc 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -1,14 +1,25 @@ { "config": { "abort": { - "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "error": { "malformed_url": "Ogiltig URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tna", - "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information." + "stream_io_error": "Inmatnings-/utg\u00e5ngsfel vid f\u00f6rs\u00f6k att ansluta till stream. Fel RTSP-transportprotokoll?", + "stream_no_video": "Str\u00f6mmen har ingen video", + "stream_not_permitted": "\u00c5tg\u00e4rden \u00e4r inte till\u00e5ten n\u00e4r du f\u00f6rs\u00f6ker ansluta till streamen. Fel RTSP-transportprotokoll?", + "stream_unauthorised": "Auktoriseringen misslyckades n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", + "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information.", + "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL", + "unable_still_load": "Det g\u00e5r inte att ladda giltig bild fr\u00e5n stillbilds-URL (t.ex. ogiltig v\u00e4rd, URL eller autentiseringsfel). Granska loggen f\u00f6r mer information.", + "unknown": "Ov\u00e4ntat fel" }, "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, "content_type": { "data": { "content_type": "Inneh\u00e5llstyp" @@ -18,10 +29,16 @@ "user": { "data": { "authentication": "Autentiseringen", + "framerate": "Bildfrekvens (Hz)", + "limit_refetch_to_url_change": "Begr\u00e4nsa \u00e5terh\u00e4mtning till \u00e4ndring av webbadress", "password": "L\u00f6senord", "rtsp_transport": "RTSP transportprotokoll", - "username": "Anv\u00e4ndarnamn" - } + "still_image_url": "URL f\u00f6r stillbild (t.ex. http://...)", + "stream_source": "URL f\u00f6r str\u00f6mk\u00e4lla (t.ex. rtsp://...)", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "description": "Skriv in inst\u00e4llningarna f\u00f6r att ansluta till kameran." } } }, @@ -29,7 +46,8 @@ "error": { "malformed_url": "Ogiltig URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tet", - "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information." + "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information.", + "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL" }, "step": { "content_type": { @@ -41,8 +59,12 @@ "init": { "data": { "authentication": "Autentiseringen", + "rtsp_transport": "RTSP transportprotokoll", + "still_image_url": "URL f\u00f6r stillbild (t.ex. http://...)", + "stream_source": "URL f\u00f6r str\u00f6mk\u00e4lla (t.ex. rtsp://...)", "use_wallclock_as_timestamps": "Anv\u00e4nd v\u00e4ggklocka som tidsst\u00e4mplar", - "username": "Anv\u00e4ndarnamn" + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" }, "data_description": { "use_wallclock_as_timestamps": "Det h\u00e4r alternativet kan korrigera segmenteringsproblem eller kraschproblem som uppst\u00e5r p\u00e5 grund av felaktig implementering av tidsst\u00e4mplar p\u00e5 vissa kameror." diff --git a/homeassistant/components/geofency/translations/sv.json b/homeassistant/components/geofency/translations/sv.json index 8b48a30ce8b..5565034ff74 100644 --- a/homeassistant/components/geofency/translations/sv.json +++ b/homeassistant/components/geofency/translations/sv.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "cloud_not_connected": "Ej ansluten till Home Assistant Cloud." + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Geofency.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." diff --git a/homeassistant/components/gios/translations/sv.json b/homeassistant/components/gios/translations/sv.json index 98e9333c821..c93367a393b 100644 --- a/homeassistant/components/gios/translations/sv.json +++ b/homeassistant/components/gios/translations/sv.json @@ -17,5 +17,10 @@ "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 GIO\u015a-servern" + } } } \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/sv.json b/homeassistant/components/gogogate2/translations/sv.json index f7461922566..b34d63016f2 100644 --- a/homeassistant/components/gogogate2/translations/sv.json +++ b/homeassistant/components/gogogate2/translations/sv.json @@ -1,13 +1,22 @@ { "config": { - "error": { + "abort": { "cannot_connect": "Det gick inte att ansluta." }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { + "ip_address": "IP-adress", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange n\u00f6dv\u00e4ndig information nedan.", + "title": "St\u00e4ll in Gogogate2 eller ismartgate" } } } diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json index b27e06b15c7..467b1a663f2 100644 --- a/homeassistant/components/google/translations/hu.json +++ b/homeassistant/components/google/translations/hu.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A Google Napt\u00e1r konfigur\u00e1l\u00e1sa a configuration.yaml f\u00e1jlban a 2022.9-es Home Assistant verzi\u00f3ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 OAuth alkalmaz\u00e1s hiteles\u00edt\u0151 adatai \u00e9s hozz\u00e1f\u00e9r\u00e9si be\u00e1ll\u00edt\u00e1sai automatikusan import\u00e1l\u00e1sra ker\u00fcltek a felhaszn\u00e1l\u00f3i fel\u00fcletbe. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Google Calendar YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "removed_track_new_yaml": { + "description": "A configuration.yaml f\u00e1jlban a Google Calendar sz\u00e1m\u00e1ra az entit\u00e1sk\u00f6vet\u00e9s ki lett kapcsolva, ami m\u00e1r nem t\u00e1mogatott. Manu\u00e1lisan sz\u00fcks\u00e9ges m\u00f3dos\u00edtani az integr\u00e1ci\u00f3s rendszerbe\u00e1ll\u00edt\u00e1sokat a felhaszn\u00e1l\u00f3i fel\u00fcleten, hogy a j\u00f6v\u0151ben letiltsa az \u00fajonnan felfedezett entit\u00e1sokat. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a track_new be\u00e1ll\u00edt\u00e1st a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Google Napt\u00e1r entit\u00e1sk\u00f6vet\u00e9se megv\u00e1ltozott" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/no.json b/homeassistant/components/google/translations/no.json index 9842da0362c..55103db2a47 100644 --- a/homeassistant/components/google/translations/no.json +++ b/homeassistant/components/google/translations/no.json @@ -11,7 +11,8 @@ "invalid_access_token": "Ugyldig tilgangstoken", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "oauth_error": "Mottatt ugyldige token data.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "timeout_connect": "Tidsavbrudd oppretter forbindelse" }, "create_entry": { "default": "Vellykket godkjenning" @@ -32,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Google Kalender i configuration.yaml blir fjernet i Home Assistant 2022.9. \n\n Din eksisterende OAuth-applikasjonslegitimasjon og tilgangsinnstillinger er automatisk importert til brukergrensesnittet. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Google Kalender YAML-konfigurasjonen blir fjernet" + }, + "removed_track_new_yaml": { + "description": "Du har deaktivert enhetssporing for Google Kalender i configuration.yaml, som ikke lenger st\u00f8ttes. Du m\u00e5 manuelt endre integreringssystemalternativene i brukergrensesnittet for \u00e5 deaktivere nyoppdagede enheter fremover. Fjern track_new-innstillingen fra configuration.yaml og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Google Kalender-enhetssporing er endret" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google_travel_time/translations/sv.json b/homeassistant/components/google_travel_time/translations/sv.json index 1b8cc14bce7..fc9d03286e1 100644 --- a/homeassistant/components/google_travel_time/translations/sv.json +++ b/homeassistant/components/google_travel_time/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Kunde inte ansluta" }, @@ -8,8 +11,10 @@ "data": { "api_key": "API-nyckel", "destination": "Destination", + "name": "Namn", "origin": "Ursprung" - } + }, + "description": "N\u00e4r du anger ursprung och destination kan du ange en eller flera platser \u00e5tskilda av pipe, i form av en adress, latitud/longitudkoordinater eller ett plats-ID fr\u00e5n Google. N\u00e4r du anger platsen med hj\u00e4lp av ett plats-ID fr\u00e5n Google m\u00e5ste ID:t ha prefixet \"place_id:\"." } } }, @@ -19,10 +24,16 @@ "data": { "avoid": "Undvik", "language": "Spr\u00e5k", + "mode": "Resel\u00e4ge", "time": "Tid", + "time_type": "Tidstyp", + "transit_mode": "Transportmedel", + "transit_routing_preference": "Inst\u00e4llning f\u00f6r kollektivtrafik", "units": "Enheter" - } + }, + "description": "Du kan valfritt ange antingen en avg\u00e5ngstid eller ankomsttid. Om du anger en avg\u00e5ngstid kan du ange \"nu\", en Unix-tidsst\u00e4mpel eller en 24-timmars tidsstr\u00e4ng som \"08:00:00\". Om du anger en ankomsttid kan du anv\u00e4nda en Unix-tidsst\u00e4mpel eller en 24-timmars tidsstr\u00e4ng som \"08:00:00\"" } } - } + }, + "title": "Google Maps restid" } \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/no.json b/homeassistant/components/govee_ble/translations/no.json new file mode 100644 index 00000000000..4fd1e1d0c9d --- /dev/null +++ b/homeassistant/components/govee_ble/translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/sv.json b/homeassistant/components/gpslogger/translations/sv.json index 40b54557d80..f73ba63337d 100644 --- a/homeassistant/components/gpslogger/translations/sv.json +++ b/homeassistant/components/gpslogger/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i GPSLogger.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." }, diff --git a/homeassistant/components/gree/translations/sv.json b/homeassistant/components/gree/translations/sv.json new file mode 100644 index 00000000000..18a80850e45 --- /dev/null +++ b/homeassistant/components/gree/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/sv.json b/homeassistant/components/growatt_server/translations/sv.json index 23c825f256f..090e7f7ac15 100644 --- a/homeassistant/components/growatt_server/translations/sv.json +++ b/homeassistant/components/growatt_server/translations/sv.json @@ -1,11 +1,28 @@ { "config": { + "abort": { + "no_plants": "Inga v\u00e4xter har hittats p\u00e5 detta konto" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { + "plant": { + "data": { + "plant_id": "Anl\u00e4ggning" + }, + "title": "V\u00e4lj din anl\u00e4ggning" + }, "user": { "data": { + "name": "Namn", + "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Ange din Growatt-information" } } - } + }, + "title": "Growatt-server" } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/sv.json b/homeassistant/components/guardian/translations/sv.json new file mode 100644 index 00000000000..54fcf49904d --- /dev/null +++ b/homeassistant/components/guardian/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera denna Guardian-enhet?" + }, + "user": { + "data": { + "ip_address": "IP-adress", + "port": "Port" + }, + "description": "Konfigurera en lokal Elexa Guardian-enhet." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/sv.json b/homeassistant/components/habitica/translations/sv.json index f4a63bb449d..bb924d55b3c 100644 --- a/homeassistant/components/habitica/translations/sv.json +++ b/homeassistant/components/habitica/translations/sv.json @@ -1,10 +1,18 @@ { "config": { + "error": { + "invalid_credentials": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "api_user": "Habiticas API-anv\u00e4ndar-ID", + "name": "\u00d6verstyrning f\u00f6r Habiticas anv\u00e4ndarnamn. Kommer att anv\u00e4ndas f\u00f6r serviceanrop.", + "url": "URL" + }, + "description": "Anslut din Habitica-profil f\u00f6r att till\u00e5ta \u00f6vervakning av din anv\u00e4ndares profil och uppgifter. Observera att api_id och api_key m\u00e5ste h\u00e4mtas fr\u00e5n https://habitica.com/user/settings/api" } } } diff --git a/homeassistant/components/harmony/translations/sv.json b/homeassistant/components/harmony/translations/sv.json index 1f240051056..15d8b2c1dfa 100644 --- a/homeassistant/components/harmony/translations/sv.json +++ b/homeassistant/components/harmony/translations/sv.json @@ -7,20 +7,28 @@ "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name}", "step": { "link": { - "description": "Do vill du konfigurera {name} ({host})?" + "description": "Do vill du konfigurera {name} ({host})?", + "title": "Konfigurera Logitech Harmony Hub" }, "user": { "data": { - "host": "V\u00e4rdnamn eller IP-adress" - } + "host": "V\u00e4rdnamn eller IP-adress", + "name": "Namn p\u00e5 hubben" + }, + "title": "Konfigurera Logitech Harmony Hub" } } }, "options": { "step": { "init": { + "data": { + "activity": "Standardaktivitet som ska utf\u00f6ras n\u00e4r ingen aktivitet har angetts.", + "delay_secs": "F\u00f6rdr\u00f6jningen mellan att skicka kommandon." + }, "description": "Justera inst\u00e4llningarna f\u00f6r Harmony Hub" } } diff --git a/homeassistant/components/hassio/translations/sv.json b/homeassistant/components/hassio/translations/sv.json index 7d3d7684558..2fbad6d4915 100644 --- a/homeassistant/components/hassio/translations/sv.json +++ b/homeassistant/components/hassio/translations/sv.json @@ -1,7 +1,19 @@ { "system_health": { "info": { - "agent_version": "Agentversion" + "agent_version": "Agentversion", + "board": "Kort", + "disk_total": "Total disk", + "disk_used": "Disk som anv\u00e4nds", + "docker_version": "Docker-version", + "healthy": "Frisk", + "host_os": "V\u00e4rdens operativsystem", + "installed_addons": "Installerade till\u00e4gg", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor version", + "supported": "St\u00f6ds", + "update_channel": "Uppdatera kanal", + "version_api": "Version API" } } } \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/sv.json b/homeassistant/components/here_travel_time/translations/sv.json index bb0f36a448f..6d0085d9c0f 100644 --- a/homeassistant/components/here_travel_time/translations/sv.json +++ b/homeassistant/components/here_travel_time/translations/sv.json @@ -49,7 +49,8 @@ "user": { "data": { "api_key": "API-nyckel", - "mode": "Resel\u00e4ge" + "mode": "Resel\u00e4ge", + "name": "Namn" } } } diff --git a/homeassistant/components/hive/translations/hu.json b/homeassistant/components/hive/translations/hu.json index 8a265ff63c0..fa56592b9ab 100644 --- a/homeassistant/components/hive/translations/hu.json +++ b/homeassistant/components/hive/translations/hu.json @@ -41,7 +41,7 @@ "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Adja meg a Hive bejelentkez\u00e9si adatait \u00e9s konfigur\u00e1ci\u00f3j\u00e1t.", + "description": "Adja meg a Hive bejelentkez\u00e9si adatait.", "title": "Hive Bejelentkez\u00e9s" } } diff --git a/homeassistant/components/hive/translations/sv.json b/homeassistant/components/hive/translations/sv.json index 60b51beb78b..a80808c91df 100644 --- a/homeassistant/components/hive/translations/sv.json +++ b/homeassistant/components/hive/translations/sv.json @@ -1,9 +1,25 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown_entry": "Det gick inte att hitta befintlig post." + }, "error": { + "invalid_code": "Det gick inte att logga in p\u00e5 Hive. Din tv\u00e5faktorsautentiseringskod var felaktig.", + "invalid_password": "Det gick inte att logga in p\u00e5 Hive. Felaktigt l\u00f6senord. Var sn\u00e4ll och f\u00f6rs\u00f6k igen.", + "invalid_username": "Det gick inte att logga in p\u00e5 Hive. Din e-postadress k\u00e4nns inte igen.", + "no_internet_available": "En internetanslutning kr\u00e4vs f\u00f6r att ansluta till Hive.", "unknown": "Ov\u00e4ntat fel" }, "step": { + "2fa": { + "data": { + "2fa": "Tv\u00e5faktorskod" + }, + "description": "Ange din Hive-autentiseringskod. \n\n Ange kod 0000 f\u00f6r att beg\u00e4ra en annan kod.", + "title": "Hive tv\u00e5faktorsautentisering." + }, "configuration": { "data": { "device_name": "Enhetsnamn" @@ -15,13 +31,29 @@ "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange din Hive-inloggningsinformation igen.", + "title": "Hive-inloggning" }, "user": { "data": { "password": "L\u00f6senord", + "scan_interval": "Skanningsintervall (sekunder)", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange din Hive-inloggningsinformation.", + "title": "Hive-inloggning" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Skanningsintervall (sekunder)" + }, + "description": "Uppdatera skanningsintervallet f\u00f6r att polla efter data oftare.", + "title": "Alternativ f\u00f6r Hive" } } } diff --git a/homeassistant/components/hlk_sw16/translations/sv.json b/homeassistant/components/hlk_sw16/translations/sv.json index eba844f6c03..f85a02855d6 100644 --- a/homeassistant/components/hlk_sw16/translations/sv.json +++ b/homeassistant/components/hlk_sw16/translations/sv.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "host": "V\u00e4rd" + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" } } } diff --git a/homeassistant/components/home_connect/translations/sv.json b/homeassistant/components/home_connect/translations/sv.json new file mode 100644 index 00000000000..9f710ef442c --- /dev/null +++ b/homeassistant/components/home_connect/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/sv.json b/homeassistant/components/home_plus_control/translations/sv.json index 5307b489a72..3f43cd0559c 100644 --- a/homeassistant/components/home_plus_control/translations/sv.json +++ b/homeassistant/components/home_plus_control/translations/sv.json @@ -1,5 +1,16 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "create_entry": { + "default": "Autentiserats" + }, "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" diff --git a/homeassistant/components/homeassistant/translations/sv.json b/homeassistant/components/homeassistant/translations/sv.json index e4778a3a9e0..b0f67a5754e 100644 --- a/homeassistant/components/homeassistant/translations/sv.json +++ b/homeassistant/components/homeassistant/translations/sv.json @@ -1,7 +1,9 @@ { "system_health": { "info": { - "config_dir": "Konfigurationskatalog" + "config_dir": "Konfigurationskatalog", + "hassio": "Supervisor", + "user": "Anv\u00e4ndare" } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/hu.json b/homeassistant/components/homeassistant_alerts/translations/hu.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index be1e79453e8..250bb634736 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -5,12 +5,23 @@ "title": "Para HomeKit" }, "user": { + "data": { + "include_domains": "Dom\u00e4ner att inkludera" + }, + "description": "V\u00e4lj de dom\u00e4ner som ska inkluderas. Alla st\u00f6dda enheter i dom\u00e4nen kommer att inkluderas f\u00f6rutom kategoriserade enheter. En separat HomeKit-instans i tillbeh\u00f6rsl\u00e4ge kommer att skapas f\u00f6r varje tv-mediaspelare, aktivitetsbaserad fj\u00e4rrkontroll, l\u00e5s och kamera.", "title": "Aktivera HomeKit" } } }, "options": { "step": { + "advanced": { + "data": { + "devices": "Enheter (utl\u00f6sare)" + }, + "description": "Programmerbara switchar skapas f\u00f6r varje vald enhet. N\u00e4r en enhetsutl\u00f6sare utl\u00f6ses kan HomeKit konfigureras f\u00f6r att k\u00f6ra en automatisering eller scen.", + "title": "Avancerad konfiguration" + }, "cameras": { "data": { "camera_copy": "Kameror som st\u00f6der inbyggda H.264-str\u00f6mmar" @@ -19,7 +30,15 @@ "title": "V\u00e4lj kamerans videoavkodare." }, "init": { + "data": { + "mode": "HomeKit-l\u00e4ge" + }, + "description": "HomeKit kan konfigureras f\u00f6r att exponera en bro eller ett enskilt tillbeh\u00f6r. I tillbeh\u00f6rsl\u00e4get kan endast en enda enhet anv\u00e4ndas. Tillbeh\u00f6rsl\u00e4ge kr\u00e4vs f\u00f6r att mediaspelare med enhetsklassen TV ska fungera korrekt. Enheter i \"Dom\u00e4ner att inkludera\" kommer att inkluderas i HomeKit. Du kommer att kunna v\u00e4lja vilka enheter som ska inkluderas eller uteslutas fr\u00e5n listan p\u00e5 n\u00e4sta sk\u00e4rm.", "title": "V\u00e4lj dom\u00e4ner som ska inkluderas." + }, + "yaml": { + "description": "Denna post styrs via YAML", + "title": "Justera HomeKit-alternativ" } } } diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index c8f86e46f38..9bf7922e3e0 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -18,7 +18,7 @@ "unable_to_pair": "Nem siker\u00fclt p\u00e1ros\u00edtani, pr\u00f3b\u00e1ld \u00fajra.", "unknown_error": "Az eszk\u00f6z ismeretlen hib\u00e1t jelentett. A p\u00e1ros\u00edt\u00e1s sikertelen." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Sz\u00fcntesse meg a p\u00e1ros\u00edt\u00e1st az \u00f6sszes vez\u00e9rl\u0151n, vagy pr\u00f3b\u00e1lja \u00fajraind\u00edtani az eszk\u00f6zt, majd folytassa a p\u00e1ros\u00edt\u00e1st.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "P\u00e1ros\u00edt\u00e1s enged\u00e9lyez\u00e9se a nem biztons\u00e1gos be\u00e1ll\u00edt\u00e1si k\u00f3dokkal.", "pairing_code": "P\u00e1ros\u00edt\u00e1si k\u00f3d" }, - "description": "A HomeKit Controller {name} n\u00e9vvel kommunik\u00e1l a helyi h\u00e1l\u00f3zaton kereszt\u00fcl, biztons\u00e1gos titkos\u00edtott kapcsolaton kereszt\u00fcl, k\u00fcl\u00f6n HomeKit vez\u00e9rl\u0151 vagy iCloud n\u00e9lk\u00fcl. A tartoz\u00e9k haszn\u00e1lat\u00e1hoz adja meg HomeKit p\u00e1ros\u00edt\u00e1si k\u00f3dj\u00e1t (XXX-XX-XXX form\u00e1tumban). Ez a k\u00f3d \u00e1ltal\u00e1ban mag\u00e1ban az eszk\u00f6z\u00f6n vagy a csomagol\u00e1sban tal\u00e1lhat\u00f3.", + "description": "A HomeKit Controller {name} ({category}) n\u00e9vvel kommunik\u00e1l a helyi h\u00e1l\u00f3zaton kereszt\u00fcl, biztons\u00e1gos titkos\u00edtott kapcsolaton kereszt\u00fcl, k\u00fcl\u00f6n HomeKit vez\u00e9rl\u0151 vagy iCloud n\u00e9lk\u00fcl. Az eszk\u00f6z haszn\u00e1lat\u00e1hoz adja meg HomeKit p\u00e1ros\u00edt\u00e1si k\u00f3dj\u00e1t (XXX-XX-XXX form\u00e1tumban). Ez a k\u00f3d \u00e1ltal\u00e1ban mag\u00e1ban az eszk\u00f6z\u00f6n vagy a csomagol\u00e1sban tal\u00e1lhat\u00f3.", "title": "P\u00e1ros\u00edt\u00e1s egy eszk\u00f6zzel a HomeKit Accessory Protocol protokollon seg\u00edts\u00e9g\u00e9vel" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/sv.json b/homeassistant/components/homekit_controller/translations/sv.json index 348c0305e03..e5a05b62b84 100644 --- a/homeassistant/components/homekit_controller/translations/sv.json +++ b/homeassistant/components/homekit_controller/translations/sv.json @@ -7,6 +7,7 @@ "already_paired": "Det h\u00e4r tillbeh\u00f6ret \u00e4r redan kopplat till en annan enhet. \u00c5terst\u00e4ll tillbeh\u00f6ret och f\u00f6rs\u00f6k igen.", "ignored_model": "HomeKit-st\u00f6d f\u00f6r den h\u00e4r modellen blockeras eftersom en mer komplett inbyggd integration \u00e4r tillg\u00e4nglig.", "invalid_config_entry": "Den h\u00e4r enheten visas som redo att paras ihop, men det finns redan en motstridig konfigurations-post f\u00f6r den i Home Assistant som f\u00f6rst m\u00e5ste tas bort.", + "invalid_properties": "Ogiltiga egenskaper har meddelats av enheten.", "no_devices": "Inga oparade enheter kunde hittas" }, "error": { @@ -18,6 +19,13 @@ }, "flow_title": "HomeKit-tillbeh\u00f6r: {name}", "step": { + "busy_error": { + "description": "Avbryt ihopparningen p\u00e5 alla kontroller eller f\u00f6rs\u00f6k starta om enheten och forts\u00e4tt sedan f\u00f6r att \u00e5teruppta ihopparningen.", + "title": "Enheten paras redan med en annan styrenhet" + }, + "max_tries_error": { + "title": "Maximalt antal autentiseringsf\u00f6rs\u00f6k har \u00f6verskridits" + }, "pair": { "data": { "pairing_code": "Parningskod" @@ -25,6 +33,10 @@ "description": "HomeKit Controller kommunicerar med {name} \u00f6ver det lokala n\u00e4tverket med hj\u00e4lp av en s\u00e4ker krypterad anslutning utan en separat HomeKit-kontroller eller iCloud. Ange din HomeKit-kopplingskod (i formatet XXX-XX-XXX) f\u00f6r att anv\u00e4nda detta tillbeh\u00f6r. Denna kod finns vanligtvis p\u00e5 sj\u00e4lva enheten eller i f\u00f6rpackningen.", "title": "Para HomeKit-tillbeh\u00f6r" }, + "protocol_error": { + "description": "Enheten kanske inte \u00e4r i ihopparningsl\u00e4ge och kan kr\u00e4va en fysisk eller virtuell knapptryckning. Se till att enheten \u00e4r i ihopparningsl\u00e4ge eller f\u00f6rs\u00f6k starta om enheten och forts\u00e4tt sedan f\u00f6r att \u00e5teruppta ihopparningen.", + "title": "Fel vid kommunikation med tillbeh\u00f6ret" + }, "user": { "data": { "device": "Enhet" @@ -47,6 +59,11 @@ "button8": "Knapp 8", "button9": "Knapp 9", "doorbell": "D\u00f6rrklocka" + }, + "trigger_type": { + "double_press": "\" {subtype} \" tryckt tv\u00e5 g\u00e5nger", + "long_press": "\"{subtyp}\" tryckt och h\u00e5llen", + "single_press": "\" {subtype} \" tryckt" } }, "title": "HomeKit-tillbeh\u00f6r" diff --git a/homeassistant/components/huawei_lte/translations/sv.json b/homeassistant/components/huawei_lte/translations/sv.json index 83cbe335b2d..4efe112d468 100644 --- a/homeassistant/components/huawei_lte/translations/sv.json +++ b/homeassistant/components/huawei_lte/translations/sv.json @@ -7,10 +7,13 @@ "connection_timeout": "Timeout f\u00f6r anslutning", "incorrect_password": "Felaktigt l\u00f6senord", "incorrect_username": "Felaktigt anv\u00e4ndarnamn", + "invalid_auth": "Ogiltig autentisering", "invalid_url": "Ogiltig URL", "login_attempts_exceeded": "Maximala inloggningsf\u00f6rs\u00f6k har \u00f6verskridits, f\u00f6rs\u00f6k igen senare", - "response_error": "Ok\u00e4nt fel fr\u00e5n enheten" + "response_error": "Ok\u00e4nt fel fr\u00e5n enheten", + "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name}", "step": { "user": { "data": { @@ -27,7 +30,9 @@ "init": { "data": { "name": "Namn p\u00e5 meddelandetj\u00e4nsten (\u00e4ndring kr\u00e4ver omstart)", - "recipient": "Mottagare av SMS-meddelanden" + "recipient": "Mottagare av SMS-meddelanden", + "track_wired_clients": "Sp\u00e5ra tr\u00e5dbundna n\u00e4tverksklienter", + "unauthenticated_mode": "Oautentiserat l\u00e4ge (\u00e4ndring kr\u00e4ver omladdning)" } } } diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index c4687726357..1cf157f02e5 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -36,13 +36,26 @@ "button_4": "Fj\u00e4rde knappen", "dim_down": "Dimma ned", "dim_up": "Dimma upp", + "double_buttons_1_3": "F\u00f6rsta och tredje knapparna", + "double_buttons_2_4": "Andra och fj\u00e4rde knapparna", "turn_off": "St\u00e4ng av", "turn_on": "Starta" }, "trigger_type": { "remote_button_long_release": "\"{subtype}\" knappen sl\u00e4pptes efter ett l\u00e5ngt tryck", "remote_button_short_press": "\"{subtype}\" knappen nedtryckt", - "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt" + "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", + "remote_double_button_long_press": "B\u00e5da \"{subtype}\" sl\u00e4pptes efter en l\u00e5ngtryckning", + "remote_double_button_short_press": "B\u00e5da \"{subtyp}\" sl\u00e4pptes" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Till\u00e5t Hue-grupper" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/sv.json b/homeassistant/components/humidifier/translations/sv.json index 2818d7b7b04..a500d9dc18e 100644 --- a/homeassistant/components/humidifier/translations/sv.json +++ b/homeassistant/components/humidifier/translations/sv.json @@ -1,12 +1,28 @@ { "device_automation": { + "action_type": { + "set_humidity": "St\u00e4ll in luftfuktighet f\u00f6r {entity_name}", + "set_mode": "\u00c4ndra l\u00e4ge p\u00e5 {entity_name}", + "toggle": "V\u00e4xla {entity_name}", + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_mode": "{entity_name} \u00e4r inst\u00e4lld p\u00e5 ett specifikt l\u00e4ge", + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5" + }, "trigger_type": { - "turned_off": "{entity_name} st\u00e4ngdes av" + "target_humidity_changed": "{entity_name} m\u00e5lfuktighet har \u00e4ndrats", + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5" } }, "state": { "_": { + "off": "Av", "on": "P\u00e5" } - } + }, + "title": "Luftfuktare" } \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/sv.json b/homeassistant/components/hvv_departures/translations/sv.json index 3a2983d1035..c1621e16f2c 100644 --- a/homeassistant/components/hvv_departures/translations/sv.json +++ b/homeassistant/components/hvv_departures/translations/sv.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "no_results": "Inga resultat. F\u00f6rs\u00f6k med en annan station/adress" }, "step": { "user": { diff --git a/homeassistant/components/hyperion/translations/sv.json b/homeassistant/components/hyperion/translations/sv.json new file mode 100644 index 00000000000..56cee0c4ad2 --- /dev/null +++ b/homeassistant/components/hyperion/translations/sv.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "auth_new_token_not_granted_error": "Nyskapad token godk\u00e4ndes inte p\u00e5 Hyperion UI", + "auth_new_token_not_work_error": "Det gick inte att autentisera med nyskapad token", + "auth_required_error": "Det gick inte att avg\u00f6ra om auktorisering kr\u00e4vs", + "cannot_connect": "Det gick inte att ansluta.", + "no_id": "Hyperion Ambilight-instansen rapporterade inte sitt id", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel" + }, + "step": { + "auth": { + "data": { + "create_token": "Skapa ny token automatiskt", + "token": "Eller ange redan existerande token" + }, + "description": "Konfigurera auktorisering till din Hyperion Ambilight-server" + }, + "confirm": { + "title": "Bekr\u00e4fta till\u00e4gg av Hyperion Ambilight-tj\u00e4nst" + }, + "create_token": { + "description": "V\u00e4lj **Skicka** nedan f\u00f6r att beg\u00e4ra en ny autentiseringstoken. Du kommer att omdirigeras till Hyperion UI f\u00f6r att godk\u00e4nna beg\u00e4ran. Kontrollera att det visade id:t \u00e4r \" {auth_id} \"", + "title": "Skapa automatiskt ny autentiseringstoken" + }, + "create_token_external": { + "title": "Acceptera ny token i Hyperion UI" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "Hyperion-effekter att visa", + "priority": "Hyperion prioritet att anv\u00e4nda f\u00f6r f\u00e4rger och effekter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/sv.json b/homeassistant/components/ialarm/translations/sv.json new file mode 100644 index 00000000000..17bab09c61a --- /dev/null +++ b/homeassistant/components/ialarm/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index 2bba72d49df..5aa6e9574f8 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -19,7 +19,8 @@ "user": { "data": { "password": "L\u00f6senord", - "username": "Email" + "username": "Email", + "with_family": "Med familj" }, "description": "Ange dina autentiseringsuppgifter", "title": "iCloud-autentiseringsuppgifter" diff --git a/homeassistant/components/ifttt/translations/sv.json b/homeassistant/components/ifttt/translations/sv.json index 57837a63b15..16ec51d62c2 100644 --- a/homeassistant/components/ifttt/translations/sv.json +++ b/homeassistant/components/ifttt/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du anv\u00e4nda \u00e5tg\u00e4rden \"G\u00f6r en webbf\u00f6rfr\u00e5gan\" fr\u00e5n [IFTTT Webhook applet] ( {applet_url} ).\n\n Fyll i f\u00f6ljande information:\n \n - URL: ` {webhook_url} `\n - Metod: POST\n - Inneh\u00e5llstyp: application / json\n\n Se [dokumentationen] ( {docs_url} ) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." }, diff --git a/homeassistant/components/inkbird/translations/no.json b/homeassistant/components/inkbird/translations/no.json new file mode 100644 index 00000000000..3cf7f2b76c8 --- /dev/null +++ b/homeassistant/components/inkbird/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json index 6f3f3666b12..218b8d6d748 100644 --- a/homeassistant/components/insteon/translations/sv.json +++ b/homeassistant/components/insteon/translations/sv.json @@ -17,10 +17,51 @@ }, "options": { "step": { + "add_override": { + "data": { + "cat": "Enhetskategori (dvs. 0x10)", + "subcat": "Enhetsunderkategori (dvs. 0x0a)" + }, + "description": "L\u00e4gg till en enhets\u00e5sidos\u00e4ttning." + }, + "add_x10": { + "data": { + "housecode": "Huskod (a - p)", + "platform": "Plattform", + "steps": "Dimmersteg (endast f\u00f6r ljusanordningar, standardv\u00e4rde 22)", + "unitcode": "Enhetskod (1 - 16)" + }, + "description": "\u00c4ndra l\u00f6senordet f\u00f6r Insteon Hub." + }, "change_hub_config": { "data": { + "host": "IP-adress", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" + }, + "description": "\u00c4ndra Insteon Hub-anslutningsinformationen. Du m\u00e5ste starta om Home Assistant efter att ha gjort denna \u00e4ndring. Detta \u00e4ndrar inte konfigurationen av sj\u00e4lva hubben. Anv\u00e4nd Hub-appen f\u00f6r att \u00e4ndra konfigurationen i Hubben." + }, + "init": { + "data": { + "add_override": "L\u00e4gg till en enhets\u00e5sidos\u00e4ttning.", + "add_x10": "L\u00e4gg till en X10-enhet.", + "change_hub_config": "\u00c4ndra Hub-konfigurationen.", + "remove_override": "Ta bort en \u00e5sidos\u00e4ttning av en enhet.", + "remove_x10": "Ta bort en X10-enhet." } + }, + "remove_override": { + "data": { + "address": "V\u00e4lj en enhetsadress att ta bort" + }, + "description": "Ta bort en enhets\u00e5sidos\u00e4ttning" + }, + "remove_x10": { + "data": { + "address": "V\u00e4lj en enhetsadress att ta bort" + }, + "description": "Ta bort en X10-enhet" } } } diff --git a/homeassistant/components/integration/translations/sv.json b/homeassistant/components/integration/translations/sv.json index 4665a9b8816..e0c12be775f 100644 --- a/homeassistant/components/integration/translations/sv.json +++ b/homeassistant/components/integration/translations/sv.json @@ -18,5 +18,6 @@ } } } - } + }, + "title": "Integration - Riemann summa integral sensor" } \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/sv.json b/homeassistant/components/iotawatt/translations/sv.json index d1d69759ba6..500a65ce603 100644 --- a/homeassistant/components/iotawatt/translations/sv.json +++ b/homeassistant/components/iotawatt/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/ipma/translations/sv.json b/homeassistant/components/ipma/translations/sv.json index 1606c8cb5ed..908b8f91721 100644 --- a/homeassistant/components/ipma/translations/sv.json +++ b/homeassistant/components/ipma/translations/sv.json @@ -15,5 +15,10 @@ "title": "Location" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API-slutpunkt kan n\u00e5s" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/sv.json b/homeassistant/components/ipp/translations/sv.json index 70b270e63a2..2096cf5648c 100644 --- a/homeassistant/components/ipp/translations/sv.json +++ b/homeassistant/components/ipp/translations/sv.json @@ -2,12 +2,15 @@ "config": { "abort": { "already_configured": "Den h\u00e4r skrivaren \u00e4r redan konfigurerad.", + "cannot_connect": "Det gick inte att ansluta.", "connection_upgrade": "Misslyckades att ansluta till skrivaren d\u00e5 anslutningen beh\u00f6ver uppgraderas.", "ipp_error": "IPP-fel p\u00e5tr\u00e4ffades.", "ipp_version_error": "IPP versionen st\u00f6ds inte av skrivaren", - "parse_error": "Det gick inte att f\u00f6rst\u00e5 responsen fr\u00e5n skrivaren" + "parse_error": "Det gick inte att f\u00f6rst\u00e5 responsen fr\u00e5n skrivaren", + "unique_id_required": "Enheten saknar unik identifiering som kr\u00e4vs f\u00f6r uppt\u00e4ckt." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "connection_upgrade": "Kunde inte ansluta till skrivaren. F\u00f6rs\u00f6k igen med SSL/TLS alternativet ifyllt." }, "flow_title": "Skrivare: {name}", diff --git a/homeassistant/components/islamic_prayer_times/translations/sv.json b/homeassistant/components/islamic_prayer_times/translations/sv.json new file mode 100644 index 00000000000..f865c5a2c6a --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/sv.json b/homeassistant/components/isy994/translations/sv.json index bd14c77dd60..334c3e0fb0d 100644 --- a/homeassistant/components/isy994/translations/sv.json +++ b/homeassistant/components/isy994/translations/sv.json @@ -1,8 +1,16 @@ { "config": { - "error": { - "reauth_successful": "\u00c5terautentiseringen lyckades" + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_host": "V\u00e4rdposten var inte i fullst\u00e4ndigt URL-format, t.ex. http://192.168.10.100:80", + "reauth_successful": "\u00c5terautentiseringen lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({host})", "step": { "reauth_confirm": { "data": { @@ -14,8 +22,26 @@ }, "user": { "data": { + "host": "URL", + "password": "L\u00f6senord", + "tls": "TLS-versionen av ISY-styrenheten.", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4rdposten m\u00e5ste vara i fullst\u00e4ndigt URL-format, t.ex. http://192.168.10.100:80", + "title": "Anslut till din ISY" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ignorera str\u00e4ng", + "restore_light_state": "\u00c5terst\u00e4ll ljusstyrkan", + "sensor_string": "Nodsensorstr\u00e4ng" + }, + "description": "St\u00e4ll in alternativen f\u00f6r ISY-integration:\n \u2022 Nodsensorstr\u00e4ng: Alla enheter eller mappar som inneh\u00e5ller 'Nodsensorstr\u00e4ng' i namnet kommer att behandlas som en sensor eller bin\u00e4r sensor.\n \u2022 Ignorera str\u00e4ng: Alla enheter med 'Ignorera str\u00e4ng' i namnet kommer att ignoreras.\n \u2022 Variabel sensorstr\u00e4ng: Varje variabel som inneh\u00e5ller 'Variabel sensorstr\u00e4ng' kommer att l\u00e4ggas till som en sensor.\n \u2022 \u00c5terst\u00e4ll ljusstyrka: Om den \u00e4r aktiverad kommer den tidigare ljusstyrkan att \u00e5terst\u00e4llas n\u00e4r du sl\u00e5r p\u00e5 en lampa ist\u00e4llet f\u00f6r enhetens inbyggda On-Level.", + "title": "ISY alternativ" } } } diff --git a/homeassistant/components/keenetic_ndms2/translations/sv.json b/homeassistant/components/keenetic_ndms2/translations/sv.json new file mode 100644 index 00000000000..df37b421209 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + }, + "title": "Konfigurera Keenetic NDMS2 Router" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "include_arp": "Anv\u00e4nd ARP-data (ignoreras om hotspot-data anv\u00e4nds)", + "include_associated": "Anv\u00e4nd WiFi AP-associationsdata (ignoreras om hotspotdata anv\u00e4nds)", + "interfaces": "V\u00e4lj gr\u00e4nssnitt att skanna", + "try_hotspot": "Anv\u00e4nd \"ip hotspot\"-data (mest korrekt)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/sv.json b/homeassistant/components/kmtronic/translations/sv.json index a265d988aaa..8ec6cc04dc9 100644 --- a/homeassistant/components/kmtronic/translations/sv.json +++ b/homeassistant/components/kmtronic/translations/sv.json @@ -8,5 +8,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Logik f\u00f6r omv\u00e4nd omkoppling (anv\u00e4nd NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index 7c4d6ee8e61..f5986f966d0 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -41,7 +41,9 @@ "user_password": "Anv\u00e4ndarl\u00f6senord" }, "data_description": { - "device_authentication": "Detta st\u00e4lls in i 'IP'-panelen i gr\u00e4nssnittet i ETS." + "device_authentication": "Detta st\u00e4lls in i 'IP'-panelen i gr\u00e4nssnittet i ETS.", + "user_id": "Detta \u00e4r ofta tunnelnummer +1. S\u00e5 'Tunnel 2' skulle ha anv\u00e4ndar-ID '3'.", + "user_password": "L\u00f6senord f\u00f6r den specifika tunnelanslutningen som anges i panelen \"Egenskaper\" i tunneln i ETS." }, "description": "Ange din s\u00e4kra IP-information." }, diff --git a/homeassistant/components/konnected/translations/sv.json b/homeassistant/components/konnected/translations/sv.json index a96f612010a..3e236f05952 100644 --- a/homeassistant/components/konnected/translations/sv.json +++ b/homeassistant/components/konnected/translations/sv.json @@ -32,6 +32,7 @@ "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet" }, "error": { + "bad_host": "Ogiltig \u00e5sidos\u00e4tt API-v\u00e4rd-URL", "one": "Tom", "other": "Tomma" }, @@ -84,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "Blinka p\u00e5 panel-LED n\u00e4r du skickar tillst\u00e5nds\u00e4ndring" + "api_host": "\u00c5sidos\u00e4tt API-v\u00e4rdens URL", + "blink": "Blinka p\u00e5 panel-LED n\u00e4r du skickar tillst\u00e5nds\u00e4ndring", + "discovery": "Svara p\u00e5 uppt\u00e4cktsf\u00f6rfr\u00e5gningar i ditt n\u00e4tverk" }, "description": "V\u00e4lj \u00f6nskat beteende f\u00f6r din panel", "title": "Konfigurera \u00d6vrigt" @@ -93,6 +96,7 @@ "data": { "activation": "Utdata n\u00e4r den \u00e4r p\u00e5", "momentary": "Pulsvarighet (ms) (valfritt)", + "more_states": "Konfigurera ytterligare tillst\u00e5nd f\u00f6r denna zon", "name": "Namn (valfritt)", "pause": "Paus mellan pulser (ms) (valfritt)", "repeat": "G\u00e5nger att upprepa (-1=o\u00e4ndligt) (tillval)" diff --git a/homeassistant/components/kulersky/translations/sv.json b/homeassistant/components/kulersky/translations/sv.json new file mode 100644 index 00000000000..18a80850e45 --- /dev/null +++ b/homeassistant/components/kulersky/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/no.json b/homeassistant/components/lacrosse_view/translations/no.json new file mode 100644 index 00000000000..9cef140da52 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "no_locations": "Ingen steder funnet", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 33f615d00c7..1eec6f50643 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -29,7 +29,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd a [Life360 dokument\u00e1ci\u00f3]({docs_url}) c\u00edm\u0171 r\u00e9szt.\n \u00c9rdemes ezt megtenni a fi\u00f3kok hozz\u00e1ad\u00e1sa el\u0151tt.", - "title": "Life360 fi\u00f3kadatok" + "title": "Life360 fi\u00f3k be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index 1a5c7f2e569..ac104f24f39 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -2,14 +2,17 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", + "invalid_auth": "Ogiltig autentisering", "reauth_successful": "\u00c5terautentisering lyckades" }, "create_entry": { "default": "F\u00f6r att st\u00e4lla in avancerade alternativ, se [Life360 documentation]({docs_url})." }, "error": { + "already_configured": "Konto har redan konfigurerats", "cannot_connect": "Det gick inte att ansluta.", - "invalid_username": "Ogiltigt anv\u00e4ndarnmn" + "invalid_username": "Ogiltigt anv\u00e4ndarnmn", + "unknown": "Ov\u00e4ntat fel" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/lifx/translations/no.json b/homeassistant/components/lifx/translations/no.json index 4771b4c05d7..49ff5dea624 100644 --- a/homeassistant/components/lifx/translations/no.json +++ b/homeassistant/components/lifx/translations/no.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{label} ( {host} ) {serial}", "step": { "confirm": { "description": "\u00d8nsker du \u00e5 sette opp LIFX?" + }, + "discovery_confirm": { + "description": "Vil du sette opp {label} ( {host} ) {serial} ?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Hvis du lar verten st\u00e5 tom, vil oppdagelse bli brukt til \u00e5 finne enheter." } } } diff --git a/homeassistant/components/light/translations/sv.json b/homeassistant/components/light/translations/sv.json index d5f0bdaf767..1f32324d707 100644 --- a/homeassistant/components/light/translations/sv.json +++ b/homeassistant/components/light/translations/sv.json @@ -3,6 +3,7 @@ "action_type": { "brightness_decrease": "Minska ljusstyrkan f\u00f6r {entity_name}", "brightness_increase": "\u00d6ka ljusstyrkan f\u00f6r {entity_name}", + "flash": "Blinka {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/locative/translations/sv.json b/homeassistant/components/locative/translations/sv.json index 7f1991f8a7b..80da7825d40 100644 --- a/homeassistant/components/locative/translations/sv.json +++ b/homeassistant/components/locative/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Locative appen.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." }, diff --git a/homeassistant/components/lovelace/translations/sv.json b/homeassistant/components/lovelace/translations/sv.json index b622e036a0f..c0b5bf9f948 100644 --- a/homeassistant/components/lovelace/translations/sv.json +++ b/homeassistant/components/lovelace/translations/sv.json @@ -1,7 +1,8 @@ { "system_health": { "info": { - "dashboards": "Kontrollpaneler" + "dashboards": "Kontrollpaneler", + "views": "Vyer" } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/sv.json b/homeassistant/components/lutron_caseta/translations/sv.json new file mode 100644 index 00000000000..1d512e32d47 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/sv.json @@ -0,0 +1,36 @@ +{ + "device_automation": { + "trigger_subtype": { + "close_1": "St\u00e4ng 1", + "close_2": "St\u00e4ng 2", + "close_3": "St\u00e4ng 3", + "close_4": "St\u00e4ng 4", + "close_all": "St\u00e4ng alla", + "group_1_button_1": "F\u00f6rsta gruppen f\u00f6rsta knappen", + "group_1_button_2": "F\u00f6rsta gruppen andra knappen", + "group_2_button_1": "Andra gruppen f\u00f6rsta knappen", + "group_2_button_2": "Andra gruppen andra knappen", + "lower": "S\u00e4nk", + "lower_1": "S\u00e4nk 1", + "lower_2": "S\u00e4nk 2", + "lower_3": "S\u00e4nk 3", + "lower_4": "S\u00e4nk 4", + "lower_all": "S\u00e4nk alla", + "off": "Av", + "on": "P\u00e5", + "open_1": "\u00d6ppna 1", + "open_2": "\u00d6ppna 2", + "open_3": "\u00d6ppna 3", + "open_4": "\u00d6ppna 4", + "open_all": "\u00d6ppna alla", + "raise": "H\u00f6j", + "raise_1": "H\u00f6j 1", + "raise_2": "H\u00f6j 2", + "raise_3": "H\u00f6j 3", + "raise_4": "H\u00f6j 4", + "raise_all": "H\u00f6j alla", + "stop": "Stoppa (favorit)", + "stop_1": "Stoppa 1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/hu.json b/homeassistant/components/lyric/translations/hu.json index b7eac97525c..fe958e40ec9 100644 --- a/homeassistant/components/lyric/translations/hu.json +++ b/homeassistant/components/lyric/translations/hu.json @@ -17,5 +17,11 @@ "title": "Az integr\u00e1ci\u00f3 \u00fajb\u00f3li azonos\u00edt\u00e1sa" } } + }, + "issues": { + "removed_yaml": { + "description": "A Honeywell Lyric YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Honeywell Lyric YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/no.json b/homeassistant/components/lyric/translations/no.json index 537cc7fcced..e41e3a0c581 100644 --- a/homeassistant/components/lyric/translations/no.json +++ b/homeassistant/components/lyric/translations/no.json @@ -17,5 +17,11 @@ "title": "Godkjenne integrering p\u00e5 nytt" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Honeywell Lyric med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Honeywell Lyric YAML-konfigurasjonen er fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/sv.json b/homeassistant/components/lyric/translations/sv.json index a34370b5ce5..f4f52873240 100644 --- a/homeassistant/components/lyric/translations/sv.json +++ b/homeassistant/components/lyric/translations/sv.json @@ -1,4 +1,23 @@ { + "config": { + "abort": { + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "description": "Lyric-integrationen m\u00e5ste autentisera ditt konto igen.", + "title": "\u00c5terautenticera integration" + } + } + }, "issues": { "removed_yaml": { "description": "Konfigurering av Honeywell Lyric med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", diff --git a/homeassistant/components/mailgun/translations/sv.json b/homeassistant/components/mailgun/translations/sv.json index aec197c766a..3ca0139183c 100644 --- a/homeassistant/components/mailgun/translations/sv.json +++ b/homeassistant/components/mailgun/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [Webhooks med Mailgun]({mailgun_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n Se [dokumentationen]({docs_url}) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." }, diff --git a/homeassistant/components/mazda/translations/sv.json b/homeassistant/components/mazda/translations/sv.json new file mode 100644 index 00000000000..24f538688bd --- /dev/null +++ b/homeassistant/components/mazda/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "region": "Region" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/cs.json b/homeassistant/components/media_player/translations/cs.json index 19e88635f8b..0d4840027d1 100644 --- a/homeassistant/components/media_player/translations/cs.json +++ b/homeassistant/components/media_player/translations/cs.json @@ -8,6 +8,7 @@ "is_playing": "{entity_name} p\u0159ehr\u00e1v\u00e1" }, "trigger_type": { + "changed_states": "{entity_name} zm\u011bnil stavy", "paused": "{entity_name} je pozastaveno", "turned_on": "{entity_name} bylo zapnuto" } diff --git a/homeassistant/components/media_player/translations/sv.json b/homeassistant/components/media_player/translations/sv.json index 6aae6d6b208..159ff201fba 100644 --- a/homeassistant/components/media_player/translations/sv.json +++ b/homeassistant/components/media_player/translations/sv.json @@ -9,7 +9,9 @@ "is_playing": "{entity_name} spelar" }, "trigger_type": { - "buffering": "{entity_name} b\u00f6rjar buffra" + "buffering": "{entity_name} b\u00f6rjar buffra", + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5" } }, "state": { diff --git a/homeassistant/components/met_eireann/translations/sv.json b/homeassistant/components/met_eireann/translations/sv.json index 80cb6773677..754fa336a08 100644 --- a/homeassistant/components/met_eireann/translations/sv.json +++ b/homeassistant/components/met_eireann/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/meteo_france/translations/sv.json b/homeassistant/components/meteo_france/translations/sv.json index f7f1a68478f..53819baddad 100644 --- a/homeassistant/components/meteo_france/translations/sv.json +++ b/homeassistant/components/meteo_france/translations/sv.json @@ -4,11 +4,15 @@ "already_configured": "Staden har redan konfigurerats", "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare" }, + "error": { + "empty": "Inget resultat i stadss\u00f6kning: kontrollera stadsf\u00e4ltet" + }, "step": { "cities": { "data": { "city": "Stad" - } + }, + "description": "V\u00e4lj din stad fr\u00e5n listan" }, "user": { "data": { @@ -17,5 +21,14 @@ "description": "Ange postnumret (endast f\u00f6r Frankrike, rekommenderat) eller ortsnamn" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Prognosl\u00e4ge" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/hu.json b/homeassistant/components/miflora/translations/hu.json new file mode 100644 index 00000000000..c998a98e9ad --- /dev/null +++ b/homeassistant/components/miflora/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A Mi Flora integr\u00e1ci\u00f3 a 2022.7-es Home Assistantban le\u00e1llt, \u00e9s a 2022.8-as kiad\u00e1sban a Xiaomi BLE integr\u00e1ci\u00f3val v\u00e1ltotta fel.\n\nNincs lehet\u0151s\u00e9g migr\u00e1ci\u00f3ra, ez\u00e9rt manu\u00e1lisan kell be\u00e1ll\u00edtani a Mi Flora eszk\u00f6zt az \u00faj integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1val.\n\nA megl\u00e9v\u0151 Mi Flora YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant m\u00e1r nem haszn\u00e1lja. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Mi Flora integr\u00e1ci\u00f3 lecser\u00e9l\u0151d\u00f6tt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/no.json b/homeassistant/components/miflora/translations/no.json new file mode 100644 index 00000000000..e7e69709307 --- /dev/null +++ b/homeassistant/components/miflora/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Mi Flora-integrasjonen sluttet \u00e5 fungere i Home Assistant 2022.7 og erstattet av Xiaomi BLE-integrasjonen i 2022.8-utgivelsen. \n\n Det er ingen migreringsbane mulig, derfor m\u00e5 du legge til Mi Flora-enheten ved \u00e5 bruke den nye integrasjonen manuelt. \n\n Din eksisterende Mi Flora YAML-konfigurasjon brukes ikke lenger av Home Assistant. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Mi Flora-integrasjonen er erstattet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/sv.json b/homeassistant/components/mill/translations/sv.json index 8cef92a32b9..5c7362eeb7b 100644 --- a/homeassistant/components/mill/translations/sv.json +++ b/homeassistant/components/mill/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "cloud": { "data": { diff --git a/homeassistant/components/mitemp_bt/translations/hu.json b/homeassistant/components/mitemp_bt/translations/hu.json new file mode 100644 index 00000000000..7e6b52a25ee --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja nem le\u00e1llt a 2022.7-es Home Assistantban, \u00e9s a 2022.8-as kiad\u00e1sban a Xiaomi BLE integr\u00e1ci\u00f3val v\u00e1ltott\u00e1k fel.\n\nNincs lehet\u0151s\u00e9g migr\u00e1ci\u00f3ra, ez\u00e9rt a Xiaomi Mijia BLE eszk\u00f6zt az \u00faj integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1val manu\u00e1lisan \u00fajra be kell \u00e1ll\u00edtani.\n\nA megl\u00e9v\u0151 Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom \u00e9rz\u00e9kel\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant m\u00e1r nem haszn\u00e1lja. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja lecser\u00e9l\u0151d\u00f6tt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/no.json b/homeassistant/components/mitemp_bt/translations/no.json new file mode 100644 index 00000000000..248f44b29af --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Xiaomi Mijia BLE temperatur- og fuktighetssensorintegrasjonen sluttet \u00e5 fungere i Home Assistant 2022.7 og ble erstattet av Xiaomi BLE-integrasjonen i 2022.8-utgivelsen. \n\n Det er ingen migreringsbane mulig, derfor m\u00e5 du legge til Xiaomi Mijia BLE-enheten ved \u00e5 bruke den nye integrasjonen manuelt. \n\n Din eksisterende Xiaomi Mijia BLE temperatur- og fuktighetssensor YAML-konfigurasjon brukes ikke lenger av Home Assistant. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Xiaomi Mijia BLE temperatur- og fuktighetssensorintegrasjon er erstattet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/cs.json b/homeassistant/components/mjpeg/translations/cs.json index 3fc5acfe8dd..9c6676509bf 100644 --- a/homeassistant/components/mjpeg/translations/cs.json +++ b/homeassistant/components/mjpeg/translations/cs.json @@ -4,6 +4,7 @@ "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" }, "step": { diff --git a/homeassistant/components/moat/translations/no.json b/homeassistant/components/moat/translations/no.json new file mode 100644 index 00000000000..bce03ad33d7 --- /dev/null +++ b/homeassistant/components/moat/translations/no.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/sv.json b/homeassistant/components/mobile_app/translations/sv.json index 68473a92763..4fd209e10cf 100644 --- a/homeassistant/components/mobile_app/translations/sv.json +++ b/homeassistant/components/mobile_app/translations/sv.json @@ -8,5 +8,6 @@ "description": "Vill du konfigurera komponenten Mobile App?" } } - } + }, + "title": "Mobilapp" } \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/sv.json b/homeassistant/components/modem_callerid/translations/sv.json new file mode 100644 index 00000000000..584cc33c277 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga \u00e5terst\u00e5ende enheter hittades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "usb_confirm": { + "description": "Detta \u00e4r en integration f\u00f6r fasta samtal med ett CX93001 r\u00f6stmodem. Detta kan h\u00e4mta nummerpresentationsinformation med ett alternativ att avvisa ett inkommande samtal." + }, + "user": { + "data": { + "name": "Namn", + "port": "Port" + }, + "description": "Detta \u00e4r en integration f\u00f6r fasta samtal med ett CX93001 r\u00f6stmodem. Detta kan h\u00e4mta nummerpresentationsinformation med ett alternativ att avvisa ett inkommande samtal." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/sv.json b/homeassistant/components/monoprice/translations/sv.json index 95b7bce9e8c..c23ee9a02e2 100644 --- a/homeassistant/components/monoprice/translations/sv.json +++ b/homeassistant/components/monoprice/translations/sv.json @@ -10,10 +10,31 @@ "step": { "user": { "data": { - "port": "Port" + "port": "Port", + "source_1": "Namn p\u00e5 k\u00e4lla #1", + "source_2": "Namn p\u00e5 k\u00e4lla #2", + "source_3": "Namn p\u00e5 k\u00e4lla #3", + "source_4": "Namn p\u00e5 k\u00e4lla #4", + "source_5": "Namn p\u00e5 k\u00e4lla #5", + "source_6": "Namn p\u00e5 k\u00e4lla #6" }, "title": "Anslut till enheten" } } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Namn p\u00e5 k\u00e4lla #1", + "source_2": "Namn p\u00e5 k\u00e4lla #2", + "source_3": "Namn p\u00e5 k\u00e4lla #3", + "source_4": "Namn p\u00e5 k\u00e4lla #4", + "source_5": "Namn p\u00e5 k\u00e4lla #5", + "source_6": "Namn p\u00e5 k\u00e4lla #6" + }, + "title": "Konfigurera k\u00e4llor" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/sv.json b/homeassistant/components/motion_blinds/translations/sv.json index eee10b4c765..5cee7f232c8 100644 --- a/homeassistant/components/motion_blinds/translations/sv.json +++ b/homeassistant/components/motion_blinds/translations/sv.json @@ -3,11 +3,21 @@ "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad" }, + "error": { + "discovery_error": "Det gick inte att uppt\u00e4cka en Motion Gateway" + }, "step": { "connect": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Du beh\u00f6ver en API-nyckel p\u00e5 16 tecken, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key f\u00f6r instruktioner" + }, + "select": { + "data": { + "select_ip": "IP-adress" + }, + "description": "K\u00f6r installationen igen om du vill ansluta ytterligare Motion Gateways" } } } diff --git a/homeassistant/components/motioneye/translations/sv.json b/homeassistant/components/motioneye/translations/sv.json index 8fd6e00680b..75bf4f2e2f9 100644 --- a/homeassistant/components/motioneye/translations/sv.json +++ b/homeassistant/components/motioneye/translations/sv.json @@ -7,5 +7,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Konfigurera motionEye webhooks f\u00f6r att rapportera h\u00e4ndelser till Home Assistant", + "webhook_set_overwrite": "Skriv \u00f6ver ok\u00e4nda webhooks" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 1699051db86..30acc3c1098 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "single_instance_allowed": "Endast en enda konfiguration av MQTT \u00e4r till\u00e5ten." }, "error": { @@ -49,11 +50,31 @@ } }, "options": { + "error": { + "bad_birth": "Ogiltigt f\u00f6delse\u00e4mne.", + "bad_will": "Ogiltigt testamente.", + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "broker": { "data": { + "broker": "Broker", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker." + }, + "options": { + "data": { + "birth_enable": "Aktivera f\u00f6delsemeddelande", + "birth_payload": "Nyttolast f\u00f6r f\u00f6delsemeddelande", + "birth_qos": "F\u00f6delsemeddelande QoS", + "birth_retain": "F\u00f6delsemeddelande beh\u00e5lls", + "birth_topic": "\u00c4mne f\u00f6r f\u00f6delsemeddelande", + "discovery": "Aktivera uppt\u00e4ckt" + }, + "description": "Uppt\u00e4ckt - Om uppt\u00e4ckt \u00e4r aktiverat (rekommenderas), kommer Home Assistant automatiskt att uppt\u00e4cka enheter och enheter som publicerar sin konfiguration p\u00e5 MQTT-brokern. Om uppt\u00e4ckten \u00e4r inaktiverad m\u00e5ste all konfiguration g\u00f6ras manuellt.\n F\u00f6delsemeddelande - F\u00f6delsemeddelandet kommer att skickas varje g\u00e5ng Home Assistant (\u00e5ter)ansluter till MQTT-brokern.\n Kommer meddelande - Viljemeddelandet kommer att skickas varje g\u00e5ng Home Assistant f\u00f6rlorar sin anslutning till brokern, b\u00e5de i h\u00e4ndelse av en reng\u00f6ring (t.ex. Home Assistant st\u00e4ngs av) och i h\u00e4ndelse av en oren (t.ex. Home Assistant kraschar eller f\u00f6rlorar sin n\u00e4tverksanslutning) koppla ifr\u00e5n." } } } diff --git a/homeassistant/components/mutesync/translations/sv.json b/homeassistant/components/mutesync/translations/sv.json new file mode 100644 index 00000000000..be78d104856 --- /dev/null +++ b/homeassistant/components/mutesync/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Aktivera autentisering i m\u00fctesync-inst\u00e4llningar > Autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/sv.json b/homeassistant/components/myq/translations/sv.json index da06f32aa92..dfe61d049d0 100644 --- a/homeassistant/components/myq/translations/sv.json +++ b/homeassistant/components/myq/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tj\u00e4nsten har redan konfigurerats" + "already_configured": "Tj\u00e4nsten har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Anslutningen misslyckades", @@ -9,6 +10,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "L\u00f6senordet f\u00f6r {username} \u00e4r inte l\u00e4ngre giltigt.", + "title": "Autentisera ditt MyQ-konto igen" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/mysensors/translations/sv.json b/homeassistant/components/mysensors/translations/sv.json index fbbcbdff5e6..daebe65e30e 100644 --- a/homeassistant/components/mysensors/translations/sv.json +++ b/homeassistant/components/mysensors/translations/sv.json @@ -1,7 +1,37 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "duplicate_persistence_file": "Persistensfil anv\u00e4nds redan", + "duplicate_topic": "\u00c4mnet anv\u00e4nds redan", + "invalid_auth": "Ogiltig autentisering", + "invalid_device": "Ogiltig enhet", + "invalid_ip": "Ogiltig IP-adress", + "invalid_persistence_file": "Ogiltig persistensfil", + "invalid_port": "Ogiltigt portnummer", + "invalid_publish_topic": "Ogiltigt \u00e4mne f\u00f6r publicering", + "invalid_serial": "Ogiltig serieport", + "invalid_subscribe_topic": "Ogiltigt \u00e4mne f\u00f6r prenumeration", + "invalid_version": "Ogiltig version av MySensors", + "not_a_number": "Ange ett nummer", + "port_out_of_range": "Portnummer m\u00e5ste vara minst 1 och h\u00f6gst 65535", + "same_topic": "\u00c4mnen f\u00f6r prenumeration och publicering \u00e4r desamma", + "unknown": "Ov\u00e4ntat fel" + }, "error": { - "invalid_serial": "Ogiltig serieport" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "duplicate_persistence_file": "Persistensfil anv\u00e4nds redan", + "duplicate_topic": "\u00c4mnet anv\u00e4nds redan", + "invalid_auth": "Ogiltig autentisering", + "invalid_device": "Ogiltig enhet", + "invalid_ip": "Ogiltig IP-adress", + "invalid_persistence_file": "Ogiltig persistensfil", + "invalid_port": "Ogiltigt portnummer", + "invalid_publish_topic": "Ogiltigt \u00e4mne f\u00f6r publicering", + "invalid_serial": "Ogiltig serieport", + "mqtt_required": "MQTT-integrationen \u00e4r inte konfigurerad" } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/sv.json b/homeassistant/components/nanoleaf/translations/sv.json index 4ca6ad5c3de..bbea90511da 100644 --- a/homeassistant/components/nanoleaf/translations/sv.json +++ b/homeassistant/components/nanoleaf/translations/sv.json @@ -1,7 +1,28 @@ { "config": { "abort": { - "reauth_successful": "\u00c5terautentisering lyckades" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_token": "Ogiltig \u00e5tkomstnyckel", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "not_allowing_new_tokens": "Nanoleaf till\u00e5ter inte nya tokens, f\u00f6lj instruktionerna ovan.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", + "step": { + "link": { + "description": "Tryck och h\u00e5ll in str\u00f6mbrytaren p\u00e5 din Nanoleaf i 5 sekunder tills knappens lysdioder b\u00f6rjar blinka, klicka sedan p\u00e5 **Skicka** inom 30 sekunder.", + "title": "L\u00e4nka Nanoleaf" + }, + "user": { + "data": { + "host": "V\u00e4rd" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/sv.json b/homeassistant/components/neato/translations/sv.json index 71f24f595e5..f69c0aecfd1 100644 --- a/homeassistant/components/neato/translations/sv.json +++ b/homeassistant/components/neato/translations/sv.json @@ -1,10 +1,22 @@ { "config": { "abort": { - "already_configured": "Redan konfigurerad" + "already_configured": "Redan konfigurerad", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "reauth_successful": "\u00c5terautentisering lyckades" }, "create_entry": { "default": "Se [Neato-dokumentation]({docs_url})." + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "title": "Vill du starta konfigurationen?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 750a7643ee2..5cc9d3d68c0 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -5,7 +5,8 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", - "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress." + "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "internal_error": "Internt fel vid validering av kod", @@ -52,13 +53,19 @@ }, "description": "F\u00f6r att l\u00e4nka ditt Nest-konto, [autentisiera ditt konto]({url}). \n\nEfter autentisiering, klipp och klistra in den angivna pin-koden nedan.", "title": "L\u00e4nka Nest-konto" + }, + "reauth_confirm": { + "description": "Nest-integreringen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" } } }, "device_automation": { "trigger_type": { "camera_motion": "R\u00f6relse uppt\u00e4ckt", - "camera_person": "Person detekterad" + "camera_person": "Person detekterad", + "camera_sound": "Ljud uppt\u00e4ckt", + "doorbell_chime": "D\u00f6rrklockan tryckt" } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index 32dfd2db6a0..2fe3a86b0c8 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -1,12 +1,48 @@ { "config": { + "abort": { + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "create_entry": { "default": "Autentiserad med Netatmo." + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } } }, "device_automation": { "trigger_subtype": { "schedule": "schema" } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Namn p\u00e5 omr\u00e5det", + "lat_ne": "Latitud Nord\u00f6stra h\u00f6rnet", + "lat_sw": "Latitud Sydv\u00e4stra h\u00f6rnet", + "lon_ne": "Longitud Nord\u00f6stra h\u00f6rnet", + "lon_sw": "Longitud Sydv\u00e4stra h\u00f6rnet", + "mode": "Ber\u00e4kning", + "show_on_map": "Visa p\u00e5 karta" + }, + "description": "Konfigurera en offentlig v\u00e4dersensor f\u00f6r ett omr\u00e5de.", + "title": "Netatmo offentlig v\u00e4dersensor" + }, + "public_weather_areas": { + "data": { + "new_area": "Omr\u00e5desnamn", + "weather_areas": "V\u00e4deromr\u00e5den" + }, + "description": "Konfigurera offentliga v\u00e4dersensorer.", + "title": "Netatmo offentlig v\u00e4dersensor" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/sv.json b/homeassistant/components/nexia/translations/sv.json index 9cfd620ac73..b00dc6e93b2 100644 --- a/homeassistant/components/nexia/translations/sv.json +++ b/homeassistant/components/nexia/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "invalid_auth": "Ogiltig autentisering", diff --git a/homeassistant/components/nfandroidtv/translations/sv.json b/homeassistant/components/nfandroidtv/translations/sv.json new file mode 100644 index 00000000000..c832c7a8880 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/sv.json b/homeassistant/components/nightscout/translations/sv.json new file mode 100644 index 00000000000..d51243a77f1 --- /dev/null +++ b/homeassistant/components/nightscout/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/sv.json b/homeassistant/components/nmap_tracker/translations/sv.json new file mode 100644 index 00000000000..b69cd27fe52 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/sv.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "consider_home": "Sekunder att v\u00e4nta tills en enhetssp\u00e5rare markeras som inte hemma efter att den inte setts.", + "interval_seconds": "Skanningsintervall" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/sv.json b/homeassistant/components/notion/translations/sv.json index 71836f79877..b565872aacc 100644 --- a/homeassistant/components/notion/translations/sv.json +++ b/homeassistant/components/notion/translations/sv.json @@ -1,9 +1,20 @@ { "config": { "abort": { - "already_configured": "Det h\u00e4r anv\u00e4ndarnamnet anv\u00e4nds redan." + "already_configured": "Det h\u00e4r anv\u00e4ndarnamnet anv\u00e4nds redan.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Skriv om l\u00f6senordet f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/nuheat/translations/sv.json b/homeassistant/components/nuheat/translations/sv.json index 327bdf8c4ca..ffe743f1f14 100644 --- a/homeassistant/components/nuheat/translations/sv.json +++ b/homeassistant/components/nuheat/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "invalid_auth": "Ogiltig autentisering", @@ -13,7 +16,8 @@ "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." + "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.", + "title": "Anslut till NuHeat" } } } diff --git a/homeassistant/components/nuki/translations/sv.json b/homeassistant/components/nuki/translations/sv.json index 563e2d4a773..9a4ec6946b7 100644 --- a/homeassistant/components/nuki/translations/sv.json +++ b/homeassistant/components/nuki/translations/sv.json @@ -1,10 +1,22 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { "token": "\u00c5tkomstnyckel" } + }, + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port", + "token": "\u00c5tkomstnyckel" + } } } } diff --git a/homeassistant/components/nut/translations/sv.json b/homeassistant/components/nut/translations/sv.json index 9a3cb690744..5af0dd86fc4 100644 --- a/homeassistant/components/nut/translations/sv.json +++ b/homeassistant/components/nut/translations/sv.json @@ -20,7 +20,8 @@ "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till NUT-servern" } } }, diff --git a/homeassistant/components/nws/translations/sv.json b/homeassistant/components/nws/translations/sv.json index 5ce9a828148..b695f12a694 100644 --- a/homeassistant/components/nws/translations/sv.json +++ b/homeassistant/components/nws/translations/sv.json @@ -12,8 +12,11 @@ "data": { "api_key": "API nyckel", "latitude": "Latitud", - "longitude": "Longitud" - } + "longitude": "Longitud", + "station": "METAR stationskod" + }, + "description": "Om en METAR-stationskod inte anges, kommer latitud och longitud att anv\u00e4ndas f\u00f6r att hitta den n\u00e4rmaste stationen. F\u00f6r n\u00e4rvarande kan en API-nyckel vara vad som helst. Det rekommenderas att anv\u00e4nda en giltig e-postadress.", + "title": "Anslut till den nationella v\u00e4dertj\u00e4nsten" } } } diff --git a/homeassistant/components/nzbget/translations/sv.json b/homeassistant/components/nzbget/translations/sv.json index 23c825f256f..0e50fcbbe00 100644 --- a/homeassistant/components/nzbget/translations/sv.json +++ b/homeassistant/components/nzbget/translations/sv.json @@ -1,9 +1,33 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", "step": { "user": { "data": { - "username": "Anv\u00e4ndarnamn" + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "title": "Anslut till NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsfrekvens (sekunder)" } } } diff --git a/homeassistant/components/omnilogic/translations/sv.json b/homeassistant/components/omnilogic/translations/sv.json index 23c825f256f..70e9ad8a483 100644 --- a/homeassistant/components/omnilogic/translations/sv.json +++ b/homeassistant/components/omnilogic/translations/sv.json @@ -7,5 +7,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "ph_offset": "pH-f\u00f6rskjutning (positiv eller negativ)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/sv.json b/homeassistant/components/onvif/translations/sv.json index 2cc40c1e465..579922f31e6 100644 --- a/homeassistant/components/onvif/translations/sv.json +++ b/homeassistant/components/onvif/translations/sv.json @@ -1,14 +1,53 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_h264": "Det fanns inga tillg\u00e4ngliga H264-str\u00f6mmar. Kontrollera profilkonfigurationen p\u00e5 din enhet.", + "no_mac": "Det gick inte att konfigurera unikt ID f\u00f6r ONVIF-enhet.", + "onvif_error": "Problem med att konfigurera ONVIF enheten. Kolla loggarna f\u00f6r mer information." + }, "step": { "configure": { "data": { + "host": "V\u00e4rd", + "name": "Namn", "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Konfigurera ONVIF-enhet" }, "configure_profile": { + "data": { + "include": "Skapa kameraentitet" + }, + "description": "Skapa kameraentitet f\u00f6r {profile} i {resolution}-uppl\u00f6sning?", "title": "Konfigurera Profiler" + }, + "device": { + "data": { + "host": "V\u00e4lj uppt\u00e4ckt ONVIF enhet" + }, + "title": "V\u00e4lj ONVIF enhet" + }, + "user": { + "data": { + "auto": "S\u00f6k automatiskt" + }, + "description": "Genom att klicka p\u00e5 skicka kommer vi att s\u00f6ka i ditt n\u00e4tverk efter ONVIF-enheter som st\u00f6der Profile S. \n\n Vissa tillverkare har b\u00f6rjat inaktivera ONVIF som standard. Se till att ONVIF \u00e4r aktiverat i din kameras konfiguration.", + "title": "Inst\u00e4llning av ONVIF-enhet" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Extra FFMPEG-argument", + "rtsp_transport": "RTSP transportmekanism" + }, + "title": "ONVIF enhetsalternativ" } } } diff --git a/homeassistant/components/openalpr_local/translations/hu.json b/homeassistant/components/openalpr_local/translations/hu.json new file mode 100644 index 00000000000..30232ae48cb --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Az OpenALPR Local integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a 2022.10-es Home Assistant-t\u00f3l kezdve nem lesz el\u00e9rhet\u0151.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az OpenALPR Local integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/sv.json b/homeassistant/components/opengarage/translations/sv.json index eba844f6c03..1b5b204fc99 100644 --- a/homeassistant/components/opengarage/translations/sv.json +++ b/homeassistant/components/opengarage/translations/sv.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "host": "V\u00e4rd" + "device_key": "Enhetsnyckel", + "host": "V\u00e4rd", + "port": "Port", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/opentherm_gw/translations/el.json b/homeassistant/components/opentherm_gw/translations/el.json index 82be1ff6ce9..9db15d34e49 100644 --- a/homeassistant/components/opentherm_gw/translations/el.json +++ b/homeassistant/components/opentherm_gw/translations/el.json @@ -3,7 +3,8 @@ "error": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "id_exists": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03cd\u03bb\u03b7\u03c2 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7" + "id_exists": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03cd\u03bb\u03b7\u03c2 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/sv.json b/homeassistant/components/opentherm_gw/translations/sv.json index 8ba8716897f..95ed092fece 100644 --- a/homeassistant/components/opentherm_gw/translations/sv.json +++ b/homeassistant/components/opentherm_gw/translations/sv.json @@ -18,7 +18,10 @@ "step": { "init": { "data": { - "floor_temperature": "Golvetemperatur" + "floor_temperature": "Golvetemperatur", + "read_precision": "L\u00e4sprecision", + "set_precision": "St\u00e4ll in Precision", + "temporary_override_mode": "Tempor\u00e4rt l\u00e4ge f\u00f6r \u00e5sidos\u00e4ttande av inst\u00e4llningsv\u00e4rde" } } } diff --git a/homeassistant/components/openuv/translations/sv.json b/homeassistant/components/openuv/translations/sv.json index 0df9a0434ab..9f1620fb980 100644 --- a/homeassistant/components/openuv/translations/sv.json +++ b/homeassistant/components/openuv/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, "error": { "invalid_api_key": "Ogiltigt API-l\u00f6senord" }, @@ -14,5 +17,16 @@ "title": "Fyll i dina uppgifter" } } + }, + "options": { + "step": { + "init": { + "data": { + "from_window": "Startande UV-index f\u00f6r skyddsf\u00f6nstret", + "to_window": "Slutande UV-index f\u00f6r skyddsf\u00f6nstret" + }, + "title": "Konfigurera OpenUV" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json index c0fdf3bbfdb..64212920aa7 100644 --- a/homeassistant/components/openweathermap/translations/sv.json +++ b/homeassistant/components/openweathermap/translations/sv.json @@ -3,14 +3,20 @@ "abort": { "already_configured": "OpenWeatherMap-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { "api_key": "OpenWeatherMap API-nyckel", "language": "Spr\u00e5k", + "latitude": "Latitud", + "longitude": "Longitud", "mode": "L\u00e4ge", "name": "Integrationens namn" - } + }, + "description": "F\u00f6r att generera API-nyckel g\u00e5 till https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/ovo_energy/translations/sv.json b/homeassistant/components/ovo_energy/translations/sv.json index 054280346d3..bd443851b14 100644 --- a/homeassistant/components/ovo_energy/translations/sv.json +++ b/homeassistant/components/ovo_energy/translations/sv.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "already_configured": "Konto har redan konfigurerats", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/owntracks/translations/sv.json b/homeassistant/components/owntracks/translations/sv.json index fd2162f153b..3c9146e49f2 100644 --- a/homeassistant/components/owntracks/translations/sv.json +++ b/homeassistant/components/owntracks/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "create_entry": { "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: ``\n - Enhets-ID: `` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." }, diff --git a/homeassistant/components/panasonic_viera/translations/sv.json b/homeassistant/components/panasonic_viera/translations/sv.json index 2c863b587ec..326c8c57dc5 100644 --- a/homeassistant/components/panasonic_viera/translations/sv.json +++ b/homeassistant/components/panasonic_viera/translations/sv.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade. Kontrollera loggarna f\u00f6r mer information." }, + "error": { + "invalid_pin_code": "Pin-kod du angav \u00e4r ogiltig" + }, "step": { "pairing": { "data": { diff --git a/homeassistant/components/philips_js/translations/sv.json b/homeassistant/components/philips_js/translations/sv.json index 418a59f0bdc..47b456c1ff7 100644 --- a/homeassistant/components/philips_js/translations/sv.json +++ b/homeassistant/components/philips_js/translations/sv.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "invalid_pin": "Ogiltig PIN-kod" + "cannot_connect": "Det gick inte att ansluta.", + "invalid_pin": "Ogiltig PIN-kod", + "unknown": "Ov\u00e4ntat fel" }, "step": { "pair": { @@ -9,7 +14,18 @@ "pin": "PIN-kod" }, "title": "Para ihop" + }, + "user": { + "data": { + "api_version": "API-version", + "host": "V\u00e4rd" + } } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Enheten uppmanas att sl\u00e5s p\u00e5" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/sv.json b/homeassistant/components/pi_hole/translations/sv.json index 0459a7596f3..a1a0a54f9af 100644 --- a/homeassistant/components/pi_hole/translations/sv.json +++ b/homeassistant/components/pi_hole/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "api_key": { "data": { @@ -9,7 +15,12 @@ "user": { "data": { "api_key": "API-nyckel", - "host": "V\u00e4rd" + "host": "V\u00e4rd", + "location": "Plats", + "name": "Namn", + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/picnic/translations/sv.json b/homeassistant/components/picnic/translations/sv.json index 23c825f256f..60959ec71fb 100644 --- a/homeassistant/components/picnic/translations/sv.json +++ b/homeassistant/components/picnic/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/plaato/translations/sv.json b/homeassistant/components/plaato/translations/sv.json index 25e86261699..6368ff7222e 100644 --- a/homeassistant/components/plaato/translations/sv.json +++ b/homeassistant/components/plaato/translations/sv.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Plaato Airlock.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) f\u00f6r mer information." }, + "error": { + "invalid_webhook_device": "Du har valt en enhet som inte st\u00f6der att skicka data till en webhook. Den \u00e4r endast tillg\u00e4nglig f\u00f6r Airlock", + "no_api_method": "Du m\u00e5ste l\u00e4gga till en autentiseringstoken eller v\u00e4lja webhook", + "no_auth_token": "Du m\u00e5ste l\u00e4gga till en autentiseringstoken" + }, "step": { "user": { "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Plaato Webhook?", diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index 4227e45b707..bd5b5570ac0 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -9,13 +9,19 @@ }, "error": { "faulty_credentials": "Auktoriseringen misslyckades", + "host_or_token": "M\u00e5ste tillhandah\u00e5lla minst en av v\u00e4rd eller token", "no_servers": "Inga servrar l\u00e4nkade till konto", - "not_found": "Plex-server hittades inte" + "not_found": "Plex-server hittades inte", + "ssl_error": "Problem med SSL-certifikat" }, "step": { "manual_setup": { "data": { - "port": "Port" + "host": "V\u00e4rd", + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "token": "Token (valfritt)", + "verify_ssl": "Verifiera SSL-certifikat" } }, "select_server": { diff --git a/homeassistant/components/plugwise/translations/sv.json b/homeassistant/components/plugwise/translations/sv.json index a22c1e882bc..072006dd32e 100644 --- a/homeassistant/components/plugwise/translations/sv.json +++ b/homeassistant/components/plugwise/translations/sv.json @@ -1,22 +1,45 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "anna_with_adam": "B\u00e5de Anna och Adam uppt\u00e4ckte. L\u00e4gg till din Adam ist\u00e4llet f\u00f6r din Anna" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "flow_type": "Anslutningstyp", + "host": "IP-adress", "password": "Smile ID", "port": "Port", "username": "Smile Anv\u00e4ndarnamn" - } + }, + "description": "Ange", + "title": "Anslut till leendet" }, "user_gateway": { "data": { "host": "IP address", - "port": "Port" + "password": "Smile ID", + "port": "Port", + "username": "Smile Anv\u00e4ndarnamn" }, - "description": "V\u00e4nligen ange:" + "description": "V\u00e4nligen ange:", + "title": "Anslut till leendet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Skanningsintervall (sekunder)" + }, + "description": "Justera Plugwise-alternativ" } } } diff --git a/homeassistant/components/powerwall/translations/sv.json b/homeassistant/components/powerwall/translations/sv.json index 01b1eccd5c0..fd0d0eb86c8 100644 --- a/homeassistant/components/powerwall/translations/sv.json +++ b/homeassistant/components/powerwall/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta", @@ -14,7 +15,9 @@ "data": { "ip_address": "IP-adress", "password": "L\u00f6senord" - } + }, + "description": "L\u00f6senordet \u00e4r vanligtvis de sista 5 tecknen i serienumret f\u00f6r Backup Gateway och kan hittas i Tesla-appen eller de sista 5 tecknen i l\u00f6senordet som finns innanf\u00f6r d\u00f6rren f\u00f6r Backup Gateway 2.", + "title": "Anslut till powerwall" } } } diff --git a/homeassistant/components/progettihwsw/translations/sv.json b/homeassistant/components/progettihwsw/translations/sv.json new file mode 100644 index 00000000000..c7c31766d08 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Rel\u00e4 1", + "relay_10": "Rel\u00e4 10", + "relay_11": "Rel\u00e4 11", + "relay_12": "Rel\u00e4 12", + "relay_13": "Rel\u00e4 13" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json new file mode 100644 index 00000000000..e45b374e5da --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "name": "Sensornamn", + "tariff": "Till\u00e4mplig taxa per geografisk zon" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/sv.json b/homeassistant/components/qnap_qsw/translations/sv.json index 8db64916805..bc5f2b35a28 100644 --- a/homeassistant/components/qnap_qsw/translations/sv.json +++ b/homeassistant/components/qnap_qsw/translations/sv.json @@ -5,6 +5,7 @@ "invalid_id": "Enheten returnerade ett ogiltigt unikt ID" }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, "step": { diff --git a/homeassistant/components/rachio/translations/sv.json b/homeassistant/components/rachio/translations/sv.json index 4932b17ebfa..8bbd8d91d5f 100644 --- a/homeassistant/components/rachio/translations/sv.json +++ b/homeassistant/components/rachio/translations/sv.json @@ -13,8 +13,18 @@ "data": { "api_key": "API nyckel" }, + "description": "Du beh\u00f6ver API-nyckeln fr\u00e5n https://app.rach.io/. G\u00e5 till Inst\u00e4llningar och klicka sedan p\u00e5 'GET API KEY'.", "title": "Anslut till Rachio-enheten" } } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Varaktighet i minuter att k\u00f6ra n\u00e4r du aktiverar en zonomkopplare" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/hu.json b/homeassistant/components/radiotherm/translations/hu.json index 05e1ece66ec..74da62c3f88 100644 --- a/homeassistant/components/radiotherm/translations/hu.json +++ b/homeassistant/components/radiotherm/translations/hu.json @@ -22,7 +22,7 @@ "issues": { "deprecated_yaml": { "description": "A Radio Thermostat kl\u00edmaplatform YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa a Home Assistant 2022.9-ben megsz\u0171nik. \n\nMegl\u00e9v\u0151 konfigur\u00e1ci\u00f3ja automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre. T\u00e1vol\u00edtsa el a YAML-konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st a probl\u00e9ma megold\u00e1s\u00e1hoz.", - "title": "A r\u00e1di\u00f3s termoszt\u00e1t YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + "title": "A Radio Thermostat YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } }, "options": { diff --git a/homeassistant/components/rainforest_eagle/translations/sv.json b/homeassistant/components/rainforest_eagle/translations/sv.json new file mode 100644 index 00000000000..eba844f6c03 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/sv.json b/homeassistant/components/rainmachine/translations/sv.json index 5077b9bb967..40295676501 100644 --- a/homeassistant/components/rainmachine/translations/sv.json +++ b/homeassistant/components/rainmachine/translations/sv.json @@ -17,6 +17,9 @@ "options": { "step": { "init": { + "data": { + "zone_run_time": "Standardzonens k\u00f6rtid (i sekunder)" + }, "title": "Konfigurera RainMachine" } } diff --git a/homeassistant/components/recollect_waste/translations/sv.json b/homeassistant/components/recollect_waste/translations/sv.json new file mode 100644 index 00000000000..5d61ecf184c --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_place_or_service_id": "Ogiltigt plats- eller tj\u00e4nst-ID" + }, + "step": { + "user": { + "data": { + "place_id": "Plats-ID", + "service_id": "Tj\u00e4nst-ID" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Anv\u00e4nd v\u00e4nliga namn f\u00f6r upph\u00e4mtningstyper (n\u00e4r det \u00e4r m\u00f6jligt)" + }, + "title": "Konfigurera Recollect Waste" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/sv.json b/homeassistant/components/remote/translations/sv.json index 1b6584c5bf8..ef7a2ab0ad2 100644 --- a/homeassistant/components/remote/translations/sv.json +++ b/homeassistant/components/remote/translations/sv.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "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/renault/translations/sv.json b/homeassistant/components/renault/translations/sv.json index 26e9f2d6a49..a3bea645b0f 100644 --- a/homeassistant/components/renault/translations/sv.json +++ b/homeassistant/components/renault/translations/sv.json @@ -3,8 +3,11 @@ "step": { "user": { "data": { + "locale": "Plats", + "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "title": "St\u00e4ll in Renault-uppgifter" } } } diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json index 28f5c911c3a..304513880e6 100644 --- a/homeassistant/components/rfxtrx/translations/sv.json +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -31,6 +31,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Skicka kommando: {subtype}", + "send_status": "Skicka statusuppdatering: {subtype}" + }, + "trigger_type": { + "command": "Mottaget kommando: {subtype}", + "status": "Mottagen status: {subtype}" + } + }, "options": { "error": { "already_configured_device": "Enheten \u00e4r redan konfigurerad", diff --git a/homeassistant/components/rhasspy/translations/no.json b/homeassistant/components/rhasspy/translations/no.json index 4dc3393a982..52682f70186 100644 --- a/homeassistant/components/rhasspy/translations/no.json +++ b/homeassistant/components/rhasspy/translations/no.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Allerede konfigurert. Kun \u00e9n konfigurert instans i gangen." + }, + "step": { + "user": { + "description": "Vil du aktivere Rhasspy-st\u00f8tte?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json index 23c825f256f..1f7f95730d1 100644 --- a/homeassistant/components/risco/translations/sv.json +++ b/homeassistant/components/risco/translations/sv.json @@ -7,5 +7,20 @@ } } } + }, + "options": { + "step": { + "risco_to_ha": { + "data": { + "B": "Grupp B", + "C": "Grupp C", + "D": "Grupp D", + "arm": "Larmat (Borta)", + "partial_arm": "Delvis larmat (hemma)" + }, + "description": "V\u00e4lj vilket tillst\u00e5nd ditt Home Assistant-larm ska rapportera f\u00f6r varje tillst\u00e5nd som rapporteras av Risco", + "title": "Mappa Risco-tillst\u00e5nd till Home Assistant-tillst\u00e5nd" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/sv.json b/homeassistant/components/rituals_perfume_genie/translations/sv.json new file mode 100644 index 00000000000..4e593cd0c44 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "title": "Anslut till ditt Rituals-konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/sv.json b/homeassistant/components/roku/translations/sv.json index 4272f65b2ae..0acdb41359d 100644 --- a/homeassistant/components/roku/translations/sv.json +++ b/homeassistant/components/roku/translations/sv.json @@ -1,14 +1,20 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "unknown": "Ov\u00e4ntat fel" }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "flow_title": "Roku: {name}", "step": { "user": { "data": { "host": "V\u00e4rd" - } + }, + "description": "Ange din Roku information." } } } diff --git a/homeassistant/components/roomba/translations/sv.json b/homeassistant/components/roomba/translations/sv.json index e2c491df80b..1c731cd2af9 100644 --- a/homeassistant/components/roomba/translations/sv.json +++ b/homeassistant/components/roomba/translations/sv.json @@ -1,13 +1,37 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "not_irobot_device": "Uppt\u00e4ckt enhet \u00e4r inte en iRobot-enhet", + "short_blid": "BLID trunkerades" + }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" }, "step": { + "link": { + "description": "Tryck och h\u00e5ll hemknappen p\u00e5 {name} tills enheten genererar ett ljud (cirka tv\u00e5 sekunder), skicka sedan inom 30 sekunder.", + "title": "H\u00e4mta l\u00f6senord" + }, + "link_manual": { + "data": { + "password": "L\u00f6senord" + }, + "title": "Ange l\u00f6senord" + }, + "manual": { + "data": { + "host": "V\u00e4rd" + }, + "description": "No Roomba or Braava have been discovered on your network.", + "title": "Anslut till enheten manuellt" + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress" }, + "description": "V\u00e4lj Roomba eller Braava.", "title": "Anslut till enheten" } } diff --git a/homeassistant/components/roon/translations/sv.json b/homeassistant/components/roon/translations/sv.json index 420e19171ae..51e3fdfa83f 100644 --- a/homeassistant/components/roon/translations/sv.json +++ b/homeassistant/components/roon/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "fallback": { "data": { @@ -7,6 +14,10 @@ "port": "Port" }, "description": "Kunde inte uppt\u00e4cka Roon-servern, ange ditt v\u00e4rdnamn och port." + }, + "link": { + "description": "Du m\u00e5ste auktorisera Home Assistant i Roon. N\u00e4r du har klickat p\u00e5 skicka, g\u00e5 till Roon Core-applikationen, \u00f6ppna Inst\u00e4llningar och aktivera HomeAssistant p\u00e5 fliken Till\u00e4gg.", + "title": "Auktorisera HomeAssistant i Roon" } } } diff --git a/homeassistant/components/rpi_power/translations/sv.json b/homeassistant/components/rpi_power/translations/sv.json index 0ca2f1f5748..554e410f6a5 100644 --- a/homeassistant/components/rpi_power/translations/sv.json +++ b/homeassistant/components/rpi_power/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "Kan inte hitta systemklassen som beh\u00f6vs f\u00f6r den h\u00e4r komponenten, se till att din k\u00e4rna \u00e4r ny och att h\u00e5rdvaran st\u00f6ds", "single_instance_allowed": "Redan konfigurerad. Bara en konfiguration \u00e4r till\u00e5ten." }, "step": { @@ -8,5 +9,6 @@ "description": "Vill du b\u00f6rja med inst\u00e4llning?" } } - } + }, + "title": "Raspberry Pi str\u00f6mf\u00f6rs\u00f6rjningskontroll" } \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/sv.json b/homeassistant/components/scrape/translations/sv.json index 2c4da93bb6d..130d7505018 100644 --- a/homeassistant/components/scrape/translations/sv.json +++ b/homeassistant/components/scrape/translations/sv.json @@ -11,6 +11,7 @@ "device_class": "Enhetsklass", "headers": "Headers", "index": "Index", + "name": "Namn", "password": "L\u00f6senord", "resource": "Resurs", "select": "V\u00e4lj", @@ -21,7 +22,16 @@ "verify_ssl": "Verifiera SSL-certifikat" }, "data_description": { - "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen" + "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen", + "authentication": "Typ av HTTP-autentisering. Antingen basic eller digest", + "device_class": "Typ/klass av sensorn f\u00f6r att st\u00e4lla in ikonen i frontend", + "headers": "Rubriker att anv\u00e4nda f\u00f6r webbf\u00f6rfr\u00e5gan", + "index": "Definierar vilka av elementen som returneras av CSS-v\u00e4ljaren som ska anv\u00e4ndas", + "resource": "Webbadressen till webbplatsen som inneh\u00e5ller v\u00e4rdet", + "select": "Definierar vilken tagg som ska s\u00f6kas efter. Se Beautifulsoup CSS-selektorer f\u00f6r mer information.", + "state_class": "Tillst\u00e5ndsklassen f\u00f6r sensorn", + "value_template": "Definierar en mall f\u00f6r att f\u00e5 sensorns tillst\u00e5nd", + "verify_ssl": "Aktiverar/inaktiverar verifiering av SSL/TLS-certifikat, till exempel om det \u00e4r sj\u00e4lvsignerat" } } } @@ -35,6 +45,7 @@ "device_class": "Enhetsklass", "headers": "Headers", "index": "Index", + "name": "Namn", "password": "L\u00f6senord", "resource": "Resurs", "select": "V\u00e4lj", @@ -45,7 +56,16 @@ "verify_ssl": "Verifiera SSL-certifikat" }, "data_description": { - "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen" + "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen", + "authentication": "Typ av HTTP-autentisering. Antingen basic eller digest", + "device_class": "Typ/klass av sensorn f\u00f6r att st\u00e4lla in ikonen i frontend", + "headers": "Rubriker att anv\u00e4nda f\u00f6r webbf\u00f6rfr\u00e5gan", + "index": "Definierar vilka av elementen som returneras av CSS-v\u00e4ljaren som ska anv\u00e4ndas", + "resource": "Webbadressen till webbplatsen som inneh\u00e5ller v\u00e4rdet", + "select": "Definierar vilken tagg som ska s\u00f6kas efter. Se Beautifulsoup CSS-selektorer f\u00f6r mer information.", + "state_class": "Tillst\u00e5ndsklassen f\u00f6r sensorn", + "value_template": "Definierar en mall f\u00f6r att f\u00e5 sensorns tillst\u00e5nd", + "verify_ssl": "Aktiverar/inaktiverar verifiering av SSL/TLS-certifikat, till exempel om det \u00e4r sj\u00e4lvsignerat" } } } diff --git a/homeassistant/components/screenlogic/translations/sv.json b/homeassistant/components/screenlogic/translations/sv.json index 7be3515deb0..54ae7f2b212 100644 --- a/homeassistant/components/screenlogic/translations/sv.json +++ b/homeassistant/components/screenlogic/translations/sv.json @@ -13,5 +13,12 @@ } } } + }, + "options": { + "step": { + "init": { + "title": "ScreenLogic" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/sv.json b/homeassistant/components/sense/translations/sv.json index 02939a27dbb..76e41ccb1ba 100644 --- a/homeassistant/components/sense/translations/sv.json +++ b/homeassistant/components/sense/translations/sv.json @@ -13,7 +13,8 @@ "data": { "email": "E-postadress", "password": "L\u00f6senord" - } + }, + "title": "Anslut till din Sense Energy Monitor" } } } diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 49c49b16c69..0fae4fe1047 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -7,10 +7,12 @@ "is_current": "Nuvarande", "is_energy": "Nuvarande {entity_name} energi", "is_frequency": "Nuvarande frekvens", + "is_gas": "Nuvarande {entity_name} gas", "is_humidity": "Nuvarande {entity_name} fuktighet", "is_illuminance": "Nuvarande {entity_name} belysning", "is_nitrogen_dioxide": "Nuvarande {entity_name} koncentration av kv\u00e4vedioxid", "is_nitrogen_monoxide": "Nuvarande {entity_name} koncentration av kv\u00e4veoxid", + "is_nitrous_oxide": "Nuvarande koncentration av lustgas i {entity_name}.", "is_ozone": "Nuvarande {entity_name} koncentration av ozon", "is_pm1": "Nuvarande {entity_name} koncentration av PM1 partiklar", "is_pm10": "Nuvarande {entity_name} koncentration av PM10 partiklar", @@ -20,6 +22,7 @@ "is_pressure": "Aktuellt {entity_name} tryck", "is_reactive_power": "Nuvarande {entity_name} reaktiv effekt", "is_signal_strength": "Nuvarande {entity_name} signalstyrka", + "is_sulphur_dioxide": "Nuvarande koncentration av svaveldioxid i {entity_name}.", "is_temperature": "Aktuell {entity_name} temperatur", "is_value": "Nuvarande {entity_name} v\u00e4rde", "is_volatile_organic_compounds": "Nuvarande {entity_name} koncentration av flyktiga organiska \u00e4mnen", @@ -27,9 +30,13 @@ }, "trigger_type": { "battery_level": "{entity_name} batteriniv\u00e5 \u00e4ndras", + "carbon_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koldioxidkoncentrationen", + "carbon_monoxide": "{entity_name} f\u00f6r\u00e4ndringar av kolmonoxidkoncentrationen", "energy": "Energif\u00f6r\u00e4ndringar", + "gas": "{entity_name} gasf\u00f6r\u00e4ndringar", "humidity": "{entity_name} fuktighet \u00e4ndras", "illuminance": "{entity_name} belysning \u00e4ndras", + "nitrogen_dioxide": "{entity_name} kv\u00e4vedioxidkoncentrationen f\u00f6r\u00e4ndras.", "power": "{entity_name} effektf\u00f6r\u00e4ndringar", "power_factor": "effektfaktorf\u00f6r\u00e4ndringar", "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", diff --git a/homeassistant/components/sensorpush/translations/no.json b/homeassistant/components/sensorpush/translations/no.json new file mode 100644 index 00000000000..28ec4582177 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/sv.json b/homeassistant/components/sentry/translations/sv.json index 45c4508ff9e..8c1deb2dc58 100644 --- a/homeassistant/components/sentry/translations/sv.json +++ b/homeassistant/components/sentry/translations/sv.json @@ -1,8 +1,34 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { "bad_dsn": "Ogiltig DSN", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "dsn": "Sentry DSN" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Valfritt namn p\u00e5 milj\u00f6n.", + "event_custom_components": "Skicka h\u00e4ndelser fr\u00e5n anpassade komponenter", + "event_handled": "Skicka hanterade h\u00e4ndelser", + "event_third_party_packages": "Skicka h\u00e4ndelser fr\u00e5n tredjepartspaket", + "logging_event_level": "Loggniv\u00e5n Sentry kommer att registrera en h\u00e4ndelse f\u00f6r", + "logging_level": "Loggniv\u00e5n Sentry kommer att registrera loggar som br\u00f6dtexter f\u00f6r", + "tracing": "Aktivera prestandasp\u00e5rning", + "tracing_sample_rate": "Samplingsfrekvens f\u00f6r sp\u00e5rning; mellan 0,0 och 1,0 (1,0 = 100 %)." + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/hu.json b/homeassistant/components/senz/translations/hu.json index a3b07f0d3ef..357d5729418 100644 --- a/homeassistant/components/senz/translations/hu.json +++ b/homeassistant/components/senz/translations/hu.json @@ -16,5 +16,11 @@ "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } + }, + "issues": { + "removed_yaml": { + "description": "Az nVent RAYCHEM SENZ YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nK\u00e9rem, t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az nVent RAYCHEM SENZ YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/no.json b/homeassistant/components/senz/translations/no.json index 6c384bb2e15..89ac20c8331 100644 --- a/homeassistant/components/senz/translations/no.json +++ b/homeassistant/components/senz/translations/no.json @@ -16,5 +16,11 @@ "title": "Velg Autentiserings metode" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av nVent RAYCHEM SENZ med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "nVent RAYCHEM SENZ YAML-konfigurasjonen er fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/sv.json b/homeassistant/components/sharkiq/translations/sv.json index cae80c6c25f..2ef8a989166 100644 --- a/homeassistant/components/sharkiq/translations/sv.json +++ b/homeassistant/components/sharkiq/translations/sv.json @@ -1,6 +1,14 @@ { "config": { "abort": { + "already_configured": "Konto har redan konfigurerats", + "cannot_connect": "Det gick inte att ansluta.", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { @@ -12,6 +20,7 @@ }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index 21a5ba55b58..458f4be3b24 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -1,11 +1,13 @@ { "config": { "error": { - "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support" + "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support", + "invalid_auth": "Ogiltig autentisering" }, "step": { "credentials": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/sia/translations/sv.json b/homeassistant/components/sia/translations/sv.json index 67fd98a8827..fe4c471f302 100644 --- a/homeassistant/components/sia/translations/sv.json +++ b/homeassistant/components/sia/translations/sv.json @@ -1,7 +1,43 @@ { "config": { "error": { + "invalid_account_length": "Kontot har inte r\u00e4tt l\u00e4ngd, det m\u00e5ste vara mellan 3 och 16 tecken.", + "invalid_key_format": "Nyckeln \u00e4r inte ett hexadecimalt v\u00e4rde, anv\u00e4nd endast 0-9 och AF.", + "invalid_key_length": "Nyckeln har inte r\u00e4tt l\u00e4ngd, den m\u00e5ste vara 16, 24 eller 32 hexadecken.", + "invalid_ping": "Pingintervallet m\u00e5ste vara mellan 1 och 1440 minuter.", + "invalid_zones": "Det m\u00e5ste finnas minst 1 zon.", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "additional_account": { + "data": { + "account": "Konto-ID", + "encryption_key": "Krypteringsnyckel", + "ping_interval": "Pingintervall (min)" + }, + "title": "L\u00e4gg till ett annat konto till den aktuella porten." + }, + "user": { + "data": { + "account": "Konto-ID", + "encryption_key": "Krypteringsnyckel", + "ping_interval": "Pingintervall (min)", + "port": "Port", + "protocol": "Protokoll" + }, + "title": "Skapa en anslutning f\u00f6r SIA-baserade larmsystem." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignorera tidsst\u00e4mpelkontrollen f\u00f6r SIA-h\u00e4ndelserna" + }, + "description": "St\u00e4ll in alternativen f\u00f6r kontot: {account}", + "title": "Alternativ f\u00f6r SIA-installationen." + } } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/hu.json b/homeassistant/components/simplepush/translations/hu.json index e5deb2bf2fc..b1f7b519dc5 100644 --- a/homeassistant/components/simplepush/translations/hu.json +++ b/homeassistant/components/simplepush/translations/hu.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A Simplepush YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Simplepush YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Simplepush YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/no.json b/homeassistant/components/simplepush/translations/no.json index 78cf864a33d..5c2e447b098 100644 --- a/homeassistant/components/simplepush/translations/no.json +++ b/homeassistant/components/simplepush/translations/no.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Simplepush med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet." + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index ece2b0a0dfb..5b2e898a1e5 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -7,7 +7,7 @@ "wrong_account": "A megadott felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok nem j\u00f3k ehhez a SimpliSafe fi\u00f3khoz." }, "error": { - "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lt", + "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, @@ -34,7 +34,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t." + "description": "A SimpliSafe a webes alkalmaz\u00e1son kereszt\u00fcl hiteles\u00edti a felhaszn\u00e1l\u00f3kat. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy k\u00e9zi l\u00e9p\u00e9s: k\u00e9rem, hogy a kezd\u00e9s el\u0151tt olvassa el a [dokument\u00e1ci\u00f3t](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nHa k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webes alkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s a hiteles\u00edt\u0151 adatok megad\u00e1s\u00e1hoz. Ha a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s adja meg a SimpliSafe webalkalmaz\u00e1s URL-c\u00edm\u00e9r\u0151l sz\u00e1rmaz\u00f3 enged\u00e9lyez\u00e9si k\u00f3dot." } } }, diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index 6a3d08b799c..e7e8ec28715 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -3,15 +3,24 @@ "abort": { "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats.", "email_2fa_timed_out": "Tidsgr\u00e4nsen tog slut i v\u00e4ntan p\u00e5 tv\u00e5faktorsautentisering", + "reauth_successful": "\u00c5terautentisering lyckades", "wrong_account": "De angivna anv\u00e4ndaruppgifterna matchar inte detta SimpliSafe-konto." }, "error": { - "identifier_exists": "Kontot \u00e4r redan registrerat" + "identifier_exists": "Kontot \u00e4r redan registrerat", + "unknown": "Ov\u00e4ntat fel" }, "progress": { "email_2fa": "Kontrollera din e-post f\u00f6r en verifieringsl\u00e4nk fr\u00e5n Simplisafe." }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Skriv om l\u00f6senordet f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "sms_2fa": { "data": { "code": "Kod" @@ -26,5 +35,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "Kod (anv\u00e4ndsi Home Assistant UI)" + }, + "title": "Konfigurera SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sma/translations/sv.json b/homeassistant/components/sma/translations/sv.json new file mode 100644 index 00000000000..d957d820748 --- /dev/null +++ b/homeassistant/components/sma/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "cannot_retrieve_device_info": "Ansluten, men det g\u00e5r inte att h\u00e4mta enhetsinformationen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "group": "Grupp", + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "description": "Ange din SMA-enhetsinformation.", + "title": "St\u00e4ll in SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/sv.json b/homeassistant/components/smappee/translations/sv.json new file mode 100644 index 00000000000..eb9b997abb7 --- /dev/null +++ b/homeassistant/components/smappee/translations/sv.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "already_configured_local_device": "Lokala enheter \u00e4r redan konfigurerade. Ta bort dem f\u00f6rst innan du konfigurerar en molnenhet.", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_mdns": "Enhet som inte st\u00f6ds f\u00f6r Smappee-integrationen.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + }, + "flow_title": "{name}", + "step": { + "environment": { + "data": { + "environment": "Milj\u00f6" + }, + "description": "Konfigurera din Smappee f\u00f6r att integrera med Home Assistant." + }, + "local": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Ange v\u00e4rden f\u00f6r att initiera Smappee lokala integration" + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Smappee-enheten med serienumret {serialnumber} i Home Assistant?", + "title": "Uppt\u00e4ckta Smappee-enheter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/sv.json b/homeassistant/components/smart_meter_texas/translations/sv.json index 23c825f256f..89cfc8f6c3b 100644 --- a/homeassistant/components/smart_meter_texas/translations/sv.json +++ b/homeassistant/components/smart_meter_texas/translations/sv.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/smartthings/translations/sv.json b/homeassistant/components/smartthings/translations/sv.json index 413a4279cdd..21591e7c256 100644 --- a/homeassistant/components/smartthings/translations/sv.json +++ b/homeassistant/components/smartthings/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_available_locations": "Det finns inga tillg\u00e4ngliga SmartThings-platser att st\u00e4lla in i Home Assistant." + }, "error": { "app_setup_error": "Det gick inte att installera Home Assistant SmartApp. V\u00e4nligen f\u00f6rs\u00f6k igen.", "token_forbidden": "Token har inte det som kr\u00e4vs inom omf\u00e5ng f\u00f6r OAuth.", @@ -11,10 +14,18 @@ "authorize": { "title": "Auktorisera Home Assistant" }, + "pat": { + "data": { + "access_token": "\u00c5tkomstnyckel" + }, + "description": "V\u00e4nligen ange en [personlig \u00e5tkomsttoken]({token_url}) f\u00f6r SmartThings som har skapats enligt [instruktionerna]({component_url}).", + "title": "Ange personlig \u00e5tkomsttoken" + }, "select_location": { "data": { "location_id": "Position" }, + "description": "V\u00e4lj den SmartThings-plats du vill l\u00e4gga till i Home Assistant. Vi \u00f6ppnar sedan ett nytt f\u00f6nster och ber dig att logga in och godk\u00e4nna installationen av Home Assistant-integrationen p\u00e5 den valda platsen.", "title": "V\u00e4lj plats" }, "user": { diff --git a/homeassistant/components/smarttub/translations/sv.json b/homeassistant/components/smarttub/translations/sv.json new file mode 100644 index 00000000000..bfa44298487 --- /dev/null +++ b/homeassistant/components/smarttub/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "description": "Ange din SmartTub-e-postadress och ditt l\u00f6senord f\u00f6r att logga in", + "title": "Logga in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/sv.json b/homeassistant/components/sms/translations/sv.json index 0020bfcfc37..539446618d9 100644 --- a/homeassistant/components/sms/translations/sv.json +++ b/homeassistant/components/sms/translations/sv.json @@ -1,10 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "baud_speed": "Baud-hastighet" - } + "baud_speed": "Baud-hastighet", + "device": "Enhet" + }, + "title": "Anslut till modemet" } } } diff --git a/homeassistant/components/somfy_mylink/translations/sv.json b/homeassistant/components/somfy_mylink/translations/sv.json new file mode 100644 index 00000000000..9ab3fa7ec0c --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "flow_title": "{mac} ({ip})", + "step": { + "user": { + "description": "System-ID kan erh\u00e5llas i MyLink-appen under Integration genom att v\u00e4lja vilken tj\u00e4nst som helst som inte kommer fr\u00e5n molnet." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "init": { + "title": "Konfigurera MyLink-alternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/sv.json b/homeassistant/components/sonarr/translations/sv.json index 7745fb77e1b..9128ea57a38 100644 --- a/homeassistant/components/sonarr/translations/sv.json +++ b/homeassistant/components/sonarr/translations/sv.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sonos/translations/sv.json b/homeassistant/components/sonos/translations/sv.json index 4f2202ff8f5..142c1409788 100644 --- a/homeassistant/components/sonos/translations/sv.json +++ b/homeassistant/components/sonos/translations/sv.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Inga Sonos-enheter hittades i n\u00e4tverket.", + "not_sonos_device": "Uppt\u00e4ckt enhet \u00e4r inte en Sonos-enhet", "single_instance_allowed": "Endast en enda konfiguration av Sonos \u00e4r n\u00f6dv\u00e4ndig." }, "step": { diff --git a/homeassistant/components/soundtouch/translations/hu.json b/homeassistant/components/soundtouch/translations/hu.json index 2e4b4c36d8a..b0406ad66b7 100644 --- a/homeassistant/components/soundtouch/translations/hu.json +++ b/homeassistant/components/soundtouch/translations/hu.json @@ -20,6 +20,7 @@ }, "issues": { "deprecated_yaml": { + "description": "A Bose SoundTouch YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Bose SoundTouch YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant programot.", "title": "A Bose SoundTouch YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } diff --git a/homeassistant/components/speedtestdotnet/translations/sv.json b/homeassistant/components/speedtestdotnet/translations/sv.json index 78b043f1a37..cde9095fba8 100644 --- a/homeassistant/components/speedtestdotnet/translations/sv.json +++ b/homeassistant/components/speedtestdotnet/translations/sv.json @@ -1,8 +1,20 @@ { + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, "options": { "step": { "init": { "data": { + "manual": "Inaktivera automatisk uppdatering", + "scan_interval": "Uppdateringsfrekvens (minuter)", "server_name": "V\u00e4lj testserver" } } diff --git a/homeassistant/components/spider/translations/sv.json b/homeassistant/components/spider/translations/sv.json index 23c825f256f..d078c6d4110 100644 --- a/homeassistant/components/spider/translations/sv.json +++ b/homeassistant/components/spider/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index 846ddaf5ce3..52b81a71faf 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -21,8 +21,8 @@ }, "issues": { "removed_yaml": { - "description": "A Spotify YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "A Spotify YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + "description": "A Spotify YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nK\u00e9rem, t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Spotify YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } }, "system_health": { diff --git a/homeassistant/components/spotify/translations/sv.json b/homeassistant/components/spotify/translations/sv.json index 0a64cad7a65..08c7e415edf 100644 --- a/homeassistant/components/spotify/translations/sv.json +++ b/homeassistant/components/spotify/translations/sv.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", - "missing_configuration": "Spotify-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." + "missing_configuration": "Spotify-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" }, "create_entry": { "default": "Lyckad autentisering med Spotify." @@ -18,5 +19,10 @@ "description": "Att konfigurera Spotify med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", "title": "Spotify YAML-konfigurationen har tagits bort" } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API-slutpunkt kan n\u00e5s" + } } } \ No newline at end of file diff --git a/homeassistant/components/sql/translations/sv.json b/homeassistant/components/sql/translations/sv.json index dec86c2b66b..fc2f876d479 100644 --- a/homeassistant/components/sql/translations/sv.json +++ b/homeassistant/components/sql/translations/sv.json @@ -11,10 +11,18 @@ "user": { "data": { "column": "Kolumn", - "name": "Namn" + "db_url": "Databas URL", + "name": "Namn", + "query": "V\u00e4lj fr\u00e5ga", + "unit_of_measurement": "M\u00e5ttenhet", + "value_template": "V\u00e4rdemall" }, "data_description": { + "column": "Kolumn f\u00f6r returnerad fr\u00e5ga f\u00f6r att presentera som tillst\u00e5nd", + "db_url": "URL till databasen, l\u00e4mna tomt om du vill anv\u00e4nda standarddatabasen f\u00f6r HA.", "name": "Namn som kommer att anv\u00e4ndas f\u00f6r konfigurationsinmatning och \u00e4ven f\u00f6r sensorn.", + "query": "Fr\u00e5ga som ska k\u00f6ras, m\u00e5ste b\u00f6rja med \"SELECT\"", + "unit_of_measurement": "M\u00e5ttenhet (valfritt)", "value_template": "V\u00e4rdemall (valfritt)" } } @@ -22,15 +30,26 @@ }, "options": { "error": { - "db_url_invalid": "Databasens URL \u00e4r ogiltig" + "db_url_invalid": "Databasens URL \u00e4r ogiltig", + "query_invalid": "SQL fr\u00e5ga \u00e4r ogiltig" }, "step": { "init": { "data": { - "name": "Namn" + "column": "Kolumn", + "db_url": "Databas URL", + "name": "Namn", + "query": "V\u00e4lj fr\u00e5ga", + "unit_of_measurement": "M\u00e5ttenhet", + "value_template": "V\u00e4rdemall" }, "data_description": { - "name": "Namn som kommer att anv\u00e4ndas f\u00f6r Config Entry och \u00e4ven sensorn" + "column": "Kolumn f\u00f6r returnerad fr\u00e5ga f\u00f6r att presentera som tillst\u00e5nd", + "db_url": "URL till databasen, l\u00e4mna tomt om du vill anv\u00e4nda standarddatabasen f\u00f6r HA.", + "name": "Namn som kommer att anv\u00e4ndas f\u00f6r Config Entry och \u00e4ven sensorn", + "query": "Fr\u00e5ga som ska k\u00f6ras, m\u00e5ste b\u00f6rja med \"SELECT\"", + "unit_of_measurement": "M\u00e5ttenhet (valfritt)", + "value_template": "V\u00e4rdemall (valfritt)" } } } diff --git a/homeassistant/components/squeezebox/translations/sv.json b/homeassistant/components/squeezebox/translations/sv.json index 796ddb0da2e..2957c503876 100644 --- a/homeassistant/components/squeezebox/translations/sv.json +++ b/homeassistant/components/squeezebox/translations/sv.json @@ -1,10 +1,25 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "no_server_found": "Ingen LMS server hittades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "no_server_found": "Kunde inte hitta servern automatiskt.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{host}", "step": { "edit": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Redigera anslutningsinformation" }, "user": { "data": { diff --git a/homeassistant/components/srp_energy/translations/sv.json b/homeassistant/components/srp_energy/translations/sv.json index 880970c74ff..b2068a4d12d 100644 --- a/homeassistant/components/srp_energy/translations/sv.json +++ b/homeassistant/components/srp_energy/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { "cannot_connect": "Kan inte ansluta", "unknown": "Ov\u00e4ntat fel" diff --git a/homeassistant/components/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json index 5dcc97464c8..1902d129c52 100644 --- a/homeassistant/components/steam_online/translations/hu.json +++ b/homeassistant/components/steam_online/translations/hu.json @@ -26,8 +26,8 @@ }, "issues": { "removed_yaml": { - "description": "A Steam YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML-konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "A Steam YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + "description": "A Steam YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nK\u00e9rem, t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Steam YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } }, "options": { diff --git a/homeassistant/components/subaru/translations/sv.json b/homeassistant/components/subaru/translations/sv.json index 23c825f256f..38d552ff4aa 100644 --- a/homeassistant/components/subaru/translations/sv.json +++ b/homeassistant/components/subaru/translations/sv.json @@ -1,6 +1,34 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "bad_pin_format": "PIN-koden ska vara fyra siffror", + "bad_validation_code_format": "Valideringskoden ska vara 6 siffror", + "cannot_connect": "Det gick inte att ansluta.", + "incorrect_pin": "Felaktig PIN-kod", + "incorrect_validation_code": "Felaktig valideringkod", + "invalid_auth": "Ogiltig autentisering" + }, "step": { + "pin": { + "data": { + "pin": "PIN-kod" + }, + "description": "Ange din MySubaru PIN-kod\n OBS: Alla fordon p\u00e5 kontot m\u00e5ste ha samma PIN-kod" + }, + "two_factor": { + "description": "Tv\u00e5faktorautentisering kr\u00e4vs", + "title": "Subaru Starlink-konfiguration" + }, + "two_factor_validate": { + "data": { + "validation_code": "Valideringskod" + }, + "title": "Subaru Starlink-konfiguration" + }, "user": { "data": { "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index e44bfe7c811..bd363de0738 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -11,7 +11,7 @@ "one": "\u00dcres", "other": "\u00dcres" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { diff --git a/homeassistant/components/syncthru/translations/sv.json b/homeassistant/components/syncthru/translations/sv.json index 76687105e49..29f0e7519a3 100644 --- a/homeassistant/components/syncthru/translations/sv.json +++ b/homeassistant/components/syncthru/translations/sv.json @@ -1,7 +1,27 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "invalid_url": "Ogiltig URL" + "invalid_url": "Ogiltig URL", + "syncthru_not_supported": "Enheten st\u00f6der inte SyncThru", + "unknown_state": "Skrivarens status ok\u00e4nd, verifiera URL och n\u00e4tverksanslutning" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "name": "Namn", + "url": "Webbgr\u00e4nssnitt URL" + } + }, + "user": { + "data": { + "name": "Namn", + "url": "Webbgr\u00e4nssnitt URL" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index 012d092de41..acfc243382d 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -1,14 +1,31 @@ { "config": { "abort": { - "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad.", + "reauth_successful": "\u00c5terautentisering lyckades", + "reconfigure_successful": "Omkonfigurationen lyckades" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "missing_data": "Saknade data: f\u00f6rs\u00f6k igen senare eller en annan konfiguration", + "otp_failed": "Tv\u00e5stegsautentisering misslyckades, f\u00f6rs\u00f6k igen med ett nytt l\u00f6senord" + }, + "flow_title": "{name} ({host})", "step": { + "2sa": { + "data": { + "otp_code": "Kod" + }, + "title": "Synology DSM: tv\u00e5stegsautentisering" + }, "link": { "data": { "password": "L\u00f6senord", "port": "Port (Valfri)", - "username": "Anv\u00e4ndarnamn" + "ssl": "Anv\u00e4nd ett SSL certifikat", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" }, "description": "Do vill du konfigurera {name} ({host})?" }, @@ -21,7 +38,10 @@ "data": { "host": "V\u00e4rd", "password": "L\u00f6senord", - "username": "Anv\u00e4ndarnamn" + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/tado/translations/sv.json b/homeassistant/components/tado/translations/sv.json index f6813633b80..23ad948cdb6 100644 --- a/homeassistant/components/tado/translations/sv.json +++ b/homeassistant/components/tado/translations/sv.json @@ -25,6 +25,7 @@ "data": { "fallback": "Aktivera reservl\u00e4ge." }, + "description": "Med Fallback-l\u00e4get kan du v\u00e4lja n\u00e4r du vill \u00e5terg\u00e5 till Smart Schedule fr\u00e5n din manuella zon\u00f6verl\u00e4ggning. (NEXT_TIME_BLOCK:= Byt vid n\u00e4sta \u00e4ndring av Smart Schedule; MANUAL:= Byt inte f\u00f6rr\u00e4n du avbryter; TADO_DEFAULT:= Byt baserat p\u00e5 din inst\u00e4llning i Tado App).", "title": "Justera Tado inst\u00e4llningarna." } } diff --git a/homeassistant/components/tankerkoenig/translations/sv.json b/homeassistant/components/tankerkoenig/translations/sv.json index f183c4ccb29..55c362cc717 100644 --- a/homeassistant/components/tankerkoenig/translations/sv.json +++ b/homeassistant/components/tankerkoenig/translations/sv.json @@ -38,8 +38,10 @@ "init": { "data": { "scan_interval": "Uppdateringsintervall", + "show_on_map": "Visa stationer p\u00e5 kartan", "stations": "Stationer" - } + }, + "title": "Tankerkoenig inst\u00e4llningar" } } } diff --git a/homeassistant/components/tasmota/translations/sv.json b/homeassistant/components/tasmota/translations/sv.json new file mode 100644 index 00000000000..df8bff40946 --- /dev/null +++ b/homeassistant/components/tasmota/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "invalid_discovery_topic": "Ogiltigt \u00e4mnesprefix f\u00f6r uppt\u00e4ckt." + }, + "step": { + "config": { + "data": { + "discovery_prefix": "Uppt\u00e4ckt \u00e4mnesprefix" + } + }, + "confirm": { + "description": "Vill du konfigurera Tasmota?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/sv.json b/homeassistant/components/tellduslive/translations/sv.json index 9b45e05fe9c..98fa56f928f 100644 --- a/homeassistant/components/tellduslive/translations/sv.json +++ b/homeassistant/components/tellduslive/translations/sv.json @@ -4,6 +4,9 @@ "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", "unknown": "Ok\u00e4nt fel intr\u00e4ffade" }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "auth": { "description": "F\u00f6r att l\u00e4nka ditt \"Telldus Live!\" konto: \n 1. Klicka p\u00e5 l\u00e4nken nedan \n 2. Logga in p\u00e5 Telldus Live!\n 3. Godk\u00e4nn **{app_name}** (klicka **Yes**). \n 4. Kom tillbaka hit och klicka p\u00e5 **SUBMIT**. \n\n [L\u00e4nk till Telldus Live konto]({auth_url})", diff --git a/homeassistant/components/tibber/translations/sv.json b/homeassistant/components/tibber/translations/sv.json index 1fda5b91f5a..604e18c3659 100644 --- a/homeassistant/components/tibber/translations/sv.json +++ b/homeassistant/components/tibber/translations/sv.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", + "timeout": "Timeout f\u00f6r anslutning till Tibber" + }, "step": { "user": { "data": { "access_token": "\u00c5tkomstnyckel" - } + }, + "description": "Ange din \u00e5tkomsttoken fr\u00e5n https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tile/translations/sv.json b/homeassistant/components/tile/translations/sv.json index 26e9f2d6a49..b3a1bc0e169 100644 --- a/homeassistant/components/tile/translations/sv.json +++ b/homeassistant/components/tile/translations/sv.json @@ -1,10 +1,25 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "title": "Konfigurera Tile" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Visa inaktiva Tiles" + }, + "title": "Konfigurera Tile" } } } diff --git a/homeassistant/components/toon/translations/sv.json b/homeassistant/components/toon/translations/sv.json index 034f5f41e68..95864e9bbe3 100644 --- a/homeassistant/components/toon/translations/sv.json +++ b/homeassistant/components/toon/translations/sv.json @@ -1,7 +1,23 @@ { "config": { "abort": { - "no_agreements": "Det h\u00e4r kontot har inga Toon-sk\u00e4rmar." + "already_configured": "Det valda avtalet \u00e4r redan konfigurerat.", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_agreements": "Det h\u00e4r kontot har inga Toon-sk\u00e4rmar.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + }, + "step": { + "agreement": { + "data": { + "agreement": "Avtal" + }, + "description": "V\u00e4lj den avtalsadress du vill l\u00e4gga till.", + "title": "V\u00e4lj ditt avtal" + }, + "pick_implementation": { + "title": "V\u00e4lj din klientorganisation att autentisera med" + } } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json index 8504a696619..064a6b2f9d3 100644 --- a/homeassistant/components/totalconnect/translations/sv.json +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Kontot har redan konfigurerats" }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tplink/translations/sv.json b/homeassistant/components/tplink/translations/sv.json index c11ec674c36..923647633d9 100644 --- a/homeassistant/components/tplink/translations/sv.json +++ b/homeassistant/components/tplink/translations/sv.json @@ -1,7 +1,22 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "no_devices_found": "Inga TP-Link enheter hittades p\u00e5 n\u00e4tverket." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name} {model} ({host})", + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} {model} ( {host} )?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/sv.json b/homeassistant/components/traccar/translations/sv.json index 274de7cfe7b..b6858100aa4 100644 --- a/homeassistant/components/traccar/translations/sv.json +++ b/homeassistant/components/traccar/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du st\u00e4lla in webhook-funktionen i Traccar.\n\nAnv\u00e4nd f\u00f6ljande url: `{webhook_url}`\n\nMer information finns i [dokumentationen]({docs_url})." }, diff --git a/homeassistant/components/tractive/translations/sv.json b/homeassistant/components/tractive/translations/sv.json new file mode 100644 index 00000000000..112ae3cdcfa --- /dev/null +++ b/homeassistant/components/tractive/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_failed_existing": "Det gick inte att uppdatera konfigurationsposten, ta bort integrationen och konfigurera den igen.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/sv.json b/homeassistant/components/twilio/translations/sv.json index c2e9f425f1c..8bb70f9cd64 100644 --- a/homeassistant/components/twilio/translations/sv.json +++ b/homeassistant/components/twilio/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [Webhooks med Twilio]({twilio_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n Se [dokumentationen]({docs_url}) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." }, diff --git a/homeassistant/components/twinkly/translations/sv.json b/homeassistant/components/twinkly/translations/sv.json new file mode 100644 index 00000000000..9b541d7ceac --- /dev/null +++ b/homeassistant/components/twinkly/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "device_exists": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/sv.json b/homeassistant/components/ukraine_alarm/translations/sv.json index cd280080774..3e8c6255251 100644 --- a/homeassistant/components/ukraine_alarm/translations/sv.json +++ b/homeassistant/components/ukraine_alarm/translations/sv.json @@ -15,9 +15,15 @@ "description": "Om du inte bara vill \u00f6vervaka stat och distrikt, v\u00e4lj dess specifika gemenskap" }, "district": { + "data": { + "region": "Region" + }, "description": "Om du inte bara vill \u00f6vervaka staten, v\u00e4lj dess specifika distrikt" }, "user": { + "data": { + "region": "Region" + }, "description": "V\u00e4lj tillst\u00e5nd att \u00f6vervaka" } } diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index bea79d27094..3c7e9dcc110 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Controller-platsen \u00e4r redan konfigurerad" + "already_configured": "Controller-platsen \u00e4r redan konfigurerad", + "configuration_updated": "Konfigurationen uppdaterad" }, "error": { "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter", @@ -28,11 +29,18 @@ }, "step": { "client_control": { + "data": { + "block_client": "N\u00e4tverks\u00e5tkomstkontrollerade klienter", + "dpi_restrictions": "Till\u00e5t kontroll av DPI-restriktionsgrupper", + "poe_clients": "Till\u00e5t POE-kontroll av klienter" + }, + "description": "Konfigurera klientkontroller \n\n Skapa switchar f\u00f6r serienummer som du vill kontrollera n\u00e4tverks\u00e5tkomst f\u00f6r.", "title": "UniFi-inst\u00e4llningar 2/3" }, "device_tracker": { "data": { "detection_time": "Tid i sekunder fr\u00e5n senast sett tills den anses borta", + "ignore_wired_bug": "Inaktivera logiken f\u00f6r fel i UniFi Network med kabelanslutning", "ssid_filter": "V\u00e4lj SSID att sp\u00e5ra tr\u00e5dl\u00f6sa klienter p\u00e5", "track_clients": "Sp\u00e5ra n\u00e4tverksklienter", "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)", @@ -48,6 +56,11 @@ } }, "simple_options": { + "data": { + "block_client": "N\u00e4tverks\u00e5tkomstkontrollerade klienter", + "track_clients": "Sp\u00e5ra n\u00e4tverksklienter", + "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)" + }, "description": "Konfigurera UniFi-integrationen" }, "statistics_sensors": { diff --git a/homeassistant/components/upb/translations/sv.json b/homeassistant/components/upb/translations/sv.json index f1f229cc175..03daf756094 100644 --- a/homeassistant/components/upb/translations/sv.json +++ b/homeassistant/components/upb/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta till UPB PIM, f\u00f6rs\u00f6k igen.", "invalid_upb_file": "Saknar eller ogiltig UPB UPStart-exportfil, kontrollera filens namn och s\u00f6kv\u00e4g.", @@ -9,8 +12,10 @@ "user": { "data": { "address": "Adress (se beskrivning ovan)", + "file_path": "S\u00f6kv\u00e4g och namn f\u00f6r UPStart UPB-exportfilen.", "protocol": "Protokoll" }, + "description": "Anslut en Universal Powerline Bus Powerline Interface Module (UPB PIM). Adressstr\u00e4ngen m\u00e5ste vara i formen 'adress[:port]' f\u00f6r 'tcp'. Porten \u00e4r valfri och har som standard 2101. Exempel: '192.168.1.42'. F\u00f6r det seriella protokollet m\u00e5ste adressen vara i formen 'tty[:baud]'. Bauden \u00e4r valfri och \u00e4r standard till 4800. Exempel: '/dev/ttyS1'.", "title": "Anslut till UPB PIM" } } diff --git a/homeassistant/components/upcloud/translations/sv.json b/homeassistant/components/upcloud/translations/sv.json index 23c825f256f..4c3c154261d 100644 --- a/homeassistant/components/upcloud/translations/sv.json +++ b/homeassistant/components/upcloud/translations/sv.json @@ -1,11 +1,25 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsintervall i sekunder, minst 30" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/sv.json b/homeassistant/components/upnp/translations/sv.json index 5ffe4a62f26..a067f819bba 100644 --- a/homeassistant/components/upnp/translations/sv.json +++ b/homeassistant/components/upnp/translations/sv.json @@ -2,11 +2,18 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u00e4r redan konfigurerad", + "incomplete_discovery": "Ofullst\u00e4ndig uppt\u00e4ckt", "no_devices_found": "Inga UPnP/IGD-enheter hittades p\u00e5 n\u00e4tverket." }, "error": { "one": "En", "other": "Andra" + }, + "flow_title": "{name}", + "step": { + "ssdp_confirm": { + "description": "Vill du konfigurera denna UPnP/IGD enhet?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sv.json b/homeassistant/components/uptimerobot/translations/sv.json index 5ad5b5b6db4..d9db1fc1deb 100644 --- a/homeassistant/components/uptimerobot/translations/sv.json +++ b/homeassistant/components/uptimerobot/translations/sv.json @@ -1,15 +1,30 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_failed_existing": "Det gick inte att uppdatera konfigurationsposten, ta bort integrationen och konfigurera den igen.", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel", + "reauth_failed_matching_account": "API-nyckeln du angav matchar inte konto-id:t f\u00f6r befintlig konfiguration.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Du m\u00e5ste ange en ny \"huvud\" API-nyckel fr\u00e5n UptimeRobot", + "title": "\u00c5terautenticera integration" }, "user": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Du m\u00e5ste ange \"huvud\" API-nyckeln fr\u00e5n UptimeRobot" } } } diff --git a/homeassistant/components/vera/translations/sv.json b/homeassistant/components/vera/translations/sv.json new file mode 100644 index 00000000000..8c7335c2b13 --- /dev/null +++ b/homeassistant/components/vera/translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "cannot_connect": "Kunde inte ansluta till kontrollern med URL {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Vera enhets-id att utesluta fr\u00e5n Home Assistant.", + "lights": "Vera switch enhets-ID att behandla som lampor i Home Assistant.", + "vera_controller_url": "Controller-URL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Vera enhets-id att utesluta fr\u00e5n Home Assistant.", + "lights": "Vera switch enhets-ID att behandla som lampor i Home Assistant." + }, + "description": "Se dokumentationen om vera f\u00f6r mer information om valfria parametrar: https://www.home-assistant.io/integrations/vera/. Observera: Alla \u00e4ndringar h\u00e4r kr\u00e4ver en omstart av Home Assistant-servern. Om du vill radera v\u00e4rden anger du ett mellanslag.", + "title": "Alternativ f\u00f6r Vera-kontroller" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/sv.json b/homeassistant/components/vesync/translations/sv.json index 4621636cecc..8cfb8ce52f7 100644 --- a/homeassistant/components/vesync/translations/sv.json +++ b/homeassistant/components/vesync/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vizio/translations/sv.json b/homeassistant/components/vizio/translations/sv.json index 82483d80fe8..dc1f5514d80 100644 --- a/homeassistant/components/vizio/translations/sv.json +++ b/homeassistant/components/vizio/translations/sv.json @@ -1,8 +1,14 @@ { "config": { "abort": { + "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "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." }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "existing_config_entry_found": "En befintlig St\u00e4ll in Vizio SmartCast-klient konfigurationspost med samma serienummer har redan konfigurerats. Du m\u00e5ste ta bort den befintliga posten f\u00f6r att konfigurera denna." + }, "step": { "pair_tv": { "data": { @@ -12,9 +18,11 @@ "title": "Slutf\u00f6r parningsprocessen" }, "pairing_complete": { + "description": "Din St\u00e4ll in Vizio SmartCast-klient \u00e4r nu ansluten till din Home Assistant.", "title": "Parkopplingen slutf\u00f6rd" }, "pairing_complete_import": { + "description": "Din St\u00e4ll in Vizio SmartCast-klient \u00e4r nu ansluten till din Home Assistant.\n\nDin \u00c5tkomstnyckel \u00e4r '**{access_token}**'.", "title": "Parkopplingen slutf\u00f6rd" }, "user": { @@ -24,6 +32,7 @@ "host": ":", "name": "Namn" }, + "description": "En \u00c5tkomstnyckel beh\u00f6vs endast f\u00f6r TV. Om du konfigurerar en TV och inte har en \u00c5tkomstnyckel \u00e4n, l\u00e4mna blank f\u00f6r att starta en parningsprocess.", "title": "St\u00e4ll in Vizio SmartCast-klient" } } @@ -36,6 +45,7 @@ "include_or_exclude": "Inkludera eller exkludera appar?", "volume_step": "Storlek p\u00e5 volymsteg" }, + "description": "Om du har en Smart TV kan du valfritt filtrera din k\u00e4lllista genom att v\u00e4lja vilka appar som ska inkluderas eller uteslutas i din k\u00e4lllista.", "title": "Uppdatera Vizo SmartCast-alternativ" } } diff --git a/homeassistant/components/volumio/translations/sv.json b/homeassistant/components/volumio/translations/sv.json new file mode 100644 index 00000000000..5eb83302226 --- /dev/null +++ b/homeassistant/components/volumio/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Kan inte ansluta till uppt\u00e4ckt Volumio" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "discovery_confirm": { + "description": "Vill du l\u00e4gga till Volumio ` {name} ` till Home Assistant?", + "title": "Uppt\u00e4ckt Volumio" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/sv.json b/homeassistant/components/vulcan/translations/sv.json index f62155f1fd3..cbe2d7174b1 100644 --- a/homeassistant/components/vulcan/translations/sv.json +++ b/homeassistant/components/vulcan/translations/sv.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "no_matching_entries": "Inga matchande poster hittades, anv\u00e4nd ett annat konto eller ta bort integration med f\u00f6r\u00e5ldrad student.." + "all_student_already_configured": "Alla studenter har redan lagts till.", + "already_configured": "Studenten har redan lagts till.", + "no_matching_entries": "Inga matchande poster hittades, anv\u00e4nd ett annat konto eller ta bort integration med f\u00f6r\u00e5ldrad student..", + "reauth_successful": "Reautentisering lyckades" + }, + "error": { + "expired_token": "Token som har upph\u00f6rt att g\u00e4lla \u2013 generera en ny token", + "invalid_pin": "Ogiltig pin", + "invalid_symbol": "Ogiltig symbol", + "invalid_token": "Ogiltig token", + "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" }, "step": { "add_next_config_entry": { diff --git a/homeassistant/components/wallbox/translations/sv.json b/homeassistant/components/wallbox/translations/sv.json index 8a60ea1a5dc..79a05e057b8 100644 --- a/homeassistant/components/wallbox/translations/sv.json +++ b/homeassistant/components/wallbox/translations/sv.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { @@ -8,6 +16,8 @@ }, "user": { "data": { + "password": "L\u00f6senord", + "station": "Stationens serienummer", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/water_heater/translations/sv.json b/homeassistant/components/water_heater/translations/sv.json index cb4826c461a..9b45fec830e 100644 --- a/homeassistant/components/water_heater/translations/sv.json +++ b/homeassistant/components/water_heater/translations/sv.json @@ -1,6 +1,15 @@ { + "device_automation": { + "action_type": { + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + } + }, "state": { "_": { + "eco": "Eco", + "electric": "Elektrisk", + "gas": "Gas", "heat_pump": "V\u00e4rmepump", "off": "Av" } diff --git a/homeassistant/components/watttime/translations/sv.json b/homeassistant/components/watttime/translations/sv.json index 23c825f256f..dadc8b53d2d 100644 --- a/homeassistant/components/watttime/translations/sv.json +++ b/homeassistant/components/watttime/translations/sv.json @@ -1,10 +1,18 @@ { "config": { "step": { + "location": { + "data": { + "location_type": "Plats" + }, + "description": "V\u00e4lj en plats att \u00f6vervaka:" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange ditt anv\u00e4ndarnamn och l\u00f6senord:" } } } diff --git a/homeassistant/components/waze_travel_time/translations/sv.json b/homeassistant/components/waze_travel_time/translations/sv.json index 84113d1284e..05407519a87 100644 --- a/homeassistant/components/waze_travel_time/translations/sv.json +++ b/homeassistant/components/waze_travel_time/translations/sv.json @@ -7,6 +7,7 @@ "user": { "data": { "destination": "Destination", + "name": "Namn", "origin": "Ursprung", "region": "Region" } diff --git a/homeassistant/components/wiffi/translations/sv.json b/homeassistant/components/wiffi/translations/sv.json index 86a5180fbfd..1fdd2ec4aa7 100644 --- a/homeassistant/components/wiffi/translations/sv.json +++ b/homeassistant/components/wiffi/translations/sv.json @@ -3,5 +3,14 @@ "abort": { "addr_in_use": "Serverporten \u00e4r upptagen." } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout (minuter)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/withings/translations/sv.json b/homeassistant/components/withings/translations/sv.json index 4adb00bfad5..8bf931882f4 100644 --- a/homeassistant/components/withings/translations/sv.json +++ b/homeassistant/components/withings/translations/sv.json @@ -1,12 +1,18 @@ { "config": { "abort": { + "already_configured": "Konfiguration uppdaterad f\u00f6r profilen", "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", - "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." + "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" }, "create_entry": { "default": "Lyckad autentisering med Withings." }, + "error": { + "already_configured": "Konto har redan konfigurerats" + }, + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" @@ -18,6 +24,10 @@ "description": "Vilken profil valde du p\u00e5 Withings webbplats? Det \u00e4r viktigt att profilerna matchar, annars kommer data att vara felm\u00e4rkta.", "title": "Anv\u00e4ndarprofil." }, + "reauth": { + "description": "Profilen \" {profile} \" m\u00e5ste autentiseras p\u00e5 nytt f\u00f6r att kunna forts\u00e4tta att ta emot Withings-data.", + "title": "\u00c5terautenticera integration" + }, "reauth_confirm": { "description": "Profilen \" {profile} \" m\u00e5ste autentiseras p\u00e5 nytt f\u00f6r att kunna forts\u00e4tta att ta emot Withings-data.", "title": "G\u00f6r om autentiseringen f\u00f6r integrationen" diff --git a/homeassistant/components/wled/translations/sv.json b/homeassistant/components/wled/translations/sv.json index a795bd52359..9ebff05a36c 100644 --- a/homeassistant/components/wled/translations/sv.json +++ b/homeassistant/components/wled/translations/sv.json @@ -2,8 +2,12 @@ "config": { "abort": { "already_configured": "Enheten har redan konfigurerats", + "cannot_connect": "Det gick inte att ansluta.", "cct_unsupported": "Denna WLED-enhet anv\u00e4nder CCT-kanaler, vilket inte st\u00f6ds av denna integration" }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "flow_title": "WLED: {name}", "step": { "user": { @@ -17,5 +21,14 @@ "title": "Uppt\u00e4ckte WLED-enhet" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Beh\u00e5ll huvudljuset, \u00e4ven med 1 LED-segment." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.sv.json b/homeassistant/components/wolflink/translations/sensor.sv.json index ddfd466dce2..797a7d6a230 100644 --- a/homeassistant/components/wolflink/translations/sensor.sv.json +++ b/homeassistant/components/wolflink/translations/sensor.sv.json @@ -1,10 +1,55 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x varmvatten", + "abgasklappe": "R\u00f6kgasspj\u00e4ll", + "absenkbetrieb": "\u00c5terst\u00e4llningsl\u00e4ge", + "absenkstop": "Stopp f\u00f6r \u00e5terst\u00e4llning", "aktiviert": "Aktiverad", + "antilegionellenfunktion": "Anti-legionella funktion", + "at_abschaltung": "OT-avst\u00e4ngning", + "at_frostschutz": "OT frostskydd", "aus": "Inaktiverad", + "auto": "Automatiskt", + "estrichtrocknung": "Avj\u00e4mningstorkning", + "externe_deaktivierung": "Extern avaktivering", + "fernschalter_ein": "Fj\u00e4rrkontroll aktiverad", + "frost_heizkreis": "Frost p\u00e5 v\u00e4rmekretsen", + "frost_warmwasser": "Varmvatten frost", + "frostschutz": "Frostskydd", + "gasdruck": "Gastryck", + "glt_betrieb": "BMS-l\u00e4ge", + "gradienten_uberwachung": "Gradient\u00f6vervakning", + "heizbetrieb": "V\u00e4rmel\u00e4ge", + "heizgerat_mit_speicher": "Panna med cylinder", + "heizung": "V\u00e4rmer", + "initialisierung": "Initiering", + "kalibration": "Kalibrering", + "kalibration_heizbetrieb": "V\u00e4rmel\u00e4ge kalibrering", + "kalibration_kombibetrieb": "Kombinationsl\u00e4ge kalibrering", + "kalibration_warmwasserbetrieb": "DHW kalibrering", + "kaskadenbetrieb": "Kaskaddrift", + "kombibetrieb": "Kombil\u00e4ge", + "kombigerat": "Kombipanna", + "kombigerat_mit_solareinbindung": "Kombipanna med solintegration", + "mindest_kombizeit": "Minsta kombitid", + "nachlauf_heizkreispumpe": "Pump f\u00f6r uppv\u00e4rmningskretsen ig\u00e5ng", + "nachspulen": "Efterspolning", + "nur_heizgerat": "Enbart panna", + "parallelbetrieb": "Parallellt l\u00e4ge", + "partymodus": "Festl\u00e4ge", + "perm_cooling": "PermCooling", + "permanent": "Permanent", + "permanentbetrieb": "Permanent l\u00e4ge", "sparen": "Ekonomi", - "test": "Test" + "test": "Test", + "vorspulen": "Ing\u00e5ngssk\u00f6ljning", + "warmwasser": "Varmvatten", + "warmwasser_schnellstart": "Snabbstart f\u00f6r varmvatten", + "warmwasserbetrieb": "Varmvattenl\u00e4ge", + "warmwassernachlauf": "Varmvatten p\u00e5slaget", + "warmwasservorrang": "Prioritet f\u00f6r varmvattenberedning", + "zunden": "T\u00e4ndning" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sv.json b/homeassistant/components/wolflink/translations/sv.json index 78879942876..28174fb1693 100644 --- a/homeassistant/components/wolflink/translations/sv.json +++ b/homeassistant/components/wolflink/translations/sv.json @@ -1,11 +1,26 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "device": { + "data": { + "device_name": "Enhet" + }, + "title": "V\u00e4lj WOLF-enhet" + }, "user": { "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "WOLF SmartSet-anslutning" } } } diff --git a/homeassistant/components/ws66i/translations/sv.json b/homeassistant/components/ws66i/translations/sv.json index 9daf43b9987..2ed5262ea3d 100644 --- a/homeassistant/components/ws66i/translations/sv.json +++ b/homeassistant/components/ws66i/translations/sv.json @@ -9,6 +9,9 @@ }, "step": { "user": { + "data": { + "ip_address": "IP-adress" + }, "title": "Anslut till enheten" } } diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json index b7c32ad8008..357008f5623 100644 --- a/homeassistant/components/xbox/translations/hu.json +++ b/homeassistant/components/xbox/translations/hu.json @@ -16,6 +16,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Az Xbox konfigur\u00e1l\u00e1sa a configuration.yaml f\u00e1jlban a 2022.9-es Home Assistantb\u00f3l elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 OAuth-alkalmaz\u00e1s hiteles\u00edt\u0151 adatai \u00e9s hozz\u00e1f\u00e9r\u00e9si be\u00e1ll\u00edt\u00e1sai automatikusan import\u00e1l\u00e1sra ker\u00fcltek a felhaszn\u00e1l\u00f3i fel\u00fcletre. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", "title": "Az Xbox YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } diff --git a/homeassistant/components/xbox/translations/sv.json b/homeassistant/components/xbox/translations/sv.json index 525e91ded0e..cb98fb350b7 100644 --- a/homeassistant/components/xbox/translations/sv.json +++ b/homeassistant/components/xbox/translations/sv.json @@ -1,4 +1,18 @@ { + "config": { + "abort": { + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + }, "issues": { "deprecated_yaml": { "description": "Konfigurering av Xbox i configuration.yaml tas bort i Home Assistant 2022.9. \n\n Dina befintliga OAuth-applikationsuppgifter och \u00e5tkomstinst\u00e4llningar har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", diff --git a/homeassistant/components/xiaomi_aqara/translations/sv.json b/homeassistant/components/xiaomi_aqara/translations/sv.json new file mode 100644 index 00000000000..9a1ed92aa1d --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/sv.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "not_xiaomi_aqara": "Inte en Xiaomi Aqara Gateway, uppt\u00e4ckt enhet matchade inte k\u00e4nda gateways" + }, + "error": { + "discovery_error": "Det gick inte att uppt\u00e4cka en Xiaomi Aqara Gateway, f\u00f6rs\u00f6k anv\u00e4nda IP:n f\u00f6r enheten som k\u00f6r HomeAssistant som gr\u00e4nssnitt", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress, Kolla https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "Ogiltigt n\u00e4tverksgr\u00e4nssnitt", + "invalid_key": "Ogiltig gatewaynyckel", + "invalid_mac": "Ogiltig MAC-adress" + }, + "flow_title": "{name}", + "step": { + "select": { + "data": { + "select_ip": "IP-adress" + }, + "description": "V\u00e4lj den Xiaomi Aqara Gateway du vill ansluta" + }, + "settings": { + "data": { + "key": "Nyckeln till din gateway", + "name": "Namnet p\u00e5 gatewayen" + }, + "description": "Nyckeln (l\u00f6senordet) kan h\u00e4mtas med hj\u00e4lp av den h\u00e4r handledningen: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Om nyckeln inte anges kommer endast sensorer att vara tillg\u00e4ngliga.", + "title": "Valfria inst\u00e4llningar" + }, + "user": { + "data": { + "host": "IP-adress (valfritt)", + "interface": "N\u00e4tverksgr\u00e4nssnittet som ska anv\u00e4ndas", + "mac": "MAC-adress (valfritt)" + }, + "description": "Om IP- och MAC-adresserna l\u00e4mnas tomma anv\u00e4nds automatisk uppt\u00e4ckt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.sv.json b/homeassistant/components/xiaomi_miio/translations/select.sv.json new file mode 100644 index 00000000000..47c2ffaa90d --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Ljus", + "dim": "Dimma", + "off": "Av" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json index 20e4d8c6d07..a52c1ecbf98 100644 --- a/homeassistant/components/xiaomi_miio/translations/sv.json +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -1,11 +1,49 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "incomplete_info": "Ofullst\u00e4ndig information till installationsenheten, ingen v\u00e4rd eller token tillhandah\u00e5lls.", + "not_xiaomi_miio": "Enheten st\u00f6ds (\u00e4nnu) inte av Xiaomi Miio.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "cloud_credentials_incomplete": "Cloud-uppgifterna \u00e4r ofullst\u00e4ndiga, v\u00e4nligen fyll i anv\u00e4ndarnamn, l\u00f6senord och land", + "cloud_login_error": "Kunde inte logga in p\u00e5 Xiaomi Miio Cloud, kontrollera anv\u00e4ndaruppgifterna.", + "cloud_no_devices": "Inga enheter hittades i detta Xiaomi Miio molnkonto.", + "unknown_device": "Enhetsmodellen \u00e4r inte k\u00e4nd, kan inte st\u00e4lla in enheten med hj\u00e4lp av konfigurationsfl\u00f6de." + }, + "flow_title": "{name}", "step": { "cloud": { "data": { + "cloud_country": "Molnserverland", "cloud_password": "Molnl\u00f6senord", - "cloud_username": "Molnanv\u00e4ndarnamn" + "cloud_username": "Molnanv\u00e4ndarnamn", + "manual": "Konfigurera manuellt (rekommenderas inte)" + }, + "description": "Logga in p\u00e5 Xiaomi Miio-molnet, se https://www.openhab.org/addons/bindings/miio/#country-servers f\u00f6r molnservern att anv\u00e4nda." + }, + "connect": { + "data": { + "model": "Enhetsmodell" } + }, + "manual": { + "data": { + "host": "IP-adress" + } + }, + "reauth_confirm": { + "description": "Xiaomi Miio-integrationen m\u00e5ste autentisera ditt konto igen f\u00f6r att uppdatera tokens eller l\u00e4gga till saknade molnuppgifter.", + "title": "\u00c5terautenticera integration" + }, + "select": { + "data": { + "select_device": "Miio-enhet" + }, + "description": "V\u00e4lj Xiaomi Miio-enheten f\u00f6r att st\u00e4lla in." } } } diff --git a/homeassistant/components/yamaha_musiccast/translations/sv.json b/homeassistant/components/yamaha_musiccast/translations/sv.json new file mode 100644 index 00000000000..7326abb364a --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "yxc_control_url_missing": "Kontroll-URL:n anges inte i ssdp-beskrivningen." + }, + "error": { + "no_musiccast_device": "Den h\u00e4r enheten verkar inte vara n\u00e5gon MusicCast-enhet." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Konfigurera MusicCast f\u00f6r att integrera med Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/sv.json b/homeassistant/components/yeelight/translations/sv.json index 9fdd341e941..8575090ff7c 100644 --- a/homeassistant/components/yeelight/translations/sv.json +++ b/homeassistant/components/yeelight/translations/sv.json @@ -1,10 +1,36 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{model} {id} ({host})", "step": { + "discovery_confirm": { + "description": "Vill du st\u00e4lla in {model} ( {host} )?" + }, "pick_device": { "data": { "device": "Enhet" } + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Modell", + "nightlight_switch": "Anv\u00e4nd nattljusbrytare", + "save_on_change": "Spara status vid \u00e4ndring", + "transition": "\u00d6verg\u00e5ngstid (ms)", + "use_music_mode": "Aktivera musikl\u00e4ge" + } } } } diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 61b0f0c0c9d..2b4fe26e0d6 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -46,11 +46,13 @@ "title": "Riaszt\u00f3 vez\u00e9rl\u0151panel opci\u00f3k" }, "zha_options": { + "always_prefer_xy_color_mode": "Az XY sz\u00ednm\u00f3dot el\u0151nyben r\u00e9szes\u00edtse", "consider_unavailable_battery": "Elemmel ell\u00e1tott eszk\u00f6z\u00f6k nem el\u00e9rhet\u0151 \u00e1llapot\u00faak ennyi id\u0151 ut\u00e1n (mp.)", "consider_unavailable_mains": "H\u00e1l\u00f3zati t\u00e1pell\u00e1t\u00e1s\u00fa eszk\u00f6z\u00f6k nem el\u00e9rhet\u0151 \u00e1llapot\u00faak ennyi id\u0151 ut\u00e1n (mp.)", "default_light_transition": "Alap\u00e9rtelmezett f\u00e9ny-\u00e1tmeneti id\u0151 (m\u00e1sodpercben)", "enable_identify_on_join": "Azonos\u00edt\u00f3 hat\u00e1s, amikor az eszk\u00f6z\u00f6k csatlakoznak a h\u00e1l\u00f3zathoz", "enhanced_light_transition": "F\u00e9ny sz\u00edn/sz\u00ednh\u0151m\u00e9rs\u00e9klet \u00e1tmenete kikapcsolt \u00e1llapotb\u00f3l", + "light_transitioning_flag": "Fokozott f\u00e9nyer\u0151-szab\u00e1lyoz\u00f3 enged\u00e9lyez\u00e9se f\u00e9nyv\u00e1lt\u00e1skor", "title": "Glob\u00e1lis be\u00e1ll\u00edt\u00e1sok" } }, diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 8917cbe8cd6..ee189ef1463 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "Endast en enda konfiguration av ZHA \u00e4r till\u00e5ten." + "not_zha_device": "Den h\u00e4r enheten \u00e4r inte en zha-enhet", + "single_instance_allowed": "Endast en enda konfiguration av ZHA \u00e4r till\u00e5ten.", + "usb_probe_failed": "Det gick inte att unders\u00f6ka usb-enheten" }, "error": { "cannot_connect": "Det gick inte att ansluta till ZHA enhet." }, + "flow_title": "{name}", "step": { + "confirm": { + "description": "Vill du konfigurera {name}?" + }, "pick_radio": { "data": { "radio_type": "Radiotyp" @@ -27,15 +33,25 @@ "data": { "path": "Seriell enhetsv\u00e4g" }, + "description": "V\u00e4lj serieport f\u00f6r Zigbee-radio", "title": "ZHA" } } }, "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Kod kr\u00e4vs f\u00f6r tillkopplings\u00e5tg\u00e4rder", + "alarm_failed_tries": "Antalet misslyckade kodinmatningar i f\u00f6ljd f\u00f6r att utl\u00f6sa ett larm.", + "alarm_master_code": "Huvudkod f\u00f6r larmcentralen/-larmcentralerna.", + "title": "Alternativ f\u00f6r larmkontrollpanel" + }, "zha_options": { "always_prefer_xy_color_mode": "F\u00f6redrar alltid XY-f\u00e4rgl\u00e4ge", + "default_light_transition": "Standard ljus\u00f6verg\u00e5ngstid (sekunder)", + "enable_identify_on_join": "Aktivera identifieringseffekt n\u00e4r enheter ansluter till n\u00e4tverket", "enhanced_light_transition": "Aktivera f\u00f6rb\u00e4ttrad ljusf\u00e4rg/temperatur\u00f6verg\u00e5ng fr\u00e5n ett avst\u00e4ngt l\u00e4ge", - "light_transitioning_flag": "Aktivera f\u00f6rb\u00e4ttrad ljusstyrka vid ljus\u00f6verg\u00e5ng" + "light_transitioning_flag": "Aktivera f\u00f6rb\u00e4ttrad ljusstyrka vid ljus\u00f6verg\u00e5ng", + "title": "Globala alternativ" } }, "device_automation": { @@ -71,10 +87,19 @@ "device_dropped": "Enheten tappades", "device_flipped": "Enheten v\u00e4nd \"{subtype}\"", "device_knocked": "Enheten knackad \"{subtype}\"", + "device_offline": "Enhet offline", "device_rotated": "Enheten roterade \"{subtype}\"", "device_shaken": "Enheten skakad", "device_slid": "Enheten gled \"{subtype}\"", "device_tilted": "Enheten lutad", + "remote_button_alt_double_press": "\"{subtype}\" dubbelklickades (Alternativt l\u00e4ge)", + "remote_button_alt_long_press": "\"{subtype}\" h\u00f6lls nedtryckt (Alternativt l\u00e4ge)", + "remote_button_alt_long_release": "\"{subtype}\" sl\u00e4pptes upp efter en l\u00e5ngtryckning (Alternativt l\u00e4ge)", + "remote_button_alt_quadruple_press": "\"{subtype}\" trycktes fyrfaldigt (Alternativt l\u00e4ge)", + "remote_button_alt_quintuple_press": "\"{subtype}\" trycktes femfaldigt (Alternativt l\u00e4ge)", + "remote_button_alt_short_press": "\"{subtype}\" trycktes (Alternativt l\u00e4ge)", + "remote_button_alt_short_release": "\"{subtype}\" sl\u00e4pptes upp (Alternativt l\u00e4ge)", + "remote_button_alt_triple_press": "\"{subtype}\" trippelklickades (Alternativt l\u00e4ge)", "remote_button_double_press": "\"{subtype}\"-knappen dubbelklickades", "remote_button_long_press": "\"{subtype}\"-knappen kontinuerligt nedtryckt", "remote_button_long_release": "\"{subtype}\"-knappen sl\u00e4pptes efter ett l\u00e5ngttryck", diff --git a/homeassistant/components/zoneminder/translations/sv.json b/homeassistant/components/zoneminder/translations/sv.json index 4f0da20207f..f3e7d2891bd 100644 --- a/homeassistant/components/zoneminder/translations/sv.json +++ b/homeassistant/components/zoneminder/translations/sv.json @@ -1,10 +1,15 @@ { "config": { "abort": { - "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt." + "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt.", + "connection_error": "Det gick inte att ansluta till en ZoneMinder-server." + }, + "create_entry": { + "default": "ZoneMinder-server har lagts till." }, "error": { - "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt." + "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt.", + "connection_error": "Det gick inte att ansluta till en ZoneMinder-server." }, "step": { "user": { diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index 386b306e1b8..8f8777f92b0 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -1,19 +1,75 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "discovery_requires_supervisor": "Uppt\u00e4ckt kr\u00e4ver \u00f6vervakaren.", + "not_zwave_device": "Uppt\u00e4ckt enhet \u00e4r inte en Z-Wave-enhet." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_ws_url": "Ogiltig websocket-URL", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", "step": { + "usb_confirm": { + "description": "Vill du konfigurera {name} med Z-Wave JS till\u00e4gget?" + }, "zeroconf_confirm": { "title": "Uppt\u00e4ckte Z-Wave JS Server" } } }, "device_automation": { + "action_type": { + "set_config_parameter": "Ange v\u00e4rde f\u00f6r konfigurationsparametern {subtype}", + "set_lock_usercode": "Ange en anv\u00e4ndarkod p\u00e5 {entity_name}", + "set_value": "St\u00e4ll in v\u00e4rdet f\u00f6r ett Z-Wave-v\u00e4rde" + }, "condition_type": { + "config_parameter": "V\u00e4rde f\u00f6r konfigurationsparameter {subtype}", + "node_status": "Nodstatus", "value": "Nuvarande v\u00e4rde f\u00f6r ett Z-Wave v\u00e4rde" + }, + "trigger_type": { + "zwave_js.value_updated.config_parameter": "V\u00e4rde\u00e4ndring p\u00e5 konfigurationsparameter {subtype}" } }, "options": { + "abort": { + "addon_install_failed": "Det gick inte att installera Z-Wave JS-till\u00e4gget.", + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "different_device": "Den anslutna USB-enheten \u00e4r inte densamma som tidigare konfigurerats f\u00f6r den h\u00e4r konfigurationsposten. Skapa ist\u00e4llet en ny konfigurationspost f\u00f6r den nya enheten." + }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_ws_url": "Ogiltig websocket-URL", + "unknown": "Ov\u00e4ntat fel" + }, + "progress": { + "install_addon": "V\u00e4nta medan installationen av Z-Wave JS-till\u00e4gget slutf\u00f6rs. Detta kan ta flera minuter.", + "start_addon": "V\u00e4nta medan Z-Wave JS-till\u00e4ggsstarten slutf\u00f6rs. Detta kan ta n\u00e5gra sekunder." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emulera h\u00e5rdvara", + "log_level": "Loggniv\u00e5", + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + } + }, + "install_addon": { + "title": "Z-Wave JS-till\u00e4ggsinstallationen har startat" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "title": "V\u00e4lj anslutningsmetod" + } } } } \ No newline at end of file From 26e2ef81757c5ec10fc5ba6a634882e1a2977d60 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Aug 2022 18:20:20 -0700 Subject: [PATCH 094/903] Add repair issues for nest app auth removal and yaml deprecation (#75974) * Add repair issues for nest app auth removal and yaml deprecation * Apply PR feedback * Re-apply suggestion that i force pushed over * Update criticality level --- homeassistant/components/nest/__init__.py | 33 +++++++++++++++---- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/strings.json | 10 ++++++ .../components/nest/translations/en.json | 18 +++++----- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 72759ac0f52..44658558b62 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -29,6 +29,11 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.camera import Image, img_util from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.repairs import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, @@ -187,6 +192,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, unique_id=entry.data[CONF_PROJECT_ID] ) + async_delete_issue(hass, DOMAIN, "removed_app_auth") + subscriber = await api.new_subscriber(hass, entry) if not subscriber: return False @@ -255,6 +262,18 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN: # App Auth credentials have been deprecated and must be re-created # by the user in the config flow + async_create_issue( + hass, + DOMAIN, + "removed_app_auth", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="removed_app_auth", + translation_placeholders={ + "more_info_url": "https://www.home-assistant.io/more-info/nest-auth-deprecation", + "documentation_url": "https://www.home-assistant.io/integrations/nest/", + }, + ) raise ConfigEntryAuthFailed( "Google has deprecated App Auth credentials, and the integration " "must be reconfigured in the UI to restore access to Nest Devices." @@ -271,12 +290,14 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: WEB_AUTH_DOMAIN, ) - _LOGGER.warning( - "Configuration of Nest integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " - "(including OAuth Application Credentials) has been imported into " - "the UI automatically and can be safely removed from your " - "configuration.yaml file" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", ) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 72e0aed8420..d826272b207 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "application_credentials"], + "dependencies": ["ffmpeg", "http", "application_credentials", "repairs"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"], diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 0a13de41511..07ba63ac479 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -88,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Nest YAML configuration is being removed", + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_app_auth": { + "title": "Nest Authentication Credentials must be updated", + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information." + } } } diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 5f026e55f31..cd8274d635a 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -10,7 +10,6 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", - "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)" }, "step": { - "auth": { - "data": { - "code": "Access Token" - }, - "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", - "title": "Link Google Account" - }, "auth_upgrade": { "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", "title": "Nest: App Auth Deprecation" @@ -96,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Nest YAML configuration is being removed" + }, + "removed_app_auth": { + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information.", + "title": "Nest Authentication Credentials must be updated" + } } } \ No newline at end of file From 44b621321758435a159aff5050cf2a88623c0fba Mon Sep 17 00:00:00 2001 From: Eloston Date: Tue, 2 Aug 2022 02:29:44 +0000 Subject: [PATCH 095/903] Add support for SwitchBot Plug Mini (#76056) --- CODEOWNERS | 4 ++-- homeassistant/components/switchbot/__init__.py | 3 +++ homeassistant/components/switchbot/const.py | 2 ++ homeassistant/components/switchbot/manifest.json | 10 ++++++++-- homeassistant/components/switchbot/sensor.py | 9 ++++++++- homeassistant/components/switchbot/switch.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 96238f61fbb..ce31d6e8dd3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1046,8 +1046,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core -/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas -/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas +/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston +/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode /homeassistant/components/switchmate/ @danielhiversen @qiz-li diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 42ca0856b02..e32252a7615 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -22,6 +22,7 @@ from .const import ( ATTR_CONTACT, ATTR_CURTAIN, ATTR_HYGROMETER, + ATTR_PLUG, CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, @@ -30,6 +31,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], + ATTR_PLUG: [Platform.SWITCH, Platform.SENSOR], ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], @@ -37,6 +39,7 @@ PLATFORMS_BY_TYPE = { CLASS_BY_DEVICE = { ATTR_CURTAIN: switchbot.SwitchbotCurtain, ATTR_BOT: switchbot.Switchbot, + ATTR_PLUG: switchbot.SwitchbotPlugMini, } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index dc5abc139e6..9cc2acebbf8 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -7,12 +7,14 @@ ATTR_BOT = "bot" ATTR_CURTAIN = "curtain" ATTR_HYGROMETER = "hygrometer" ATTR_CONTACT = "contact" +ATTR_PLUG = "plug" DEFAULT_NAME = "Switchbot" SUPPORTED_MODEL_TYPES = { "WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN, "WoSensorTH": ATTR_HYGROMETER, "WoContact": ATTR_CONTACT, + "WoPlug": ATTR_PLUG, } # Config Defaults diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index dcb33c03882..41d0d7efda6 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,10 +2,16 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.16.0"], + "requirements": ["PySwitchbot==0.17.1"], "config_flow": true, "dependencies": ["bluetooth"], - "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], + "codeowners": [ + "@bdraco", + "@danielhiversen", + "@RenierM26", + "@murtas", + "@Eloston" + ], "bluetooth": [ { "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index f796ea05e7b..fb24ae22679 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -33,6 +33,13 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), + "wifi_rssi": SensorEntityDescription( + key="wifi_rssi", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), "battery": SensorEntityDescription( key="battery", native_unit_of_measurement=PERCENTAGE, @@ -98,7 +105,7 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): super().__init__(coordinator, unique_id, address, name=switchbot_name) self._sensor = sensor self._attr_unique_id = f"{unique_id}-{sensor}" - self._attr_name = f"{switchbot_name} {sensor.title()}" + self._attr_name = f"{switchbot_name} {sensor.replace('_', ' ').title()}" self.entity_description = SENSOR_TYPES[sensor] @property diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index e6ba77fa164..65c7588acbd 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -33,7 +33,7 @@ async def async_setup_entry( assert unique_id is not None async_add_entities( [ - SwitchBotBotEntity( + SwitchBotSwitch( coordinator, unique_id, entry.data[CONF_ADDRESS], @@ -44,8 +44,8 @@ async def async_setup_entry( ) -class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): - """Representation of a Switchbot.""" +class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): + """Representation of a Switchbot switch.""" _attr_device_class = SwitchDeviceClass.SWITCH diff --git a/requirements_all.txt b/requirements_all.txt index 5a56eb1b3b2..69098880563 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.16.0 +PySwitchbot==0.17.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 110f0446b0d..44d4e9fd0ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.16.0 +PySwitchbot==0.17.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 41d7eba1ad006c1a0ecde74a81d70250aeb9a357 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:34:48 -1000 Subject: [PATCH 096/903] Fix govee H5074 data (#76057) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index c7909d3e1af..624a38ebe9d 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -24,7 +24,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.5"], + "requirements": ["govee-ble==0.12.6"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 69098880563..690a0886451 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.5 +govee-ble==0.12.6 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44d4e9fd0ee..1ac8b0117e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.5 +govee-ble==0.12.6 # homeassistant.components.gree greeclimate==1.2.0 From 33651d14df58401eb836897fd19aec307e3d99ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:36:27 -1000 Subject: [PATCH 097/903] Bump bluetooth-adapters to 0.1.3 (#76052) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f215e8fa161..40e63ec7180 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.2"], + "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.3"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d4344ec256c..860d1538727 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.0 -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 690a0886451..ba339f73b25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ac8b0117e3..0f26add462a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 # homeassistant.components.bond bond-async==0.1.22 From 56050e9fbe374f550028685958c65a24de9aca0b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:46:22 -1000 Subject: [PATCH 098/903] Lower bluetooth startup timeout to 9s to avoid warning (#76050) --- homeassistant/components/bluetooth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index d42f6b4f230..39629ab6d85 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -50,7 +50,7 @@ _LOGGER = logging.getLogger(__name__) UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 -START_TIMEOUT = 15 +START_TIMEOUT = 9 SOURCE_LOCAL: Final = "local" From 32b1259786578c8cf9beee4c8d1ebab6ef7aede7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 08:54:28 +0200 Subject: [PATCH 099/903] Support multiple trigger instances for a single webhook (#76037) --- homeassistant/components/webhook/trigger.py | 65 +++++++++++++++------ tests/components/mobile_app/test_webhook.py | 2 +- tests/components/webhook/test_trigger.py | 63 ++++++++++++++++++-- 3 files changed, 107 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 3f790b1ec42..498a7363a61 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -1,5 +1,7 @@ """Offer webhook triggered automation rules.""" -from functools import partial +from __future__ import annotations + +from dataclasses import dataclass from aiohttp import hdrs import voluptuous as vol @@ -13,7 +15,7 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import async_register, async_unregister +from . import DOMAIN, async_register, async_unregister # mypy: allow-untyped-defs @@ -26,20 +28,35 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( } ) +WEBHOOK_TRIGGERS = f"{DOMAIN}_triggers" -async def _handle_webhook(job, trigger_data, hass, webhook_id, request): + +@dataclass +class TriggerInstance: + """Attached trigger settings.""" + + automation_info: AutomationTriggerInfo + job: HassJob + + +async def _handle_webhook(hass, webhook_id, request): """Handle incoming webhook.""" - result = {"platform": "webhook", "webhook_id": webhook_id} + base_result = {"platform": "webhook", "webhook_id": webhook_id} if "json" in request.headers.get(hdrs.CONTENT_TYPE, ""): - result["json"] = await request.json() + base_result["json"] = await request.json() else: - result["data"] = await request.post() + base_result["data"] = await request.post() - result["query"] = request.query - result["description"] = "webhook" - result.update(**trigger_data) - hass.async_run_hass_job(job, {"trigger": result}) + base_result["query"] = request.query + base_result["description"] = "webhook" + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} + ) + for trigger in triggers[webhook_id]: + result = {**base_result, **trigger.automation_info["trigger_data"]} + hass.async_run_hass_job(trigger.job, {"trigger": result}) async def async_attach_trigger( @@ -49,20 +66,32 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" - trigger_data = automation_info["trigger_data"] webhook_id: str = config[CONF_WEBHOOK_ID] job = HassJob(action) - async_register( - hass, - automation_info["domain"], - automation_info["name"], - webhook_id, - partial(_handle_webhook, job, trigger_data), + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} ) + if webhook_id not in triggers: + async_register( + hass, + automation_info["domain"], + automation_info["name"], + webhook_id, + _handle_webhook, + ) + triggers[webhook_id] = [] + + trigger_instance = TriggerInstance(automation_info, job) + triggers[webhook_id].append(trigger_instance) + @callback def unregister(): """Unregister webhook.""" - async_unregister(hass, webhook_id) + triggers[webhook_id].remove(trigger_instance) + if not triggers[webhook_id]: + async_unregister(hass, webhook_id) + triggers.pop(webhook_id) return unregister diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 0bc237b1c11..b7b95dff392 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -840,7 +840,7 @@ async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_clien @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("tag_scanned", store_event) diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 2deac022b1e..e8d88845f5a 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -23,7 +23,7 @@ async def test_webhook_json(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -62,7 +62,7 @@ async def test_webhook_post(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -97,7 +97,7 @@ async def test_webhook_query(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -126,13 +126,68 @@ async def test_webhook_query(hass, hass_client_no_auth): assert events[0].data["hello"] == "yo world" +async def test_webhook_multiple(hass, hass_client_no_auth): + """Test triggering multiple triggers with a POST webhook.""" + events1 = [] + events2 = [] + + @callback + def store_event1(event): + """Help store events.""" + events1.append(event) + + @callback + def store_event2(event): + """Help store events.""" + events2.append(event) + + hass.bus.async_listen("test_success1", store_event1) + hass.bus.async_listen("test_success2", store_event2) + + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success1", + "event_data_template": {"hello": "yo {{ trigger.data.hello }}"}, + }, + }, + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success2", + "event_data_template": { + "hello": "yo2 {{ trigger.data.hello }}" + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + client = await hass_client_no_auth() + + await client.post("/api/webhook/post_webhook", data={"hello": "world"}) + await hass.async_block_till_done() + + assert len(events1) == 1 + assert events1[0].data["hello"] == "yo world" + assert len(events2) == 1 + assert events2[0].data["hello"] == "yo2 world" + + async def test_webhook_reload(hass, hass_client_no_auth): """Test reloading a webhook.""" events = [] @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) From bec4b168d6a0d8a23e07f0c91864cbfbe006d08e Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Aug 2022 02:56:50 -0400 Subject: [PATCH 100/903] Bump AIOAladdinConnect to 0.1.37 (#76046) --- homeassistant/components/aladdin_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/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index a142e838f3e..008b8f81c89 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.33"], + "requirements": ["AIOAladdinConnect==0.1.37"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index ba339f73b25..df7bb65f9da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.33 +AIOAladdinConnect==0.1.37 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f26add462a..196cca0c3ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.33 +AIOAladdinConnect==0.1.37 # homeassistant.components.adax Adax-local==0.1.4 From dbac8b804fb778ee663e8fb756ced68dd930be81 Mon Sep 17 00:00:00 2001 From: Sven Serlier <85389871+wrt54g@users.noreply.github.com> Date: Tue, 2 Aug 2022 09:09:24 +0200 Subject: [PATCH 101/903] Update featured integrations image (#76011) --- README.rst | 4 ++-- docs/screenshot-components.png | Bin 121315 -> 0 bytes docs/screenshot-integrations.png | Bin 0 -> 121315 bytes 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 docs/screenshot-components.png create mode 100644 docs/screenshot-integrations.png diff --git a/README.rst b/README.rst index 05e13695a58..6f5e0e69892 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ demo `__, `installation instructions `__ and the `section on creating your own components `__. @@ -24,5 +24,5 @@ of a component, check the `Home Assistant help section 21BEC!?oW?B|aMwmzpTLbp3-0YUq@$IGqWzby#lwg2}@{i33CuKRxz z1oDfKJAUI&LEhh3RJ1Sd1ADrlMZ0R+G4biV^2%Z##t9fM!rAOQIZPUN(l|LBVst?5 zNbL31ek19x3@rc4;2^A<+J|43YwWJD>4C(4rK_Df)C}GEhq1cPq5EGILG`&TDqB+? zKbWJL(mnQ;+x_ZTG(%<5b8>R3x`k!B*kC;nHZd&2>g42P_%oLHKpL9>hkc9=ccJZ2 z8ry&W0>jg33@iVu<^uNv*tF%n;rPjVj(`hXw>dc_r9`hx=SE6idn=5m{>!7!@axBL zp?f*X@l~+@=?-4P|8yq_Mq+{&!7eIl!o$N;AHt$SQ+T2UW>&4d@Mu1J6|}_Rf4cJD z0N`oXd!&b3ZKL>05xn*@l?H}q`~HB?67fH2#QqO92VqZIj%a9lTAHmdYm5#bvuY}f zD>iDd(`(yXvLjjF=_y}}B!i%7yrSh(ECTY{G03*VCf(%!wRbo9-Kk zrtDS8pWB-qdOlP^yW%++NQ{U+Q|Cb1__V~lv^4G$51c}(3b3ZpA+SK#!*z`*z zLhYwNM;o~R+sXcf7~cPP+2e)$e_X7SqoU23>_1Y?rf9L<*1f-bBA z)O>szM>B!4d*gX*CQYBya&yUzI|EzPas<@p%8jij78_l1)N+Ni(>cvp+kE~eQ;7Nu zwfkM#ygsb89U%&!c$IN?W1#8NA67|B<$n*?VQpJX^IWwvPj!Kj7L2Z=NsBq2HoF>0 zW5z8vP19w{Ivl-BajHzp%kW{HF1_Sb6(Z#uB(87FD5&VfoT<|#s;VD8WYaSsJxF+X zdREXF%zFGd5jRqK$+9<3C1f)oEfQ5^H7KWMDp)bWk}OZr`)$AvO0BJ0Sl)GphNJO# z>?j$xc$CzCUj0BS_?f-VexmVv47aIU4-$_b+@{U-a&OFI6ga5l@83%3dOTN=qLRis zT=p}cbv^i{!+EKG7<_p(qd9>eh}y=eo~_gs^pBpOpC4o>zt}`zFT9+^Q0t&xiIJbq zfxo{Gi-{W$BIug^l}S%Q#yAok$*39!S5B^6v=AOTPp{kAv3XTW$$^i5n$SbPwCNFoN@FB^AYncj=={XNrR z;NwW~U2*-QUJ=U`Yh4n{t-ZkKmCULTgb8^rf6I}q+S{J z^61$zvk`;uuufaoj5i_UgJN7Q;^bC7hr$Px4X-==lQMip}}7_-hQo_nG|(gI8J={ysj5nzC^ z3w7=@E^bY)renUqftQQ=p4MOFCMV;GS!r6K#r|HWTO}6aoNoBWyf0IKnI_H>@1cP5 zuNE`t-r1|MU4l>b*)*4ptSQGS)5dbSdW?%VV-NPNOt^MzA-aK;K{?9{u6+JB#v^2# z!`|!bf`4sweqG)Vt+BOax?Y3*p&d-&#_j>c-AdyY0^!OJwe_4C>?9)H@NW?@K{xBc zCSlF`$8!TXoPOvj9s}2>&D&UxOZ8Cdz`TlvO z+1=x!pV%an2s7XF`sfdj-Du`U{+*PK_sPQ4+Ke;>L`lwzXD3-RocR&h?H8{QN2iyRJ_ ztzkJ+@N~7MELqTBqv(V!W_#87``uLw-{I0l;aaDO;$g*4wL`e-DZA6wIwj_|{O{Ak z!rb!fWTNX_glV_u_J_Wu8P|HCh+Fkgi4`;6)6*@kNIe-0{T)T)T7&&~C^l@+J2dBC z^q)=*!``zDeA-m3b(oTAIej=Tv8(vdV{s#SlRlN{tB&tG2LSe?7omsYa$zHhO*K=_ zx-2)2XyB)tUJBx1K@-!{KR;`j3LQ+X*I55-xz+GIGbLN^3s6w-mnf>@CQ!)Tw4~$0 z=d`D8y`naaK5bwUA0ieIR|6=KZLEcdR?~}M^hx{pq+=o?3vAMuO{-_8@;K_4%HG7( z<+@{C$aG>7OZG-$XJnZB569s3AyG1b1>x+G6h4{P@wm669HP6ax+kuqRJ}9crjtZj zteP&|mr=nsIE>BqWTEEf$#x`tktP|H|J{`PAO+LQ-Jm2rFE20XR-DEK1)I`2QfgqC z<^3)!t>J&Sr5gb}I_Og~JMZO>QaJaW| z)Dl^YY2Z*O{tKU7f9h}$oxf22H%d=;^i`4>1#_pXO!r47vR{Xnz<`FaHR?$f{ikNa zc`5o=g{W;%WlpD>!I=>eOEl8%hYl$$56i>=-_h%%E!931lT^H=>Uv;;Pw%PQDtMFz1ze<;EZrp6Lp$o-9^0 z2N;a_<0EfSgQugONG$>9Y8PqObaNInOd#e1j6-Ph1 z*&d`~6Z2twukw84Sk`y(-mV6!HK?ES+8b{)@GHFZVER0XWIHtZ$}RK!U`&r3edb-TwoS#An9eqLp)d(L-( z0{@UYjigtM@?y!d=>Dh&S~)3=q>q(&?#Jtl0P0Ir?FT5Z@{=$TuI4R*M|N0(VJC~8 z5G~BnKWGu}qLWAKnaC*|be;}9~c|H%C7nj&aWZqzXNFI3E^!(ZQV>U#??T~xKB z)f-aHqFWkhoV$B+F-X<4bvVj9k@`#j_9|uz(;Aq7uPHW#)vx-lkC^iJ?OMQu|M>T4 zoOtZ6T#Nblpqt~z>5gAY`)<&GEJI`gmc<|I5A$QPwOxL3OcA1F64>L==bw{^Wz>xypI-+ zBf_xAM3kQIwy|4|E8CnqKPs_YK&uBx`9Cd)mG)_9k1TefoFzZwTX|(gxXs~~c_`1% zz9mLo%j{NwEEJh5<00K>99%&-Wm@e?9|jU*{jvWN%VuwT$( zN-I;mO_hXMx^Gu3$9~f7gRXV1cXpeMn<^@q=JOIg-1!@)v#(^YTl>iE614o%Zk^>w zvmw6w=w;BLby0E#wG`yeJP#ob3Gkpz4yTQFU!gvAgl-R-t{Q8tk!)S zdAY4DzwT)Rla$}TJaGL*(Rx-bX?Mh6VH3iayt!e{m;0(MdE>^tj%$WS*=-@0@QBJ4 zfovx=9%yy&3&JZxWoCPe6ARj>l@@ArUbO3AnfW=9xT|pB<|~J=8O6ZfM=~Mlb-=}A zGsLCvPQ7-bp5E#ALl7S^FSvsX*qQre ztqWxH<%qg=UYEP9cgp@ElkKdwL`4lJf%j*u58Ykhk9l&`eihbf#o-k5E#9{X9;+7AxpO5oP-X2Ex2htUp>>zy5h_(>anxFo15 z`Dy1#>$nLR#C`Y?I2WHm5P(nAV4RR=K5zQ7BB%zB;X4-B=9u8Rb5*F;uGn6V_tEU| zfM73=AIF;lKtm*@e<`Nu1@r<`^@^cv#bYr{{&qdM$u`Afk0QvFeDYoz1zWa-q*Z-UgKq_0Q#5aC zN`W9;hFps{(eT#bC&q0NevC*S4?f$bm_5)phxYrA&#<&CJ5PDc9&f!r1ow))d@L?d z+}OcbS)m)qQrUFJhfiz5rk?jVc>(3WF(7ArkU)|?+gWM3v{^W}P~W|3<39m9;4%_? zYzM}X^4FW=G!E>!VwkZaLwq6L;oS>0+qQS->uTFP9H)HW5X`OmaQ0uGF5T#A?Uo8X zPq@iK*<(EBNAFFhl|Wn8zF(dMh5wkyri zHzFxaFOI$DMjjpjoyH-tLDU)4_|z`@iH2M1&Y5YQICmrCS9|Q(^vkCwEMzblWMf1_ z>plSKf&kAWG027bf|&BQ5fJ1)Jo9~w0w(`7DwHSpq=1Gr;hmZOHsEmXq&9=4u{m7= z{RaK0(O$mw$hzdy23~ZZD^J@*;f|s9zV5IV+>*MQ4gWoO7yDh>31Ez63p85NYHk?Q zKNEAAf0E~bdh85^pspG2ItPctHJ^V<`)ZvoDvCue3o#K85%p})uHtZyvh;31g}kA- z)&wU|@-NBA)ETQ_7hzqP-fWKeXP0e&2BKs;a&RXpS7l%(A%(hAfA0%VMq1Ao8mo zouN!g$)O9bDsX+_L~U2|QkztkqO=?W=AnTcjSo;JbPFWDI0t#>3803D5iX&M?ReP% zu`>!sFB-`r5PD=uwfFlF*O=R*!o6J^IM|=A?)M9p+x7LVZgO3QZ$@6yZH5On!hye9 zh<1~Aq5h6@bVOWHwRX12wrf6Jd2PsY>&Pi{aS;FCA5hOoV!W?^=?hrdX%ukI0B$UK z@O$aYH?7`Nr?odF4}=tTYYWvuHmg7VQF#S+ooWJ@0~U<3+=g_kxhdMeQe9RuWqViR z{RSNDg5J!pe;oe_=dvZGaz%_n7;Z4kMn>b)BckP7$TcH5|%f0cmvXq}gqa-sf+sbDMmbA`=3-_XX=DVXo4r{+M#`mCtdc-_1VJK zVDb31pskSn#jm7yTSrq&Ya}|i%V$O9%^%u9(S*nd+ik75wwNd9+>mufbyC${{ySYexZJSq#GeR2V8|XPz%qXA02-qT+28sx9)&ee{y0jFx@( zep6Xfb0jv>ZJ>^LE5cs7`uX!|0x_<1UyvAalx}+fhkB=jWjLLQ!^Hh@pk_oB;vOqNvE@Ezt9fjYaf#`|m#wHc{Esr}jN_Yz!NM)p^L znb_+6N%hY!{S(h0S)ifu;YQoU=8~Y*k8sBO87ueE45FR{XXV)0#^~$0EV)8^}Yi{GhP4a!=ZLnEf4|;~&qh_Ro=;ib+e6n7IO>Pobsntlc-+ zn>IW8uy>aVevCKTKa&M7(zQP?HM%$VU<-c=^WR)WBJit`+=Mg zjU+v7r`1PPVO;FEyj`gXPA>kb{t14mb+}d($=~;qE22M_se(nLSTamY7)K_UgwEMM ziHguA9Fz)NvR`u2LGu{>U_C1+ps=|6a;LaP?!a-R zTo;5FfN{CcoMh1 z;MS`^p9c;co#I~H$^{25`=V1INqlpFiQwg{o$SWsPctgxLYI7Hb`V=vbJU3Kiz9B+ zzd5eK)s}DITo>z|tA(L0sukI)EgPSl%$u2dSI*-h>mM3E4UzoQNTG}W zXc_uMi0U3Ocy{dC+0@H3tQ*2DA42JWK;uy?jExHzKW8Y9T9kYS(DB-h^U*r!ukoWw ze&2)y{rqqtBa0tDDQr*3nwqhlaOOB|8-ZJPIrdB_V8I(1+w8FZti={CL-uA1feZJt zMj?*Ux*rFc0@^Tor@Y?DG(4r~Tl1Ecv=7}8KK#;I{Wi{iixk0fN+tf(KNS-N-6b51 zXsMb{DsERkOl*DfmLfc^UaAz2N|DmL*-RUVw}VFXfy7mAvpY1bLXe;t`E9lC;LX60 z@XGsIH#g2T3$g0poQaTgOfMmFe+QGpFHrZ!I+_oiUIdE5o?=&%vW=%7gOYv6jj6xD z7Sd2jNENyKn}r?Cqv+ldT{f4-9!A?MQ98<*5mlt1_wK(sXVXWxx>iyzD0@1Rq<1{I zPNh+%!>}coGqU_eR@&K5<8XRWH(GFvW>F+xp6bkY6aP>x-p-l08UqQiP8Hka?`g-Q z$uNvF=9>q<+^#ZBNIaGk)-|pAtvTM^>x~$FlE#?oi zD8jv8N#@~6_W{LKCb&EVONt^`Bnrcm{35z6#ItPdw+V?QUk11_ICP`5M0kJ$Z)5?X znka--!cyOK&k!!cd%!)TXQ^gO%<-OFoIgL)2GhJfXD3RXFAY1O{$aM`IvsK6al0jd z5Ec6!b@Y3BU981A=ZGMIMqWc@3VlLu7N@Okn{&{!`!MNcy|2mrYC9X?MCzD6<73R=)YezG@&i&{VgIa zRn2#2!7olrzlvO*8D4mQ{*7U)to9Qm-yuB0n0E}>z?`>BDKK3uGT&@A`u3zVphPuZ zWDP69XWP-K`q;LKiJ1{psj;16zYz!z;6r8e7QLphT{CH_{>8PM_%Qez={?kB92Lbf zZqZY~K|h{-p)~-Y<3tBDN=g#8lP~nD)ifY$i}rJt0y2KA7-3laLt!1!XWZyq#yyT-S8jlEe^ZJDW zRYTo#|IB9DM2e-(JR5*C_Yj|tnC{?s-dB5t0n%OcgNO>3>2HyKr<9G{z7kJvYY(mmzy_ZR5e18%P_dg=XHHM-75FE4PKc_0XmI|V zUn65KfsS-0$8XI(&rL>%wmo^j^9)k`W%!$Qmo2r9a#ZOfJEVZyIEa*cf%H#!C@gI= z*1o_SOzyX{`NOKxev7MJFzJh4`W2|*^Qw*ysk98z80;%3HXeBYTRkV8Q37rV?Zc-W zosf80^3OShY~#1kIbO3F4oS~Dfpr6m0e&+*$=>X*(z>p z>Xnz>Fww!0^PU4OdadP<=>1aD7P!Yr&EOE)AeB8a1sZ7UrX6duH!P~u*(Dt1Ek6!6 zLXE0l;b#^8-A?yy1s@BcW=UD%u7?NFwsX`ArH2{qnf;x$>7qyAbg4Tcn-{bn?QLdz zJaKHll0ep#tpm$^f6*XGiQ0!bfn8cWzVOk_$D?Cc2(Wnxyn6GAwD8MO*_ZOk0a+qW ziBP!wvm!wX6^VE^x9U~~I19N&#g??~>;C^JLvGFYMc_dDV?S%XnQC7DkmIM_( z=aA>XKVfXdz28-xwc-mPUQ~Q@U{LQlR)$2P(klc(vZmS`*u&6<)5xZsTe)LyrFm_D z;x7icT<|9}bKNky0tzDbxrRIyGNVzzDN`g$|3ZY&3B$)kJA5<1G8lNXXn#Yx z9(Wm7=JZ@m<_UuJy*uYLiCUsmwi=&Vauc3Qyp?n(Z$yWsbo}*H_E~1ulM71jFD65m zy$@M$p}KahZ)GPqLWc8PW6qa|=}0)V9Vnp^Hj0s5lh}sTo*3x#YuHKpCAWuRYTOq} zd?0PE`&Dv(;j<*Tn4404)lZy_}6RWBM&j2n-nN;h;I&6JR3%o&YH{Obgb1C*_Q3mwZ0PXRfs z)ka{Db5GiNw5#=b@}c72(ve50ZACg=oLtqqkD+Z3j2K*=6eGI*b=!hKqavL`ajS{V zJfaB$`ZWDg45I`37-bi5CNTauE&eKrd;FyksAY`7AIFiEcgJL^=UmnK6jPB})pdbf z_V>_ofGz5yO+@VxBP0!yq(tt~WvB4Ys#}fkey1IM96KL3|GR))cP{;&5^yjE7JJ;_ zFs0^ybHX{MZd9sQ#NXIx$-TdkMss>WP9uPWtXIn(nc&vDjEe;wSz!m`*Yb&3Ze#=g z3?g^=IQ5p1g*i~enN$1sx#HPmX8^*73`mYc&$*CBb)3}4m;jeo{TxConW#(BS_JRNR zNHkL(wabU5TF23rq>b!c*=G({Xo!@Y8~iCW!ZuJK{wSwaWZF|GrYKjKTgX8T_}1uO zVq6z4=@rI|10B!zXx`H}_osrFr@{BUM<1ZjhP=%XcaJgtdAY#XECt?o^e*VvjL6Z; z+(Sy2ZujpI6Po*mq?D{?FUhYIbdeXD1rAk~zTzIFzWzi}HFyz+rtC=wGv@<*PdTop_jH$y=+QN4A1Xm!|T#vr;r& z?88uSLVjLhqh3Jz#1OthuKamcPsgMl>y#NVje)C5*MYN^`=xp9f<%&rG`(2e)+^20 zz=ZbU8QVq-gh1%otYRO+X0N~{n2UUt1p~1}EoI(;_t1u>AbEYrcP&t*lRg1&@JWGk zSE9a6jdv>$#_tKm(R&(5)P_;K2AE0{4o0_9>~}%av3GW#iT800K}|JP!U?s~!t~G0 zf}20a=nC*O1dhBq@z1(j#E9MHzsTO(Yzx8YCzKcEGdIO|{KF#xa5Rokk3>%geK$m1 zBHEa$;8EbFTapPP_$u`pLkex!s+Lm7&2Y{p1qoTytT{ z(H!Y^9OD#7H!W>I0qA?&M;?y#muIK+TsJFMM`&(x;w4GUP_-O*F3)d#QQ$IMrkK{l zF6%hg|KZTD169tWSWVNq+U}av>AcCsx3nU+mp>m%Oz^DhG5=aFZ;s0in>B$;;&Ej> zu5G{b5gv_+G7&ap(>Zx=E8x!5#$?J{I)u3g9o7*Dpp>UNFa>r|zz&JuRr#K}FOll_3k?zdb)MpBX4w zw+CGu+Vcc1aNqB{G^sevO#1f%I7Ur#WQN-oI9qqNHrCDw8i3YA%T#oVBdKLGebH(Mqz#Of@X0ZS-7Lev2jXB<_1FN$Wi{o}Vf_9w0`NMf2ov9>m?0YC(_6+Nh0 z1iq#t9ZaI`1o%WJZhkS*TS3L2wqCxeP;mHDod+m1=ZZ%sv@fQjY>`!$5Z%kH{2^22 z#CBlhcCqmeV@(@F*wdJdviAKY)rE7!p6AD?gCXrAbvoes06L}WR>{OLQ?{2)P2AW0 z**BZtPB$ovIjU?n79IK=^KTgd&r+K^$4k{$0+^>J91-D%DjcW#&a-PP_! zzWatQq9qa4qq<{|5i7Gv(dfJOtj8YGcJ}c{4C^5)waGg?2@G$4r9+B%`tfEaP1YuMP7;#->Z+D<4C8b*!y(nQgz3bNtwKdpbus?C;QL-2~TUZlV_xS=z z<`!aLfRq^a_1nXN0pU&P1b?m&9S6{j`zlr^Lx&wr@1tbtT`py~KFBTTO25vhjbYg)~ zxEkiP<3O<%3kT!A<4YJ@)dKegEG*!(Wzalqvo4Uy9N6AMd|TX0S_t4Ad;h(3w_J^- zzeuR0~yB5q$6m$>Zol5cwIUd7jd_uzWke`u07 z3eAOC#iA5M<679OLl&s!5O+8uSmHt^Uka3{s3dzTzcPiZ+@VP+(@c{rMBGMTDf7G| zN~yn<%UzSO&5{86TrFMNHcVV!5}s6@cs3N)p8nM-Jcbv>-&oZLal<6?I9zE7b6HvCKPb->a<9dOh~6;UBhUp#5RO!?P(zk71AA1`HX)~9rHpt+ zr=wX6h>OH^f=fK@w)%ozr?{ybiPNNcNU2Qda5idL) z2Kr7uNeS9qD@Kr^+|Id^1+~EyA7euOUeltC>GH)wh4%d)zk1>4{q#~wD#|7)B>YmN zu9_{pH^eyX;n8NpVjrHjIzQfLqH?EEU96%{-y*&7C9p>3-qZ0wWLmZmt6P`Qk?sZO zXVklJ#P5+_QZ^|lICoxqS_z4_W_e&5&DU2gl|OzPG%hQa7=-b2?_u^al`ekp5~B5u zE-X;yo@Sl)UD2*@WoUKLm?DHawV7(3S`tq}M0IF>>SH{m6=a;v(1I>1N>U_W#!#m? z{u{eCXZy|Dzn#YiGKo0YVs&i3SR!gHCYvI?r5Pa3aZgKx=RFcXPM97aDlrx=O!6X< zx)+OD-7y0Vdt!Gh-HmQw!8Blm|3GdVv=n0Gyab9`Z(SfG7{95*9scAzN=3%+QH=3; zPfTLXvqo#}Olt$D&p$dbt&6lVc|d=)UrIJ>Z@KEE&mFOmsn6S;pL{V_DJLE0V< zI2|_3a^}ejc)W4(F;|yH=8XU#^pgLOlMSV-;|;$`SP#zF?7~Pv+a6q|#AZb#vX72% z4A$Vk$iP4FHf6L<7FC;z*B}#n$|wIvj}+=SSt{&RWCqYPpIsOq}n&na3q|sIW7|Qq$PHP(R}S z=9u$xV}qmfTN#oB0J={iicBFpmr1AC*)!j#_RF|_N!qlZU%O+6s+CM zWtMaXCuL)R#{&M+EU}!DU_{`6mO?xCtZLE*DZuD8C9$kRH^$`K1;*7Ayw;hqs||B| zmBR<8BJP{v9S>b6Hx%&P_c*?C&hSm*XU1bF3T2{GRfi9@H9G->4I*XE9f!J^A}{ry z4x+Ky9G(_QF`=9T$Sd&Cg}89QeZ#LacG z&qz?F-14w-Sc52LT>)!_Ildrf+X9iWJB4rCz+L=MDPx;WUO3_nqHR7;c(tt2M#ab_sG=b;z3uUEp3s#4~CX<9R+q?h~F|g^^ zVSb^Ws0Rd<$KBLadh^3_oi7vV<-!)faFi05P~!25*JC735cV4OVWj$US4cs0yYc0A zc-42sF|vfv_nktR+T&`|I_&RDyFO*Wj<(;+s_*W<+qDe)0{;)E&3`T6hcD0eWKl|P z&x%w*)H!k5H&vo%E7qt8o`!%*z~`(dEgc9DtQ|EJW#>#l$ipHoiM{#~vf_U^2A>OF zOT<eGTIYLb)PVPNkN6wYj#pqS39K3r9^zZg>)9w-53(Z89*j3Nt*+}rZzX@gV4zA~! zXt~iP(VSe5N(e>?;`^Wue{cBOs{ZSC|Cd3TGe-65@8AlBT#kwLkGcYYO5pLCa4n4k z0glC3jv&8+&Ms{P4Utj3bs6qC)?S}wdz&S#d}w4&#zmf3L4b1HX<^wa&TdBA$;4@&qQXG#LW|k9g3$)p2#&+gxYT;h|%kC zjpTxSExyV0?>nX_?V1vom`S}0YM#(^kPqMeZ+?LXp2RTqQBsY^5@=doNMC?d4n zr=f5PtZ6YmbCRzkEmEC@by;H4{+kGx94}s4)IUsHY3gyMcW^6v&(@w~GE3bWqtG!e*oQCGjxxv#)l!9m5B#>{1a}(d&Fi)zv7|&)%0&IlofVEqiT-rQj^WrI zWxHt+k(euGo)28Ahg*QUZ**)ArL3rAc@MN`uv$7* zt6z<(YY_dHtgj~U{9rb~eF3p%PqU(2NO82b-293JA2)GbvedTE3y5F2-9Uuj>1x7? zG{tCq*F2ldWV%QrXh4DArTeidg|s^V9XUBJm1nJZjf}yEwiAZ&H62G~$0cRjo5kcyL%T$gEu4PZSiRku$L)e$bZ91uo_3Dx z0W_*TUvQL4Tz)I0iOi^SGc|FUcrqrw)z@QFA1hGkWl>hfdF3gqaJ zCqrIg(baarfOQc_$Zx?zU)zY=QEq@H_PBPsJ7{#b5r$oUu_3VMKEC;`+l zW}?%%nvuVT3uDa&M>|a7;sFA$`T_(os*TPRg@%a%Qc=th4Y@yb31U|J5s5FrY{2P( z|7~xmd+#Of&4JQJibxdqfNl^f_BA!wwVccm5A(@dZpq#VQKmbhDEfsL640P z7o})>^o~ip4X7zALuF+u3!1CkegYyZ3#k5*H8S8N5A|l%VxoIKkg@_XUG_VmqfvR| z^`?~-eNWS6(zr0iL&31HdnUc?Lsg`23v|WvM4zf3{mc*@pWVi9D9}PE6Z7WbbVB`J z-~6wRgmOZWb&{N&G@3bO;DE6`tn6o=9hvH$Rn2=^)n?!0snr{BW-fskTMad8(BrkB zqqE!}Bw`Djj0O8LU(yL=OtKGL8f`a!o$W9p)Wp^R7+o(nzs>w#hBy@?^x-AyvKjg5 zpDj*JaYhA~YpmNiHfs3rB0|54eVSDg)!V3Lt4!S16zZwu89s{a6OVN}&;AOdoRH@O z{WS71Xv8gKneG9gi3eOK>uUUU%IH>&J!F=WlXF7d4uwMsKd=_*nOl$k!(}SW&#qw9XmnlXSO7I3UJ!ETZ7(|98K3m+ z)VGU-Vv+BW`tRrAazfD0-0)#-2_!vvXJ#BnGM?msRg|8717I@M#yE5G8kc=`Gfu z`*2Z3LH~@Dpn7_;+~0M8bm2DOJ1a_52DDQI1~MQl@7dBJl7YckflL@+IHL zd(}77)J)~)SSBsJi_#WxJg4**yxDZzH%3z}ebay1JJMhJP7WX|9F2Hn%0wPbEE0;N z2ns}_q7Z(HH)O+m^WOI48e7fr7 z&TXe2HANi7sd`NY8y?|$QKa;{Dx-CA?YOQqBVF$iuT>-0Dr=1iM@H<;BnFF?3;i}O zT98wIjnqzzahth{WN-}f+{u&40N&@5y83Ocm8TCmaeUvcDc+&iU4xNYH-L_aV92{k z3PWS}wCtlam@f5xR0{?vk`$NwT!~y=51h^-gLUFJ3%qbRTh;z0G7bu0t@3Byza89n zZSy!(qMyIEU#{QwnhWOtEeY;od<~7`Hu13BqBc=0(-z``X#@Z!02=4niavzvPtxAl z-pG`!a(Ov?l%IXZQ_j`Yx;(YjW6D;JFg9#;1^TU0u*Pdz3Xbj*60Yq3GaV_%%Tg_kpJ z_w--#tT(_lfoG%qzeIGe{jYD+9C^?r=yhrZT>TYsw7@PK&(4Zzi znxMma43{wLh^OwB!uwM0KIdowR%^=6+0))8oBQ4UKa2A4eD^)1UrGZS9}Zo|Lc{+H!z+e?C&#|$zrSTssyn564sI#wIlzcu<6LVNW? z{c)yKON0_6NlLg>_OpPo;nRF$+NXqk@!ibNt2W)5l1uaNg&a;wJ7w7LQ6k1TWD6YH z2e597YNd*S*k#OQ=J8rZ^Jfzc_mkp8=J==kUo_yrG*-tP=4L8)@=piA*@|Y-56Qiz zGp?s2|B6buE<#Y|iMO3Fta?m!rl%1{_CJjNLp)59BV>k}@n!0sz~NJA@k?Q)8LhoN*84#c9{V zrt3VouuW@sTgDVz`51YB!luLp#;1Wk|2qp_-ZTc_MDdB)k5X;=6^*IWx(pFly08tI z80i#lCu*2NOEU_IQ@;EoPzi);7t@r(t*@M3qvDOXzxhTbH3O_IM9zO3NC+Z9XqJoyQ9+=j#KKrCeK>)_`tU^T!fJ1FIc!s8&Uu9d-g zt#Qz#ya85fwjvtK{^+dKI)SVNzj&~xoA}LF*y{$qs#JbW>LIU_$-I&0H>D(wvNKkxA3t%OOxr}G>X4;eZ zK?x!S9_qb)m;od{BH}`2WqJiR57De>k6St(k^YBRiACgKo!NnJ@1cxh zE2-a}t_I$i+4Ao@)gl;<5f?4af{Hy^_-nyy56I(5fy3n9(0FT$?a7nw+MYYzZo#LM zPJ>V8VJXBG+bg%FX$Oc-c^SKYv9ceF*^&}Ht#OJ+7%%Hr0{6_luPez42$N80p1RFw zi@uD>cf)zN&!VvwxYyG#Nqjc@yJW6d+dm=pbBW?aZz1|ZmO-lkNm`s7*6p`>3meKk zEy+7&(p*`r1PYM`-j-q0eG&&^Z40i49|K7XH(^B{xtNII#EJRIu8*$auXI`|SPsuy zD89L970irKMfKjd`Mf8*0-3RN$6$n;F>D}_FpOPklGY;4hcR4V(yNuCtE+zBjqsN` z>#9|X@8(mWu7%qw_!v;N%daScNSqs-tl@}7#7(iuw_dT=scAlINjE*hfuU<57GvY( zI(R-^{m#e$^8KA3F`*qXuge&t@#uy9p!+*zHEg4GJ3fha(Mdi}aE>FR!t@`3eWVLl zkz^qBB=YnQ#Hs$E)C8ovUI_CEugof)(W!Nl5sNSr`t6dJC#J+hN37ABjEiQ}yP{g3 z4&?NVVS~1A&vN)4PVlT=^Y@_3&Eeu>N8H2jWV_IBi5{rqn0@w_catz(j%)N4^X_eN zx|sW@>*Z(^^YVlK?6(})$gSVgcd=t&a-WGYfRz0sQ&aKZUXm9L*GVj)zjy<|M=88Q z)hz%$pFIA`E}gR#6u>~Sa*Imnh>1Iwv9$_RYo>^r27EAMaQ&eyb2l3@7?>YO^el5t z$!vq(yPq&6GMnB_JJB2#K^Bg;6XsQtkk_YJq)vz@9`Tl2d4+yf^5NR1vyGs@w_j^< zK;><;Y#pB&@m?PWZ2%o@;O`WPD0jZ9JU1;;zJnhIBsTz%J-_?J1 zda2{CSqfBq#-%)ZCHo^Git49agZ@yfYZ1ppg58dlwBH;!d_Cds@U}cFC5;ghbs)@> z<5ZXShVcM59Ufyi>@}AQ{vP*g0jJyKGP7w|fr#d(oE$ERgnw+Ojm48I;h(_~zr?@-BY`Pco2D`=g*@_o9=U^kt%cej9O(9_Y6Q3=4NlQmfy!zVDoVItu(Ih?Eh zud~McLdnP5JZujA+cl-OaB|r}npuvL#)*NW>wWyt6}VVbJPC#ILq66+MM=lP4bSg* za0@(&kcC(B3L!+A?REJr%hITiqyv`*GD@KxE#hYbbV@QcY7Bwlz0x;}#AiJoA96!a zOUAa{>Xl;&>G#M}1_H$0p4e)V&V2uH%noJEeL>JXNXhA<17_%Jc7%{}8L_Pe++@U) zODGP1Ubc~+!6(FcmXyE~P(5Xv^-K;T9VG~`IsOX|Mocws%^Bk#am&uQd z5~?mYy6dy4u7O+Gb#K_Yo#cA4cZIug#rgE8g__GIE#81oe9SPwNwE*o?SwCRR z6AnNJy6*;a!|x;AM;k#t`Ka^#X1kIo$|rKQzD<4&Yy{p*3^){i9hF`Tef6)U=THv( z3df|HfI7DwwfzRYqI7HhNZ%_K*L)m*dzM8ENoSA-6 zWLux~c3Io+pJ`d6ZiCnxn~Nu;Rs>!WFs2s+*#RRLqM1$`eDpz>oEyV5bZcq6$^!in z?gMW>lt5+)$s`^IbndgObc)mS zXU5KTYO?1CSsi~yz>BI1GnT6*H4?3eOkQHh43)`J=qw3#%| zL%`!B?x;n>Ld_mm`mDz*@F)S!BvChGhE#C&z8imD!V5+(p|jFb4{ z;uBpx3v}fL^2;6)1-Q8h*V_-8MPI)(Lr80X*ErTgo$w4*!55R|FwTw>DRn!j_Gq;^;t@t5jX z{eXGFFtZi$dL8+C`gH7=i><#a`sEz*+r;OSOo4(N@Qu36Ra&;|CRFgpdVUAHlB9Va z^CzEelN2omEj{si+t={jFeKtEAnv#O3U zsv_O!pQn70|FFGmiC1}>IalUnXk#Rl&R3$ijEpbnN^Fdg-LT0RaV+mYaOQuO!$B3S zdH*H4QFN}_zcm_Ln@)CjvVie^T71BQ@`UQ%jze1>qaPP7HX3qpGT^LeW%L=&xHQW3 z=GV98kzgoA?;*{x2E-^R7mfd24i-{oqv?2;KjA?23+RNW{z1&CQ0Auh*12JiCt$r*z?$0|t562|J_Hw2&MqHy?hHX7r?AP!vZ zGx3gJ4$Vp$r$t9dU%h95e#R4jKRL2xWay~#7GC+A-UYwDmN?syqNLttc*)lu2Zn>; z5wX5v)jwuUi|KlqKqI88m<<_x%ZkgMtW*b5o*?zMpxCUY^N0*lXk&Azk!3MQ8R*60 zfr+fZa~-LKm$3?BH^Pbh##5B2>?N>KMcw_(0&lf{l_0 zv!0$d_gS^`U-mVy@a=8>!A6%4K6%FL*mt%$Z8IPQ`Oa;ZxG^HjZI*OPNZNlGPIr@g zLQMR)!8Yw_0J{4XrFK+G6uQQs-I2i54A&3XNy0p0qu7T16y4+}^&g4joZntHDsoo* zbc~5Dj6W>7FMZe9)cb4O;?-2ZoY6@^Z8{EOXL6C=BSv5$g^~Z2c?*tF3b&Kb5vhYV z$0Do71j~d@M(R%TnaX7+S$Sst_!d5AgKZ|Euj|F#9J}N`RNmN)@reIwH97?4DF+6H z5fT>JYn3<^f=aSDO)Yk~LW}bj&e7K;qeIy^QV9RTUV$&7aDYs+Gn0TViMA1Js zkysdb^cWvPWaJwI+h3^+%Y$3HJ1|5L2 z@AC5dSXjN`#wn;VPfGWi5oKJHkR2kA^ImrpKVA_?IMshGBWu$MKh^cUXcjq?U~A=8{Lb#w4ABO!^VQr?+f0#LNdqBm>J`b>!js4Xl)X_zcKCx#{?P3 zQ=P8ntKC><2%~=Z)fA69Gh@w+s2vrXOx}})pVhD*q1MXc*cn`$jmx_!qb=@fFk_qjBVpoRv zlXod22oJdt_K94tKvd>WP=5Y!n-vc;d@U@m1cIDiH~1idc_~@`T=+ypLS5Zw$y6n{ zx8HFWG$|vHd)jtScZ~dt43pTLnR43Rr;D`G@|oyTr7O*YByE=LG=&G-KK`@!M2n?j z`6#bzW}o$`MvXtH)JF>}*rP5jT!`{J*kxArhh_F~H+T*6)D+rxIVK+N=EYEYZfEHm zH@N3kiihr{t6ZqmzN%ErV^$*{R^S*9QU~6i$NOXK9I!{K&`M5JR7%i;FU-^ig0w*N zzh%2y57V+8p6m%%(00Il^|iuYe$3dr!WimjX=4Y_5A1-pce~xmUM84dcG8QV?wqnG z>+RQEI3PhB9oq#q*{CAU*#D`NDT5>^o<&{>5Q~$8mdw)pt%0N#z3>TU`_R)BwUASO==FJw zVHnBWkJVwD2)_H>T(@yU%RoEa;K*-Kcj)ZazLn^HXk-#&d7H48C_{uOMR+dM1P-SH zg@}{MZNTvm9^xqTJ_9G&USUz_Pslq$!3+`Lh{!1ZcoD2)ZJ8TX=m=9VA{3LHcaT~x zCtHf>nRpWX`6x;eI3TvpU!riEES$2{P^*FE*)A?!{d=$ zBhFhK*AOd0tYuMg&1!Kv%zuC-M_EhHz)g&Bo-}2`qY5WXVu7+uk1F9*mINt|@>j*z zdAT5bKPwK)+Z4tLoT?q&DGm0lJD$U`Z$|c;nJQ1WHa~gP+~{6k7M0455LyHdLVU@1 zIT(OScC8KQt{b918gBPkx-dw7FiDcr%xoB0J)K!e>5Z=yj^!xX-XQa?iG%D%rE%^~ z7CmP}HNpRKk^ff(T})Wh+8W*;QzBZV{RV6xWqd2Fv~M+f&4QXGe;!B}buVR;i>Du$cSYMC!ob;ftQ$zYLH&8eLy z?Y*%Hiv?(3=hVBttZS-#^eBp%-1IlpR|yp#$m18c{VKNo+G@=o`_q#Z<1G z?$3I6i)}v@Ucp$F>El@qv@*(f@8FqJhUuv|xGk-3pOA1RxN+^oukB41IB{(6v^@1z z#PTXIsw%3(<%U{q`I#}!y>WQkPBwfEG-Mr0PM6U4uJ4F}<@jQ*nSx~}s02o85^E`WPaCoIjFD;P^XVYAR-(uwg9%)oq zQnABD(eyUM zKm&DowycCT#nEvVY^?TEgIzA6>$fRBYRGivtJ;9qr%+${%BPPcL=sm5hxlU&m(&8$dhWvZnb?K7*?zh0Y zVf4`L^U4`-x{E zJ6$Yx+9aboR=O~h6ULwWDn4r5QPR-ytSuJPd+r#zQSvBBzJ^|?F{1-OoIav<+DS=iHy zXKq_hm^p%9z2H)2b-A&)R38{+E=i4FK&e>27_AsC>qvwyE`CJL*ymd%DH?PqMB zX$u&_;)JwIg?=F~F~i;x8Nfs(Re)j^`adKLWA2);@A{y=GyJL$dQQq?#FHehpA2}wfqy^de}*8|NTOowq~~k+4EA(moHqPoC zEf(qFEVSn)e6^9bm9q@cxpI%iU!0D`)wkDvnk3Na^rkOp8Kq#9B%QQm+hSEu1=w&V ze1oE9JqFbjp4ZU)!+si^$J60_M!mcYj-gKLcMI1HIG^ULn!V)jc0~{l&Zv|A)m2r_ zoTbrG5pkKLO^yNw{!^qpCe9H&BMgFK-VVI^FWwMXWD)Ri^#%&e5o(OF&{nl>$WqbA zx4dhUsUXA;kAn1Z{vM46yj) zZ2}8(#*T4JnDmeP%YZB%yfdkOQmx%kv^d-TLH6ATBD?QZi{5Q)j!FRpk)iVA-C&AS z%~X+gUIIk~Y#>`8fo*2CX%PPHK%I-4^OHPK)`~- zAHjFxJ9a|vVnUvxE5I9H)CSh8G#TnHNuqZ?B0rJ7zZdbEGgvSbz7E(mPJ23`b-Y9Z z`V$GV-bf(qX$W1yWVHQmBvOjv#AS^EB5p8AC1dquV ziOi-GM=kX7jMtVYu)ZdOZ6*8wiEoqLoX_n04q&LHz0&`r8ijYboi5qs0>RXR0C&cL zX@Go-S!q%Iu8UV~oKf)fnoGDpZi_zod%FyMQ{OO3KM0>hj&r_R$r-i`X7n}@bmLj= zpymd81{!aRWx6-ZHdcJ@(Jd@GrjSW+2Ftp)*jKwbD_->PM#%=r_J;^$NzLfp2(-kp zPj%aMnH`ac$|k4z??_fNBeU#Z-#*!pB^UviOpTXw)O}IcN)>2>(Dz^7u8j)7|0rW5 z);kKoE7WsdOyRBWK<4sbTM>Jcdz@@MUmf()2mj6ghIknK_(nmZu$Z=KvWg|si;Xx1 z=4_@QD@*w!-?gaH5B(4Jz8aPE6kLF+^S2)c;|c z$Hcf&L`^INf36{Db6&*qH?Ns_b7D&`=zEl&-YXV#P3;l-SvuhS;JX^EAm0nFMX5IH z^K$_iE1M;`foWcyvJr%Et7#_0N1)(+jyYk>3lI5CCtjM+$=ACsu-4b5-!kcn1kj z6%ePAqG$3^iH88?wF&MKxmc_ax094oQvFtes-e&+r66b5xXn$G{fHLCFxJo08Jds5 zWBFX9mudtUma)&~NlnMeaXUh2%+@5L3ZlLL#=@_>O5gyv<{*r|}-)@?3P!`O5d+f!%{MuayYG zBcHU9g)b+8UM-c@Uu4$fh{w63qT1Plk*94N`G35YUf;&RaG-=(SD0PAR@aQGxgt&+ z0HdJ^Doxzc6eG{%;k^LOKUm>JXr{4h)Q1YKldK4+7ZE7RgJ>A$HUWkvm#7*?ZATo#!?Ib`PE0S2gyF^(N|>00b~ZRyHgSMx23VMl zG<@NmBFB79b3%eVmVRF_35OXd1{Ek9H&Sejthg#ZSNv}r{HA)85Qi$CZr*@2q)>tG zBM3Tx@I~2r~dMyrO-wgV*;30j%9^sCatd=_XL4sD9carM)&QA zKF|?P;J|yH44rWv%+M1k9&vH9yL+%{Wj1W2aVV7b+b6?jT*%Y)x@qb|)=f52!rn&k zW6U@DoYpYI=!89+XFf14cgw4TLG078?9F^<4GNJK-Wf$bdMw|ZX0y-? z%y%pR_N)16Z4Mz^$VsmiJB&_vk)S<1stq&RwuU+zMH#*@BLGnPRepZ0;pR3G6ma~E zta(0$m2g@b5$5zVrSYe|0ukvPikrTlVsF}Lo$T(`hQe!Pe8wtE=7C&=8D^;K*}leEB!CWhR5UI*32i7r2z86WWCoWrmQ9L}PCz;DwS-<*MA` zMJ<8r&g{gIa@4M6{~28x>hBe7&sb6<63n7}_2Et@nRRCja*v=RWjS&J6ms1$%(S11 z@`3>F%YXjL|N5Xkkf6!&xm8^>P#JExyP3%KLmYB^Qz;QHi zGiM^8fH&CRPl= z+PH=!_b=+}lQ@GX6^Q)_zAl{xy!KlpZtg+D`ATX84M7QfG~03*!Gr3G*oZ;ju+i6; zv-!RjjIqTGW=(*|Wz)wJWMCTfzXfj|8_9bD=Zn(X4L6jo%ec9B^9s?c>Dsuhm-GwIN_1efS`cSW_VQJ(3~0IPd1D+0r)@H zo_lZOIxCrzihuG(Zi?n@e|@b?lYpIW4)vAzM6sH5_PbFH)W_GDXDcbAS8z*;`; zdE^1I!fB3Q%Ety{h{AfPRvZw3j)F>@&bt8U3a(oQO4GP@CbC*bJ`3Lgh@dngO-Nr0 zE}5Boh{W!KO;QHX$u@4W2gFgDNBo}o+z;T1sU!W5B{uSdsKr`W3TwLrEz@xgv@OL`_67CMObYoLf zxJT?km%|dz`<}2*1H@n`(H7!-I*rG?V-#S|S@~<>c?((qZ#f%@pYsBkbNmPR$3nf0`>`vK zu&=tx{%z6b4s~}saFG`uNdWQ5YD!$_<@Pve)os-6U{RnOr3*6}_`3ntfU|#v?+d1B zO2pw2W-=p?+po9cX!&v&1gA-F2Rrv7Uk_U|cHENK;wyEGJoG+comRds4Ei&6Nj$qe z-|S?lqNdguL-b0ouXQbmGfCny+su2>?rafOxad*&_-DRnoA&WKs(N7!zJg79{02+5M1F%@T9}HhOiT82Mo=w7rYM^Qg{Od zhvzN)s}UF9P5B+?m^$46mq$y;--+T<6eENr9s>A>_+}`|i$yr?p7KEhb>Z-F3cHH} z|D$?|B>2;Rw(x9b2%Q$JB{X#|(Lg*+%suy)rbbT`$%_vJfoasn`eNJb1%&S{Hg^9( zP3KW6tT51E`o%9UG4V>DW27e_cbK5|SxK3R`ztbO2cF_k0+3t`AcSxKCIv(P(!fTY z*7&|>@mjjh42+h<+cbILFF8m8If&q64}7~v=zNX2FfZd$y)T0f0QtWG_)OdJ0@IjB zF;t+bgyE(saZ*RvK1SXbxFrZ_(D}TRmT<&?1J9u{W zYksE3`)~=dwGxz=FB#~E3`+C8mn@udBW_AwHlOuvRH+*7%J4?wir z9|04=ZecfQdTW|*~EAB|i^0&x#1@?NW#4v0QXoL|ZJO`gPF1_ZCgnOQ z47^;tJO&7y3v1YvZns|)jTtoHyTDqrn9zgYKB4*7YQM|zz}olj_W!i20*yN1#G+cK}2JdlTC#qR9@;9wU)rnR|3t8 z0+p}B3yl))ZMxKLR+J*H&i*1RvyvElfe$x(XTgtG!Dq8__$t6$Jq%(gOQexlx?F)r zrMXvk2i`6r)7riE(65fjEv7fbBtpswFs_bN5T~0~L)_w4;sRcn>d;2Yc{L4|_Z81WRDUakwG6mi@xUc4GxbJVT$X;%Y zn15HdvFcjGtBIS>rAMd>CJ8y8rqe61YyR-XH?JFT`EBd}YXfNfncG#1{GGzPe`5~; z*+usxco2zuiZt1o4Y)Mr0LGO@xwsQQKI?w9rjwK0uof(Qp`9Gz8K`_VwO@?V+YNQ( zW&g7Mxr!;xlEwFG(|Nx~GgD3SL8s9>Dnp({(C+Y!1#f%9qZN*YSW`mN)aWSY5pFLV zrdxrX*TAW@3*)9lVjyyBG^>d>h3!>tpGW$BFHQpq9mW=VMUUeEepD<_uJo9g-u3Yf3oQEbDwct7;)z+DGl>yh!UZQh#?zU zYIbtj=(s)VEQ8Gy+z1cf>fX2qV&JlVVkBf0S$&<>XQjcf?SLhLFNzzj4H9P1bt@~R z*NlWW9;RG^L5&%?IKMQc{sN%0j{+MtGq8#T78rTTW8?RTf_t+K-hy9Km*d5|#2?N6 z9c?k$Wi>Zb&A{GsaQbQbOU*Gu9xbs$!CR9MU@9+erQfHy?<-Lso&5w#I`Z5WY^q%3 zt43T_JM=&5Ra%RoA(B7jOpwPEH<2GCeM8#FymzW;AI53{RyP%Z>U+TPX~F)KibxeY zefs=Uf)U6c9=!4PIgA+7j~$e&#wgcc&$pWyD_npEE|y86#MNGIaj91QFdv~Vztuli zRW$n#`06LHvt?4cwn3B7IX@FCA?SDu zBPYSDIu>OE>nx+7nWv#_&q)9xr)=;ewNhu_dakQ6f zm_ep%3s{49vA=~pqWKDR zIm(gB+kIWEp8rB02_W~x@!NoPQa0v_s+rKot2bHz3w1ot1HtCaAMPV|d(e28ZOYHQ zH4*m|7bHQzwWcx?9!7(`TLN(yg=^tNM9@%NX_E6+@yOw0lFu&ZO3z?^nwh%E_S-~UzxI7S?89#? zg}OCxOJejbWH0&rN|vT120F9FlX%y0s1CvlQ@R{boNx$5Tr?<_6dBfqh$vT%w(L+0?jV66%fhz+pT;F98?7gex8RGJo`*IE#t1NRuF^9$03o_SWeKOBC;{@LBs6ak@r=P$ooX0Qbgf_#oe{**} zv&<}&#bf?zX2T>!xOD}lshJo&SE}Ou>@(mXSf0`&^%m9j2|l9aL8M$Zk}z8!)KVHW za`>5f1H=U;GTz0G73Sx=3_Jpb=Lhu%e9OHdD2Jza(;oGMxmS1hF3Xl#G(7vJO?PDb zkh3gDoI9uQRb#gj1r3+Lc7OBNa%L*r#yat12DpD8Lt05#oIfXisb1HZF#gT`z3i8< z{ZhW1q-~7bQam8DT38$V^mTq%c|1#totMjodP!4$$qZghJ>yoSX7MGGykt64rhi}$ z@M7fFR%6fGE4?B!Z<9+3JZzpnJL+qs)OlrzyOk(v)QVD+2lxjk%3BF`Cj0d)1qbq|1+^{<*J z>vl7IB>@MGg1>~1rp!nRHy=S}%ibFjuBHJ7S}q*j*`E>P zeR2O=_Q-CZtv1LJ$AwmymS(fIIyC+s!TVB&q3D0YSO34lSIOJwH=)6R>69v*^RL{s z4F0cJ;lCZGv;1WR|J}Gr0$ai^W*d@gYAD?Sq_P$L%*ZI0nwI7g61K8p2KZzNfq(J26!8CLKK+;cl}Jr=3slH% z7KHT;2-VpE$D_-%{ylChlLcZsks9|W!7sA`D<*SJPR?bT>>d_KJBaOHDqQdX_D}_$ z_N@T=<8~hny23~q97W2(Z5!J_=|+#z{beg9HPu?=0Z$^5|L@RQ|2G-zCOa(P4`4mA z9@l=)8m?vdzR8MRwQrkK-TwR%+@a<6FYoK=zmi!0*51~q3vf z{nt|X|48K<02~0%fRLL0&-q%~l`=L}sBxQnQ96&g$=uu=6Fd9t72s;)1VSv&|4v8o zgQT5<;SZ0=_!yI!Q_BsZ{y?3=;-p^tkHPY@vwZVVe^vCQ0w5)R`d`}DpQ0~`73ytW zvR2S~l#dP`HrHBM+Ft6!;`JXi*V~+WDoOiZMmTjq> znu)5Fn#Oo$`nH)0|NJI}+0;+obA10suT&;Z5o2$=4+}0z2C>eoIV`3=cB8_LF31!f zN*~+Na+jQjAk)aIWnIPZuH4+YKIOSi$rzInlW(4FJz&u^;STWP1}$GX`2StK^7hn+ zfT_T^lzkYreUMruBN7{Y>2u?M%GsK_{ZcBkw4pgvEIqaV!C*QS-=Fi^P=zJ?Dq&XT zY)ADFi?UQNeL6^# z8Ynlt5RCe`^zX-20-lu#LcIs8Ko=e?L&NQ>fDyfsSrdn zz|roh6x%Ho2`qwDNK}apwMC^x)L; zZKrqQ1RgDHxh~7og2rt%sgsua8oO0B9A1-qX6NnpPUX%k7URx24N%kagK?FXz0ALL z7UO*pz^2bgDyl#QuLQmm$)iV`gC zUnHNeO>4BINsvJ^H`CHN$`n!qQoCGpuf4akrEb3ErKQsAeuyjm?Pna2qkkZ2w^p>t z^*rdjM~lPGHN$lZrq*iTxIH?2esBlgjp1YEk`Uq?R0?npD{+Atd~{yS;mP%}HL6|8 z_D!(6UYoKTtYT^Kb|MFy(u@BZ8ioELV*`Us=Y={Mz*FiX)#|q9ip?)FF*%w3cQef3 z&@Tltt(Uvw1yze=tK`ROwB$thk<2CR2iWLXA zfJYOl|B|{WOCn|S-@W4?Cnq;>rkifF?DD&)a(G$Pk_$*MN83EMoY9mKf{t_YORa8h z5!Je9pNFf$t;3CviZ@aP{urOuX0u72uO4agXISkMtJEa3t5lZj4(2H|(GRP4*tx9m zwC8%q&O{3ZHKH5e{7GH8u~&KJ(Q>UjclgHBK9E@yrREtGl&4P?t(&8o`QqRSLuJ_k zpX#YcU7SE9hj4YbuE{qu`Y0hIIwg^OZkJzNDL5Cg%&6n{nvA0lX1b<9`K$87JPu3( zne)kuoS(>U1B6Imv7YWaRhu|lXir*z2v&}WTS3<~a2u*_<)Q#;QY&?JJH1^TSK8`H zXn~AfV~DQ2VmydG3>|lz2nO6M(r6_779bps1jE|&n!~DETCK&oGhrHVCmVyOV^f(2 z?8nMS+ag0f9w~(OA(C#vv3&0m*wol_>AS5=PSvEXQrvWm+f%#VeAup9v#)Agk9E6C z*U<@>nn_M)JR-2HR82kKi%PwnaJhu9ld9;OC}VAMbycl?Ag0Cm06AC7m^0nEHV`QD z$(1U5Gqp2UvGj^PtD7;|JfrKWo&WhXPEKpbM|Desm*`UtjA7-?sozZ1axLQu2!NZz z&N{>2bae})$lZ0RNyUEZ-QWC0E|A)yZLsPK6$fOie3KLD>^2g1N96}2Z`WE}awiKU zOoQ$(41Y2AZ$*04qSkd?GN&ILIOkr*k@05c2*7rI{|v{IGXl1rmO>&H^<*H!Y5chx zhy|LaaE^EP_INeg;@@Un2M22hP?9ZVAyrc??;IsnDoqr1C&;1p9CB5rs`*l)`kpUU zbNNAn4FM{tx-aB(KT(EcKreGBh~ySpZSt=_bTf)dS#Y#d?&hHEt3P`fhPK!XUDX@v zrx&khz1R^MwF$hP7`&xT^WpKe|%1C*T2ng^xD%jZgtH+z+6hSNw{}kV*jB01s!+ik5f&33ySc_@BJ@! zt4qzk`Mu+PXpRNI73nx`{gIdSedAkNuUOulj()_MF#yLxc;|0As>yn}we%?OrZ+ zF-u7XU=IC{Ou!^e2W-A7)qwm137M5}T>4_1!X2=C&rR$z;5zHL^yGw75JU`9f zzSmJ2pCl2oz5hN}z8jqsHslqFd-G?ac42xIgT~iL^Tj?>&cmLd?{y!B-O56}=h4mH z7-NzYPp*hdW;bi0GvhS?dP#1amYDbcLx#EmSj|f+Td&Mwn)K|;sryK3B|_=e-y!{w zDUO>lPQ%#wtHvynazHv!WPY+q6Ms`IC7r*;IP%%;FfL_+i)OANyzW^HwYaqF3!=i) zULp5Q=hX&c^NUEE_+p4^E;Lp+~H13MHZ zUHaz7>|fD+g{;2CEfL4G>OYFwJCh=I4tEoqmZE)K7+F5QVr$humZr}TTaF)fuw+F@ z%yGsfZA9tC)=ZD{%Q|-wIxKc3SYPfrFR{1hw&dQML9S8e>ttO;x>W&eLD?o3A=T(6 zPVJ-z_hagpWvz5%^@rsQ(v17KH>Lu1v+>{0rb;jQ;fiXDO^lb`$I_&*Y5Vty_a|Lf zJ4F~Yb&)5ciqR6kM;X0Qzsp^%@d7+o`bWftLHkiTrQK%uGtWzMn91@+F`(=+b?wyG zeW}a593AaI-3>b96~D@>vTWsPgNck#2P1wXhlkiEuvn8@N{Et zR;Ig3uf3s3beljF=*%sX8^Kcy6U^k$|0F+2xw4n@p@9F&gk&YH@Llt(G10wzR=^>e z0LSG`kD?F5n3(O8Q>U51t$I*55P1a0p0tx$VghYW3~>Tjr06f?=JVQ~B5X0ac9S>m z&q!@~yzt8b|x-$A+N4>P2EAH?O6#Wd*9wKLSE58?=0)28EHga=y35%QQkuHDd-Fgm< z8gp{%re8E(JnId-8eb0y+g>EINDhMK(evJ~#hsl!b^%LSb1DXnpYjXp@5YJ`4^fjs zmkPB+iCIf;exe>H7w!zB%Y5bLYk9>sZ|0M4<`!;=a1ob1y=$DQpLsr>4^*$rn+2Pn zm^xdjnS1P)8N^3=1_0Ugnft4;FIwX4C_#68Z6bVTJ_8rRnwrPU^Mk)u){&b}PBr|l zBo6f$t+XY_qZ*H???-P9v|%`>u@xwziCKg zz$q`J%~l0hAi$62ivR7vqu?5gxzz@oqQ(I|wu=OeV^Wnz?>&yC9Ul4li1O8oa{hyF zpR%YUdz1C%x`9p8+k*@h6x#3?Uj30h8ZNJ5Y4#Pang5>GaUIPbZwJVR+4&zk1f<`` z=TkEKY?q{p{b6UEgj1vSPi~8lK2+!!q)wFXk=a+~FxY*JTSjrnb$4 z^ggb9cHLgZD`Vpg8_hDU)o`$(hlg*pFxk>h&S1xfWl!fLS^VYsyxq~fY&x`=Wa>ba ztXD-s`>wm+mm?DW`d{LCYdG{Y{aeFH=@3|;J}l_3!`@Rxn$^HP`Rd75>p_ndskc0h z8MY-j9adtF^UoY|*_E(~8pH?m%-Yr3i4P9iTxnTRaM9@z-XWT`HXEIyk_-m5+do6W zfpan0zH9!xRn&x#>D!|&u4K9My1@JG>_{s6I_qnZ>vdJ;Q6-c-%VW?jqhreuK~jm1 z{DX4W%^krls*A;bnpWkTX-{}`>G1|pug&~d11>YN?i_A)m}ms;+4#Q*i>Wc`Dpbm3 zxv;2m;%NeQry_0vV9J}lLp!&1hz)90!Ui=_8q2?JlU^^padp zA$wb8*{j1Gh%(ikZ5Iy8fHCt`c1c|6fygzQbEJdq3}KQ|u_7kt8j|di~F?@q`8k*YtE=Df(vh!>j}Qkl^BRqpx2%E^bHT zq)VYvx-t+xT~EzfDUV^i@DfOTZrVq19qS8$_Pg%v(Ue?%3~BsNhLIPix2w}?Z}lG> z*=wSvT?D8*z1yx0-BwmRhFh(j&dRoUW-c=Zk`Xh9WF-xK`6m-$`8Ugj?4SLHw-L#j#r*Q#Hg zTMnMmBB)UCeAthL1mA1{}B)Z}~CX~Mt!{f{m@ZJ>eG(BD!vZ+1}+ zw>2FPW4sNs`2l@+Tq8jwax9rz^Z_gUj~ScYcWg#?Tg)D-i4r3is5Rd8!286&mB#?~ zF_F^j`VczW_N~b5hiPJq{*lMaz1r%a8Gze_yr#CuI8W%|)#~`N%jZM(N9s(!CQ!=n zFp|0iQfuya9eeHvEJxV(kPJIH z4c1p--#2`$)irj9)U}zlfez|l!76F9q?VyK zPbHqd;b^SjurnmH@X*cC!%o-QGB0c3u*;ydpkw+rhS0H@8~U`ltAS6bGZ3GZ=792i zY2l&2!RU}VrsfjYmeuBJnU>eKlSP>(eIImk1ecXC(qs7ZbjicH1?2Os8yK1la!fEt zJvrlF0SlBl&rY4!Ej&4b*zc!pTTS7$I8SP)#+9}#d4j0A@8-Zy_$s(e+D3g=wpTsS zSEe@RTka{OP<8d!5wslbr)0DC+PJHHp@JD|yQ@i2ZE1H9sBgUn=AeIJit}BcKX4@< z7!~y1qb5lGD(^|B>u+JOh(0llvf?D^LZ+ttT=w4UsWahDVKi}ONcVZ#{Q+WJAd|2$ zS%V&)Sh{um|A1XHk9n@__@etGPbNQ(wNd8_{sRQdoJ`D~mMk5%JqEY^PSZe=+8B9g zN;cEeKU4~KWwfkA4VrU0C{cNw<>7bsefhSfs0_ zRb*u&33sxwPUBNwp`jU8#BunU<7+XEYKd+aYtDTbo=ES$d$M}BBJpab`QrN2On@N7 z$&|gzEVO~>@_D1fQy~W`aPW?CJ?_mb+e_bR+P?i}b0uWb7LIzG zzI0fCO*lAsv^{%N)rOd4pZkOToZ-lXDs#hk_6*^8x-mT+xK#~po4Gf`np+oP>-D-m zt4%xKs;>EW*?{&5Q`6($VOe6?<6K5v?9ub)Wbnqk&{@;*r@{eK_}J4+6VXf={+aBk z9tgeF(oSeJ@@TF-U}lSRDVw*@;=YVA>@AJ>ky`d{G=tzYp`5vo?RG6SF~>CR1)I;C z?^1GK#)^6{B5OFZ?#HK94M zGFz5d?uL%!uxas@`tJC81+P6xDv0ay@3-jPs{DFZ!LD;MsS1tk4VWxzK`c2Y)mt>Q zX7N5ZMW@kqmE;C@`NfizRo^vE`f%++UDCfs=H8iR?;=w*kuO_L+1*u_J(-p7aE?M) z&e))RG(_HK&Mjk0yZJU-tcKIHLX=@ojdf&is{lWyXsd*L>~a1e-xSTMnBS=_ulR?y zm-jap@eU1O_ZJBAB0~B86!FdGSaSTRi=t?z_npsZ7JiCwWD;V*iawt0PPgDO;?u@J z?6{fFwm##@5eA6te9Bj%a9G4fRhcl zV-#e}!0hIsLmi51rbE~fwpCK-N9XIp< z9K$i_#n0~X^!u&`UeTfPE(!>StKTSvd}03Myu9rv#wnV6nLv~2^V_#B(!pwOI>BVD zQ}#0FDZgqN{JdL^${BceyMrQ~#(4l;?pn3|2uh-Z?WuYseNotp_T81>)0-HZQzCbn zlz}Df+2*y7qo4+-CH=O$G2R2-^ad?@X~4JTM^W_6Asy~I;8oZabRgz%FPshsR~SB1Hzz4oJG&BJo?p3VxRhNcN!4+Jw>gBSQtwSfq*JM9f*BCmPp_Ox95=KXI2bP0LO9n z!(K5$Nzw|t(x|psaOiNoyW4DomFzViFj3er%<2*YcXMO#EIX!S%O45ce9`0~o5cQE zDU#NWB!a$@$%E1HDwp4UBnC*yr@3}r4@FJl?dXz9{c|T5i0%X(DDaW4^P!{LQtiZ1 z+7lZpq$E{kTmkHlQ6Nm~5Ut0S@h>4f$h9VoxW7lO1HZf;^v6@$r0c-F2a+=>Xc*Xw zk_x|5bx7aGTpx7-1Id3e2U(W2Ho5EAr0A~(<;#0@%z`{9HrKjTdtCWa%n#gerR0PX z+_C!-3PQJ!oVRS`526S08_zuuyarlnPNlX_hMp0vEaKK#o#pYB=;g0RNBa_avNm%ZqqJUa$f^4^xH5H~RL-9{ zk1`qP7%Gy?{`TX6$`6@`tx=VnZzAjH*K%~!%)*8bes@1MT0ZO5{PJ-C+a*4$`TiU+ z|LOFt0j6^)Y1TfRgI1(5bALp@A7?a$mf!ZSug@1fV{-8+$@w2au4Wh$+;gM8ZxOUk zCmlIQS=)U>*S0;BXB+CUt|Qd9wKXkA#K$)OIg(O6;oQC0!)evw#fSPr;`#BKR>i}B z4Yao3Ga6MNTK5qeNGkS4`>7lkS&yxu1U%l4m%bhBA#BcH#n<|>hmje`x?QM*Q-@k> zbJZszG~^nIVYfn%)Mm-?Sre40+1+L+w`t{!5~(0(!lqa%8!C zlqwl$s3G2A1Yg9cSv-30oj^xahig&*InXO*%X}6iW&jFCqnQhIo&aSjLryBo+2K-zx7Pz?7t#gyjx1!D{t&ax{7}aTsqnjo#QuDhQxsE zM`;m)rf&jiRlKkUGcFixGsM}5S&a-i8mo}lA);LIEQ@1?X<+tWttGPk`ChayZnbiZs|FyDmK^7M3q;|~_bCPGn6IW*ogKlOs`}MVcW2C(Vo+Z>rl8i2 zYtw}d*l(|jZZ&hsThVs=_wH_rd$gyc@@rXwwf;8A2k{%1qm)O=i7Qf-6=RFn7n6+x zPB+RN!!k<-@&z=re^XK|-%KqCT&dB$el8^a2=x=W(NuTV?>w3lG z+B=3|BuHF#J#b6DEQruTqJ)M%$rp~be6YA-ob`A28{uXX9^<}fnNEwKSTeSv82ox~ zp*DS5{{D6F^>ucqHOUZI$qG|?i)o9xSc>Y{lH~`fV!abCgN)qNnR0{(CTagN9w>rX zQ0#E2v7GUAp)IfvFV5TVod^k5P5XT_ z(bYyXO0<1rYS%4K3dPmZwCTA=%Sp&AG7Zao#IMERi`6W5VJP0O;5X{l*MX+@w7n{G zmIFD-lf(hGCnQu@UoCC3QccSk+?Gl>JY5{vFGXe8v7PjMwsV1Wr&N(9(Fx;YO$=Uf zj?mbj@Iha=G*CiqOBm}N_LhyJRgyVPX-}J|qMX|Hnd8$5-m??2}yRwLWUvfpL39sWw z!uGeY^|+}cPx|pDyKmv^_^PXM^hoSHZ$}!r>VRY8is7mX{Y7_b(hkKxirrxiT*ym3UKx+7py6&4b zML!NX#3_Rs2M@%io^njN3F69-6Ml{N?pNxQl95F9X!Pkm+g<9Z>_fhcAjW6N^2Kx)&(mV zMcAZ*4rte17Qg|?4shHGov$jApY+v9A4YRc49aT&<53aOK1@MI)UK<9Dgmi5G{+hQ z8R(SQ+SJpGki^(oV*Ri>R@m_|RL3xag9s{sNPJfIf?XT955|cCtT$Y3ndKAdAd(=7 zi?|irVO&g|05L=~U2>c*kPyyA(puj|;bczI8!X=U8BvAY7_WMw{*5?0(6HaS2q=xUFJt7(RB-ZaKd17lbKsKyFPt+Y85S5mvuik#@MV%B% z4P{5Dt{&opxuDE@Z8oaM&%2Ob>NP+0(zIyOf!iL#Q}yjV_s|}+F@=|%aeK(com8T< zw5c-2Qrl5%gDLAzZ>qQTc7rIRexA{dcCKG!ws%XvIlS(wg+rQeZwuK-9)74k+z#tC zSOuJ;N*nx2(Dx(lw zG~vyvt@Cjim|XnTm{5Jc`|Il|0F)hnlzr@D^6VB7)mZU!EJ)BA#%*{vzSGzMv2uWy z)X(8<_81bPU}A#R%S~zHwHJi#TITM2;84D__pv?nnqe|e9KgmrpER2b~KSZ8$#drC90z9o%_geZMtc^9i; z^zQQ}B3n|4gR7!Xi6Zp@`l2nGJbOaHlOR0Bejt}zH&E5->@J2Y+v2!v^NrF(xX6gFAk6{caIV>dUxNX1fY<#{|U?9A%4 z-=Gl;t-};gdPDI=FE~9zaL6i{llE?|mRMdT%k}A1P|l!2Z>e#~<1Zz<>!IHlA>Twy zk!i&$uhx~ghWSdk_^P;Ub|tv=)djQLlqwFR&qxWveA9v+{yE+G_+$h*3WjREU*?)q>dsl8g0(_z}VkKH_JM>+k@PMn?nJGBg5(?#l-wU3Xs zoGb{BK@XDeoy8bNt4)9W&;&g52=WN+GB}pS-aTr={e4Dsv87-1=}Yuv3UiP|2$z@5 z2pzzfQtR8iXYn}J-n@6WOkfXW`)ZBT$s6+w=WiC_P*m?BS#$ua@o}0mMlloGCRTCb z-tj0lnB?SlgcS#_1(4znuahQWVbPB*Uf1fq`qw;_9?fZqhSm>#!6Q*Z(R}pn#iAYo z3L%ircr3rrtJMHPMJ_!`rcETR@u%&76;+_P&@V^8D|jliA8>!Ma4?oplO0x9r}u7z zgwRa66|e`#9|M8;5!JE{pzZf-%J+N<^qN?AuVtho^6+E^;JL&6Xo&} zt_7rG^FjOCM*vfoGR1-l$0^fu^RkJ2UQD4}{;at8<8<*r4|z;0zD}YOMPu4p8QT%k zb_zLjk;Cl{^C_iKPZ0`Al2X)lD8w3#Lgo{6kaP^m)#1uV5%vP8`%EK&=6uUZgoL5g ze($E+>=ySfnVn*bnJfJxGY&!Sv`vg47zumXWMzS298gsgm|N z99Fwbd@*4!&IAT)8Mt)?*u=(uj{E-HysCOfrx=FMy|LTg=@gw11Z9jaK3!lC zRs5T=F8i_6GVpZXRdxKR(N~9nn@==rKEpTofX9_se#pfY>AZ?e7mgl33lA7wwI~!I zSU6x3!^oKM4LRajdZ3Xt{aKNQ>$I9FAV?_J^+Rm3LTlT0?IVUO<-$B)-&|F$*?IhB z9!P{l{yYVkp)Nf|+9cy7$1zeBK+ohKQv(h=3)^~~_FIW<+ zuWxWEIW+Ft{XlicD_1uMED}-w?5Z)TEPGu;W+7E+&{6~UzwItjZ#?#<;gyg(S+r>w z6g8`m1STdnLDrlyEi;U4c(y>I!aVQ6=aotDodiI8+?AHiK02ZJq}z3T#z;E(pU( zTLHGPc%93Wz!IC*T`T7JD3*P&j8I0pj!aC9rZR)Wh>&WtE=fg0x6P>?Om*T;OYQ>s zCeJajO-1lP8`ZasOu5_b@(>P?Z;U*vdx|65Xk?iFMq78RtBx2&;!xfVWYQNQav zFe72GJbGDmwQCZPTUXa5BV5-a^L(QuW@WrTDKk;}RPdtPUxSv`G$Q-u@K`%AseGiz zOBfP@*H5gch_BdKasabd)gsgcr^1W>pg4$Yd+?%G#$Z1ds;yqAG&%@$$GhSs+Wr%*@e5ig?mwd}sF-l!x#c{;!hgQqH@Fl5zX8>v{pqoL~H(%$9HYuLvb*Rrq zN^Q_taJ|h!_#ZF>tjh9XRZC%=lJiBgyt?^KTRD)KJ`U)M%d`y8mAOKW)pWm_wr>ny zx7o_H@E@r&nFOx9aX80a$J~DLLld%lXWyFE+C9su^)VdA^T|{>@@Zb8cHDPA@(n(s zLuq`KpcTHZ0A(VRF%mO}sJ@>p?&?!bIhN=)z4@AN>*RRN>cvBeVe6-&elr~DC`9FB zJ!Om2YNIIgO1WlkgT8nYD>OFZ*fM>v!3ttk9u;4n+Wp8%FM;WF=2+7?^gO6f;Uj`I zkxQ;#6o-h0J7oIfzl78it&hvpuT>k^i$fWgi+Afiw&#r0{fA&yQ{o?o+J9KOlNveq zxG51@z=xqpF_HrWyqF@kghi356J5If*+=`sjsv>rP4Yh!x&6TC=i2mKVDA8j*Rf5( z5nQ%%u)@6)iO=Yp8ynYBFiH%iTGc4_z=`NMu30G>!?^5;L6Q{K{2#|dQtnFN#usXj z5BCR(v_F-dx#1Y@3C26FHMbDF-kmkQKHBCiZgi-)sF91tcj%IP6H0y4C7)@ zZP$)A$(QG>4N69r9Pq;nd>Nd+%riWB>$zJ(0r?kI(`6Y9f%EJtYsb51Nm!Rsnsj-! z58wwpCnU}hsOUIPc8K|l<7soxbSTDU4~!BH6rQ%I=Cf6UB?r4a9YgxJcbbr3zl*+P zASbc78wtc|yjFH0V`T*aHdCvI-vd`_p3!!H33VbqDT~x^hhzKQhb3=+JCh%6=?ogZ z)$asX6@vqTEFZ5)tH349FAEWZ7zQSh;fuR12Rgrmnhk5$xYVR(o0(TT^MZz#06VRf zbn8(!O3v?mGkQC5!J@@krkT(uirOh+ZYZm7E$@0k^ZGY%ifA}kGzR<@2H20MjquTD7j)cdAo( z1I9+lRb-~ z_HdRlT-&Q}#R9}DWaim^i_>Fq0)Z!rfhh{44#XWg;Or~kHAO_kJzx2B80!Eq^+a}C zqSTadh*ckf69?fmJMMFrMYMe?%6su$s63%{TPEVEVrpE@UR%?SB-bxRcXtWs@kXE@ zJ}$=Rv+9<$G=NAl9~YEFgp=9alKte|JYvvW(#@=P#NZM~O{ZlW=jkB9XLVEYLPnQ& z;vq>_EiNJ}Dx-;YCwU<&Dks~vw6nZ*eZ{6K>t=%Ty^brBOl91DDdFby?g<@o9j54O zXjzt;=4BQbbZ<=9jlAyr?ap#?zEwnl?;ki%^7XmFG*%}!_%k76tXKozEL`@bGNfkRi#MZ+DQ+>oY3;eX_lO;Vo)d(7gXoD#5o-D<1nwR4J{$?+9#9k?J0RER`0` zC&CQ&$m;ptZrkm4cRb0(da!2IpacKuXs~=XG>GN$b*Yd zq_k}eM%!9Z;LxQJj<~k9VUDUf%SZu9t<#Dz^n+Pf0_2FQ^Y}y1D5FhV|IQXn1rF0o~ zMJPUFkx;S?+=37NWzq?lBj;AhuM+*<@cD=0(g|BfP`&|2?`>V|0RSb&8QcL@Yl2V> zum{5ei@+k3xMtcNKFz2knIpOg)LZH(H8`L?9ntF{kDZi(;BmiaCcWu_{p;X#H#d1i zxJ_5kbmuc?T41(8U?v6^0-qalBM15(A z`k=->((FRI)GX%)135R?WrAFhTHQ#eRlWvR$&Mnh(n-N-`W25jaiCFL;(c{KC-bG@ zaqWL|V=igbQ#XoJhtCxB5ytnVBeT?Rh6Pu^e@tKI0h@Cx3;#~&DZ`36IjrY(@IS)%)nSW$GW zo`V6|#zl%y?$?+7rra3Pry%=mPFYUVuJ#8p2h)JB_i|8s4L_HjSgg24$n=B$@|=ls zW<8FD)_XyT`y+S{_3bSa>$Ba1wT1RcLcj0pM*6~I(gzKm=@Z`4Bm9L0oV*g-A*i?i z(>0@jNNueeHlctO(2ogBQu-BhUjzg@u0m#79mxFvHJKCSvJa%W(i*s5?I!C+(rSZ{Sjl1bWIs$ ze=hpn!jR3#OK-GAg?gchMFC&eUQJ7~lc;K-z0{|jt(v9f>8^+jaZ$vAkk=LCf@c_s4)JGhvYI-O z5j_3qBu4a;HOJaXPAazTGo0NBIfC5WNJBQfjj)aDFOq(8yOi5>0~iZFpHl+Z2h^Dn z6oOeAynV^IsxqmE3>t3{Ka~bRz;ehIoIwte3f+Dl!_k#aElKPi^+B;`bJSDTIe#yz zk4yl8Umw%Mp?RlG*T7aHK|~uhmSWPUbmuPjCsdbfN|NiXmG>9P6$xbU3Lv~%N4I!Y zs^P&&$YemJDVIL;Z6KQ}XW&3bEUcpn1c5ar6QoN3L9Cc*r%T66fmD9p1W5uyyHTKT z{h$){KJT_dQiJBUT{0Y)P#(MIdSszy5H=hudFeW<*xQPlKMm!P|1q>A50bt#{qT%^ zqOa3t((|n*R1kF3W)Alz)gtseM+8x+%AtKk(enI8w=J>7JIV-8(NG;?VEXf8Kc8b% zxcl%rV57usm$jAMEZOetQQ}g_4>_D}H+r*55GX~g>^>y5HI$UO3(PI!lP_yX4V#eY{2ffhCb%qM0C;&$Jor(sfbjap<>ea{IfK z+PqGq3CLO~$laWixAWL?$C5#gf{#P*o+FIT_;iMC^M^5f-bk!%`5blnfRc=##v+`y z<1#(QV!1~D&Q*p5pdVZB;xGk|m{ZqHcU|p^UjGg!aen)>XMxT&hvAmnE@`J`H)QJB-!C^)~)0|Q|_@U zt)2Z#$5X6hIq(=Y>n`fuh<2u7>j`p_h~zytStGZlVb9YJhWHJZ{d^6q;V{a5u2gW; zrd*6+R@ClyVlommMX($3aNBL$)dtN>hU4tk2W|utT+m!H8se##aQkzsmsDL@>d;Pv z+q2tkiTw-w$BU@UG~yDReuDxbjGDo3AhQ@gInX2+LwHaO1RkL8OS^|>pMU-V5{ywi zWuBhAj)=bX6v#96iWrJ$Vt-KAI8CFTiFRyrbYtcrs)_7DF0EE#eeP{rL&uItY~|LJ zGO+HK-@{0qB*4hhf8W57Iriy4tYA$+t}cq_!M|NaAD;`-*>SG%4`BLH2ZhV8fyvfG#No=dHfZa{aAL7WqT*1JYT&y@jHEduIi#ha0(i;$+@;&$PG00=|Z^BTand4IXyYy-h_- zlPC*a&T8ZiJ0S|D9BKRa{n3uebvP|*3pU$h(Km%`f`kX0l@ig+))yP{&w-&bio!^0 zD?gHeY9z+0-!8pv=bGA_h1$&j>sRf@Z4mp&yCc80XZ?dw#7u0Wkckwz@_g$Rzop`X8TDq3kTP#1>4T51&ZHm~>QYPlf!er4Z8 zh1co@>onq&K3NeIKu^^@4u}>sD(9<#Y$Qg}bK7G}$L)y^ZP=>T^2zBjFz#8TmKOD@|8k<9Gpb<( zZ~%cpyn`Rg!!2m%i`9B$*bn)c{CM#gOox{_japKlW?A1tV||}fect4Ufb%@@`KLa_ zv(V8fMuC!g<3Zy)9Xx^7i|igW+C%wb9g;L7=W%X?3r}&;6Wz5!-4>oq?Ds><>*{oI z(D8-Io@WSC9LBb??t{6WRmHGH5Lvx6X8D2tijSIl0Au*{PoE}~lnZJJ;1I1ixTjx2 z@#JHbTq!7-tv1?lF}hj%I#Qr_hlqm2-3{qm5Lra7qqN>@3aA_a>Twgq&sf2w&n@hZ z#XzJUvtt+NKH(dvryd|ZK_W7b2GHmi*behsZDB05IeX-V;JjT9@Zr*Gn-e(xoXaU9 z&x@6b^y>hGn?WO(Wq*Jl#R?YCX>E(gDnd9hhf1zcIAQ7=oW?gkOD`KO6WEZpet&D< z=uEbrl(-{$nb-ztuxY&VX!Pk08HyDR`D8HcI%DjE-SO_B2|w-E9UJdV24edKDWpzR z`ppym8s2{UZz=QNvekiw;nRj{j}6O)vV@NdY65;Jk!y#y^h>FfeuJ$Tdim}G1TTJ7 zne;C@ByJ}(PB%1T3UUP>bssG^KQskH)QHY*Q&_{RQBkE!x$A>77lk1=Unw0ZdEYeg z9*Q}@2bPnAbfm>TJEwEI``pk%i~~a3^e1sGRKD|>H9V^lio>c|xk(Y9Z}cf1nlxMFL& zn$gf?`b%brL-8gbP29e5XL6t6xeAF2KJSS+4FI3FN=)4-I(gyB=fp#?>Twf-0CKzQ=Uzi*30F`y9ox#N6nT%;dY$WX>s67ENB&P^hJ6Vtg4Sjnv|P6MX=c1Lp| zg^b>EDHtcsK18((PiwS^3rkANP3Q#~yo6kZy&s#MO{-;NBmgMlx1C#`(b1{;_^=Fq zQ+Q%g?iU8C{J^K{sXZ{1kC#jDJ*jEXlEuL}IV_tp3@ z<%wBp%u+~8t9L#YP2*3cQfR1l*{m*mH+ULd`Sq7hUO+RIpS{lK{LfI^wurEWj0Hw# z7-K8R0b<0jq^$RnrzIkVw58PxlZv=CY8#U*>cStn8!~4H$rsB2og| zx`#{S8ZkeN2c05EeN3R)?lf`3GGBpmzR61+ zhzX59zsx-B%hgY7w{X;oc^s?V2wF}*H@I33s$(vOeal3)v3739yEEF3*W4)`Zf#nE zAaS2Dak-|@OSPUpYkiR`7)9!-LgxuPP7$tk>iK}?Ll1OZgDC&2yw$CD=R0)I`dJQ9 z)64k$p`MPy>e8m=Dv?{v1hdLNYpQ(r=-0uegm+g}4sPD({CWsUGY>9W@9}47MS3}xfVjIWWhZ3ZifDJm z4*88!odLI@D&Lm2%1;oR#XekbnP?75Uk$I3<26eTr4;mHV(cY}#iLIyE2DM1M@ zZ1g$Z$K-fK2qk#L*|sA1V4in+=#v>t5Mgt!_4U6x z>nt%Elc4w`sumc?GdR}EH`P(X3DK}$*8cj|zw#U?L*_4Ar7`?`*TtcM^v!!(9}W>w zO|#vndPU^+gicG2nkC04*y6GB*U^8kSiTCd8ET zK_}30D643EB+HLqHa;6gzz(sZ%F_oWLUdo`Wnz*#P=1WdfBrhq?mf=s)4`~?M%4<~&`+7Gha`y+`8u2&F+2 z1)9tqlzQ%Y!Aui?yO~%egqAiO4 zI3h(NzBQb!rCm`6%BFs}xvxE6PK6ih1h+9^3{mNqp=Lk1p0o_SYkD0ipyoM|00ykZWa$Nh0M;PfUx5Loezghp&CTgcdFBbz`Akb5*#vA(vC=+Hp2}2*|2&|LUq38agJrdL4~00!H0J?fZ!Eb2lO*V^hW@ z&EGgY>>B#M7wG-|d-l@O&s7tVp8GRL`}XqkZ!T{NDu9|hFQ}RSD@dH#CF1^73}tqO z7ukr#Jo1epGYAsq_MR1?N&24V=>%rcj?~Y6K9}DMBU7G?gA0#B5eZbvFEpWOe_uk1 zgEhtf&X2t;_`)#2+ejAI&M8fo`!^tQ-a_Vc2!U|GXLh9&;YWX)gLTz0QH3vl%K3PTMC_5dEWc#9 zJ{GLr|FsUh+M}{L;83pQe9`Kvcbsz9b}tjv&bW39eWIg?*UrSxmvd2l@a%C$qcT ze-l?A#r!|FO^cTUxh%M0|L4Zc0{rd&+0siw3OxRQHf-R;NRt2028Ck`;3fah29_%{ zwyFQSf%NV_;9dN`e=Yb!jOhRKJn}F8|M)b$wWDKWfOpbVS6|=S352O2DfRE5{d)=P z9veMqfXG66u@OS@&zC_#NlCj7Y|0P(pF`2>T14Zx)*&?|bEX*)5%HZ$bY68-P)H(sP71X6Ap6?^V`F1N0hdmIXR&aIO2KUcxYv=$s;6}KZt%kh z;JWzmvsjf~Ol)q^BcVXr*ZS8NJsL?j7IEk0h`YN$AUDtGzq!uA$@vmRT2fxV=e_wW z)FH|t+F>MO`@b`X;N|{?zW`$Y*Y<+%dQkb9$*kNJJtV&IeDs;`C};L3fG=PS5DMz& zq&l=!hg&Q(O})R#k0XrC{a%)bs5UvwVgpW)8*|{LByMb2ZspnJTq5_c_|B3Uq^$RY z2z^mjw;dH8B!{QZHrJz%=5ozmV}(o(=R)dW<2M4%qgA(nprV$WkMG%Kg$bb2AOY;f zAi&)*@2ziIawk3Xlyk}$s+BJZ0!YC>tvCNx)(%bNik<)otG$UcfPg#`NzSXsMgOb= z;C-95$UNcT_#IFn3L!r1x^>|Lw~|5%yr}>}Ftd&8x}s9RUUiGtCJ$~o8->k+nQy_v zZJukF0^misSicmK|0rw|HC2h(OFNt3~weDJyrDPyF)@I@+3$Al%Gh~SIu(mf@8oel3iFxc?7hs z|Gr`#M*#>B`T(Z>2Z`qR`?HF_vbg2M4>`%MYaJ#M#3G4+Jw=&7M4X|?X^9M=tCKSY z9{186<*R9G4gg1`+WFvfyny117avX%DW5(%scCCB@2|Emd}bO_@BTvgi;SGy#_w_o z!p{RTyfNoAj~~0^=Sh8gdUnPe6e2^-nZqX=aM|Rv`h9K_wZ>PG@-6W8pyg)m+5|9z zY~BL!rnfGwo7m%@6vFueF89A47YJr$WKhKOwDJK=C%3En(WW0+?+gG6SXNaPU+3|W z!RS;)qflb&D-?X{=w6Ip77+bDuNIxmTLGjBRkha*;{uHgKu@Xf;gaBJ`~lVQ+FV;j zTJ%i4j1?#0c_*vArjq{6+sl#)TS{1zfXhUK+;kv zJNy-)6ntR`G3CR!6xEhvM{)G#%@j{iHvlQs01`|5g&_}Z+dyD%e&JofI@q{u-DkyF zd%E7LA3MkbAkWWE|9~I<{l1@F0S|BAMNE%>)ra||W6PM_@iWVNAY<(I7wfZt&tIKB z?SPjwuym~fJc$3F5rVV*Ic)sv+lQrP6Ca;uPA;xtkFoRodd_(MgujD(S=BKyxWFf+ z?qYx5)OD^J2jG|v89Evr13IFT)zsfzJ=1{jrWH;(Ixu?=z#hHg!F%)7dbQ@V2Ok@3 zN6A*n{)_|-Ms!*L0+8^Pt(E@yI6(CH0#69+|EHAlt?~cj?k%I@TDq>$B)Gdf1PK-h zE{#iqOK_)y1$T$w4j}{!?(PI>+!}&AjWim9dvLq^ocBKCea8KN$N0wgySw+^UA1=A zsyWx1^8=tYc9!$}fTFIhzO+3fLssYE&1*#K-2rgdZL1Ew4mMhnEGJ9?*WB>ed&7C? znsD-b^Z@4*T)+6eYTt`70bAeA-Y%`MaoASc$PDnR)yx1^NYd^F}m{19=#5~_Vz2Z)#`fXdxR5f>}EN>P;@F(Xe;0!wm2*F$ux@b3FO zr=ktD+H?mBx2VEI_mfqlwusKFC0od*M4kNgFK8TvEr|l)oDSZ+bjhH-iAKU8wkC-J zT&(O>?P$&DT!G5uk1lgXBKU*ZT=uFzct4bYwP6me+r&mJ{y>Ff%by>%jd9Z!qoHw3 zjWidOhUn<%l-VJ*-9;5MaRRHQ7#e>t>=2!iFl?71_xK0gCAk*60=pb;%$J^k_ovaf zvqrbcF5Q6_{4@bY3#bEOG}TB8z#CFQfQG^6cnuED zf9WFLgj_2nXxjaP!!olcn-BdRlh;!`5~lc~k3=6&!U-yMYL)lc4?dozmIyLbtgWNw zwRyST`@9>!%_!!^n|EQav0^~rcAWs1wSU$laa(kAVhzFoyR+Q2XJEZ7T*o);)s|@> z8V}g;2ypC;rjWL6=(WUlvm`I9HAhOivRZTMfs({OZw4sMQ1gUe&G_#!YI&==UpWiBv$a zZm1kW3Y8KKpv27G>5Ms(&2YbA-t<1BHh2IkqP+?TG1koNlUZfvjeOtYe)>_h2H4}w zg3$ZIu_|5*3pJC~B!GPK^73{8U$joK6yH^3ZqtFJtD$VErKTDuPn_PekP5#{n^a>> zyA~_5KIESA6qA+fz~}p%CZu_stRA;KsCybbKLJ{i z7wBz(JNbzF=yT_of|)Lu8&*e*#g;f~;4|klQ?3_(g;nHK$ef|0NmZCv9hR1%?Dwx? z6>>tprtj)m10FLIH(@_1(m0%W(LJ~uoGvWI$*JJ?O1h@CB%@@6AMDai9jT3kc3CYr zD<5R#9AfFkx{i#sl*ObySc*myasx{m-|IIG$#hQ?E~s+ zFJ1qgBIFmu*gz6j*Tg_zc}h7n;Wi}kim7n|(4Z%^oote}PO?N4BsOIqVyUYw64mgeQEWrb3Zg>PLV!DfFKP-#lq~ekZ zbT~oPuYzGIpiut)dEX|l3@ockN^UpM0PRt?P8z3TlLd#tk)FzQfVWALgN~?x!ImB# zPspxXTKzYu%b=DiuDsfc5Pnm9bxceFYQXAh2yI%e^A!K1R`bKAV~fh!z+Dl;u1DIp zZ<|_FUoEIa$VuHI_m)E!2C%Ww#g;6Ulwv*9AAk(RJ%Rh;bxOgg6{%O~!jAo|O-#vx z^E;Q}NFFKaTF)8NVg&{+3{LG*rJLzDGHFvkk_D4aPo1(PFT8lsRj;xnwbovSabO(&a+$|}udc7HUCj1lwU?UfindngYX_#=v$-wn zNyzEqnQ%)M$G5*of4rz5vglG;xgC=oX=L?}y+;qLp*Gn~GWT^6%XTxYX2>U{U+Twh zZaa=zl(ffnPbcmB=C4{Nw->}r%#c#~yHmssslI4rtAe{~gbrPqdWvFyYGqA1B#QD1M zSZ?KThzisMEm$srj*_TJf!yY9spHK;nu%aJYJGAxrj2(-hrvl4z=ssqXCQ!Sui z*V*!fsx8M`w5d_esct)%^LwRT{lP6@55^T>HekoMai(XMtBBdwvWR{#jH8dM!V7CN zb@VjU{m@tit6vE6=p~l9nw?<`0j*Lx+MMLmqh@!vr+IU*S-R;Ju3PymLEJ0qIX8edftU`_S+eCZG=<&eN>0Rbf_20$8FaIDXx6d z7p$-mas@A#xKQDRx_JdRjT~$bwCmhMjGD6#2RTif%mv?l12WisVnq2` zHFL%UgeLe;F2UgI@%evr)r5#5+>*^O7%`}?$d%H#Bq=Dd;^3Q>F<}f0`9UtC6uxM? z$bHjr+MC&Ln(3;8C0#$w_a9I zZL^3GGs-b+JzKi+aC#uaV3#b#E?zGY1T&Vh2Io}Gv#VM&xDl9d4|{BQ0nd_R+q9ir zW46V_X24zodokpaHaQYz!Tg^Q_cgN0{4J*1FGBs@(4VCl;M1#d&ssW1k!|G4{NbBOU?PqnQmVq1DRZXge8QG`tCu zL{)u|SmY@_nS<#QG5+eI{LB8Z4yBcZoe~j5hJ}`3ELIH6~l8Ts;5?=aS&E4RiQql?{nE1ZcQm$;3CX8-{B-1@sMgh)P` zB1MrML(jQ@<XFw`%PCFQ>-H-$w6d)Zd+_9F3(4z80!_j>ppW+Iz>_egk<6Iqz+I`r?)SQ2 z9pFu-x}Y`os3&i^B{Cw?AU5J}-_a=ujg^Ykl&ljQhWe&NQ93GTfU}k|&S#$fO&uA3 zF)u2eWV0)D7_9nV8^z!&`S1sFKHMkk7}u5d8`uOgGO@Gb>3x@&zCXVJ+$igR|M{bB zBhDDg&U&V=-(+V4h^NGF0|mm(4^Q_ecLEqBoU`|*K(Ssnak9in;NX@YfEjEL>VyT! zTHvP?78aiS8*-F)HE zi;bFEA|dh$rfFl29y{cAHOjwvSuE9(zmnnnHV0P z#&{MZI;vzH0Xi~Ri+q>EmTy7VJk8wxRYq;|E?aQ|pM5v;(K8g9T$k;e_Xg&vZLFF+ ztT+`Uw3a*pv8C-DQ(?;=I$`BaFPGQiD21lKefy@-IcDgURnE1rqZwA9LUFYqBl7#_ z1yJSeUBdYGZOydvCn2Gu*#Ci`R{pU2{}_oTVskSV1B|}1Je2O&9Su1|sV|(g-qZsn zRihhqt-p763_da{qzbipo_Mq*w6-)D=`{6(Z8Oh^odD8cHpE&{vpawI1ebC!YO=0R zp%jj~A1#!ZzZKkD++G5~CBn`IH%dL)90Q%goihG&l0WMZRuK4OzGR}Vo@lYzAGJh1 z3K{>wXZ!c0HEpnAVo+04lWqI*k4Dz{$fAc8Mor9R2cV6!d%xN0g;Pnglma&NGI@x< z?*3aDnr{JkjHU>z`Drfy+31V73;-Nf>qO6o7`EMI1L`viIAGKhN#`@_w>!XcFcedj zb=J!L7GoXxQiCmYhcI5;j?wG@!2lEXia9fFQuaHCtepi9Tr&C0hQ_^)NAeWQ-0D9F zF4)$}Ek}#~de+7EFAC}9*Xpn)*OKqwzgs&)ISr990RZz~^mb!bx_{9*{Hfue5PNF+ z+g7x84g`$Jk+1KZmX4OlyM+H>w9HKOVVHAoiyR!;B%IH|g|z++=EspQ;&j^=GK_d{{YAT7vaPI4Cnu!Xg4A< zA|feZbhnN{x%eHHPJ2TPOn9kx|2Tl&*EPS{6Z$XAy-_ep8yAR%YP%1IO1OR@z}II* z{|ybndPd-zq2%53utHpd&>xb(=bx$ny^@{1VuH{X0M(h>;Ung;PKGP_*318Yf8M~u zG5!hxA@Te7(ENOQa04yss>E_0)>O(N)|erQ2tnvR-M`Gizjx$3Vh4fX8ft1^Mn{!C zefqQxKttP5%R1e^CA6r?+*ogTc?ki_R9QnqN^>w6kD59@H<#Alsj#vV6TnDuMFz}v-XpSX)lZfJfMa56YKgdwUlTGDlnHf^D=y~Lkz7+o29chD;gUIS zoR>@LK2JF+3oUr(zvv7BkK$B7RUX4Ya`$>DEO413nKYf2vTuF}6+r&*C@CvRp6t7D z?B4^o6aAli{yQ-3bvyyczkmN`p{Q>PJ)PlY;bxa~Qbhu5#MomL+}hY`0wcQ)`rkKi zD=N#z87gjv1@I#$+*o4|%f!pQioiw7TjnBJssuTibe_vAUT!YQQu0QsC0zSo?CnaU|Ex8o)c^I5hsR%)!ETTYBoHM_ zg&XOX@a>~(wL&p~*+X2T{ojDN^016}dTAX~^fO|OA_cm7w5Q!ak{``>(&&nQ4>b@nCx8!C?f6>Pp4-?INtVx&<2!Qz1= zAzwCz7|JAxTR!~sc#lU!6gD}jhKQ`cECw(?Y9Bv_k~cI=_$^%npTar3)p98XQf&d#8OgHu^pft zz=W>_u(UI+(~aebM^n2UOi~q#0w}IEmh(4u17nE{$AI7ugIKAb2^@JnFRdvF!sp$4FQ@AQ zUYqwKsI&2+N(O0QW2~ClT1iSu3cn==_&BlOEyR}Iw=8;flts(Uw7CxUp_rthU!cmf zBpQOK%mPo^HhutZ&22NC0R7>ZS8Ki8#Nl&w7$)X_mkW5?`(%_v3c9k7zn_oyhFvcc zu>6{M*W@K<`d;uyoDZS9kBPb68D{a+ucW}ay`sj(S=c@q;V$4xzUZvH%v>P|o)-D0 z7diI98}=hJf$Ghz;8)|w75YmR(f27=ZJ`l}`O+Pze{HGbW-{1(t0>I;7*LO$UyoPL zGokiIg|Qkbq_9f@MzwkCkDgzj=M`BVjx}{%TwgD|xcJeMPVmcv=!25 z%jI3UBN-gFsOm*+6%`x&HOyLu|7;8C+G%4MW&?_a>Co#j{`PjYRuz^aro4N5qIzVk zui;}zEQVm6%@hWrB_EQCfj}grr!SN*7XC#1ef{Tc#Odw14BQ~4Mke`f9_@0H{uk90CiBGr z8y2GSc-0khy|bLV>5rqnp418-H?P0fws1AltWtMy?uN}j8WaRn+@WH+RFdd%+5c!j zn=$?o{-}6eP&rhm9wrlNddLHI9+%!BHDGo1+A^zrb_FTKP&I$==Z}@`V!NlKhc=VZ zVNCn8U6~ymRwc_sevq+442Sb!O43O}iLXz^@SB z>O8!>$V#dxVUGP=v-|MUwN5c|kI$tGSs>S>l}5u+AJ-JfORzK+KAekO`h+q%cb)bz zmR|)P*)h^?n^lSbQX(dRW76NwvnIO(G&qJO6IGWluqwAIVDd&<qFRi%V(;A4CJV%wB*63YCt)CNmHX*}Dgb z+aCL7c=)>M!pz$TkUBL!rc53Q*!o<8F{l-??`)#3M)%>&1j26TlEy11?#D|clUuOZwF44G`D=NS1 z0^(Q5{prUwJc`z5&T=*<^-tbWC3V+I@b0go!Y2;m@jPQCJ|3}bWeiioJqZK5E(KEj zApK<$7}yw4U+)t#+hjdH8zQEacQ0jIWuYn2YL%FG6SRg;T_bRG636!|DWwx_R(sSa zLcz&P9JX8OaTBz>)6PIcoG(@L*kbrLLF(2Jl;OR|*W$4Hw(pd=_|Bi&@jq7`i)*? zk*ue%m~H=VCZ8=;Jx(O=S1Adec2~jb7@iy0ZzGJvt((;GB*W$w5T%)sG>;cxju9-7 zyK%F56<{1V3v)c)0f^*P>4o1sXU*JuXQssk&2S7CN@MHzaR1&pk?s zh(q;6-qqe)cb>(8g#{w}g{U^;eiBfK&8+bko6FZ<`2GJ{L z!dlnKc<(8D^O#k*)-4mhSaL;NHDF5+|*wQeVeczGuvjMJ}lN-vrW+mv+Z<+&OLG^&vsgqDB(sug**qewsOJS&zfudf3?5_1AK2B%4Bx^Wm4W zOVUL|L@5p3X98=Oq*|t!Z^jXvZEVw+?|jRGBtxk?Bk`JJyQ#X?f?D{Uwf7Fe==JDa zCy(AN`*G0bhSq5$8Ce~)qMTPGy;I|65 z5d6VB8t6 z$J*rWv~C4v&KZtv5?t(n6N_O=@b0&_2a_0{6=lZUeeWu%&xKm7k0~Li|F*`;-BDGG z?>YQtqVRbg;#l2xg98P3DnuJ> z_hG;s90Q!k*V$kQ@BD5e6-FlW`X+&*5V!r=4-rZNMnj@ORTE`pWj6r77|X$oleslE zb#d-N_U^Y*LblD(YARjl%qiGBQYw~S$^Wx}oI7eqweNWRWwsu>?0cgdd<7R)pJkvl z0zCX({HJYMGq3gJi&PV_Rb7H|blVKy8FaN>$pRh`(^pw6Jkg?KiJ4xt-HxFZl?J3;BirTC{U0$=9(WbL?PxUnC<;X4?KoGKoo%t5T=g` z3JSgDT5Q~aVmOuDn&#a#&6j@&Jq@R?1m2^IKm&bwh}li>snf&?Gc#yZe4s!a^I{Dx*4ZP)#Fj`^61T4q!<2cqq(9qD^o{W9LnCnkkH*p{(l*b>k zTTeV05;nYsVrCgC2Sfqj2C1O4o5eBimgEx=vL~!>F40c6&C6+P5VuSE3$4%E0-b^U>1gjFa5Lw2$kS@H&j%(wSTjp8LKY&&5zAMMj$Fd7U z&LYp%?$CD#$(&4fg|hL~nhhg!piB2kO}4AL@}BJYI1}}UMsZZ@W%jKta>ljs&Y#Ra zC?8q=q&l{0J2f_n+8Jtj&wI2uyWu!iB)djj3UBwgZAFsA^}}LoAw+A!U|Yk~RWDJY zYWzH>aGhJH@wA<4lIOeyg}#M11U`!$uC(^)iU^syN!{;g%?)TuZT-mI_-cShN$e2G zC1O?_jd}pNGe{8E47||f@V>XqU!Ron&#bT4pnCD?`WWN`r-0ddta|T!*!82q&3zI& zz-nM-=px+nj^ARoQyw0KNDD!9MsM*Wv*QYWcH5SQu7>ixDW|rf>_$5}kj;Jkz-z;Y z5Y&QiSt#kog3O!X!8$6qvr-(oqF_FnZRu=gU7nJZxV7N=2N6AC%vAt9&`;!psimHB zv09l%%-wQ0HZpl1yqb)NEISfhL%cr0{3Ii(AEfFtu5@Pv$5{w;2EBrZKDhM<@u8gF zCg(6%lpIvs6WnL>L>K(*sv78DX4?OHHJ)2)JzIq5V=_%~6y7M^7muZ0QH^gdXG@ZU z<9mwjC>YDp&}@2HI5+gcd6jkKdhsrHzQucUEYg;;#J~f7=F*kw{HYHQTt$zmIzc^z zhZAe+ut%7^a0PdH$Y>*;|DE;AtL$1)#rKLut>)?GGl!CqS>SOwEeL=u6Auq5fv%A} z*^58WlzrFytIRC_tHpl~IB|T}0KThq-CkJKigs3Q`WqTS6F>^EaokP1LW#cPq+#cG z2aa6x@idIBWrFW6EIv786j3Xc5u47RSv>HDk~Np9K2jM`-)7y3l;7g^=bXK|q8y*U znDQ1O{(2I$`2Zll3xUrWnV2A}-)(che4JmCA2u7VbZ|Nxt*HSaOM2t1~sJ{s6Gk#uwUh6abnf=*9h<^-TxU+c9 z>%-tX;}d@zWh>T#tP|kF*h?YkxG}tx6Gn4#i)R~Pp!o)DwH>%VhcE^9!vV*fnDWX* zvpa6^=w1&B7aqBq)Cx23&=JIlnfz|rsT$ekY}p<(;M$*3w-zr1nePvNw<->uh;aSk z3<-9A7Qps=@-8|4;EJ4cvRdTJ{akR%+E;fq)M~II`SRC+(_aJEIudz9&&HgiO%=7h z(CT+D<-2|wm9vckcn>BsJPYJoT|JAoItADcVv=%A7AOc0m|0pDi(1j(Qfi^N-^I#X z+ZjoR?)i8knORb~TC!6{6#Gtnclw|o9cwSXH72lDs_~V$^CIRg_0E(CiD*cN<_0x? zwX0z|1D+fm9tGZ^4_dQ!<*#ySA%_9VotyoN(BWUb&`?oO=L8jl$Q_|&iKeRh}DmLe_|IJqpZZoeixxPkOTe-sW;8@n$(=|&pYq>hI#akO_ zc9rF#-(0JCI&v?)oqa6RJ$sD)9EZ2u>cs5v)G)K>TCl#ym5<=?v|$N1`19d}+IL$& zdmi6YwSz(#BA|}K-B}ybq3w1ss^mr!tS$Ib-DX%1f}hDSF)G4B%uaj_T9>kb{TQaD z%Wl~|6SDgB$>;YrZ3{DCt2BG$-->Ve(W01EGtfl~gSTLz^1-pJ|SnUfZ zqlo1g%oW`5;x}msoC`QKZeRZK3iN4DKrykYYrn&iP(G$p_jf%>y=)h8%fZG`0cRFc!s?n0leL+}(?NwbNl)7B2_HXk%fDuoJ`R z%?}CrcH6IY-(gpL3yZ^@t9(yVgdiUfVR158`^jEA;=(?xuCh{>D4(K^=hCr|gmN-D z(4f^x5~!NNv7;|15%iMaEh?`zyQ3F6MjIWtbn?Tk03TOM{F#y*BQ zMJ@OC6Oh8~e|;;0wK>uht3FfIbUY!i`aUDr!JCZW7j;{%U9A@Y)q^7r9nA5IPOi|- zEA0QF`pL=)Jbx$zYjGwBcyRuT^%UuG><&f|Za1xc3bLeo-iI zpV!#Z{?1IiS5TN=!$al+YQa%FfI8|ANddhJYkn08_R&=ZK-#xo;dlxtXlxawn)H<9 z_{U2}XbIz&V;O81G})C67Pf|*gq$oYp-5etzJFXQfd}g=x3NuGD9PAG^Il$an?buc zP|Z)$iLEVA>p!jMsRNa2SE8!xZDK&-?NpJX4mf6=ZU5az|4gf(PEv{A8Oej^UFBah z4JxH~+iVl|7xJ|kuYVB`UAa{WehtwO*k1QGI-q45Z7tE#mPGTQc8A|X-=K&8jNzGA zQHhT^#ALyexEpt4zxN{xXy1}=51hJByMprmE)=4^Ak-2;FQBFjV-ttq2mPcpF`t^E ze|T8SsRP?=znrYQteD#W-LLWv$IATZ#8&;|y57m+6iB}AbSBJaRF~0>$?B-uHDmoVHkUF8`r+p=xF^YtrXyQj`Fe$LMk%a|f}CC_953v2BZ8DB~4v%U~H=!AIeC{b{_LoDftlgXa z{$Bol=7yp>R)+Tzgze4Fe3sO(7!I2bGbUk3(oujCS60!VLG}&Z3w1 zEit>|dv^oD)?lK8tTvC0=CvmYJ41Gd_6;9QYFKj z5U4wGR3QM-0-oArgj|Pi+Fczk>b5yh0j>Nc{;*q5Gs%|@kTW?$_iZoGQ;fre=<2B6 z=e3^)PFIn5EmuD(@Mvfvasr;ne*Kz9SCBHNw;auk(yCaKW zvO#4?u!@xO?&c1TH#E!NjBsT0Slae@ccc|ncr}t~;c4rTLGJf+LWAxVl?+YcmIy43`$>6 zOIcG5rq4}P$#ZXAk_{=G!GC8|-8D~7VIP?>S}rb(H14{fAT6G#KE${44nh~NqdR?K zlSH(iTBa>($OdD7lELJN(6t=B(PGg1YDFsPF0vAnGDG5K_{o6~{WJA0G6o{=x3MU1oT`IF015OdZo-qMsgnG1wUwYhRBS(>W>I{N(*! zT*EV8p;}giyG+2uMrS@I02l(-z1enr^mNxvuBWM~)TFl^qkL%cWWaZ6pcJTvb*HGK z=VQIu^Rn4vG8A_gqE7F^ldm_LnAk*z6uBSIq9LGQO28rRQ$PnxCZAmzzIwx7i3h;3 zlz{<6wUg}{AHP3e_<KJ?gJbS zecpu)$eY}Mx=J?sCZtk1jV2pyH2tgZE)T%tnXPxaN)&pZyD-MZRZU0+=mWbf4zwTn z2BaG-l@m+)&5eOV&PJQK%uK^aioClt(D!WI(#QRBV`|i7w5o-d&PIcDETK~r%0b1Q zsA}e;yB`{&`6^f%AOb0GDA)(mjJ>Vbh928>3D+_$^T31gP~x72zYh3eH}5CNg?5p+ zy}@(s*)YLoEi@iaJPZ-ONSWBA4H0H~jB87}i7_`tBMP~^53v_e6tOlT5hHo|lE+KU z%~4H(IRc^_xKaAq1SeCR>#V~Y3XRj_UC*-_usEHWh?P;z!*WW8{^s38wY-w%V25gN?~$ts`niFnl5=&5D4{7S*M2#f~-a=%t|%zk}%*W@errHJsQMN?J$ zaP^3;NbG>OQS@^Yr9T%$M^H|Xk_x+Rv-_5i9f`XB-_3$`~OTN)80h)@1 z%ATg9-SL@BDB5v_HS)OGiCRVSkgO9!4;Gk#>$`{c=6$L+;`TF%Lb7 z?O`Jcy%&}pVi*-qh8A{Z)S0h+Z$N$Sq(1VMw>DT!6U~qd!F;Y}wQH~KV(sJAZLgLf z0nEnmU!F*6ypQ4HI%`#^sK z)O;>XXEPxYd^V8Yce-86bGEbVTDKP+!{xdn)KVoc-xwP=@4AB9^u!_dHMp&~YM`k= zYZs)EA5P?AaCHQapk@5Ql9>c@;IJ*utw0;pTu$y!n&jOutKELUQJ^Ll_>Bs$4o!kie z-P`bc=*;$oF7#a0kxl)694!5;Ye$Leup-WcGvJi`bU&t-&**%-5^>v#a>D#m5bH7Y z0%g?b~nQ%Qbw^VOi-^{X29EGMe<0lfBOhJ9mf3u1^NWd}|4eqZ`=;K;R$D5}qH zUPN|Lg`%P19LfSWWx$Ms@|1_EfgANIng844n7n7QXu4)hNnJSC>k~yeNuQS$7|^$e zYK{ySrcCq7dJ!p)2KmM#K2Q>E(#>|8g<@2lw-zI72CP(r8g@O|+rC`uOD|F5oJ`&b z*55{En=Yyo4lHqe-uhdivXF?g@Z7nouhS4D6{d-l=`mm<%-#-)n5XG&Bq^p7Z>TpQ zU{<8Qgs~8A6PYbGkHl6Hnc>V&iiFc2Nstr!L*C0ryCK5qQQ+@Tu|#-zn?6zz?`KNe z&onASQ-+;_|8O$2wfJ>rz7`Srf%ZScy83xwmVwM03Sht~^o0y9y%HaTxuQsIUk2gedN z2R^@4%HXF$K*pB1zdHKO(t~u`%vhh zRj$=aiXW(C2?OP~$*4F963b2Y-*wq0JFI|CF6ol^EVU99drF~ir8`Uc%Y`-28A<_> zi?21EcN#g%RQ5}aQ^&U1y|-flx7$|#v?szgDLZW|Yz>*gF+^e&K&Rd>U03X!t*>;_ zK)n$PSaGbI_vv!Mgu=EuDk^FU%q~PFtwj=@1N0P8$X5#YI9HkdQk8LDB2ISv0kWD` ztsvFR0j!qA<1T5F1w8HaBa4>%60=N%Xh?2ETnzv z=viN!*%^eEw&#UEG|-oqtA;9$+jhB?lBk=Ve@5>kTb?-`UXi}BbtNwrzKLu7>!6QuX?xpr zHZ?z9xhg33V<-01^zO^Pg@zzv1TsUXjp%sbp1ye9tyix(Na!br=vCyheD1`YzgtZrh|p8hIDNBWHR~7*IdHm&UoRXad>6msk)moC zKgy5{ProsQ#P3HVAAu-d3}>^-4+@win7jF}g_&-*xd8iib^Hp;jX=@JpM2{nOhg$O z`osJTJP&vQs@+W7Ped>d^JD;pr{w8S1m3I}DM8v<7;7v>y?WCwUvfy6A*f{L3ea+c zD{9esRC1M;gu*~@GU?f5CBw6{UsBYjwt(o+S%Z0L_*E(%y&J9B6%{kixpF4!uI`lZg z{b06CZE<9Dv^y9n=5(tths*U3JJoE#ha@P3mK5}vFrS{`?4Ty;Y;Iv~m)pMYV_2OQ zo9)w}u`tAFQKQDesGn_v7!NS&+sTe8?3X!kf8Zmk&9N&W4DA3UCFW?zy9;9%e`LSa(* z5UNpl{C<1^q?s8Qw|q;v(i2r9u6{3eaUIZpJ3;5y#`+;<`DcV{?SXYCzD8Jb3aISX z2R%r3UsSwCwWlp6o+Yn=CWk{+=0}WGw`(g#K<{q!(SdvmCAj%hAaOl>G-0%LAGtZY zZYhV#&xG(Cq!A|+YUB(mmj8rPfU1NzE;Rj|g5{1mf}HeON0H<^af0YpyY+M1cSSw@nmrZj;&={z>bt!2BGfK4?trwc2VXc}8+n$4;YA6{ zRR%hbqVIE7JGhSXG5)SG70AW)r%lag4^x96TY%$DH^_4>tfCt7s>A29AK`kfLgJAmK$lI*2)~vIr6$cdQMI_a+L>PW-kAxnldLc-#7=rH zQR+JXV|g`eK%87TRqnC5j-_J3-eB7yp3u76hU(l|I|1BBWsSig7h8Iktq>Toxs%?7 zv$;ry(kM{?Gq*_ZAe#%it0*ZCT?3US;#tm*R($4*rKnF2qWqAS)zJpA_9o!&(u71*MfBn)%}0uYVU6{%URO z)uY68=&e<$Ky4LxMcRtHnu`ujuK~wDoZiy-v%6c7;GO*4LMIp5k);i5yc1YaA}CAo z8saHUHO1K^3Q7Ygv@HDUW76fKD`yND7emG2$TZZyowWiE&u1Q)&*!(OsGbkSF8Hpe z2Mk@Gucdv-J?$#tx0K2nSxq8X6#^ILXdfMhqaOPFd4^U zc%1v*vQYeL9^FjIvuz?e{ z9_7d{%|1w7_oiA>_18bH%C7w3De^Tml;PTclV`zsIoFFWDqXeaX{p&R+{8$KNZy|T z4bP8{FoU@!ld_suts%I~cDm)c$-6R+{~+UZCuHDhF0s{VZxr3B{>nrsq^`1w=|nk~ z%9eB*?2|3T36-6ao$}LeQnap3RHLf3c4P`z{&YE78wPHNW&HZkY}9k+j%C#H>R9y0 z2|uQIYo|${i*J2Ib$XdYHk~gU%FlAqx%qR1MF-}Icyz4()}w@+wszHiG9xt2t3p~E zd3+6=!u>PF@lXVe#39)G*Z?Qq>c8yGQoBNU*@->@ECQc%&U_eMD`JTx7t=B z1C@oWl z7Aww-jw0KyemZ#(|tX9wKbh(14Dq=^S+>u~0} zS!`44t$yd0nnT5mk^UILhKmpX2yz4$Jn^!YJ}mF@H3U**WssPC%%Oa2OJtDGasy?p zc2nn?2o#$qQ3yyFungVtSA|ZFz^~_wV*F!Z|9TIkAtG3BD6i}CsLFP{J&h6spGMa> z+@L$=-1CdI+#vY*A1vHFF0R$xUC*iP&=#xDob8lp$u?$rS zPIA3wM_dT>>*&jF{?hwG$;aVLT*#o_M;C4!EdA;ov=hj=4(m|!>(NpUMDBHt5;+)Nwb=SArC!@}C zyKi2ejp6AwSJhb#d`rSqY;s%)e^&OeqL$F_sguiD&!xq*oi0389-OBkqD!8(`9(Urcn}%pxh30u@$mXJY~;%j|2WoBb#M z$f?*$Hu>N4gIZfE&Y0`1)ild-OpANRoG*0 z7t7ceZ+gSE5%lve7PI2`Ch|z5ZGP)E;y0DM#8TAcLAY*)=hNrWEU0>3MS{Eu2ZEn_ z>zQsEdr%|YvhjQ{3j~`EP4ZmK-V7t3e*>xmQy1?^^c9UlJe=dEBsn?@E&N_1^Hpd* z-JmNf)`^4U7po3l4H;ll2^WThPcl{ue=1#bgtdHVTR`JOktG>STI8HV`_|i@K1FuY z_5YCfmO*iKZMbGgAh^4GkjC8!p5PGNp@VyHcejw>?hxF)AvnR^o#5^cv-#wlcV?=l zYJQwkH8np{sY=nE?zPw2?DeeYzOEN2{85*;9nGV^RtB54?V<#I}eqp2kz;V zIxIp^Ux&@?ZKai0SJX5$$mad*Dl-s<`6GW zJBPv~92(Xj+ikhi8cewut%70h_{GY@T|4q+)isbWiMN%i0=mRoBPb4n-NKMug^VZs zrh_$Jy9HlsHPJ5I_u90sLQ#p6w|X5e!LtrJ89p8)<@oN<%}}A6^Lj-gew=9$)k*MX zZapd_Hhwp~qMnlWT!%PhR=H4uIOK-;7QOfz&XfnxGUN5uVo?IbsG95&d!l3hf{Y-8 z5SwOt(23^TTeqRa-03FvNi`G+UNS&yeLr3!Dmec9DujKx9;JGRo|5v5`U1Q#e$aTm zR0MFVGrms(0ta|5<3ab6frckd!d;cPSVWd`f9t!usfZ7oyp zKGciEoYTgxpualiNzw~-lG14@yU=hf@i0CXDQ%cfH0sa;a^LHVycr`T2#5;3mgpns zy;Wc870Db9GeNOE&Ve59xy#AqcaNL)eGlFcwN_+XgJRCAw+gT5Y$V)cdX>yDc5a_& zcCg$WUyyUe#(sJCR4th|^=Pb{v{Kq8k8|>>ow|P0Nm+Qy05!+$L%Krzz?2}bqeUkx zbkf$hOM+)N`6k4V<(hdlCd$W_V-E`|@neZ-JLtX%3F${AFVutDF{lDf73Te0%}ckfjL+H>mQp!`MNGXZYZcIe zbgX0DXP2m|6luxDatn;0Ac?S4%;e?d2ark<9$cQhYb^=Yk&J7tmLa+OWAYz9>X9WF zm_L91bPQ?M40LVKP*WRjbd$z@|6VjpS8pc)(2#SRaxkwlqq6364g~u9$>7vo9eFnd=vTOeAb;-@RHO>67MZT7g{Z1`t4e<#k0d(vV_h z9phkL*e)t8o_I-U8ovM5@Z!61DAos^{LC@Z)2^D{r(A$@knpXZ2(IUHy6*C%Kq0e$ zkV#%uAbp6NjRUTb+hxg88T(eACozR)`U3*KX}M#)q7^GoFi1n;;hRJbNuhCP<-ON5 zq=|5_TL#i7rF#%g>@lI!axwqSdi6ejfG;>yzjd~02gXIXLChM-QoHJ>-$iAUN60dP z#GW3FsiKDbYKBcIZ}85^1Pg$>;xz&rWn{2*5I<>mtMK&>0BtY2N_WwC?!tVJx6Ndv zxb~7NTw#%d_7%Ck)&*0pS4M8ED`l-V1 z=nVvsuIhpAh`klA>uR9!3r;X*zV+@UE`m==HiZM8TKe#edne6-Yed*KGQ;40SEA2=0rp!4O5{5%bN_=_GO*p7X$yKnxJe zO`C$4M4H?JC5xgh)xtp_8&!b&IyaaCU{SfEw+X|QXDD1wI0TKuB;i>ZdsXU<=w3Jy z!3Rw+P)XHn2(VwuuD0YCJFgm zA~vhXIxm8?XYwdelZ(7o>fts1HwJ^Wi2J(@uX&z*qfQ%T1;LjLISbxH-Kn3>G&_5H zZYcL>K0VZ2hJ5)-gdG- zGMz2*g4VtomSWfCuZ*Z#`MTN>rdu(3Sac zy%_^}ID5!=y0yN6dTc&;<2~>8M)*W=us-0{twgJys57`5DX!YY@Heg2F+$DhDT^R4 z2TfLi7wP9uAKTl!s_Z8}EQ90Y;v&_`v=dfA!XRyvHzP!B8ZCV19?c(Y78>yZF1h5l zZwLU%EXQL`RMg*o?RVAQN2_eht$j|*@p?H+FZt(Sn{ceR=H_blGnIz#h?Ew&ZI%MN z^Qf>E8}+{Zu*isjIKYR6u_X+{+U0BjkiU)kMVmevpp!^4ia@=@lNUW}al6vc^p~+l zd?J8z#+ND)GKSgf*LmWSjyNx?w+d%|t!z9@9%($5GklycbMorBh8xoLDLsq47OqEzl5tuR;JRou;V8~9rWjoDxK+^o4VEKfs01m@0Ik^qG5T7 zl66z))TqWd`v>28vSwK2GgU)FoNCDT@giN#MhgdjI1+Ff7Cm2IQ8)(nRP;9VF+LHh zA*{Ajv!W(T|84e1XXQ0ym&m4c9YyjviI!x|E{=2|ie#d0z>~RdLamjspY0rBj0GRk z9$K9&qZ1Q~^I+9+4%_3J5~v2he0yP@tIPQo_iKpohW=;CE!6=> zMF1SX@I*?^tOR7tBI{v*E|);>_B;qh?9@YM$AK@`O&HgK`Bxw7L6qk0#h!4ichN=? znyF$x@rolJ=1EFu)ED}eh?L5;6Q|G297iita}>A8#XpDyET||ld{EXEO192+(47|p zqy?r~or{E^kohQgmvGS>uOr|H$`d*iepy2zZFSKw-Z^?eK`pBbp0``PJ$rmI1Z&QgOF$+0dUsSEQntP8V_ zFaNWqul5dOH=_g2daq`L-BV;wqshIp&fTnXWM)pn>NJ}hosf1NB22N-IwL?IRW*%p zS2p@_Oq|^U{`RdJM+AAF+fJL#v?3nV}FPtn9=r(!{pN59nFV$rJOwEOv z?hp$Z^rD}fhNj+UCL_yL#jZ(&n}8(kUZmK z?RB)0FARavP_tY_wepoFL1>qh*y-7&23EXEt#&hjD1W-a?v2Z!ceKXJ2d!kV*vo}y zq%}7m$_(~Z5XS6(4q5K7mn;`YH{4vD1wjyh^sMBCgmCukG3GciuPmjP2k?#~pM~p< z2qrdjTryPK2JDVWW6~CG#T$fR(B|2wghv1EuQFzivrak z%nN@hg+i~?JloqdiSL!#L)IH6Lm5y~i91EC4$3yy{(3&a+fm;_M*BC%#!1jZ{FL)0 zn&-<`(vjX$7ZdUavmTsSAdwf@2o5avBp|fvUy2V4Jc92{!9HGf`g?ZPWXe1d#H_np ze^WY`#AG%<%MJFiXef$eFBk6G;-%BrX=BG-UAje1@P^b*u-8$$nX)~5Z`pdf4JT@_ zn@4fR^JVKjxZ~aJegey($rJ51yHee}IS+SGA@3AxZ+XX0-=IB~@kZJW1K!U(O|xk*c8wAQ zzeg?|Zxa<09jmj%jl>t!YT;xv`kMjqxO8cK26#0FQ$?{nwug9F^w~||KKZBRUr0R( zB|+KKn;RQrW!l_2j^jK67eh6-YXPWNkEWY_`q>^M@>{0|FCXqao+W3lh2wXt7wV^n z=!T|ff|BkTCMlwT&i%5I_K1kLr+-%Md43!>!kTpW`>@#RZDo_t@L$or=j-+OTiwXW zOM1d2kV6A1r%xlh@6m*dBOh~f5{+&RP7lxBTb#HOUnyVXfV#fOWtHAO zf5+SJz#c+Oh&p1nw`gFPf$=>p&1$`s-b$j3K5P;5><@J&nSF#)T3}(Ic*t74zO7gy zI(C2@)rnj$O`?~7SIN<~in8r)TH#1XBf#n)ETuY;@BMO=p{%d1Pad__^#k>4HCp4} zu|^REDrc!AO@(tpPW|X&^h?xjW|dm( ze`1a*NvFvo{B5n*GNn((WmP@ASl_X}$$4-}Op?fyJp&r*4+S1DuPCkRQ?5S~7p;_^;lh2NK0X4n1`)gCZy55x=JD`YNLiSihk z0?cMC2Hj?rm?nI_OWEavt7DH7AmTvM_;Mr zt((=2kne3($NGu%m(}!GNAS8NUpfHkIyNIC`1+^}-4K;jC=z%HG|<4yy+2cyJIU0S z$EaD2g{x;rlEP&jys<&?)pTd~BbH;BthBtOjXcx-)UN=4f6v!OfTxEWDIhT8^bzek z4(RMt(8mmjbyS~SDJiu5a4%%zyiE=~Gb@iyd4Uf13Nxd%ZJMSvYB~>gycCfxh-Zpb zWfBeBHvR(=aOAUt8NK`aQ(*%DWpme0ArbBLa~vJJ_Y=D;`5jcWcW6A^(EbsM{5y|I zE^Hp6l``2=*P*L4#~ z*Pg;uZMyN_#5H2-=7hsxHMdeu!A5E9*bWiA$vQ4c=g>((LBkHH1?!RVpIb>3YD-@n zBF=UaI0Qe}jxmr z-p|!~X*AC|)&AzXq8JM^T++oNEJ58?*%hI0muVS9P~%!@w&5WFiAcKiBf~fCPLbs- zaZX(6Y8X0-H-AeRCr(dhmB@tf=P?rPYm+f}) z_@g*Mk=s0b9iKHW$8)4@b7!$3b_aplAh`GP&z{;-)^JR9Y8n<=Y1GyVogUy7vHg{S zpC4V?i}=eQ9{B{Blh|9aO{thEQ8V@Lc!quIFM;gui;O|31y2QR%uiG_v)IERJS zY-*UR^no&1&?P3H-`<9cGQe4Az#5_ohWOq`l|HCo4nF78CchNZS4|;+NPy17{amYx z9?cozG^Hej!GY8`Nswf6br0JWkw1ZcXjZ~3-DatGiO*+qArspsT!wIIm0<1C5p>dN zf}MKzx#j@md5PqHh9Tb`tuLMG+TOh@mwDgpanSJm5EoEI`fj)PEiEG&iq-b4q!8I4 z!|n{;rcbrred(90LzhwQS8eW9Hk+N~wRM!Dp9<^Di#=!_s8gDf6? ztbd8ugLAJQ5n)31^7~PKTZYcgPDiZ=ARlJGoh&w%)lsF#DtK%)Y4X~*a?&O61G}?E z3Tf7-M#|>=?R+$N6Ysz1G|7stJYOu>T_^%pFX# z(X3Cy9pGs^64_pdW3}H)1O< zr9J#9_@=avG`q^zo6O@dRFz)*jR|#Vjy&UOXs%=*y%pzp7>|&92 z{Gecc0Y18WzAEmVxypK*>u;V~8b6%|-7;U5_-LX6vVh__}swaK%z;E|@kg%Q!qek;61%@hd^iiM7;8_tFe&vxWa5m9aPzRE(TTn&SHO`kQw2J!_<5#s(aRM^tiQKC{Boo zD>^%f$jqbv61|FyXV~y7Oa_CA=FH^GJE*B}De>2ea%& zdpqLkDiY!qEK2@vb%K)~Ugl+Z_Kp|BwVx#qQ&LZ6t22o&PWEbXs)6~!44gBimC9o| z>WCo}G*6%Slu=aTBmGd1rgQEZ$WgC<#=?a7X)gX`~LaKKLtgzKxSxKtP`{k&YygyP{+KvQZIk zykPDOB_};F@I7hA{o{3e5M7$=;jG+YAKCcU_EEhN`oaoeQ;v4AYi%udIA;!#~ikoph(?^>2zG_>gc}SuP;${)H--_r%ev2n}2Fj_sDQFMg%{{{?yhe*eQ`71&OGM z{K{|``bm)^;fY5=w18ESIV&AgS6mpFLo*f;8~co*@-&T(mDIJ^v%WRXcnLiaO@R{} z0=-{lT1b(`U#>7|)~)71z|bszZHul;H}rBwArD^@GGY-NOMm1#QM88q(yUEs&e{q? zl6sX?WkPN2gRnw!#x>!rj&wmXO@=V*<0aUfzBmkYOzeZcux_2v-P@`mSWJ{vHLLWL zjpJOSl|CDD7PNB^)3qoUbB-o6dfGskj`Cp)RCV6;l%4T3Yh&Z&>_#=j-VCe1;iu|k zfU{YFQ#$bu@GWSAm;BOvWE=PBo#<>XUI_z*UWX=ds0r;{#Is$45%h~Tl^-b~$1$#$ zLzMt6G|MEBBJZFE4z671?j|9w^eU-so#|*qBQIJvPw1e>xdS_WrG$#)knT&}ua_bB zw=mRDy-76&NEQp2E>5H+Y_mR)GHEgOj|c=cxZy095N;C@tL8*R?kCLtVvU^Yl@1>mS|yGMGA$u#8Cj+H=XaTi5|#Ugz8ptN)XB zFHhtL>4#{Qt9iA$E<5)KJczB6{ok2gO#6gtewfUt7bMtOo(%Rr;;J(6*}#bUB~0?) zf}M?*ky~M+Wi?h8#4z}dI(@we@6CIB?b6amH|n;=N5m)YW%AWgAN`l3P)yylFE_~r?-D!(Sz@1-Xk^Vd}+|cV@a@#cXQSDhB5c$DQt$W z^I5YJEsBtH3KJPKT3Ea48}*11_CqDh&E4CWv*89zyAA-=Xh0mD_5G8Kj1;94i8cy} zKpsu=@noS3jQy@{D9;5Ex~VGV&2MR<`vY{W9V$%;^`JB3?LxY-DOB2#DGV%(;L4Gg z4$MrOPJk&{3VZPup#}g%B905C)l?VX2Bc%hYkbe5Kar{qcXVvel`b;gP9$ql468E= z=ZvW9EGv(w#0|o}W>GbdR&(l@3pJQh^AB_IWNj3~R9!d8f<+BSSce9;W0+J7^9MSG zs1kSg>ka7?=+~H0Bjld-6{rX{q=`kQ)2+PN%7TdZ&Ro|%rD>LxrLa&Kgw*hyF;UF+X4BP^%qgP{_?9tdYavC{i!)U3b928TxKP|c_Vm)qS)*>xaLo!^uXe5V$ zPPl%$q@$Iwf{`&2qt@bPjnv|SSN?F$w`0eN!iWm zz$Ac=<2j95ayls1P9{9rQd!4%UvKBCptFEt9&u};8u4`#r5SvF8$B|ZZ=cw!MTF27 z_8Sdad@<^1U>Q+HY3mK&N+)d;oo4Ek{xQgT1aGER*fOU#1%rxLs*0UbiMVRN?-IN8 zmSw^aNCABBv$Fqc+>g2b*%fJod0U!+h|dvk^5uC^qedh>C$F}9(*eHo&!^8e^vdcl zp*cgjTgMSoKN6G|5~uUW_ead4SvOrC`O&hG-;=6nG_*wv@j z41~bebDBC~vr+4^2f_L7uKJ|>s~o@!qNxN?3u2YGI%ZM(Y}i8}~zwc?Wy zEU1jm_k51)yD17;x88I_i%qgaH=&`#0t=VRjFaEX8Je)kLOcr4qZ#uCOZeTnqa?*h zW5_RXGB4B6)%Jfnf5>L(^E${Dgc5-=n}Hk6I z3IcmQn5CaalSQWu7#3G0_pV(WYbCGK;$ecwTW=nKeL#A~7sP97xvpZ!2}K1}g|to7 zUjuoH6-{>ZkQ(;L)4Pt)h|;K=5S^Ia?>;W2)VcJ}&wZI@)78}X*GIV^v%NihT3XtV zxxG=fmhTNyq(6Ta5f!BXf*o}3xcL4FoY-|ph!mNSki4)=fi_7DxUagprfw}leTO9=7>$Hk+pEFl zMCfYK4r2F6uwz<6m$x(I0Q`|Lb092S8>`)k-=!aefM+t;t|D(6*hCjeI3PAws8PjH zd;yta;t@5f?hHL1CyPb_^ri>fmbJ`R zYPg&_&;H~e)jh0pm;5-pJBMDL+()ZR1{1fp6u;9dStVw+DBX!ve8X`#hQ@ebd+H9n zrs)~NsAD^k|9Q*HgS&!i=##i?;Im#}a{TvGv?m6FT3g?H>jTOHy-4r%ykTFi#>ezQ z^w}vlRDvDJ)R$+^Auro<`m9!vM&K7BRWN+fw3e!YRnA2`cCPCS_kB(AU)&FyYva7M zOuu+y3mQ10i(}XcZY1s2s#dmgn>2xxv*%ZwrVO16S24p{vooXC`vpR+tmL7W9a5lm znE(Ux=jrGpj5|uLTq^fLIHn+_>erOont#*`1TxAGwqra?*Z^v8pra}z5x!iKf$ z<02~cy^|yVv_GkNmjhi~MjNQY8#k*j-I4h0ti}WSx0f}e_LsX=!RbJZU}LbNts}cD z1aRy~w%$C$YP5b06UwkN>TvuSAHP)-)OR5s$Q$54J8oZ6?JBL*Ye>a(i$xt2Ia0K5 zxg}`aySnc@l)-Lkn|z;|X%S}ShSHgeGSdf-gw$E2lpFusqHGpIPDl57ej)Sz4&Hw) zqeAV2DuxLW*x5MqUMq=s7}T56H9r!R+%bQ&8C}BS5IpKW5{QY2lSKHuq)*+Gguh3|n(}CLK z<6uQ2fR7$))8!{F{o3iWdhn-Ylz>ZRhf?4B4%_`^FPA2XiH>eu)V}{a`R-z2gT2Zs zrJ!lXruiuT9uT$&x;rbDxj9~*Zej*0Gz9KVPiEApm1*tK5f;msnc-#lJi9f-iZSL> z&k5fd=Gi72LxkefH3EDqhyN77Mu~`A(llrbQ|{ME4zU!{>a6yG3 zqW0n@HcTp!#n5mrIDAOcINf4b?A`L+w^O#TnKHqG4jYw7LR5p+|S5KVic-Owl z8J1%`sRkdmer>DH!Jc;ZBH@}K1p8ylVpL@0n4mrt9j~Tp0s`o;E3yr!oAc%F)D&x1 zl$T?r3RQRXPE1d)Edww0M4RUfNd5EU zFlemR7$3}7YgMYp1Lgxhf%1mIYGWBe=?8O5Z_)c1-o638tL(m?zZB#}D?El8%RMHC z^w<|fX!nu4bNpE-t>Cg#%`UcA6so$ymT2Ve$2Bkkky-LxK92>>(5GcBa^M$qay!jr zJ_{w${@8Uva;-N0ozs>$7}Bk1_2Ow$6l=SnHICI0KqQHgjWy$06Q=?vC7T*nyY<{&CBn^sXW>be*wz z;-~03@sPS5Ejc9cmkAgX47#0LkkF&(N3zW@NJ(RWf~RUjxW_wdtCdzajYc~W7!OS7 zY>Fm{d=yLcOM|buAhDg%#dt$2C)t)}bi8@il~RPq17I|J*5_&+NQH1Wh4EsdO z9iu2{Q~hnm(9gus4V#*)B@_%Ug*Wfr(jlZAQ;kkPluS=U|Zg)Digv ze`_*GBGBN7S)N%>WGb5!K(k%mSKRgM)6hUyON2AU=pPQ$5j$0EaM2_7pt?c#={g>p zD@%FvJ_2y8BYpG34Z&lQC!w}EUg%m9D4;=^qTojwnj-f!Mlk)pJxe?wc_+f7L<>*} z_lrrD>>OjNdQiey5jYqp=D^OSvka{^9^K9)A--fg83wvDfi9 zM9_#Zu?cn+RgPppzgKES1@-kr3oiaF^S&%gEbnJ&rukf^paY`GW6lE~IiDq=J7x=RXUeH$#YC93l5 zR*Mzr6LE?D6PIhzjRunWLdWqO4%OdngMnvVSMU?P0K^EMM5h|gg;^tXal6=fgknj^ z6#h7^2I(ceouHy(E*#$jQ&&KH0h* zOD|3}JEdNo++XdR&q_yI=MXM(uX=|>5%Mnkk)`4NC7uthm1Dn5VlSlinDLiIKi7YG*8cqJ0aT+Ym0Jm|ot$m)lP-v*E@Pv4BUa-Ah(7=jI`+)nRc(gwZMijzM!q z&05*k-quD{RIu%ZYf>%~vqvD;@T)s-zbw z3JBD)KQ|T#iwdN*e1}53zd8z#L?%p7;29<(1vYKDC`t+_L_DEOO9GLEF6h0H_)&;V zJo+Ky`n6_NHs(95HjU5C2!6zPtjDcY-~rtd+L3a|wne;7r{k_CyP_x%yJWHW%e%F+ z(Igc`6c*YKh9Vxp<3Z~RMw#|0%+Fu{B>J|z`5YmVnEgF)WKPoTeB^IFStwI0xgPi% zI5iiWd_nDLnh7RLq{U<=btyn|>W~X!!e0JAcl|fRHeWdqNQHP9HjS zwmZwG0PAkzPHBG7uuvU4HOX7Bk)ksPK2&~>^czl*Icjx^B5l$Vy9+2N!#Xs@qCjQ) zXdDX>Vo6O(&Stw+ds$x$r(v|ETTf^yMV6u+qtKFSUpCcMrI)FiYNif2r1GHer;

?*gA zQ;2q=;b8frsh;H=ZbV{4PL=4U{J!hF^?rw5oZ;g5iIzZ8B1cz;<2~1}ke2(pKEj>L z-$CgFAXalZ*bEFp!V-xeba_b$c=6-13iAZDciD(w5p9p z<3Y8&{DM6*M|gOtN{cNE!RIK7;cL&fzm4W5}D!1*71D5-{9dslmH`z=fuT~tF>3{D{#(#-m)l+SgvleRPD}i$n z_r-(PHjc0CMzeb1uEuKe3o7f_w=ruu;q#eavD(2TYP{d6squ|NP$)jC7QIVt_{!LQ zJ0XSjwev@n0v!e7+kHo0W%XWM)js7P5fhJIv_P!Z{Bv`0=~l6+N+6q1MHVe|P&dea z2F);*QB{I!=2lf=fFb|W=wi_04+im|88Od5+XB?epgRv~5fYT0Eec*{)Z~3)6I`4U zhXnI%oIG(O4Ffca0I^!05?uV~4@&4}1jI@rLsQTm*T+3=kcV0_1IZD+>XCH*_#0PZ z1q=+#D)0l!-=fRm7Lzy72sOLAW(O#ozU@J`-T(t!FuZiU29_;j7}6wp4Ze?nXd zoVdZZQoIG2=D^VYr~{K+KV1h7M6^bX5a3}%o8*#6q^~660i0sGs!6{?yMTDsJ}1 zoufENqzpV>Vvn^i5kw{(#^LkJ{1q!qAc^olyc7Qd=J&<&)M%yt*An7?T0!yuFERB0 zOYsNkc_$(>87(awu*~~_+!25@_%k{R0GgMwZ#JK=zZx5_=f((iYMQ3!--Ja(bS1HZ zS?o8&0ro$Ehvpq%1G*AA#>wbQ^@8b0Oma4Cp8}9OxH@)F9~3eofBe9JRG5?M+%~Wu z-muROUmon)d%|0n{ttUJ1t8LG%CtNG67T3EaG`&3U;IT+HE7eYBsn?R73iV@#pkv^ z--8ek5u+f`9X_WV!}kk%Y4e+BLlJ=kp*$r}+oPE=!>Qbame{;@Yj1$Or%9>mSHNot z#A+Dye4d=r?0fNn)zj`IQ4Rp78gEuS>;W~j0ie+BT^U;hc9C1>TYw2*V)4$rB^_JT z_8J>nf3ir4QsD8l3wvS#F#P5s<^e2m^i|IbWComwss^$&qBOX@w&Jb$_8)+CgwUoD z3NWJ*rEuGpXWqSCUJyD*8{-`MjYN!eTAQ}>;k=!=|k0vgFafGVI*UM6@o_2m{&gW8Wv;I0>DdW#wv z~hJ$pvI^Vv1I|a^) zvp@g*(97MoyHE$x^E{LA(!l}D^lZPwS1S#>90T6zy2}A>h)TyjWowY**{kl``|(UF zW|{P;oqHC5CAe7!0O<8;8_LbTm|4M@dI+w$xw-3hvgufpgQ=HIb24ycP=#DV*ed?4 zcy-S^bFD1_?&Af**Ct3 z3%HsacysBu;=^e6VrM(Sr$n{X9w(?qwk3hiBW$rzLA75dQLHcCn2t%*0H^ZJL$%Z_ zEQ;K&*L#z&gj`lE^;-5W%-ftofF0Wv&~jf2wI-Todd*~O11eytt2zgLcH3Y3yg-w! z)ggIWsS5RQGl){ISlh-Eu&SdhU)9vqICWtj2PgA^*g85ontfUCfYjkB(86N0 z)M(#)RS67RvoBy;_lXoN?gjEi(XSy?bsc+&yE$ex-vkEDoRx zT1S9AAS&1dQj3!hkA#&!fC*~0?Aw)~$^-)g(`-M(YJ|oV!);B+ zqt4)K7vf~&n)d7HGK6Xr&9#J&odS;7m{;4WcIjAIP+8ccu9;a;RUMm^wXvlT;&5xL z0JwxKEeQ)7+i)O`&K=DWSdXAfyjeg@LhAF#3fv_4mcM->^<2P`_~N9s$HvBX3VgKv za8m7hfO7Xm*Dq%g&I4hHmFtkV1yB+@mhganT8*z%8%JojzW|Ff@uqQ}8Mxg+f_DC1 zjUz{wCAHcxF_=}fo=d-K$!#iRPDyq0Junqkc_*>8E9R@8fN=TlW006(x@NhQoJv=EWvLV3p z25A8BNs2eY-Mzj0Z)34Vm>2@ZL`5BA{$z`VL_%PEIu{*nH-`C5-M8!;Nn!9k@{?wg z5(GOI&j%(gH@#E2tS7MLwOjUPD>8-3*Q`U@oFLa#uNF&qF*KxS(xZfKL4CvqbP5f#|y}9w>V!GY6e>uqbnF=a8o+wRO$U}hS9G;i|49(RF=Ci9({@B&=c&yQ^w>{0(YA%C;#y=x z@;YF>>C|al)_oUAsWG3cn3qcJfo_G1vkOS=$01n$FYDIL+-KM6>}#0XW6ARP$UipK zTRT$!H5f@)6M*LS$-HwQgJuhtP|_8-l}3oY`PN<046fBW7b5?)g<@%yL7uIUHi2f=`0xNG_xBW|4u2Rv<|_T&VskZUn3tU9 zy_HFq5l8dr%?%BVFL-X@+*|RVRVOKrhT=;|Kw0fi*M!Y3C*`{Icdf3vhi4|nmGqT2 zz9POn84(l-*t&X!pPj4*9UqK@L_|I(O1A0SG)Nq(pT2i7pZVe$dpsA=r^gn~KBMF| zWV76?y4!^584+4T(0PbpVq^l!+uL3%QZ#B{iL#q1)x@UQ7=zNi>((h(So($h%{t1btUg&$DM~~^ zDI(pP$fAj8YWH|Rw{^36lYT-Pyk7%(P~IAUe_QW8_WPC_Q4i$}E_8};O)Ixf=(y3Y3znt4(eZYpkz;~2(J(A&Y zJ;j1Oc?riFGdYe+MH=j z#F>J{Ol;qJ^v#2Ri_Uv9yy>fD@rBlAeg#FRGgnH)XAfxh4`dzwM0GhytMv2B$zbv^ z$t4CUVSPO8S-;6>=63(0=axd^Ml_<-JL7)bB^mqjeAC`Mv4^B~|7tmX`m7|C3Y|I2iFf-g26O~ajGNM{nGTK!zZ#HG;NiPlSdIOCzTcA5l;x^r(DDC;mni7 zf`VqFR%G}*c3C(+qQmH~UG3R}eos0d{9igm6POf;UPPg27;&FrwF8sQt13-ET-ljC zeOO`XPab1ip+Q839W-Dr1*?DQ5P@x}m0=-N;Zas&ZfgD-q1Dk4lI#aTQyMf+!|kuR z6acmSRX`b-5)4_rJj@xani94o0gA-`*~6;%+C(}++JQ>h71|ql&duRD?raNS>-+Cl zL1({p1RL*qMq7IU56Sf66xbFh_zr-0Ox*JlCVTrI4!PHV{m*-@0amg9zW#ryCG!9C z{|y`x930CtU_r#?sip&rfTxT`|5Isd`(@%EV+w0#|DlY`e~nOQ{>Q68@clmpf-R#5)c@y?64zI2 z`1L|_MI#O1SRA1@4`&fa@?vtP09acc9{*infhm2M{F57zK63;~fCLmwkaE|+SRa%K zyr~NDKQ4rcDcdOESp3gAAXPk`FKq2~m2tr6_3T>lT2*ZSU z5q8C=6?6EL23lML(Iz=x=hfmE=0hhKVU_B+p%UU`vucsx;6*<|G@$D(LbI3{(Z5Nm zLth>F{&u&ADRl z_z1R&nH+m>9;Q;`a7#l%JT1MMNSz-*G7=?XnB$B=o^? zhPWZE0RNpfiILEd?>A$8`;BhB4KyEq(kS$XqEp%dPmgOe+O|DF+3ECkXKc@qwgj|_ zk=>%1kK?|ITZaUjV1OqUekf7}y-58X+mGIcO4NxG+H+dkAPTIrarDqIBr%n*Je{Kc z=-4FwxVFRr8Aqvj9ted9oYtmT#uDv};vJa!S%;res-|6_>&ydB6K%x%VNA)$IJ)QD zjVisL+>uCyi1zmOdT~Ipv9SOEuE8D%oL*n3!NI zbb%hF(d2&@;?1%C^(i?YFeXmtGiA=6hNgLv;tZ|IWV4E)Im$>QR*q70WDX?FCi9q5 zI77v{7Fdfg3B$~&Cuqm~{iA)T6>o2TedY(r$VxqLW@g_u^i+PG)q5~BAUJHK!O(&8>kl+>C4-^XlF-8pcitD==f%8y+xC7KyH-Pe-$YgN+_2C1Bu&{n64Hp*$^q~&B~{a!f8K2AJo1&AxVE07lN zUWWezeL;f0D8s_Sj3d+>Oyhj!%a$$E>0sl=jo7zupMDp`#l^a{w?TsjXw#+*diCms z0Rsl;YpiPe6mjO9MeFzC{&yy0%f2KO6qOiTE5dPPLwdHswIh`+c4~(Bm`KD%g(D<5 z$k;;=YR=Be4kRJxqNh>1ZjZ6Wj>s?s#@7N=x(tX>pW(p>3|IMBcqkAQ2upDZ0#w>A z%P)W>w-89rh9xyiiM)LJ@fruINbvApsP)}r2&nJ>0*!fQ&z?Q8ZQC|uOGQ|YGM|l( zjyAR&KSZ4Q9ua4PP{a7LFynX-x-@Vw@Nd8UhAmsRASp=|YB@Q&qRwOT*feAsG%Mje zH{A;A)vKo~z@tWu(u0V7EmC-G6o~WY&BNlwi?ul8welWWN$0gum~lT|A1mR!K339M zpfqgQP!DXTh#WFxh<1tKJ@|sHTes>$g2LeT+i%zR?kf>^=+Ghj{`>FR<-~rxPS>to zan@O9Y0=>;_#Aib+NIq{m<}k&_?-FvDau%gGYt?vKR)O1@Ng~EYS*rfjvYJVj5E%_ znP;AfR;^m;{d@@<4Jg9q%$b9~|NdL2|BQ?boep??{GQK=&!5-EmR??4-MV$rwryLS zd+xayI&`Q`58k|g5oeXj$uGpGe=No0A5TSbNvW~*7(synh>i@!pdM}T__gO~Q5LAS z34$|f#LiUOEc|8$%61%7LaDY|;;g|u>cs(dRbH)@fCweTm^M|};W6sEEI9=zJDh^j zorh7ndOJ!MY(nYUo!Wg8>QEu)^hM;y_aU%(1LH^j3G+;r-HzM`3G>V;Q>N(h^mrrU z%=d^m6NDPZSA`iX-4q$qrcKis^ybZ*b)}q@T~7!O4n~tEO>_$z#n0JipRGkhbp;Ub z_wKE)k3Bu?`Kd1Ky_+y$f))%HUU;GQ zgKylpu}^vt`5s<-?KOS>_Tr4=12_(V_flQVUb5MAF$D z6GWUlud6=U# zsPYaAdwUBCbuXADM=iN(VPFq#x9tgzP0MSEsB_UyCI;-QBgs&;u^6-1o*p5r3USTN_6 zmf>hoG1659v!JXLtV-yUsm~Dg8LcXgbyWh3r7ycTEhB>9=lG&9qcB^$cCGF$`}5B~ zbq|#%6<$X$Bc}jj#HaJmKVMhS*|S1>^cIRkx~ox`OrJhoSH{^pQq^lSp@3j7A1mnW zN$b#|gBF)nh2refPd~+1Uwx%T*)`W(gK^`=p?&-I`rfMpT}z*P?m7Kl>_rk=I@wb< zX3Q94OVyye>WURBFk{9H%$++|_ndhu$Q;3U%@$VP|HzRewLt6Et($)CRe|rDy|+vU z9QQzhmXeZUeCs_F=}ebn$Bxwkmu{Kv5OH>lq_k|@^~NulzHA%vRk>>m-~H*Go8zIY z&ce`MZ4npkevC>{aS0MqvvD{j6KSeK%RnN7d$dNwIx^7zRKursrs{>3yaH9gsXAdw z2Fms&p>+2ll(Cm<_YqhQ9fc)11D4EOSPF{NiM%UUf=Bm5)RzzIxz6k<-J6K)ez#jA zKq{LEyZ1swzH+N7Agf-^91mW4=_M_O_^g@l@;$ID|N85%tDJvV7ZGQ^$B{S-QtJCz zRtD)-R8{gyO0oKA7Jk{6fX{Xw#OJ#Y;->>g@K-`IW+WZOjO0|c9K}qvOixO|A8PwA ziAk86oPv#6Imj(7MSN%oqW*SrHh}ZcD85F9{ljb5BT}#pLNB& zI&>fWb%VYBSSEmP!Zlx@M90_K#7l~xG@!Bnh*TQ$i@eUt<{4wUwpRYxGbzpBB-8I=G%XibP zSu?j0zkFe5Cj%5_mp<_|X0O_z>bwFIs5#~7E^SLFaX5rU) z8}aIQbMg2m)A07B`S|YdRrqD@I!s%-1;cu`L#xJhj9&<+3cj?n!XM`%_r^C+@caZ6 zzB?I3-_1tJ^pz-GzD)}>OH#V-DWiCE8CKMuzyJiF(-%=6-iyG-b&ZV`;rO+H$Z!ed=T9hw3Xc`fLXc?3Ⓢ;^VJ17@Cw&%qh|j=e7wk$i?6 zzeNGT<9LfH&bSa>FWoN*d(tRMs@gMGQ{s#wnr?zW{`jNr^`j7`2yzFXwV9cF0^dIe znNz59EFN3FnO;4Gf}H8-<(FU9ZmoP*t}w699lU>D6VpJWMvZjOo-2G=X*^M3&({6# z{DxU8cc9EJEjDiuhZk-cg$J$}hRE=cO6h~WV+WHnFh_~HCqA8uhd%gIiL>?Cye|aa{2gPF{RvXAIVnd-eaJY{wxcJ|4vQrkbd03K@Gk zqP}<#flXYyLVK1HOXkk!2-a{`pxG=P=>Du$*RtGHs$N^%F9N zSJ|!cNKOI1+OQwXkEG#fVG)W0%237uoa&iaf&$cHd~zHAHa_(rPY2Mwv6d2L9iwaE z+U89#C_YZRK?=f&M0MRj=#ocYa#qeMsGLFLWhToOG-i6-m!HmHFb=l9-GBf6IPbjk z%6q0ffjv2Iz4exMUt^}^4qg|JPZ7kwdEboQ$WJ~CSMZwnTVCT+Pd%l_&3T&vnr@As zeDaC0#d=&88g!kz@4ox=IJc@$Q*MpS@Y!3+`D@sV25=zYA;Z2VfyF$=ec-oyhd01v&WQfjeI|S`}Q@qI_6Y& zYs}3r!s8$Pg)jbEUcr@-7Tvx@J-mAR`54*1qr>QnyAG*$ICnj!sLJWeEe9(JJ9`+} zamuSVVR*j|#unjJ!6$a61b*(l@1aPgSD(TvW&ciy`uY(BwRJxSA6+VQMm>g-Mb?Ho z{yyyX^AP#Y-A?PJod+NF&Zbo+`dpP%X8z_D587%Cp0nFuouM)YF@MP|&* zaQPdC*ymWItFF3AXH?7}J!yU8HM1v+ZWI(|bYF8+x#oQ`Q(~_XGhFt1P+;*r^ZdLH ziaH*Pq1NdN$G{rSgdVSru9tV;eK)SW@=BcndV?czMtGgPu0ewaX;H!{2l$@6p(ex` zT}D~JP>k{!IeRX=e)jzJ>C;CS2z<_b?i4_LFHB!76nTv-Bw2VdT{wDYd@oD~beVba z#TRwp7A_JCDxW7sAEQ?CI@t@x!iISO3s8=wWCdW&nl-w>Hh;tG zU_r`C16^<3&EIn>i!;8@*S?>Jm%g5_pD+2Yox2nM#|%XLdv6dv_zKvuEW4 zqc`S^S7K*Rg0V&AsY=w<`t3P&jH>4sx88ayzWCw`W3B2i|K&L2!Gj0ub$WgNW$;F`>r0d?#i=)=ey{li?q;Uz2MF}@5Gm1erbGr42}Wk&;S!x z;*1TsfBp5Bc6q0$aR$>8%P+cVvR*}}?V3tzA3?|YqK2FA=RROay z=E!i5H>?~^{O)_ar!tGFQ>Q88%G2HS;T{^viAHtZK$z)r{1r1HTX-yH)Yo5sy>2yQ zM!?YMYjnolBygKMs%vK_D!j+3OSa&)*M8QnhbDXm-P$z7XOE6Y z&vs2J!8ryt8kV==ezIg;e?K}%U!Le-F7d~xutF(cFF}PP- z1gWK}Fr+;jovI5(7xPb3@Y36aLkaB58RoSN>csJAwsua~yTkmHu7~ctT=)(ty7(LR z!cpuvx(}9-6mBNY+!|R%vkarab5yR|!h2#_%79mF^ku`QJJ{e%@x$_mf7?G7M=*`D zK1I>XMo>;c;I3hs*Yn_m59&rsHX`#F?qFHShEX;=QykL`)!x0+6}EQ9_sjR{wK&_K zo`(r5_u_CyzO``HX~P)cQF3HW4&tM{rBJ3>A{hk9V^=u4D8)vVZn+wGbBf8gXXG~GV_rdEnd#X%$E{ZnVHP6BK>8b^jLXL$u zuhqW5V!=!o8M@7~=Zu1k*KS^)`Tlure4k9C6mD!tpsOhx8~7f051w$UinGmo5^?w2 zzalX;+t^~IWy5&9ao2@PP_?d*CQ4Nq&2eS-{BJU5uiWX-g)uxd1f5$pz&ZUo;P!E6 z;F%lG#gjLjgDcPOjk8bdfB{`wiZDAxsji)srDmc)rB_B~^a<(`f>e5p`t*JTb@HIM zi`TmA2(pLXkFsqCjg2;xS#)H`u&ahUtc!YrKZrB#LvIGw5tyIb|Hc{IkL4gE@A6nI zkL>YOoUweN=Lge$kD)W~4SH;_(U0f$)aBn6rd6JYbq!bN;dQV_lJ1hu z8cJQkd#4!W^>RubPwN#v5odpINx-6ADabFfe*qN*T4YEN1~#g#!|KmFl8PN!Ij9pB zf@V<>IFy%u{{;Zr5m1r1D(4{rYSC_2(4Koij%( z@_B06xnrlQ$VO;)Iwt;;6&e|z!`EMag+KoIL%Tb%l4$lg&|rW5=_h>i)z`Y4*4?sr z@giM0K22o+yLauvi_gEHdr;^y)vsS)#Hh-lZ6&}NscETLx@4(72CuPQyS6yJe}AvL zeNPmsTbRB1=9{`F!!`rrvDkZb(@i(&`Kow)XYia%=d6@7@CPg16ilAHD-$z=p}#m= zg6uOxUJvhsE)g66%rNNQv=Z_9`2Hy}DA1TTSwPrlhP)0wPYN^k5_#Ht=1QCqz7L*@ z1qq)uGf);l6ejMTlP|>?MIv4N=+Exxcq~9Du-Ws;LYME})6AatLE%Y(#g=cm{kd3(IMAY9*UxkO>{{x%%R<0cLJ@o0& z6#ujBn5_zWjcp6u{BcmwNC9A-i{eNQ}d z9Emf}nkfx(3rnzWUz#fDm9Q$cAV4pwB4GP>nf0@z*n-01GQ@-gBVIj(=)fSN(B~!I-Qi*i=vSs-7 zmtXMK8*k!|-+tF(<79;uy=i|<#oyDXV~)nIC|_DTzttTxa6`+F?!4xG;ZR4@b0OKv*${KrAYE7 z)1ZOdXLO#xu|yOLY~ZjzACE_YKtV=-@t!?F&DgE?r=I)AMOULCXT~jyvwq z1(G8H%5yXFA`5mF($3(1tdu!|$7EsWO&aBI?5{;|&e|P^@Wbpi72Fvq#{PT7P+W0# zZ!M@yDBiyNdo`Z=WGW6MReqFe?@2}8 z5%0Y7j&3w`R(>&`W#ijpk3EJDKKKAnJn@8fFSn&CJyOo#`8Y0`jmPR|@OUiIlr{;LNPX~04uZ1mX*+|MQwEj;2MjcrF-ok%2_rO21RbpA0k!9WB05fvf z;zf(J%Nz~h3{?R+|AGtjm<+b&QjpPal%gaj+nP!6VetiUsp^CCCC;wJyiRx5$nROv zWJ?PbIE}2dtW;N|%-=_ZN9a~~M#|~iub*1_>Xv>pGoYJXM`gU$@A~NqT`MNMYD6(2 ztjMz)`Bqhtzvb3jF;E$(Ik%E|f1cc9bvRz<@dbQd_8A!8BL|E!Lvhx+;=5wO%)-R}o$-Dt6e)t4zMR3|F^}atvoDb7^2nej zY{X$toWI-Ii3Y>k|8LSiNXa-hFg;McgQ30J;Ot3XZ6DMj9246y<#3^A|t}^m28(`@w$i`mQ*C`SmL6<@bBwP3!mMe~68N^`~ z^yp&!oIA=%)+H^SSb3|jz!nlXoRamf`LHHswcm% ztrG38Tw^&+lmz{VNusdccsri{S1Hn z{yWyG$`#MY3N15)waWO|dl(!PjOHzx>#-ia`}9@<_6!VAi7hTZPF1FMD$$y)UB}pi zalwTb=u5Y7#kadNE?B{O52RT zXQ+Gs9jnxHC{pj9=VD6==#mq-{@zYoScG2jT@D_hp{rgYv(Q$ZcRM+Tk?zp&kMAh=lm5fx^4TR5>nG~OoEli87_%huveXv)hy28jAd`}!F z=%}}f(f2q;h^

33Qa^y@i#1+7E>R@6o=H;X2RD1_Mvr`rNrSI)e5>w?Fn8(tVK6 z*wMZD%B_*m!o9D_v{LAEwBEmapjW4e;EAZG#NND$J#SWwf2)RC?6nwd6v z&CGXf@1NCmK^0ZN@7T=BVdn`~|xYry5(VM1_aqs&o3_?Rzgmul5yxBM6SDuASvSHvyJp=e_Zs5ZI|XqP~3; zA%k7deP~jamW|vy{)dv8D*w03qn%+%$w2rMR~c)*h2BQY%gwye9zIt#c0TaH0~LDL zJaubiqawwIBRB?~)3eZB(%$oezolSgFer*9zE4MZGd~C;Y7C6VvY6$lEwl{^YQ78BoA}Ob;TSgtTVeg(6}EQ9d*^w*5@)M+rDEl-qt-}I z>c?&BN8^H?&CobD0u7?W(Y0Y5@`_54npcQYRfsUKM|xo~8bpP`qMqH7BWXC4O*ciW zFl!zafzeItqkCKo;#9)v5>pG2>f-j~se)hc1=J1=!8!HoBG~TX6Sh3hRu%lL%q%of z75=NQy#{^y_C;M)F=(hNb1ho7)GEFIfB`yDghzxUUsZaSEM9^ifA~SW@vtJt-~bdn zOI4!dHoElfP?fJ;s&dL_Q!6?epQ;!4$M1h&*^;HkKHGFfa;y4GOHI{+BRVF!JW=r_ zZ{D;?C)D33|As{i7ir_9(BiSTsdMbwwM*Xvh2F*u8c)`}+Iz zv~>NFe^bX>yCQLP#dXA zJr|13EVVydbTU-Ml&*&3F1}blKYq{W$MHbS7!p*4&J<@lF{^D{|7ZSuC4!FK6DzET z4iYo7Qg%V!he*3L<>7-lhhV|>Upxd#1hyHzny8m_S*6M3zZxAbtJ$v=i z_m-)I?ELxv;M;G$(buv{Wehwo-|cqwd*099y?d1SuC2$#*k=HasBU4#^!Wenod(ie7JXVQP&N$E5v14t!<-NM0{nMc{r_axum=h>t>ImUds(wnm#sRDFTmwzH#&?K}6#<&VB&7oB!UNludUJGGFj z`?QzTB8S@knvL6JfDwql|0O&;D7|PAx$(l&I0)Il$2opqMH{PN4b##+jYu?Yh=vH;2%^0OPhL#aYB1(t_lO!tDELl`7w~?TB-4J% zu=CBDA6H5$q!zWIj+N4iO7~h7<{PUdMePe4MP!SZfubDpiAxlX9~P*z2#`Pq#qHJ-jmF6`KSGWfsA(kSQ1L zhep0+@sg0TH#1A%rxe)ueu1ekD2FI+2+QTlm9_N+;hO89q~S?@?Uh%oML zp4(1Jq#v5^Ndk~ZUY{uQmWHPbCv)ClJZ_c%D({>s6&0-n; z!3_C_IWKFgy*+WhELpKu_GIoef?>FFuP9y)ZqNDJX35nnO8Rp*NTE(GLVKcfaP5`J0|7}kA5}}I%b%Nf$>LR8WWn#7 zD8dJbgEIoE?T{K-8P%4@!UF<<>U&brK(Zjnw2SG2ZA~4 zjAFHMn=IM9LrNI|pk%Tc^vyH!oV$!-M#e`!C^UoxD&J-!SbX%shw|*x&&cReqm1A_ z$0|#1VcrV`iRGbk@4EYLx#|D@Px@anKpG$4#0s-RF1t*wH|IXy2+fZ6 z&D^_due;7}c*A08ps|4PwKsFG^t-s9DT`ZdSx_dYb~x2qF;Fz8Pn~9!Q;v-VtPfQ9 z`t|C|jW^yTx0v^Ei#g|ImtSr>4BjE{;p6{(Y{L+Na(&kb(RltSA3Sn>&g)~}-yL_| zY0CI^n}7sGjrYOttU?jmwr|^Ry_G2RbP~AQufP5Vx$&kO&Aa_SxoYTDwsGTiM3~J< z(uOeYw!iIdi})a_(u(rB%a2Ydq0LzMA)|eD>|)uypy@&dOhP^Ug{`S(RGVt$j$q9p>l;a2dQ?7gR1Nn5u zkCI`OxH!8RrJ#Vkm;;ilmY4KbZk58l+lBUIgOL$;-}8!OemEnvBL`$AE+Gcv2y@6V zN_is;9FQ-VE5^8&Qy8x!8Pn*iiR6d{#)F*9cXQ)phK_}3^VNyW0OV;u`GxTj12OPD zaslhZGXISEZ6wfQ9vCuXH2DYF5!oAqq~Dl_G148%h%psUCW>p9quD1 z#HE}a*k^9Qdd>pYgSH}Lhtx(Ht5H_k)u|x$DwMF{hsReeDHk-WAuTJHk)PLZm-iR` zA)^Mmq*C+Mi_W*_;5SV3ojLo#g7UVE6CZs&z4Ir zy+qD9^GrF@2*&;TUu-_l%z7yleD@xwTW$ZWo@Yt-(@&R+`dwsQ#t3-`{lBgFO;)e| z)4puHIJ0NZvH~V=f)%7&K(C z{q05l`q^va?d#OJlezaZjL=oh-ZST;@^hbTcsrbD2_tfSiVEi(WXht~*=O6o`(Dsj zF7Dso3X}-htJkct7LVP#!o#&9VFh#Z+lvSEmy3+jce)Y&cn`c2J`n&B;ubFa-gZR1 z54@T)XU;O$*kuJy%Jp)iR1YvpE14mge@J=tIsZJ{`8j3GCx4+}5)O+3!}Tj2SILII zG8eB?=g#(duD|gHx&DS5?0w~hLgEJs8E$~I0Ot;(){^(6GnMFd3XeosmV}{ z!I20X#>z^~^qEnX)Xn|4X#$|dEbnq_74&PNwoB< z1k9N=%Z6C;H}CK4b9#qaW4Jez(w@C~*?V)&VPBv5hfbtyxw4X8EZx=%LLgr?%FU>^ zM%jcQC^aNR>D;Art~;%GVV)5R1o766g67n_Xiye(Y`D=;(nvu|1|k# z;8-n?Ie~LKeUTTQF*{|R^1^e*JOl>xFtq*dvxr209E0%)6uBs)7~{*xfw z?2z5}zABk-O%CnJIZ4G5V{|0Z6Go^=N&<2mNjee{woY4()L!Eqt8zQGp_ImRj!6Bz zBLhT|Sr|W7WAB-9C3An?AoG@OlyUP{$&97zWX4Y!@^i+v z;6l~rxM13Ra{>kq-iO(?@7^c#*KCn-%h$?RMoC*>l(g}`XUNxUHd)2Z9&fhfgTdq> zktkWJr1h>0z2<7U=DKTbNa8u?o-0j`Ki(=0%!6W$Gpx2NJeoL_AU!SpFhK}Gn3u-y zoxjV4JY;SV_|wcw=RCY%l3Zk(jWD|1yLZ|BgvF7P-cNWT%VXmvjg4>}(ZH<9)yL{2@92W;}3U!?E7uUXF-bJlVK zwPn+`-SW)))8w|{V`Z!BK9qt@Qia%)&m@&CA?ZdjD>|SX!dYl<6igqo>!Fdd_vLZs z@7VgRh|rvJxCpfyqE)WC>Z*u+8DkGZJIX~O`gmlgNCs1;+Yp@*3&>J_S%c$&XO4Az zr2mf;K99!_P-6Yynf&I&`@y)xoP07Vq60kq@WXPh(GT47>yQQg47BvCggjC<(cnEY z-GiheD1CU=Sl^9|*2tcbjKA3tmvUA(*(gfpKC{*}GoN_QW-VPW-!9IORqJ-hzP$(R z#{9D~sfFdFYGtKPnPO6McHp`cPY>*`zw;KhA zHPnLVM-fCUFOp3|<>7Ht5@L7d(5vKzn{Je$S6^-akC00xAbG^Xg^P^h7jD^0GUv`t z=;eFT(R=a!7MX@hM~pEROukD+$%|lB=6e*6?7R^i=e~{~R1l8IeRGeF?FQ?$cSZ&)FXo9D`*{gK@4x7S2fJlVZvqZwZ}Z@La+DDBfi188m3H z^fSuOX&pP-`A+-Ic?o0t{IhX3JodR~pOyFDeJ^5McV5Bs&}pD}(&))dM5j+ehgd@g zsE6D)iW&3Lu;@jKe|MlIS#Nk5!H1n&+? z2wArx4Jz@#yN%TDvCzdjsOALDAIS^py#H9>SV9~L`CySzype=D#%f5SJyW0X?2 zjrIJLAUk&LmAOCvVfwj4{b~Jb<)uN5a8KCEjLq`yl<+j&70j61w{we#2|!q+`hU}Z zkiLI=%_vieF*vZM=_{YkvWY`tRq&N#o8$(ud54mUr%CFAgQUp$F?!6l?Udd3jF8M{ z#)$0AeS)Fn8o7^-A%PU*NhBqQKA*X_J$v@dvB2&HV>##bgQ);0C9%Mm=FIiVDa`$f zRcS)TiX@A$WDdlV_1F#L5lR zjw>Y>wx}WfPN*S0>K|u2@R`4FkgY~3BWyAFo6v7VN?-7AquiMN#gbBNdi_Y?(p>e& zA6dc-F9=1AIb#=Gc%ju?Ve{rKl9932X!ha4Weoj5L=GQ-0`|qFrKMYi3_+81wYat) zSQ*xv?+MR~PWIMptK6h`6r4O5AMM#A+>3K%`)wW!&Q>`Km2rA{nzariJpZvWa-1>WlW9;gXyOTQHFSu5)oeL2N^RXSsb~i+{Wj7 zp&l`bnX)8|lyFBsrNU9r2p44@8uO=eD}2(qQ^tPRV{V;yy_7`3eWS4Z#mjRq)Telq zUqBc6X?aajg5dY~E+yXasieF>HA9 zoHLPNg#;#RH@dfNc7B~3yoxKyh@?gt+w;V`vgf(~g@ldVQ>1^F&|f)&K8xszNSOf{qX`42k4z+t zi_w-_AuKWbYwQT)%`S!OB)??PZVrgkszNA!1 zPnANP%PM&0#98Mw?bPfqWqwyYg1b4mPSsa-fnnyDA#`L3P3D8~!WlHO#;B}6Lgc1# zy9BhL;t;gRSFc{phWN1$VR+7***2S`8@v#N(%}Z(v^h3wLo|3!QYQ0BB|%xu+!xzC zqj<1S94gajOLJl zsm*cU6T0ctcQ-r(3L44~6K~LYMXyHO~*QMQL$>_Vyc2Bn3 z!jX4KkJjN{XU>1S5uDd-IAnAjFzhX9-2ZWDbn#O-{M;nXK&xCESee#`b+p$Lm{p0PhbNFpd2R<1(TfwDp z#}mvyQM^QHMI?3DASpVyM{Hq*<|Xd=*L$+}iT6Z|qMZ!tjY?Vw^)Mc7B6A z$ulw5C2HJ`OF64phVU?>0GM}(GDe)QRnW}e<%$)N_Khk^*JhQayV?4+sV&t?r_1CY z*2psxm&y|p7RwvoE;l!RKzcW>D(5t+BB$4>B;9LQl!j%B8%4|r%^^k4QP9ZdSjGrS zeqoIW;$M6*L7sc&Ss7(s%!vQKEMv!vmD#gqTY;MjIqtJ@)*=fOFkwvYkg!NEBd0i( z8+Rua41wjh6)SAU$d5n%*j6Iuz5Vj@&sL%M;Jx?l8e984R=b>l$B@Ocne&BZZ|Tw{ zHk6aelL%tbEC=zEeIuqUa@*LwxX;<4!)bOxEy5l`9qW~`kw%_1%1G?--n;Ku z#menM=L{$qtWEda@aN>IC!UlKKls2V1tBB>A)HKT2h2+h}5iH z*7i*fWM}*%BRpY9@wB3HVVB5jL;bLPt-LjH?qT(q`rfWtExDm@2PtBdv?4~RjFQ)_03n ztjWqexoP_@S-LXAC~1d&U$k&?Sa|2MRU72TILyxTr=F9n_CI#nd4u73tz zNl%s3+b@*VD|^P|F$H=Xd{(=fN<~xI@VO^UptTKO@OKTn8hq9~h-EmU4cs z-z5?e8Mn&-ap#v82xJh#N9oE=v#T}q^xn+p|!qoA?o*@^YaOUv5jWb@X& z^3%$#rnAkop0wholRubwKn`Rcl){oEC5sj^7fq7o8+ORA>vu@iVrkODC~3t_0S6Vm zpn}G^3k8ydMXC=K`5{ow7A%-AYu2u{f;Zau@MoWuCmwrDp85CF^0@h=ps;ddU1-wu zcw6{~2?eefnj2`C0m3lMXRX?`ZI(a0JG{vG^XAJ-FTN=Me(GQH-~asACYxYkYZL-r ze6+B%SV3s7TeqIA$UG5B>dQu9edXm5b}cU|(UF2mZ2PIh`>I@F%Q(~l)^AW>&N7`+ zC8Kb&4@Hdf9scZa^W6WnODVAS#k8r@Y=d;(yM5i}cV?a~XQ z-x?*O-h4}5d-XM|-10k=dW6mV2IY)0MfeyU0~22ft>9i62;7|&I2{SXd8E)WIv+?chWqg2pvABv3#j? zdE)xsQp}9;u|N`#7sq@pYc?FdCL#vH*-KZ-PrnD-GhvqLso@5$A6I0^hAqMDyCgGl zFKWGJ;||FPdCpMU2q$!d5?Q|VAuq(}iSum&6+Z|M{c6E7`**{d6^(*cBTS|+m$&!1 z+U4o{?#JKl^SNnrd`+oWJ@5LUW09nar6q;XMo%|NX2i4y2TJN){mq~gTN1IoBR-Qo zkG>szVTr=@?>3~-W$=iH88c>NEoSTo;|Gce(tfJR+A|a*4eU$6?}y=O8@M&1t8dX2S;k zKyJV*#yLrxhT)lH96TcmC+A`wGYTX60-=uoHL{`KyJ9?|{QV+3xDK8(o(Cn2(0dGs zoSVoH);;4n9fK$Jf#=Pwd=!^fP_ay!RIO0V2&}=-FhGeiYLzsoTvC>=*={<))iPnu zDw+4|1}R=NMJg0C8g0lUh5}ZpSW#KOeUE(p!)lqdbdAjXeWR2$N?aM!K(pqqq41gG zOPb?PiZI9^sIidq8E5pcDbo=Gn7f6xO!`U`k{w0>MX+mq$|=^mfU=ZGyn z2)JFlb(8b^o-fTpR`?C;H`vh2_uqTZE~bnH8(|2AF_t+Vu?UGLp4iI9+49UNB!uhX znOnYWd6?%Y&v4yhuq+bSiPE6xv<`+kF3R}Aeiupa-e*hQ&?3=1yP|UFYF@O=8>IZT6+*4jX zGi|VI*RD3P3L&|E-bn`1yqa%!wDF5aAt_UK%y_@>m(^iypK#4h+jiOO?AR5YC+0w%Y1O?|R2*H`wVmJ&!QBasYkWh)jwa|s4J{d{NpOfGM$vt)>>brpd?k4 zsAXv8N|cYXduaOyi{eT~4M5Hy@p^k6>`n&`K4p+HX(BCEFm0rLIKGpLD625L5W}qK zbUQ5A1n^Gf(#M32I6%ZMlint{U2apDiv2j;<{BS3LVxCsZe6s*x=X|t5i+}wK#J@J`jC^6Rta{t)8$4Z zRkh?2%R~1HM?$#q?GFqp^0tukGn4|AB1zYylhzfKIO_!kvCS~; z;(L1&PM7(fuMf+1#b#xSEl@LbDWAI~<9@3)YAR&mu4nlt#^;z-E|i<>x2toH;L?3; z`;a6b^pty5_sKO1lctf^AG`P}b}wMBvpRPjbU58OoQpQ z1gDTB^~prw&LkAv{P_t2LK^VmPrO`&4wov+n(Tk75^~xbPxrY(ulIfO8exBf5ZP=C zU)#swyJ;9*TCz;d(8Bmdi^D>N)BYU#WjguI2iy6&TM@IZ+$D@JCyg0#kbBP_I=;PD zXU2U?1rC;*<<7|8KTiZM1(7<|aeAyA7d1*%yiVqSBBgsA{cN^aZPHQefSc3MLT^62 z96LbBPKmpu5u-wFXtkM{{7~4bEU!GOy%ppoZW`Jr7IBxR5D>aA+Mb`yCaElFc))m2 zUIZj9#JuCZH?rqYLs3Nq4adZb)1+*AXE_~~IX9Hye{^7I`;+j32)t=9@dVce8#(aSe-#P5w~8b$@w7f z!<35R`aLWsWDqWad|bb=!E(}_e0%GQM;6UoDpX#1+9|CBmuTUn1D9+9pt&zJlF6rF z!3mPKcyt^}WL`XPUJ0uZ>nzykO|0x%c`IZ3L!RFHf5Myu_4n?G$ z)PEfMN}X@9N)o+f;P+xAD)@9V9_nv<@j$vhT6E#4KgjsNE=N!hD4iwzHzQUn-q2CY|dY4XV_|hm=!7^X}Y`7azAHnE}wrhHCSJ3+HLmJj(`!Pj_2yW zG=0H;?#B46GgoD{ugG;pD(VS1#g?SJt9dY-S*Qt)I6qS-M9N9;4H-l5W5ka%-EG0W z%GPeFBpBM=QPY(>;&P}W-^7^#Yk`Z@Ik`nOv(%QQ8?ip?Nes5*?u##)Q0#I!tmsmt zC_$R99&b|Bma|P0r0#n|2fFf!f5FO%(XrkB93~#%M-`=Mth`(0j%zY_yqTRx5UQ5f zsY}C)Vk(E2;7k!uczn^Qw8gU#T!O&L#%U?Lj~9MX8E-Rse^X@MFuh_OgTug#O7sCT zUplEATfgH>>`|Gd;(5n}%t!U4#zhk+EZyEfhE6r&J3V$*sa|cmfwSObjqQ4xw2d!) z5ntHmakRdcW&-&6?EXM28wdM8EMA}5s@GCn?M)?};rTh?!~n)SOA?hb-oeJ!6B7`e zp3PRshrmYf>%R$Sc1B{eMOb5*?OY$NsYY_`IfB9EDOc!)9bWI2PocNSM$~Iy4>r{d z@?Fs-Q=9(K?G~iByPwM5QGE7_Xr5RLFzr*ptjESoP&x?DD~zO#%<#t*h}RIYbimAA zW2uL^j&oCiFA`bF_al}gJzS?-u%2Nq7pgCPGb}7jfVC%*QB5QzLou1MFInL7+n-8;?TuKEV0wN@FYog<`tI&x8(mK-LNL0)sz#Ud<+rCNwfDg_qRCZ0 z;ldYcXhk79IXaOF0uGNPgYxBq3<7Slw$4cGgu1&kDI+Gx1xI?sixdaWr$ z(gTDzyD#tZBI%6lHkyKit#YZid-QC%r8u2bD+^Ih<`m-@^kzQ}49gpUwz~~Xm*vkt zsSc^_&IjaJjISCUyS?=?`tfIb?c23-o#)vai_zz^95+|}``JU+2fDbE_VrvImOl!K zE#GV>X53bEoJel(liK%cYxeh_;N$#4ZykO=S+G0wu~&Lu+DmanSzF1JPO1`WrcZj6 zo~xb+nh#U%dnFdDe#bID&sy{9p?|AoIUQ!QtWwW?3c;SM0jeqQ!rEz&!ani~{RtJv zh{b2?PirOW9ypI(-2rL@#?~hnV^yXfv#b|A7pt?}Pgj??uLa@!I(5;g>$5m*R1X3p zjdkCQBoXk=AI&YKRE3_4|JI?#uT4798ING&piShAtCQ@T z+drQjvR(9OGW)WUo!3!@LLV+9|UxxqF50v%~+`Q?ybPlK5O`m;v*pZE$qYb+EnwALuJgx5f5d`imKzXB;I`whV=U0Ywx`Hkwey@$zBduRES{Bx2}5) z>M}+IhHV~*6#cu$wevt24^b3wyjP!pUDBH%D~iEHv+QSLvx$W#IEuxy>{YDYX_wVd zs(jODTAO|;vH(!e(^E`WAirU!FYfc6pML0ZAXZO?aTh;7d^A;#?w2DSI=w1M2;Ld- z&@<0fyf@xOvUI^ai||LOPc$k}lCZfHx(2_v5w^`)3f(Sr9n0YXC;OJTvr?+d(8oQ3 z#f%A=#R9#cJ)i7z4v5gySo;$@|3hDg*(p&p!EF@O!9G*L6CznJvNX}fe1bH!C>Z)T zp$CLFreTL#HH!0murmAmnX=~(g1ro%RH2sy_&8SennMWNH7~BWQ>ReB4hA{C^Xl2V za=GkueE+S-!-1{f$L(h}Yt`F3fm^&2gMthSVGPqKoH>JB2;!|9VCfcibEifQY1E>H zK~3AcU;I2TsQBRFlzQD+oJVE{fx-04$ngv$D9TuMkxBnVnyV$0Nl-c|5yHS(sU>09 zxeb-2yYbeEBokFCK#+FQ4DX#=VZfTMZY^2#w?Aj;V_!V_+70xJK8DZEKHPo2ywg+b zk8Kogqa2gT^>O;li2BVSbUqHK0?aMLx2R0ipd|CPOIGkbn^^~`avS|nS${CCxn2h! zuQqA{;)v~8iOaX>%ZE0dd9yv?{sd;)`E)$N&lEz{{O(=YMHz;3WnJ#J8q;a@d9IRc zdQW;0liF?ljz(G4RVw^b_F2AmuSPP3%Q~*#WHA8(EZXE_DweeI9GA=&bzP)Z8`Ez0 z^)_U+R(7FhMNh*Ar6g~>5Bu)fDFL&cYN8C8+iB{Q2FY@GDLaIz#mWm<)YqTaRd=#) z`ZjhFp9-_zZqvLY8!eVUD|o$qnMZa-|1Lr$72nizk#!ccI^_2uQhJ;q9s0GYEN3H-vp!aPjLSE> z{G0wONIMww`9VjYmiSF45H9qPoyUXSKG=wH-O!~(~k&-SqVJd zM(#8!2{m6KPe7Q=U~LFtNH9_HDEyD5%J|Llx z(Bd-j@=f*KX6GI8EaXG?O@!o6J~3@|bcL`?$2w2L=vu`!NzZ(rTXIIj7KhN@wg>sk zzYHA>T>D7IZ<2V!x_65u?t6EBMA#ebbnD?y!8=V&6cQyl|18;|#R61{zbQf)hUNbS zfz72g9e?IM@=oOt-Yl(BE+?UxM77v1p5Olpp*jx$S$PpaZ>Yhm3}ze!nM(TqdRlk8 z(z6ma2^#KwSGg4#p5`U9LfM@M`L-&wu+R6K_I95X0VwWmLwHk6y|>0Uy&ZJ%78>K? z={pEvd@1duqeLM^cXsdYm#6N;*V%@?0&sMvVkyiI!J4Y34;zimPKHvvdD<)tHt9s; z``0dw78P#mkLNczZn_-zy&z?C8~5J1#VSJ$8{K^#8IEYmt5sYkA`xGI;gjZc8a56 zlA^Cx+g8PA@AX0YaAp=JV0_6WsGb6e(4ZOm;oJXSP0^M- ze3>grbZ-ssbY3<(R+UyuDRf(nD) zVN(TCa>XXbPR9nua>ts8-?k-)$cP{gU;$GeH;8=lWC)}bIMo)+KbSFe@uxmE6ki|{ zFT8)?(L!Fb;wc+lb{BVnHgitLM z7?4Y`?xpyJwf_i98>ZK5yHJ}-jupnsKgZA5HaRIZxSR^JV7I#9UFuu|nM%sIMFc$B z{FV=5S5iz4BFKZL+nak53X%wYU3re|mScvPH;VtPRu_H9(^gxa2kq-!V8crL2EGFM zPLCaHR_*Ms8E9h>(S$S(bHj1$*Oe9QbM9R7q9uLoujkPWHBIZyogp&i2&U&l-`j ziU-6pN(VLr+9GbVh50PVo#9{w5A#88-9=`WLA^-fKT%79dwev7@RgOD>dTX|p{HH= z$^T$=3#WltP~hZ$$8lb8W1_s#J{skbZjptfy=gC?qV-@2HRN|@h7dK#T%6K*34xT- z4vmf9|A7ksemFxCj0hBy63FN+mM0{QXNnNQX4GNAQU2EQR9^5-hrmXY-6{|jGnSvN zVr{amlF8DCepZ85X1%C0@z^EtnMm0FM4$fz_@tH07zVnoOvS@dHh*9_-G1u!ayzV3 zk-Y!Z9Zw85``S^S!tX7C-f@}!?JoRW z1wF~@pr=SAFXnjQ*#v*rpRENmv)B9Ea0V1V{5nl+pCLZksN{k0>lwE}h|4FpOT-`V zPV4w8o=PnGp`3|LxW4}eg%S(a!93cn z8w{}znV$^*;x#`L;9UHO5-IqS8&^&Ny|qdcxCkOH*Y7TV`g0>NpEzNZbX^1H6os*_QG6XcY&WD;y@F^z7|3pFQ1dxqI{UqVo!uL|b7X|~7>=JIMx$i+Ooo4@0g9gXpn+#53B7G!ywu7x#(l%Q1* z|Ba@^1XYI1LJGg|yC+SBa&!_w=sHYg*xR#_PU=TTmGwo|8~jYjy-o17-tMoj3a+eX zF);Iv5NL*l36cK5TT9txEG?O&Zov9Sg)tWFJt|GS8{uLJFt9!2WNFUM)uy)4?A!+i zcXTymr6X}uOH8WSRU5QbR;*+D9hnoUB_?mfmyl?(dUTrD+q^AQ8gb;97*i)oC!##Y z$H3LLqxqg5#cH9^s{O}W5N&dpAahndE~k22N;p(F3@#V}dfw{?cd7#epP#Aodg5uL zoz$%+fKLkVi=6YkBX!fwpgF2W?3KY+EPA*B#RJaQ|8o08F{;)zBX$FvBiW#Y4A zw1_H<=xiuzg(#K7<5Ji)V!co}Rg`fh?`Nqc z_3n?dJ)+OpRAttRr>63fL)e_x`)>*5+4Vu!{$d}c_h+8yxntL?njI+9b6r&Dhy4KZ z67z@tTD$=N?sqNI)^m$Z+xeH%Pt96__fs`?aGe}h>v+-MyFxowcA*Sk%u6+2G|o#co=X zd#zi5a`JavsY+V~=gXy;LIFAP9|M)s4h#lLw&cwW>CL1AszWilO6%X{4VxMv@0+PB zMCq1ZPZ9=`;k-V(kv=hr-2^r(-}v(wuy16L0|l-Ph$8G8_w;x7r|AvT_L*lH4u7zh z_-q@%fRT4R=gQHx#n~s9hT9S)>Z_j@$@eenOWclW$9XL?Q*L~1_RZ9(@+nOtv8JAV z^%WTxhOEWxJ%YL5cqrh znR-fY**t`qRwLzxRy_&L@@TQ&o~crLKTV^=cf!_HG3&FdOXTuws{+&EVq5Z$T6NKA zyl;91I$$o5QOSGW>mO)bneY_O*S{AUGx4T>|5U4KKh^Ch7SWh%Z-Za#q-+f($b}0? z$XED8)Dr`@Zy5s@!W>O-6vrC1h{zp_Q)LyVi{U!F0kU55nQ5a9bFrF08%GWg#&^MG zv|N7|t;2tNn3z6J;DKNP7^s1&s<1-KqOB$?T;a0uCJ2y_&X7wIcri}ac(0L$(&Ks> zE0H^ewyxn(3C+7cf!6EO*hVfC@#}mn#|e&nII}+{uK8~=Sx{)4#&Bh?2~@bIaVSgY z`UieYW8aEUJ)Zws!0+mzq(+9$5{UUxonDX9UhigK(g#RbRLyR~hT97#V3jD$iA``U z@|v$N-JPX{-4GXA?y_@Tz>Rc1EXik8%=)J{B>|?>%3m}YEzVxQ(r%<{9Bm`hQE@hk z+F*Kc*1Jy3=lGgmX6M;sayWM8q6Yw<&u_AcY-jAdk%hzSN2@QZR*-K+nfx|Z zrH(kZhFYqMRVxcUQH^iWn*mFyLj}(I3g&%_t^ZKS+15RlryLDO-6o9WLO>FR+rgfU041L1q2Ii ztg9gVqH4+Kdqb%2CP~sbP-pC|sO;$aB}2rJ-m15wgcuBehDAeY5q+--)|d+VD8IyB zr#jCsgeYx_Nb?PCJmt}%KoQ$`FWdxm_wz-N>qwUibEm(znvdsiFU4O37ir0f>{ zL=8(Ej=g8hT*X4pLfX;uSeJx=&xa@h>=@sf0DJqEBOlDdGx)0+)~#K4MorW0Pavyk z)kK*=;|S64+g|2&j7wLF{HsUpPG#b0fkc6*op+NPl+6t5QLF9^-tGs-D$dH3b)^^C zt9YBK{8m-Y%()4Z3adl~_NxpbA3Kpk@%9XpM38HN&|PF>K8{2-eO9l;(-?@Rmet!*Lu2W-HAaDwTG4;}PN zLbBqWX_ZIN#{qpXV`JCH>jvPt!0*~qpFgfMm0xw98<*KKI)&;5GT_(9{raMBj;)s^(PMakdRvXxYUfqT$&I8x z$D3Nsop`-a%7UQ9TA^yy;>h^yn{A!S!_8)4$LTEeC0l~FJz0hLYfCcZtH$oH=;nE% zG>plv(xxif3UsvOrWNLf1+ZoxLexlX`#jp85&0zJ3roy!wyxe_3~vbg9#0B!tj}q{ zOsmlCs+%Xj>LsqwE~6Hr2(|F!JGPkq%~(C5$Wl``jUVV+b|3UM?}O1#U}Ua zgF1P`){?;QhTtut&oJXsWOE*=rbtRgeVtHtAfb;h2Y*y1&)v7!#9V}xy~Y_@jbY7= zO|SY186n6EN-+FnR~6d72v}%Sz916G!$8Ok>BiH+hT2jWvurb}>21uRbwzb^^+lrv zcdGCe@E{dXFVJw%N~-?5gvy)-YW)`Q@~AFJYo0jQ<-OepK@>@TEw2)teNI%4h4-F3 zH{W84-+#6fP0Vf#xI%0HUGKG zZ@-D*{o)97`kWp&(h1`JGad5I5Svlt3m!_axkCfT3TMl8_)xPEJ_prghv$Bf<7|qQueq>ic~6YR3j@8w1IziN`!8DHFP0 zpXG1(pVDAjPD5DLdL3#k)32`4f7bg|#SkD#O}2=|M+1QHj@liT1=@Y~je5 zH2Vz&7H&ur;+8eup8wU!?sZ@$+W;&((u-Y70A0mL#zs{P$pb*gr@oug#^}Yk0bIFP zP^PDN67>=Zv}^5ul&joyG592ObA(VKYkU~7KB+C7~_u(Ba$)aVP zX^`O$H0`E`P!$<(O|}58YAT!A@ZzVaY(*3DuLVkZf}Tj>^p=4FU$W_<3qi7Pi`*|b z?-vyI$Jd}cNYa|943*Cnt(7$lzix}#7@i#%dh4jfOD;Ft(E=!dhA6?&Nn*POTnJyX zOVX3s&S;XSV%z0HkqPik33IN>s$7yfSVfuLuKQn}7T;gaF4kJ~u~eRjPSuNI#@ph^ z$xM5WOhBjSFz?D8eUu)-T%L*-%z&4rq}wazfcJj*bI6tX10ZoYAW*yg$i!37G{NRg zj$l*Dq*fRiCq6Gi?WvR0n8KzBJ}?H(PeM!Mps40xo@PzT#RhYdQqR4LC5r(~$Bp^J z`zK*PId%)fT7WK5sepCpV~cgTSXlFpKIBXU>^bB4cW-*D=;V?Ayiw<7Z*XYC-}1tC z&qn#jfc8m)%g8G24waUZRjTmVON}B`0vp}3Xm>{qrN22g3%OFV7PJi4$qEYp|8yV_>$na%Mq(z1 z)wqV10S(|-p9Fzm+sqb6tpc9*mjHA!JpSPb?0udu|Cv)p@a4|z@fDyZj{|^ji!<)! z`7as__W-ST=cu-)<#nHfu%5IGbe{yEK*oW>WLkr-?)BGg?uWCpjaEwcz`bvAw%%1x zgk56HX8WVOij89SW=0G>JYJA^7u8MMt^c!I#t|SH{g}FwU-T+TQxdc*y9l)n75g@vZ0(EbuuzX=) z`t>H8IZ>(CX8ZL{otNv{z_FW?m6KN>!%g4id)sn5!DYY5YO~SZwRX3%B;@={m`>vh z+k5^iim^;r!+aFImEC8}iK@m>LFM!#-R>juW zVc`B<;wD2h9BD~O2w^7?W*@I?)< z3B!GmH2?WUV;(c4wpgx|~zBw8MoS1EKq`6qM?J>lIMMC>#QRg$}6#2fM&!=P3$SywP zBRjQi!c}Nie+H~(STv*B0UOa1ufMSHBb$fEjaSFGsCyhMKVqwi_Z=63bhGdX>rJ^GU{1J0~zkd^hGG6z!As4B}6v~ z0o1g(Ro}BNO&2QcOMqpQ1kh8Ko&n1GS0-^rM`T>ax*oSXfXi*)Ms1QpJm6%rcc;~u&z3#VSSgL3z5j7?e`8lAG_jh0FVLsNLc~*DiWl9nrdezy?z4xu1 zVuQTv*}k2+Ki5>uWFJanDqvM4*eSzM-8~4yD3**PlmSpPV!k2d{4)V{Pb~UvtWDh7 zcmPVrw|y&`!QS6u`%EGRH#{DJP=Ixoyj;a8=VqR(mgQHj{A00ExzLe+IJr%{4JD47RAp$^Lp4Gl#>H+ znC27MO5Q6$z`a1ZEg@#t4+Thea9B>(nWX)J{+nrqqIZx|iHG^K5D;R)X>AlsBA&{V z&$=l=WuJV3xDD??s+@Z#~ULHUA zUbH*TP}?Mn%${E4cRu!dp>N>a2kWW7jn#e*_@|C0kC_TEt2lReoPL$d_1;4CC0V}x zn;4FJSU=iTj~ASshOn@Opsm6#PDVV;I2p*5?WeKw5*3 zwTrZ2fF~Z5r7@zprRB0eK!ry|A?BIM9YEpL8TtHnB=;)Isk{F5btgp$Du>StEklIr z2)_mJuU~qZUpmv@1}HM<#5~UAM?6LRaPku98PNb5y(?3efVIC6m;@fX+zi#~8&7lM zj!+~A|tNfMAuml^W`c%8XCF4!**b9lc%#WC#bH|VQ`2LP=dQy>XY1ddmHB<|DJkDgeIx&Cjd`{v z299#r*e(1Uy|t~4rW}hNm5o?Y%Mn^CR?>McN(aRptJs~_$D5Nzhe-q-&(31Ml~yM* zd_K>r=XC_lw^Tf^xgUSiJ-%v>2vcRi0dVYG4-&325?6tmBf(*Wah;Ki8bDgZW`!yc zm|DtP+lzHOz4$;yHPB*k(`qv}+;R;duohcRUmQeMkzDzTx@H`$g_@{Z^#}KU&`9M* zX>hp)?5BPoP9P&ISFcc=cfq1nY+_%@2D>%Ho9NXU2d_=vUJ&oMf-m3SVmWN%$M16U zTcy%#Kmlvyab9$bYhZR-di1K<=eJVaIauYN``*^RLf`?{-Y)Dt6_lY%1=4A?-s}&r zo{zVv0oZUiPC^NUnwVCXoOu9n&-HSQHgZS@XyjY_xGKNf^hz6x&a{De=1h))tX0EZ zJf_~npQ#_+u_$nnb%f;^&LBW|Y6X`Yhk?6Co&jfR@SQlcWBDxuttKkBwz2|r1BFR^ zYn{bJ+WgOIw@t{Zu>Sz1QWrHbR1E(~e2aaUXx7OOckdhR&N=P<3&jqx&xi^Agi8L@ zVH15T-S?=kBZZM4Smv)XgNGwro&fvZ<`Xnz#*^qoyw07|4SHEaVQ=PjcK%lC@hFdM zih{^@x%MD3f5B(&T$%?benas>jAiK?vtfa~P5ez!-Bw43K(ZtJQJ)OkUO!x_#6^GV z&0aT21l9uiRcJ%-G8(DfurpUbF(O~KfNu+a@bC*)*^Hu8EWXKff>xq3{a$4ni_!WG zi~husidww?LF-}d#f^U8^Ry;=DO{eoQ`z*ck4w?=v6krXlpbpf%ZpsAXthI7)gqxFa-0eOuIL!8ACXPjVT>T z!rz)8Qy?wS)t`i|yvsU^=<7p%Tn!}-g-SnP^rw zM`8y@b1BmH0IVoIt>?X^y6KhoT_-DYRr^#IP@9Peo(Ny5sVJLb-VWR4t3bpX&Bri_ z;z?tS55u{F5YZZp( zK`U4iPuQnW!5Be8##QVS>$HTgc$C|D3SHNBm0tXwA=ZVw6d$ZC*iFnTwsdM0yUMZ{ zQPO#w$@>?6n6-BfugCc3oxiY>$atQQ1t!Dv+PsF)rZrJZmcq3289v@&@V@3zIL*r7o#e_Pl<9`YSEp{{7= z7iaJ2X7#PTxtat;^nP1Wl-Wh*R)H|cN}gO2s>voEgAYx+`NVe+N$#B0y zKcNsSl=8myL>Sj>u$tC8?XEg>Ct?1>;B5i2+t8qBiQIF>Syr~19scd*Byz9@SSn%CetHnxE-;0dO90vK|Fws_l6&F!y7*s^zzPU3rgtI2t zi`h_`lobdvj-Ic!K!G4Hd`!f?TWBY^hPO}@)1|~Ktd2a0!qPkM?8cwGEvWc~*0=A3 zwu$ylR(nI`SEBa_-cRzRZDYcoU?vc}Q}&)&HSYY?e&FU>#6xmuHC~I2up8TMfr6|3 zvkTXbkjhQ{;He&M500J|*#%Q6rg_?B&QwJvOzX5ZLUKSD@IBhpMT2ba=LP2C)QA$K zsKBEKuY4X9o-74{VV&Nj-2nX00o|1@D6Cvv)Ah`=5MzlJi7?T50idg})7IQ<(3MO{ z@`v_xB>Wo|w*EZm^@nVviRfe!$39HhxNcs|CiDmA)(;Qic!qVN6#1VD62C8)sT(t= z@u7@mrVslmHSli9gnEw%PmSq@<1>s7exlq7r}h-2cCO58PJDsIM1)puVY4|39KS(IUesx z+qmGMP$=-pR6;V4LRQDU_el2K&C82|#8ph=k>=&Yq?fr417ud@1d0`I&NepCiV?gV6NEhn zKML;A=YKNsr}-%uatO3H<~6%V=)KW^hzBho_9cG}ht2);ZaLhGYE!~92^m3alFzKb zs~@#u+7TmPc@clRu9%M^0Zag`;+Mf7YwHLj)cO|PEEcO&p2$pp@JfEA2o;82*@+ZW0f+qu`Cu#{O`V#GJ<#({e~H`pYTa;sILDZb$N$`lV1)CJ_8A&x0X> znB4cxWXa<`U4|Gj_B+CW)AXEDwU_VH-GwJg$6;yK@!jTVL9EkMw#uIUY#P2?1PXyE zY7meMh(2l)@5_im8Bvwtr(M{7v9SU*iAV3GQ9aAwCwstBU!T7`KRQIKUAa0uILv*A z1Jo@%)=c?oje3X0Itt@3`7Ew9c4`_% zo7A-tmE2b#?zES+%ngle;DQpLq{~Sdj%yLmVn+%oy_*_fqN1WQ4r>o0Kyg?rJk1ng zG7h7E%z4!Qz4CN_1)Xl-nOR+HU)*1I!aE{vs}=BQ1|}n_cn19Xr>eSxlsGbM2%ll> zm#5G+vb%XZm}VC@*Rt!GW-k~g`gwMXyS#4P>>17SeW^6M-+hGw9N+|x`Id>yxaEG$ z%-c9g(#6;?W^y~_l6c*bY~yUFwFVmNb^H1J0&r-}a$P*TgZ_;_6hs_rVJmXL*K0nn zNnEtmeu@@Ug6&g3D%7AQlNv|9$4kcIzgq;t()sp9YdMzA^ALlrV7Hl4ZNYxMNHc+{ zjtgS`ghut)jbX1-^#}VV#N`M#0YRJX(}4b3zq@t5+y;1*-e=pE1>>#hgn0)dS zVG1`I5br*_p!7G$Wj5jkU9jk)*5{|c2;4#M7;nq%i_IfD za^9sHL%xS(RjZ5VStBxB0AqLpk%n&)dM76CYl!v?YLhwF=PHKY+_Oc3kzFLW>DZTXhO&W?nHoH z@gCk=$!D;N{Ka{7sEY(p_iSv`NBo~%fCj+k@$n%%BgT`J)^sJIXZ4%YHKK=`MOr2% z1w-#m*p7?VWbOZ|q7eK(y&FKYkWFVcCLdVa;`PoCHKkNaiD>OzSXdfE<>J2~I(QeNW`4X_fAh); zG;bZwl_dZ##0&$UJ^n6)EUth5{Hs2U0$2YD8`{$;+3Q^-3IV(Ly8o>KANO&~iem~x zZKtZ|$uiCB{dC~YtQ<=!JgK*^WCE!};6PXR&9WW4`+>6W3A)gevLMi;1jInG`u^8H zFaJg>+mvg0>DjAiL2sdVfUmP9nrh(>sh0BpE*#Tets4Q2vM{8jfQ}#&NBE&wA&cws z8~4U25F?684JiWQV0CijJ`y_St5iFopkLcHW7p+4!+c}7YM7}90MQ-;D6;>^jvO#P zFZcTvGx;JnZGS`R?x#yR8=ztPuNF58+|89$IRG_zzUVJGbqNr&CbPkFr>`p;FYkd9 z+&umh5Ob|?>cf%omaiUwqh4KQ84-N`GpD&!f`ap3A)V&EG)!`4rj3AQZ+4~Q+SjpS znH1L?{6UlZ%iYU0uZ=MvkY*W*#z8*8sU*KXUDE?Fa&|f`_Bz(JqhqvrjDY@#@~lKH zAg~rth>t_BKKf$N>FrVhu!4Z-yZ7TaWkw(>q4+Dupfb7tA_UCDn~{h|hD9CE(l^du zfxW}R4RF=}cb(eziv0nG)SFiQM@y2>ZLz0Q090&kQw(e>NQG=FefSM=h=0%1WlWCJ zUXW`lpnN}UP^`-0ZP3z^G2d`Z+W~jrLe2tSEF*h1uU0lPk`YJO6XlW8KL%|`g-pR>E@a{}6mgK1oBZ0t;~SY547 zqorz%e)~*rcHe3{pc#t-v_1YK8a+TORzM0#mQ+pj=E1zxOZ-1$1SR+RaU|O%@`N^B zpPu|Jf^eHwp3;U#O(1mZQ4U!hV+F{xc_*u!4S31sFcxFadYJKvYl#@+^cr8nZuU^v z>9$U&qTmgM;X6a!_9hXZS-9c{pK+=G_g#Z9rg(rj;1FoC$7a&yY`Vqp&YI%ClF!25 z0dfebe<-v3zTY9NR?R%V&@S z0!;h$V$xoB;Oh&Qg}~w28j^-$xo-29Z;9R`BwmYu9=%_+2V%ii-hHdyXVVi&Z5ttB z|9h;x?GynKdk+Jy(KyS#JjDyNI-`FM=NwEiW(Ru!fpA{Uvkw+eTM1{J!020r20M#Na4Gm?P}_69%ZDHKY>lCIemcG>x6b3%dT)!Vc^jN;8ZOg zM^DYZ3Ao*-sXc2YPig|L`ZuXlIkT!NknD@cvz`@5o%UpQl{7p$Y175tGLW-k`zKK) zwUSx5BXH`uXI`&9{r{RqN*+T3(u!N>c4~hIsQ*6$taFv*{@(!o(l}33wL0a6X$X}{|++v zuT0!O={Ip(D^0MRqGB4rWgnZIL^;vKrXjWFYob_AZY==6E&#)|i~Vo7sO2dxr*-1R zMymw-wXYg2_DhtNn$_dB_QArv0rb(XJc?rUs>PqwD>W5>X@;ppHZ}Rj4|N$CnR-+- zG*W&LJQ^_uFfOLhKL`K)Z+20M@g&wPS$-R7rrh&voe7Ze8}c{>y`blYOF@y&gm~xN zadg0ZHajGt1)|}{!yD^n)?$B0_59z=pXqpF(xB(%`{+>G1YwEhP8k&#douMss4 G`Tqcr1ZG+Q diff --git a/docs/screenshot-integrations.png b/docs/screenshot-integrations.png new file mode 100644 index 0000000000000000000000000000000000000000..7b5297340e72c01cb20f856db41623e4f2def610 GIT binary patch literal 121315 zcmcG#byQnl^e}c=LaR(SCx}g`!6@Bb~5TRuU^$8VLw=)z1(BEDI0jcdWF~b-y3Pj^{4f#SI;FX z@-jMprbl^bAv%BHLr|yFS}$J_%Wvl27UJ?2LRVK~SMQq;cD|qE;z9vfKUr5-^Tuu_ zpMy@umjfjvTG~(~+@$Uoazv`u6D~VoD-cOBF)>s3&FjoS?_gabW?>Lb(zEs*^xK`< z``;K7c5$zR1q;|zA273^$ZRYdx$#40a67n}!h6|mYf<=mxAzw)QOPHJDed&zqL(FG znu2$nWF^Slyo9ysFRvgU!dsK~KGw(r*=cvFsi~^c7%tm893=k{&glm2>~Oq%`w)n_ zN6%vB@IvH(kr!X=j$L2M^9lWfMz@PHkL-H33~4jKjDH(A!4C~VMoEwhyu{VSs%aWo*Dvv36;)bewR6nK^s8V zwBqqOH}GcB%qY-H%pGYUmPn_wze#v$%$s;Q@CSrwj(orSe#D?Y0CtUBBg} zDD_ygx>8&l*^)~R6HItg)f3 zn@nhX%QvmTDxOSG!?LeEuc5740rztvt*cQ9vBLXLuRoJ;r9R#)Yu;b&GcGPJ)_(fK z&e1FG^^Y!3+`D$muzVZF1X-g#N~(|o1#d`U@(N>|Y1 zk#Njg5Txy>_OqmmjEu}&f_`LVB){Ect}2g$!k`Bp@=%_1a3^stU|L{ORPLk`;gTcl z{N<60=!ozLbYjyJhE@x{v>ky$n{a)4y3@#=%t(T;hoW#P!%l{J5!#KoV?fs~;^C?D z$ka62CB<=(u#>~!xSa+wZ0l)5AuM8Mpr(dKBAbN6=9yt%j%r_dU4~k{6C25$qe;y} z^>-~@vTNdUP7U=7WyM-0&{l&bkF@-_^|4M8)eO_e8`oUUMK+peLmMT_u}rRuHHp*F z-bn17+A*H2JubuBU=^YmGA_@Sbz8s~MoX7cho-bg}j(x0CYnu${P_LSl|U%s%vu&Uck zHXh>XdSIw9Y^b%gfv@^zopvH7PXxRVbi^)(8IP~)Bzu#9W8&Ti!!ZPmQdL>kdbSL&A`%3$vX}|Wbh!DQDZtRAEJ>ce!%*? z#Q#Oam77Da@+ue`7y2vc@qVT&I2gQNl2vTc7xiQo06Om{^q5r_x$DY+)eO<39agqY zK36dK30AdVjDCY!|H$)64Ss$U+4B52s{JP*(~RljJ_h;jW}V^C`E%42Q3|7vdWyh} zMno=_L$WtyxsIy^6I3DZbTNijX1-^_&!#AWaY4ysuJc(NYa0}$h1g`F8* z%z$^$^PLe;_hyAkngnU+rUeY<4_s2qkq!=ZT4`=wXq(g2D+7`LOsc3pI7JKi(X&L`9m?U5SmfUhlgy^#V2o?Xpq8oB?iG?92- z+SurjyG4RhGe~ONwY^0e@@(6TqAVBnj4;}$$qfXrK|z=C#(tZTH*F}$cvZ!9)TnJV z@Y5qX0Q=)JRer$5$Oj}+KQn-+th(GLKoroPXxicbE2Iqdy zg*9ZS(Q4KjCTs+yE7mAE4~rrOZKvlYP>M9(F)=dsb!B|lkv3B3CU_Ecu&N1Y{;8$A z1-`6;$sap1k96Cz&2V#!_|MF!nfexVPQ^4&444|^9%h*7k}RFQ`zto5ay^_fp}qTU zaPVx7-zQoY&Dd*a$0;SHD=GE-`GLhV!Ed=;Z0PwNnDy0Cqk|1@`y>3+mHnIUli6>) z-ND=Q=aB{X;IVRpx{8_;;mnmY9K%MZH>+*l0)wady80gnLV9DKwxRC)7QOO0626_~ zMx^4!amfun$4kSL9gsy&5vPUj=XYLblivy4C=;|0*YhbCaOvkSzFk@ummG%dD+0Y} z1Pwzcf!0ZqK4rS$uX;o;;M4rz3mt=6YlGpl=A$FSnHnpM{aHJ`MZ41=VWMjB?y>&I zBmoD@z467H4%ByqI~b@wO)e(Z1%WrIG^=en^@d`hCV6*9&rhe%PG&+8D!c&Sek(7; zaT8*`9|ILQxK*r@=A>^c3WN}{Xw-*)GY&W(xB=7p|0`@vGGTS;xYoTpZdzvtG#3Qj ztKdmLxIu7&BzDuacU}~vDZJwBu^{*Z_a`K5^CT;LTjrwKR z(|uW>bazntr61S2-!|@HENZ32T06L8w>OTM!}@${;9?DOYQTr>$Am9+Yu@HOTdIEC zYJT>hOnUgYqV6>QsylGw57P>9Qpsz|7lna7=+ntP$Ie`nbdNwvN5vksnI->pF`%|6 zfG-}y-e0UAIC$894YWSpYok~exc85`_za4k&*g=^Wi5j*Ir518`xahK+W98@Rp8}> z$g$l_P;u0^(GBk9vJVAHV}GUA8_mlA&42Dp!JZbOYRg*2lb4NleMDkK>R+tCJW#6y z)X>sruXA(M%i^6m{-%yz#dxG^i`}SoPLZJ=CpG6R%hiI>Y&wb=98+HQ=MgE{=Z0IO2fidPb?>*L+^2 zHt80NCl?qrr99o5)7EV0Wg+r=-F1IZ=7F5}cm`rX2eZ7mj@n}c2~ID^Y1eZQBF8Ri z{-m<)q;{Pe_|ES_l*H!a&nPTe3Tgs4;!OOCHp{*7v>5KWCN@tnf3AC|7M0m29ju=@-~?Es)J4N#vFD^O5d1VZoG7|nZXnR+tRB&s50!py ze+=kBBb7Hvl*dL9o~9)^ertvG=(|1fVzy3l#IVoISYf_OUHWS*^an;L6t9#y1nJW3 zzB3yl={eZQlQzauzO(*fKy{w!l-T9FP1I5y_+?Htk#j9Xec`4*^H*(f+T2W_=w9uL zWsXz-W+w1)*D`cmiJ|CYGPI@}+uSTu3(?xN>B*FP+>&n(|AjMc3}MQ(8~X5ZW$5?O zb@L{f(INRD0yG`c9EdoE@74?zBpS3M!Oxx$w3O%Y0OceTCegu{@RU*R1YG@L3)e5I zD}jo+Y0bas;Suilyfj>H+A$b6_@f}G@W0V$*Y>+g(ACs7fAEv+^U|o?z*!GQfsShX zOR)2Kw{iJ2rK;=xZ`T_i5^iJb_>R7n)wYkR!>0iN%Ls7OV&Q4B#PMOC{4QQ(bufC`r|53N%AGGf= zzV&VvPTnHl0j^B*tqo41--%-0(m4-=?<~7^bqEfIeeG@DWS%S2gLVmve*N6;L(_yC zltr%K+9W>W@g`p+-8lY*pEb+aig8u>1-{$}qp9nR97fUEKRHqkvbHka%6fd)rF1ra z@C=hgD?OJ-Jy}VPnkw_IZljL8wJN`xg-l_&af|F8^efAA0xi$1`gen-ppN3Nf&Bae z&e(G#b&npN?Jb^rOK!l#uC=2iUq%6rvib}x-T<+ixZN?E#Kru>9DZ;db&ca?>BJTv ze(2@*X6l7Z$VPS{iR{S2P{kQIX)KFNWL`+=&69{ssd}EJRSTbR%bQ9rU}9g(!CZwS zoTZ4(kLT(Pvo{K)uWvO|(ky~9D>KX<}DsNnDwWUB+wgmJ4WRY$r(=-YnW_I;{LGY)NXk>1e@4 zl)THzxe}MTS*j~g@tkhLabGP6y2<$Uue02|I>qNDi(~Qon<%=)D|VZVGI0)1ZaiLE zuv6=*??$E}xPujJ7rSpO%G%qBp5lXHPZ%^p8H3{X>bE9;>BEqF6?{jq7z{>LjV zR&(=O#>NcNPq%5r1H}RuH&rGQMmI}k1(MPZmpf^i$w(;o%bht9ue8@ZPVp#(o$BdZ zAlbg8@09Jl_XOJYL9?MK829t(7*)D2vE&hJ`n2Vkm3HRrYf7#;v-l^!MVl;@aP;ZO zhu1E{bLelSdG>x|qYa`d0(Z$^-#&gp_<(fM&wW?iqaUeo@v~ zgP%L{P?CH-mYtg-ukJ4^$pq1wU3(=~@6MB?pQ;P<+-6i*28f=NDT;WgVdfg0eRvY% z&!1m&ym;!<1onCpS9ZBP&LpZe2?c&N?~m;PIXi(*g*$=p45D}%`bfgTcO&-=sY|VS zL?xxexe3@dRz0@+i{H|67mvw)eI=1vPf`3AyzeJ3S3Q;+iFcmy4;Po3wa-Wx`c{E6{js(FFZu5cui)P*Gdr9*4UfSbJBpeO zq2*@>9E8J7c7#>U1clYD@6p`*$+k0DD}Z<75PJU{tRT?#nE`M1u@a19apUTY)1Ib5 z!_;=tT2@?ohdS{YD(xztU!@>OR(~tN+t2nR?+pT%Y{RIDSuf~0u=s)Z2qN*COi@Qw z_4f|>W=~72_bIm$wP*bmf9!&D@#AeDKL1r&)o!DNOA}mO8XXk}o}Qa)$)St7D}}hp z^2ET;v@PZRj2d|lD+scU|1O=dVCFt7Y4OQ^#tqoU}A=AbP(g%0~ZSs12a-lWiGLl+s(tkP$D z(fD8=HzE;SHH(08J|!|n4?73je#2_nL`Q{{c!Xm+4&{v%8q_8n{d%|6v8w(%Gf{h8 zH*mBlNvgNB<0Uj%lA@#|I$y9#6ogNY-PB?GNUk8#f6eZG=ILy-`03*36VXg#l}L^2 zH%SBDQ8A!!%U$(ZR?rYX*XQ^ImhSoHN)fm#b^lM`YZaC9p`Ix?miYZN@29zfiwlnD zI)|mudMW#40!PGk6I6sOmk|G-T|uzJ=$GdQ$H13LilIyAzBmG>sIc{dT*nu{3(@hm697C`9o+iTK}QTdvH<%#I``QaHC_D}B6NT;$mst-a3Bc9S$> z9_e5ydFMQmp{@y{i?mWQ;>itXX=}8m@or}-aIfSXF=9N#7FI2azn>}f*?C{SU!a15 zbQ<;E(xDxbbP4LyGkWty5hTaTMrqHva%uLa5_$}RY*PlnJIp6_~*4yg}^B% zd+^cP^@p96#$O+xG5z+NrizbGZ68;;jOx?T6DSNd@iFbMfB(5yjv>$uFmXFq@c2mp zB-1#$9D_=ajq!9^6Izbxa+TEmQnPoS5ROEF?ya9c>-W*TPqCHPm9%>6b2=?&*x39B8pR+)ngO^^E^XM6S zm(=qUi|~?C5QBL=JR8Xv&pfrKtVeA0jJYMQPn-p2^vnVrP^Xq>n#`mZ9Zx%scpsQh z*EqJ^ZZCCdN$W}@^LuL= zyu`B#S*Ba-0)n|1^alF+<^|+Xjy|%O$TwCD=v}nJYNcvlIN>Y71xH6o1$fLG%0+&& zrvB#&i|y`dIJ^vP@0u!o>J<{3=3?pzx*k5Et$|5R=^&?dNfp+m8aeFv(zo9x-e!>z z3@LxW^U4n1SVNQp+s1Rp2rh%;X}GY}Y!-?gv|r+n8ta_#2;Z1#)Mv`)ssJnY?N#`K zoQsg92-P1O{Y1neE%;`UbRx*+?mPIhba1$1?I|G&bjeK8?Li74H;_rpg>mjrVUh@=YvDe$8N;XSu0l+Ze8hDPs;j)@3wp&`eA| ze^H`mgX_FvrBH8~N?I+P922w-1tC(YYf`UvLX`eZO1SWyvNr?v^3bqEQE7XDNff6( zRUP-EC0WMi9HsW(UPJD4NNq-m%xIA@Mn6NMR%mp!kmsLA%Ulj^+F zX$(|;(?0u?qE?u!)ptawB)^ zKL$vAH-sLex)D3 zJ>m4XxKJ=tL5lfxgkFN9_HK<86LR$990ohMCL#1z+Io9c#Le=V$A+ni*JojO{@no$ z$(2W zi0VSC(rqzIpPqZEfZZF1j#;W!_&yvdez>T(;w4pT53`Gh6uE~EVmf)SBxviU+Mcah zdDhWXNe(fqi=CO;ixxT+RdHH=J=GBF@uU>aaI(!Z<45=T9U^}=uh@veob8Oi(W2CkEksXM5S zG2*M5dwh#aznM|muYcuZ-WOD@KBu`Qm*iUi=^og3##Ui=u} z^WCBA2p@bB@+y&&%FQUGB*`qv#)kC54}h&yVYud-{}*j{U{QB!A@yD*RARS|D)l3s|XTSXBCpeJtzV)+Ky9nD_P7b}+S_ z$LOuQ|F;dbzWT@aBqVhvy5`pRXRkwvRu;(#dbgJ?xO0QdFs@r=V5MY@lUdr5*bP+8 z$BYcDU3`ilOsqlqA_``sP$mt8|FeRQ@jl}>h8 z7bTSK#_@3@!zp<2`7IXPZ!SSTn0GEJU7AwYxtse_y;h^XZA z*Y^`4Gm7B@nTFK#OJZ!{6&BOJ-qa@Zo90YEjZr?4GzKu!SI&JU>j>#dV2O$h%rOk} zjmwI!@3t>U|Ai+XK_}ay2NPpl>AKsagshYKrsD_r#LI)E{jh-5N6=FzB}4Rlg5{ZS zZfjszBu-D@zryI9`@^cH`5|FHc%qQE=ryN97_8ent(4qCa1N0q|MAkGgEQ$tyg}Jm z^40h6GF(Zyt-fFCoickuF<8;q{#8v$6YUCHY}QNIo3O&XQD z2i(SxOAsJOPHhSv3lz_nP?XXRJaefDUIEAWh7aW&kMKBY%HiChS9eugRot zu^MLrp&g+a-n)goK_HWB*JESg;De%-I7~)sy1VyjK^Nd~fz(Bi9|znrY{rjZw3FPwhcuw=3}Gi@I9nDQP$Zc@;n*X`(6U4bW+ee_@!4&XC}Tm7h(5ZA=Iev zsyJJgs&ATZf^nalIopaTm+&_^Hb~1wij3?qfkLQ}Aqr9Uuz;@Hza{s>CuyIrGQHCmu#D0tHFzv^QR1iI*3%WmNsZAOwj!}P&hIqI+1bqDl zZQBU@#>GxWYvCFHYXS4FyAKw%?vV0!qm_dN<2A!(;Yt=_tye}CC-e4|OzgiejyQM9 zXCS47ht-q1F_$)7zs%gC(2?5}k;fA(^`3uI!Ciq9wFM7?4=i`fMEdQQJW`M2fB{U8 z5t}T-l>IlxOTTQ^TqU#tXpKJyQLtX>R*1EzQ8C`Rd(0tPtG;VXG?c6R!%BAe(b@M6 zG%Pe|98jz&WXFIOGEOkFOtzL0D#+(|X3(fP_fq{LtxrYW<7<1k_^f7F zzETYbgkkfL85b&eJ=|jQLwt6;{%sh~<|}s3z<8?_q$n0(VV6)LV#Pa^bW(0(&e_r| zuquj-a`3Ii-CzH9ThiR%M0MJl?zUpHrIWVP1G8?-`ooxoLXs`QA zMk2bn8ndBKkMj^et675{fwGIu_z67|dXCPBio)g=%iQHTk!9==E3|>qW8TLbBNBsY zIEX?TL5(iU%6mqTfVYAjmN=yt&m&KdZpN9cCn|z#IT;2h;vR@WJy2_RO)*azq)oVQrJf@N%O(jruG=7_kl=l|MCv^&19u)|3rlXRMRtd zkF0Ry)8Ug^oL}uq^*j&Yb>s7LXs|Pi*P*})Ic$4rRUX0KHcvuMJLYM^eZzl*_lVL2 zoXI$DSXqmIk7zj;eQx~jyv-!$k)@-8O=Ck#REjJ5%`UFq^$1eB2E48vu!>)G>%s`_ zJON;KE90Zfh6vJr+U#I7U&N!od1=@$*GGKoZvIiZDM|0YuvU|&oK>Wh)Od$&M1;UX z>Iqtq=!?RwyF>Xp`C?5N1jkNA^$Vh6!7pX=jG`9({RXwGh9m8E6pmSucZrE z?V7hF)hCh(!Hy(W-3yo|6TE~?G(FtmyZK^(ikkys7h8ctE&UoB2V9eyN{Xz@*|?F`uo-IRXZ zde%)D_?(q4Fo%SXjgp~5eD5%QPZBp>^vuaZMG%x_9MHMQ5+JI|^wRm`)*~bMM^dY^ z#i$s675o_w3pNWCWwsx9&E>bq-F5Wc!$LwQ!Ytsa;n_%`l+qn`Lzfy<)o~N&QAl6o zaqkd5iuto)5MXY3tMwptfbXa;&O5(BphR`>Od{czu@C)cJDGctKU0BJv8&S!AcxB? zd|p1oLp#Nj3;Da_X2XO|SSWiQ2I*GDHnx}CqmYv^RLtf53E-O6Fi{o@&B9_#3gVG( z+TgNGWmKXn4KUkk~kGz!pf&LJ0RpIYEEnfE_^(OIT>TK(N zhskr_Qka^sY5i#cmXR>z!ck3m0-7oldVVGYO&?Vh2 zH_3|bqD875yaRQk8y=<=>`e|rIu(K@q4V}9QuZ_Ns+wC#Iea}+$j41k1!0_sRZA(D zf7ok2RL1CMfDZ&sHoPimQ98j!@K70(CNR$hgQT61Ze)y_%?pVoO7NR&#fx7f z+%@>|=`;Pt6cfoTq%V6hp;%dW=6^;%UOae07sov>otrW^+=Rd2B!6vF_#*6R$eo57 z9-lZYL)TBq&rBZX_Gh|CaWUxA^TT6P#RDrZnbsSMH=1AU^UZ3wa5A|KJ_(Q_>dvGv z-{0;OB~5rP*bg|HTd0Zti)G2PM+|q|TyX3`AcwIy%xgc1M6%W1>9fs`H)xBdrF>?n z*ZN2r@uQ`Kj)=-{eFg1fxz-fl>%y<#OOgc`*-OXJb`f^C;RpTgfjj-y^v?a3XZHg2 zvA5MHp;N$3wAJ_2mOYRA$yI7F{*mJVB-1K(T%T6_J{`q5qUd8;=B~WZxX3Qi+O+wU z>x796-Kd05jMGC@cxQx3Tl#g%MoSWn&>Q=lh-pI6Gv(7fNVH(c_sAZ%GbZtJ5u2s0 zfphXOF{1Rm80`3iZZv#Vrhk+#z1=DW&M`JZOj@t_mIL(dNZs9TG{zJV72bugr}6tW zf_(Wyey-_6i-C@5OAURdimCM27e>6AxplnJR1Bn6DX~38$1tS=z6kYtY||-aTrZxE ztJLST1qhZP9W~JG>Kf_d)et_$T9-L&@6PH0sVkad0@z2gO}t`Ljp9v@jLdsa{hkb< zSgUQyNYlhTaR}a7dHk@uu&UiRxu0p{b$AXb9^F9tHS^nIQ9x~f?Hxa!SQrM(Y`f2D zH2(?(KO?%$m2vZgPZ)I123aESm3D7Qiv(P7uH#HT8k#pAL)<|0v zl4e`|+$^OOLf3ru5VnovBkxQHpd3{?p+^F(jKYkyGCzrYy8xlk+y=9vbChht$Yx4}N05s3s@d zsZpZIaI9?qU>fj*28QSP?qrafrM8_m%ei#y+fqt33DMM|U?R$Ry3#4DIy6qY;8Nhj z1d*Kzk@eIenjm?X)+hA4vvB7h-a`c7!ZjWF^2eGP4mm&7N#VVu1)Lv^cvL zBqTI|m7+!7AWla=8z#6j*|yVKR+Xt|=5MeDMmU3?a_n|*zPhGJxOPln9oT;}wLM&KHSh{MjaE*M#f zY)A6N<?ZawDa#>dND0Yi|Pf11*FvYIPewvQK%i`~pqzpSE@9mEWOCX->#PxPy# zg+-|fJj#CCScg;vz1PbueB0h47d~Qj(&N~OZdVZ4NX?6aGTLG8CHlzJD|3rSFO<@q zZN-Z-RHVH+WvLD@*^s^cSWM!8F~eNs!Vg3a^su*Q+DZBwOvkn5{q6ZxtLD_?S`ILb5;UVfQ&)0Dlw!OCE;Xs%+4`L*EIy2Ef! zTO3OoZ*6yEk5R5;sZ?qEx~`UdeR%Hwqlttus8xuD8Ri=Vb&cRyqeTrUS07RtMn~1Im40!yb=LG9Z)09*=X-05^UpkUIoiC8t zudZ!>G-$g4;Wg9^7gH2x+wk?Vtb#xnrN;JyFJ-%H7$;d6v|#;{PB@K(IK$tSaf84V0aO3$##nQ$Jqw?V`A`URCp;#HlbUr4KOE+Z|bEZyARZfrobfn?#*aPa#5 zrnwYv)BEAeO75PWZZ|||v1nvoH*3`>p%iL|_Vr{u6ZgSrRe%ai_R z2jxi|#gvZWmZtoD)OBKooWG}%mfM5*4XVTRIr?mNBpgTihp`-8IX)@crl0${7JWVd z#)M1%!M@p2i+Qgb+a}E&H*t7CVU-lOsrWF;H1Z}b_4U&Z9Zz9vkO z5#zG4_}*t`N=~1}hSi2~KLbfO<`ZCVe~3hej(>}*u=wh{#A){)SgyvKc#Ey~LjpN- zkUk%Y0OKmW=sfN)M(2$a2Rqk>xLp>xq}wCo4{nfR zh>&}*5)VSgpI`-@)kQt90@ojo=2vQp-aCtM!JORWzU9P*YO&qQDLr)Ryd&b_UA&esG*T050l|DB{#Hs^L$(+)s=bxV z$-1KW4;xbyie>Yrr6G(=bW;n77}%tl_*I`QDhtIY-`&3eKm*J|yJ4@!yGWGbpI&2c zZmjZZexn4Lo}9HFyrQQRO=U@gE6oCCY85V%3w1L>LyeJPl6B{gZ;o5tlkNdK>ok%a zldk&YfB_sevrq@1ZUwv9AaH5aIOt;}gg&9o9e<-vs5?!@?&>X+IR8yl_WBzAX{3P9 ztD@LU_o1|*oA`jNnC4Rv{SJzlw$5VH#mwe)LQ~Z=sEZs&a~o|_csm3K1j zm$b>L1|o63n`v9Cq8i1lUZ~ovtbOOd83KTf8qL0+Us*{s_M}Cc_Yun9%VeWn z?!5mY8aTE6yeK9oWUIQ8*%q8nOS4LlG8@ z)4M7{pB?iSmBaJbW~3KSdEfR#hX+?E192O^D|vV--5h{&mmElP5c4h{n??TizL*%oXvhr=xRF5|dQAq=1RNfud=ip!*x=_lU_Iy7xUtAayX#gT~>r)kHj z2L4%M$>jVaI_kPDBZz!Z$e5lG=f0Njlbuh6lURR@BI?ySOFms1i+U#h`tvtqnYv!G z_+r|vmZm6WYz%Ec^WD~1;S1gXrZ?96^Ro*U7P~IPXA`hz(irUz9=B8G%y7rW+Cj>fz+M7aM+Bzltv ztd%rw4yc>c3eEr$q+Q;qe|df7mtl}C+&huUwF@P7<#lmTAM|tM@!LG}kVo;xI~7=Y zouD7XQg1)IG~OY$sZwt}t2H3|&PViTwEZuin9}K@H@$(Pn?vNd#96Tx5+xe~N&_XdEThCz2oduZ`Jq;K_bj?M%#`C+Vpw;r(q_Hfcb{ z=y5dJXAV1i-*)YYO?r~Er{dE{EGnie`vK9J!XRu|^n97G~g?n@#9GHcII8I90Nrdbf-rT$2o z(HJFe{S_nKpb@brTlLWX`=_d@=*@#|j#v~2NuBAZ)OvnctW6xUDNSsy#STgW0Y+2~ ze%4eOCG1OF+`k{X=U?=rMcG(63#xx=tp&qPj5cUqTJtjtOgmcDxNBnZ>q*k(q5%yk zFWGjIbf&HP*Tw83zn(|_Vjh%{sVsIfNA20|s2zjKEBQ#oJOl@EVkEKOh7v`rU~C?`{Wi*at-5=h1|Jz44~y^b$D!A(VVGIi zYF|%c%R;?!bI1{HcQqeAd~oWha5?z6n&5MJsAeP%}16@ z10Fr<`d~8g8mCPic6-U10({CxIf-X&cG;n$vW|j*++~UMqY(}W_ zMK9bW@zs(_Swt)yD*i;fiXAn3!;D=(eEMmR2&cdWg&vnqRbP1sfG1BC?r>&&&ds>= zPdid1zmn{2uz$O7I_?~!V2Bn;Y82VSP!2|7iZteIU+G(3`uu7c`{beR7o2w=6p*Hd z)~(!pltj0^;0LHVJqwur6Lpa#_3H$@(kzT>CTgE;vGKYx#Y*B*&_gUh z5yu#0Jgja9+3>g6Bk63r9k%u30ajQAMy{O$QLU|;H00(sFObV!!|a3Xu#+o+$z-;@ zyw3CcXX{^V0OWOAG)nSyY7WUgDBs1SZ{Jb>dRuny7&wXuKx*b6dyUumJlR$|Z07F! zZ=q)r9z8$HwKd$YHW{=o3151OH|nd7zv?V2xIkw4VoSdGSUp_T<&k&eMsUH?2E*BiI9zc zppf>~I#OIYdhYeg&P56c+kdpZQl?9Aapcn`21uihirmMe{pG-$LmJ0LOD8D)?2NgF zIrMY4l~1{k;Gpw0n(G<^o597GJK%Rbc6|rb72nd@pJZaMF(Y0hCyKS%vkpdee~!qu z+`#K6v+Ui6$LL#i;~!zrNz&~B2>S8piM~Dpk*>-V=;)g7R>>bwYx?ePKUbSeG%5|# zE3zsrtdZ+PI2PlRua5+$s|fbfC7&g_uPJ+zYU9&&|3mD@yo;a;!+;W97SsD6A5!-N zw_*bOuIu`4-k5k$`%xhoCEG>Eyp-SJNB`>LFNUvYsnL{>WwGBY*|XzCHLV9Nmpa^R zw*E_s(&GYxzyOVWAzat1p%NTePVyQ!i0Z4O?QgVEAv+^facB-aF?o-?I?++?En{;t zX+FfJn`2T!iPO_~as=_48}x=C8*1ujjO_HxMUrDVXls&30N7d9Tf+%ZKK`{Q+0 zz~KHa|NehS!N_s!s&-?;Zq)7RXqv_fRvbmyt~R%Fe#?Uqmi+W!bwwKwFaFoF)XDE> z)i?XSFLRZHp&hqj!8|o6jqMLxpMB)jSI#x1;pJT8fRTs$F`iw<%S7JNRLt}ki7 zI!Z%~9Wkml?@ay92Xw3HX{wrcJ~+7WONB3>a`@R!X0!RO_?PA3pInqx*2BQhtMGDw z1|M?iyR6C&mWf@~)0UKRDz`R_p0s9x=aD$nBvJw%1?>`8v*WtP$7WkR zS;#%$6}u6A(tCNJ1Ai7!l7I%SHOGkGRE-q`LX8LdEOFXewc_n#zpjgpvoQZ$;V;8W zh=n(`oQZuaRj5dzt zcUjvTNH*45=5O)tTjV{dZc1eEDMPNhuieVK1Nka3o4%*SK+GdRa5^r$hyv$F4j|?Tvm{B;A^118;X7r^yuKWPK{9+aqz%s)>-7F>g z9$~2BAz!ECkhPn#^oxe?86(n)bMkT&vPV`YLY#AWR#9c;Bl;cun`%XrnVo37yXaO~7iXLM*qF3PUD>Y$!NK(+FDj|d}?TtpN zSe@Ni$RkDsmEd-j`?mE>`JzX!3QOj1U;Z1fG`AqWg94Rb1lC-J_353D_eb5z0t}A>YB3eHW_(LcJML#3f8>=nRU2H|UZnkUQ@f~tfs-UV zh(hN^w2OW6mOL)lexy_UB!lA!hAor^(=Dg&DW6$WL@a11n$ta%=JcAcEVwtWQ!+@A zfjIiF{XO$=v}UHPRsSaNMQ*$9kyi>kMEEL?UFGAL&vbk}W>H$$-M7o%lvZ{7ZMeDW zH+x(=^#}iWN)A8u620L6qV6rDvh3RRUj<1?>6Vi2?(XjH?vfUe?(UXSP-&#Q8$qO7 zLb^NdJ$csquJP_Q{$qbzWAFXZamyX^x~>`Lc^tn3i=Ax%dizt$#_;~*>IXviXzYzJRR=W}tP3UK#s9+vOC^xjOQI%G#h zL3)HtC!=aD)<&f6vC}`>+nUaX=?+hWMB2KGSKfEWQ{mE1B65rRpTPc{`xD}A$_~r6 zNyN;~jLm|!1cgE#(uGV|-Fd$G@Sunna~gEI^IpU_pgV?HnaFz5k}pPVPx}+7ql@DH zagTu2{gr7OXy#16#>@P6H+c~4w%`+1E;?HoQ_fFTemXClMv~8J&gn(C&7+9}S1BBI zH|O&6#KG}1ZUf|zT?4-BmCz>oqfP^UX5U}#qCh8!VBv0|W>m&{hLA?2hub;B_H7XI zQ%&Lav#$+xz5Rl+(L7IFnP4WK+>bstec9=g*Kyv;VtY#O+&0JUrT6z<9k0IWqaK&2 z?32JKgyGc>AH0{>t?16acUtPmd3Y1~?C7y=f5QAo)9&yW$umB>uIW~JqtUd&f%joV zJ0I^|gKxBUKCYZbs*m~@+-jOAG>bh{wFT@#9rC>*mNinmvx;eL1Qa8R?#Tz9T<_h@ zE{2`baCDjS*xZE6G2;o1Ll)O1UsyUU`mZ3M9%J<+1frF`NHU}UXv%4xSXLu)h&!l;6Q<)5mtR&Q~U^4^TLU?KG`DsU}>ZUQmC4N~_T zM1B+aQ{!1BavHH@xEY)f2bM&xjrl-_9J7Z}0zJ$zsr=?`$qHRpQaH6QFY$mR$CVH>Ahm6Sj%skmL$)|fyWj1VF zgDBkn)hXJxL#ef}LJ0hNtzQ@?a189jrs#NmkeT%w$IFqHkuSt@9s&>EJsQL+`?O4( zdBK@MQY>nyM}aMgVkDws>(+37Bda6!R9xT*O#X`-;wnY1;MXrXE&oFc*O|?L(SX@2*im zJX3iJ%ih&WOa9Q3p4Aa`h40p|25W%7&VavUzqzPGSX(?*2o(~^lN?WQNYzHW?|~tM zP39jIf~%s@9G=CFe$NgsZfG&_IiP;;XaE;#65Vsr-O9`w=DK?TY5=mmXu|5}Gfya9 zoZb~Y!9G#YN;=Rsk-;+p-_8o%O4}7(e7S>SjrA0x4@&tUt>xB}TcwRxzg)GrZBqKwLKd~S6#^PWZ%he9ed5UeK zE?5KDrq4gy!>Rx1bKR}ka-9%BO*?dbYcCc<4^+5Kg-nM znjq~hLD&`5AJ_d?ALBEkn(Uw6##GK4`T^Jhe+NWRH_!J83&QnEDhnMW-bn*>PJwIH zamrK2iz&GfSgAGoGnVc`uV=21(DKC15dET{zON73iEtS=TDx^s7LXC6$`RO@7;@jB5sEX1~YF23D61b4bImnzFJr+%iHRP=qUw-^{i0NUeO>7ub*dhKNf<;&)@i z!=(9@2h|VB>}A(w52{cqMnkGeZ^UXWX-6kyY{!r9DueuCmA0F&Os`>7Zt!m^=<4g1 zCAM*JAN&*>#;a5NEdOI9?gdn|pb}DPU^sj{CM-R9Gt0?KL)1Gubl!p9A+IuAijVEm zF>#Ww=}f{}nR*HQ+Th1}g1g>gc&(OpmbEr;w3@|;+$~j|@SumtYZNSXVMGSzNMFm( z2d?k5e%pKF#1S?fmNfmOD6_CdaQePckH4HTxp+KYPBlLEbq?&+fM!lO#rE| zW`MZ2L#x8I-`PB5Xfhxs(b1qKghTP;End+NV3&A_AJp~u#Iwz`FYs#W0%(=zPjo9PSx=B~ ziEaS(COOyh20bD&`n-Z<-febQtIjHQ)|fg zQXOj&SPa+~n#grcM6>wJ(7D(@4dr)tQEigUQ2-oI_<8~%swyc_>QaLU-sg{~f{6uU z9kuy^W|#YmQn09ke3t9Njo`~7afEFu)36LI+Ex0y9`St9`g^?k?Pt!*AM@U61d9au?MLR?I80V*r{1|G*FR0muKbZ#ZtM&RBuzo02+n}L8$=QW_a*=#1!30}WH6f~ zwVv$}^bcOgPQxAe-`RW_oRmUE@}=+HB=SUJnV8~YLO0K@^Z1JH4@XiLAV2$3zh^|U zRUwDH4{< z#SD_RJ(jgFe!b2gl=IqY%&8-`rspRcVLLQ5>;ksmzPl1v<3LfR~PgqBUwCRW$nCKABS`@7>anZEBQIL8TpzjB;J5rdl% zW41ntzN!9Jx)tv#C>(C=d*{}4!zZI%=Yvi`zT+i5Ms7$>!abk!attS@nwl9J8nw#ya^7rWaTg6MGjGfq{|x@hBrEOu5_dMrC1T}G*s{`5aO z+>iU6?WUJVHbaL!@mS@fyQs_PhMc4*WbzttI?!Uc;NAy3@r;M@R_L}ZU~&(sju4Kp zG;=vFQLtH#*ZcJe-fpttnY<#L=o|}#LrVA)C$MmU@u3s8n_yRt&#eEu>piP3q3Qdp z10K%rNt2ogkfNV|R_;UI$kbdaXTOxlwalefntjGcH+) zcYKA5EXLskJC5i+eblH*88JHQ;IX*4c)>F`+%5|}r>C97aiwR6^MUzBNKU4iuw zGjk^Lylw%Uze{CBx!;^`qiVX08&^AGKjqqrzhyZ8VK-Z|J79p27)Pa$WiK#7;`h)b zpQ+Dj!^iHh0MKcFKkyCcR}GKE;-~$a=NGBjX#-zU*qgYA!XxrQ;4)xhxKKil4i|!g zyYId~gY6WlHS^uTN?htBa;+)Pz+;zzOFuwEoO~I1_GD8{UrYz`&6pXVv}e?QLs)7B}N@Da7EoBRCthS zWmIF>JDilVSsna)$>m-lbx)1kb<^7W&zR?k_xaL^f9BE0rvpXiowu%c3 z{{5RL(PSmcUaEUWQ*PMDFW+Y<8SaZ9SkkLyZd`T?OZL7hisKo_v&u82`o4W<+GW^7 z>_c({3)c- z>GVGe!UKUZ*&&IM(8+|E*Yk)v-N+-+uVa!Q2d>^C$X$HnfXD5yG$SwpTP8W8!MFo@ zV&kL-wVJfpYpqa10Q%oQF>(g_3n2JyNb}S3-}zpyl%-$n{qDIUCx4${CJ_|$NhJ7A z(_Y{2(qgK`lL>)y!<3zSw#w}|*ZeTcJt;fgF4T;|3k5z882t4LepVO|)G~fp72sNe zzP~>1NH@o;t_3zdI#{o~#KFi-=#e^P-_`%FG6?z_Nd&sqe(H%bN5BlN)EE(r%W~^|WtiNCcq4 z+~LcD{|~peS9p%H&Bmn(8UpO?+6U0!t`>zh32FFlgRxz_XZ^}Z$!4-96N~XTLLOFy zNmr0Wz0h3i?Ya5Z@NZ3F=Ly`Mu`CW2(~@ZP4+7;ou_kBVptnQKjMH}6EQhDde#r^A z>0<;H7NB#K>op|h3AD)VjOTt=j?^IJ{uwZSIB7rKSm0fc)h6zhnMSWeoBTzd<9;+H zl}=OVSj$y5s_D2{vaX(^yE6Q7c=vpK44E9ld>Fj$bJ6V0ug}wMAy(eM0hRyIJi*g{>-y>6)JK3XRQdk|>G8i{lmF{L$^ZS2@xXlv z79eh$-(OR6-cQNYfTsqcOF88>G&YtBjJoom#v=&9mzb}1K6?TBwv?0FwU)7jF1_eU zIXQ%`^9~Km=KV2vPXgkthLtfye45V&)!m;3!ONee;)z}V#BvQk+@3$`-5LvBq4K{Q z0dMDcty5>67_eWnkDyg~LaUZ{?9dwY8ZyzW32mq+`|EVD^@?|1lPb~pq9!@hZS zrg!9MLD$3CVt-WQo956jB_X0&yG8t+Io$&^s5qo&|Gdpqain}6zfONV$uY!YKr%nN zka{={I_NK8TOu_-(;rm<+pC4hz2}(-8BXMpQBE+<{RgQ9EVabc0Qr?EFg}<_epv4q zMAYH5+I|K$wYsE@?{`%pX;C$ofFc$NAecPQ&JjQaLEcQ&GE4;cE}dt}7a*2ZAmK6# z=LI}@)6mkM9aldlz+bHZ=mGUJeF?y2Z)O2}?GK?|G?8d0gHiiZd(|&RJ_UeuA$g`| z+S853#R=j7vMUi>@AMegD&hO4?JT=uomxu|zP&I&FdD&c?g>Y2$a_)$j2`&zygyx* z{3t+0HQ;!Rc5uVgdJ@!nz2bYelOHf#WzzGYM>{-Y@uxr5b%4lo5Rekf;o{zV1AM^9 zvmxad4>AUP6IK2PRq!t%h~Xlt-mjIB(b2kz^w0^}4|Ali2+NVd(XP5Y9JCr6$`dRn z@(v-t!~dKC!ksTrUiw#O;?MF{ShAi!qd@^Dw7P(TzR`HO!bI0z5c1&m3`Q&goH`F9 zu!9UINvq$EAr@$6|4uR2eJm`8Q1S4Zv9YuHM(2j#W{TpT@0Hs1YVqV zo*XrNLDOcEoT%X{V5m6c{}MwLpy=!C^B}tJb2DpmtDNNLl0cwPy>l}*X#9YwVCeR# zFK$M67q-mJao7*ptj*S_-##lg5#}fQ*%ahsPi}=%JkqLY%+WqU#YQ(HaMP6SA0BzEy6xF zYjmTI9H{JgvJEAOMFdBe(@F+%O3FT;0`Jw=*z_lrvF&jY7A>Gorw}J_rJdbgs#LXJ zwM7<6s97=|F|(rA;!(Z?JgJ=Dr>h4BQ5~ysYzPJf-B*>0{j4cAZpm3s-|(&%P0mG( z-;&Mu6pSa9+FwJ(^`0(!uFhk_=^}&WjmgHw<_ZQD-|z%njA8K9Fa-m`2ymPX_74Fs zE3)f)D9%DR1iV;Z&Oy<8y&AB3)Z?=9Tw=f3h<2cqkQ)alP1?)-*@nzRP?Y;EiL>K4tILeq~2J7S;~Z5@UjP zel!8Cw5UbSz8QeZ(s}x+&pnsES^)y>4Dikt+#wI=@#W%`?RU1>)rEeJQ5)~FF5na? zWmZT5IyvGZ>=dDTljCd;Od^(g%%{{ZVBf+Ph}F$vA9_U9FmyH+Fy4Y+7aYZYhUWdn zpvB{fW1Tej7kpHrz7CqxGt`y;YR=5{Y=~-6xfGNy^p9H46`OWHXAgTrYgOpefNPv9 zumgZRYPwcN0gsoO)42gpwk{SwZ71{*OMD zG&ikagwNAbQtWX?>TG_sJ_A~3&EMVU-g7Wb`*tHxdCh*;QESev&2#u7pQ+lsLg8FF z@aGiY;+P}!qd|j74{W8%=?c)1E!m0Z&Zv5t`o#iPFKK* zwPb(k^?+GEcphpma`Z7~Jtie^<~=v;TRaYReHD{W5AT#`NA3x@os9?Gc;&uN^zjhy zY~#znyU3E)X(qZhMJ$-wH(Lhe!GqIro*lwtFt4|PnXo2b#_GXvekV=(wRfavw}8`9 z<0Rvvnc$+XT;1-({CMt)Ha3IqrT~}}+JJ{kchtKatPCpvwOaX}FK*=YHrBzkt={@9 zfpL-{YoeP;GQ>M}uG(Fvv`=PWT(IA`&RrZJK3n^lf4KMEoc?L+x2E^B$NnOSKwldj z9qo`u9{Yv06#px%PSta0A#0YDbF;+$f*od6cG?Roaxk124xM%J`~ z`?lKD@%(`M@8GuiUSBh6t6|y2gc`IS8qXG zf=M1Dk-+5}XN zx~X~C7b*l42Z+i^|5Ee`PGis;{#hW_r;f<7!=YxP&D0D%(C4ez$ejD9sI6Z6ETK@H zs%{Mqg>{3zE}~^*z3pmY5~&bVbK|-+g2$i@fNbEDk$+*w)J#y{%8dD6|b|0rD^VPKiqO z@h&5+Tz_k5$uu}FKXjy^kNj)sziCz0Bg3wXwwjLF#ptHxhy?s5P^Rh9h(9-~GrJJ@DP|ZHM)X_W79IS(@Lq*H@>cm_@EYJ#Fw}_tzFE zkTYSaqpVkiuM#p({q6qf^&Ve?hoD|Dc-0U9Gd1hC^B*<- zIY>bS&|2yYegTK{tmeuwc(iT@P80E3rc)r|;^A#|JnPD?1rlDmKUQP79 zFVt`cSgobFt(%IH_JHs}>mD1mM-3V3AFY4QcTXi8L?;rg{nDUu)kSSwq` z%yZBMiPL6!&`64{m?5-(BHhID`fEk=Dx(^^@1@&%iZ=^d@nlWATPQ!IEB8Aah(krr z5J!9P6>>+QkWx$`ultP)^LCZm=c?%V#-qte4t#h%t>&k7k6W2N_b~ND^}K zY&&rTl4Avwq2havsh>haRoty5Y$ppt0~`+2FPqRDg(F^GuRZ1Zie*eK$Wp#;d^lxi z4ccki$+Lr;-_4%M5a67$$UZ$#{Pkevd{{}ij8VV&EUs5>GPLg$=Mq5b_h8R_}5C+CAU z7840s;t|pkm-n*6PsMD`u{12)957VH7cr8uLJCs}_m5DIn_kC`m9BMH+TRze$J>ZF zQ7=`gY4xmnZI2V<|Gtj)Tn-FV^t85SUwoyS=_l;adt%=J6(^b-bW>Us_w}a}jPfxw z97-J)RIcbZD3@DvM%cfdJ>ny<3=h4#&C8ca0mH4<=A4MAh=|vslwIxy-dm|285&`{ z?|N!hK}Xb)jeDKwD1?v>+&f-KAO*mF@r`)Li=%WqbK8&L6tQxZ{^?V<_2<-3K_`8_ zN2I@IBB2vL{_$5$G*M#D%X(auf$8CVn8UL(o=$}}WO$R~?vy!@WzmJ6Rv=EvFzRxF z%;5|5Z)DhM@_7C?t_A)tx|9Dpi9zu3Md2@ZJi`vGoYC%T6FhO#{Okf>#-R@{X-$(V z`iU5tayuhUeGPJnO`LdS{foF`%z(AICL>Sni6@#D#`g;OIMZK!&QKn;uceQ( zN3uYEKo1kl=_4K=J}c1#%*VNzo&TI*z)US?GyQSx#Th}{Pxp5D+@4!-(iG`u5Lzsl z)eD#5l*sdV@=&jw`R#i<-6+_3cotXLp_Za)oavAf@f$}o*#=P7omc~jj#5peb-dmI zML|OEgZuGFRq>I-KsldQ&3jnRE>d9r)sp&oHsTO4zSN;M{EWp(H zIE#|MP-P<4k^$9+(zlnBe-3bo%KJKB&-9mis%qpjmBGv?-YoNp~xh|;3}E8TgQY1*ThNzKGdtJ2Wm&X?ZXVK9Mo1rYL< zwvoq&FrnBiN2!;`b!goj*6V*!o)zBGKfF1tdPJ%|JP_kG_j22d0qn;4Rhe%ov;7IQ z>54;4Nb5w}N+G&A`v?sJ9WW@@3PhGjLfLRl#82&~{RCTk&BpE?r9P8QN1YBvfK)!^ z=6o#KxM1Uwp`7 zm*?B@cy;docsc8#N9D)6%Yof8(ncg_aUgKy2YMSTfKbuIK5>2x&i^$7ZdZ|Y3RMr- zV=hBxz*Rn$c}9v)z%q9$Y9LkR#=SV3(w95oC1}lFA>@2|g1Yh+JGsi91huweNjWE~ zP#q;K|MO)G+FPqSwqy|wkO;$3_#F2Vzg>GH+@S~y*Bu>}{u7=M!ZaKg!IzJ|px^_a z_O5;rR9ycnPNWRMID_pstl!pBRi3|h3{u&KLL~NoN0Pje!aazxLHsKxE2MZAH2Eb< zFb!-8YoVj4W$XSscO-?oTbK-h^BRacLrfkszXo>&;%0<&$&wPJt6f zkrXSE;2%Zi20ZdS*8I}K-v3`Ep8W3@sQ!Djq6+^X%59=BFLzG89`Zqw4NA}!&2tz{ z+kb@7uzSl2{70yaD2R(G2R$=^kP*3m!f2jjbp9PibFRws@(Z4_%0@7s({s?z&2xUw zzp9jpXPQ3+o#`bcB%V0HtpE*DlkUEQoY+uyQ#zrRs|?nf%v+R7*@)@QzXe!s1M-tXU`Iy%&(T8f?3 z33x+ZO-)VU$cufS?tUa+il?eF>}RrW^Eur=uu=t5_CN@0f#mGC|u`d5u%C z-Sqi05)nql6x*ksFP@Y!vFlmt^JNi)I!{Qh4BBGbaeBM;DyKH#CEm->0(zbdaQ#Gx zO{py;|F~L7|IgH=kYOuD!`H?7jBc!CJ)NwPIhLXlWrUkVpl&fVq=oW7Z!u>OJjMB? ziA(&pnvDUZ6R8t}{}{;+Q&&;-@qgYM%=w`hu3K5Ma}h{@`uNwVh+0OWpB9FzNx_@yY-bNB@~MHLW(rJeWB7)C$e2>%sA zC7%3c8iYpuD|YGs#XP2e&rW%uR$VEg9Tgp2^68WCD6b7@4hLXJsL+lC!MrX8g0iHv zw0ANx?N=c8>&^4r5ldkz*m9OQc{X09Q)43sSZjqhrMaoJR>%kR6DzIp0SaTn`)qw< z2S17|{*IEWZZKR`%Fuf_u}I%F@E9ctls$29KzjTft7Px(is6m@UL560D}6tFb+i+f*JwJ0(tP)|>VY8rlb^nj!$bn3G)Xb| zvMI);V8Ayb>b2j!TWNJy<$toE$vX@5igKka>j6T99psI&Z^z%>C0XBoUwLwR*XYG2 zi>P9;nKlqnv9RksRizonJ~*D}qtm3y#iq4qzA1Yc^Pw!&F|Cfj7E@YMH?g?nCT+Ty zOh$$=k;08Wu50qUBpalvzLZ-AQ7)cPLF#a{fCW7`&Q3`s}kQ3o*km`=unx z{d)7kg|YdXcVE)%hn2Vn-Xdo}XZ~@GjYQ0Ayd)pL3;^OAzt4>{mz^@zOy@z#iDsm8 zf(3DZzI)dE@Flo*>bgRHJ1O7{(zz((tV7d3V&X`zqiRKY&S-rmcK!{8>??g%eJrDT zi2Z~mt3Wz=rsKM*^_XwV`c&uCtDN*jTz5u~X*r82ol!wcHoX5^u~?DD)XIZ{#f%j} zf#rGB?J5h~ck1&18V-T)pIrUogQf_rVa(<|L-J6{&ybi7zQ+cW7t(qKNVz`Z6mj0v9 zXB>ZKm;=h8CA#4$zqmHIbu+ zzqKv1JKRQBfAi?nkaGQ^*QxQBOT)yZTJ*DnqFqbLib8C>c@9RieWANf2`}NT%Kt}= z=4l`A^n3nJtuhTx`MMS2#G8)aQ5+fb%KUt}3^XkUWJ!~q4IMQKDlc&a%4GVc3MOrL zm+VX4bCQ)i^!%P`e&V&!2xzNg4}OPX5zlY{H$eTXw{lvUyd3>jX!Cs zG)+25Dr&|NUCFTe_Ai}y8x0;=`Bh?ySRR&=CV7kEyhrKB*P*iy$BD2yJglGg>G6JA zgdO~LsW_~Z?BKX7E%|~Cdz0m9?>x7K^Lcpt;r)C{x=EXx*^WmFI$O)I6C3YPR*th^h`m8s1m-`l5!>LKx*>pNO zI#VDVqs*YCp5axHL53>qabKX-7G{p@m=s+reoTXSYj(QqG=ToI-&OB>AE1iIj zYOI%>qEdBs8fLomWi=f8gXy_P9lxwCCi!GL8U9IEGTt^upx|3;1>V-6wh{~Lp zsT6oDvm)(JPB^zFvYz|Zi>ev#U?j>W=b}?ZVRBT}=068#2(NLDUn}3kR{6@2ujzjK z`0Yi|At()`-k$G_0I|-23$%=qQaUU${&NAwd=-%859|-s-oQ^BCQ1>X24L(i{P>21 z&prrIDBHCT)e>XbC^uh+ry)Mi@Jf%ziNAH!qRI@a(Tt*)9Z4!yS94ayczb|mzukuA zIGCCgd%M9B6t`+mq|#7MaqRMg!kvI3FXg>&&IVq-%xRU}2fYk2=Ek5eDoQd$?D7;G zzZ{FyN%)#SlGE3?{lc0mx^>-MbZkFZ^iI2Q{GrV{-GYrXczr8br=Yu}DmlJK@n_pJ zp==~;=sT9ew>C%)B6rE>Lei8|Qr(w3BZ0pM7k=11cQa}V%?{?OYfSs1K5ZbMou6CG z*I1l^6V?!LIM8%Jo?0-muxXm zP(y|?U-L=JkZUPia?BZ`_Wqd=I`2-`U%Ko?P?O+eQa|7cH0eWJ4n3yP|Eo-oTb+5)D*y$ zt+v47?*fmDr3!5DZh^>p3#9lZj%Cw4q=*+*RVjl63I&5=I#>qG_F~XU($=krHaD4FMB&NrGGw2m#PPmW8dBE zep8)Brp#ihd=>MGLPnN>{48sR6FW!8N>-Wf=xip+b7Noi;jl@y#=m0zC)3?SbY=BI zj#Mmf$*@<6#`2{rghkBaA@^GE)FTWXxusr*!kz8R-bbyTnE#k}yXkn!Co#uy1==+3 z`s}BRkxaJF{wQ|V4wgij|5t{PGthHOdd}0)4`2wNo>nKglj>*yyI`)n+&XB`tPX)Ti#gy3n!w;6w}U#f>t>>q zEa(tC+BG<_Zx(N_8L%11_Vbfbx^>hVavkW?m9Zw3gJ-$`HM^E`K7yBgs%DP>dMu); zRbgYuXy?4}wt^OUD_$>H~362b}O@~w(Sz9V(AOpWSUhgjl!eRM?z)kV$*nyI(IEMZSg={yNg zjTO(4bN;K7WkXPoXPZN;@Bb>(Etv+21KA_o2*{0Mpqa5=@t;Jjgf$k|I&D4fyT7j8 zFgT58OJ_dEh^FGxP~EePr0TnN{1nn9Jw*^fwG7W4&}B0+C`vx#8XG}J%a-HN)Wl*W z%ekF(NjL|A9)O2WzhGyEIx+_nzp*d2vzI5zk)T1`s(UNpidz(sh^h?z%(nRus+#XG z4&;bdNb!)7;Jok{?%VUUY)#U5gI4~Lu2z>8fn3fXBsBs88uKvdYFhJvHev}VcZl>6 ztR>Ey#S3hH{Z>&U8vnX|BZ^MFmGT9W@;UfHsTfYJ6JRel1bWVn63>!W`e)bR#r~|) z-gKE-)(e`0(*<-^f>AR)4DBDbsh8BX&XuyJ8w^WieSh+Cj+tho=^-OgT;|vt3}qsp zi>H||oW3*345O9()=y%U_u+tzO18YsPDx|P(;fS6HD3Ah@NScLH)*Lf%}ecM>2|W+ zQC5Q`vdnXY@~pqfX*k-d)$YEz%u7xle|mxJ%h6LqtMlFc+0`1C`Rq*e2bkyVO3Jb% zxVS@5R)KjI7z7~u2_*fKwMm*>Elf8K+Uv!R1`fF%J@=(H#EpsMQqSFe`z#mXs^ix4 zaZ;%_l&^c{%&LA4UORs849jm=R;&Zxigv7Z*Yt#_DaOuQ|ikx`b}q_jJA%7!XC~U*^BJ7upX;U2-1FY zbx>e9(deXPUNNQ7;Zc$87qoqI;JNkse)PS}SECU{bmNZffYyw`UH6!(uU7Q!n09Dn z7pZaY-DwW%~N{Cc}FpZ=Wc(freKC=vl^)ioi>hsIs$Pmlh z{)MT#&r+k_!4j`wc$8kGlbmGpyLNxUdS%0n7@fR1i?_TvNrIA&&c;Wevg7~80u;n- zXC68y?XrzKXW+H4zdb)qI#A@f!w&ZoVf}lB+fq}LclGXYkuF>hj{-S6$VjI`5M20 zz$7DwzkXy# zgm-BFuq6GiYRi$5H0t9)P#;QBKg3DK$j~CcM7mMKMo}W;=Y|A%D}d`0O3-&dTcmon zsIV>iTU%{dxYZzF(Aj%!j;X}#kH*sv4uYh*o*HEL&9Hc-G%SMso7s@hzULOMJq1Dw zG^q86=wI7R$pwWT>^#qh{G_T+uOI3qdo?OVYQ&0uyRNbr4J(8{T2mQnfxzxCkYN;u z&mr3mc;*nUM6fF!{4r~2_XTNA_pJvSEf7=HK7I=dOfOZ+7pwug5Lr#lv;YU|j0NLf z5Igb!gTdd(hNp4f35}Wp5-MKIb+4VGEKxByrMR7<6lD?9k!Yn~_nK(Gh!1n!2*&UcZ_UZ4MXgNQ+rfwEymu zd6RsLQ-nJs46BEU@BDpN_9#p6mjEt#?59T#rR;;9#9u~j%IysW$?MlWR9Aly@<%Si zQLZa#Ca%?dh9_0Rl_mSblu_`=90=~H=1{w5F+9?K8Sxp_i_c#(VTNC4&>9(?<`h$C zeD^IfQ8R2_y4pfG+i<#1ef?$nc~@0fq}(6g7_Q8yTQ>+9f+Lr5R5%T*j`fU8L=ubS znrn0^B9cHmt#&l{%k70o8ZrY86t?$PFc?lB2PNJD9gW+w%IR)aGvtta1sp1f)LUPM z5*sp%@4lOE>gl>ShH`i%$9xJKf!q2qM8x$-do5#}kBXE9Mwpts;j42l$nA~t^Y9Wt zH;z!-M#*6@3wwAtpE!F?j}62cB_IA#vkZ5LpVkss`az9 zfsV|NGylVxOqmySMY#L;8BnKa&C7jN$AJHW6>u;5?hDz9_AcFl{aszaA<1hY-(UL? zR4tJd^FfL3l&Yp7Iq5vBwzu1UZuJZ@3h7C8^808uNsjf^Qg!?Y?<~jGaYRBJ z7GC#XBy0H}9~)uhEiW>rP{H|`$He^%qQyqp=h0Z{IN`XxC`_lL&%a5(o(K>)sLgGw zsgB8>omrAAQ7BN28p{=USQe&XEYseRl>2iqcGit|EZYLx9%zC|Nm^=~hVnm#{tTr3 z3HCzfadF$3Wn1wR@2E+=mVyGi;J`jd=}Z!$5LURDwm#G&At zEjPO-gI=&I3eP4Q0hx3usVKC|pO3Q$MSei2$3SHdpWa)$5vD84rMmq7~NiP3mV?lOgyixV_ z!_9iQrakn4r>HzC_u7l>erdP}eyb;*qu@m5TxZQG3O{TjRC03ZJfH{Ue3fK&Lfm8} z&K8scqcU;-_VGN|s_)+O?jGGo4wrFX2N2-QDtiz}5#VNbJ%YvPZ{;I}Rlj=SACpZz zS)Y0ch$S`W(#R}NygA-GC2RU7PsR$#zQ{^Rbrf0*n6ite;`yC+>NT0P$Ij9{@}TIO z7VhtCnXN0?Xkkepu#^3IJlV9~Dj!O1JM(&=fIkn@%^sGM#q|4JdL^2*YJwR`a0dI? zSh1K>g-rB8!m>R7y%62hy(+p}?9`+E>3gnOy>EDMwJmh1C5=B&fOVk1fO$!Xn2+pbpMWewiQ1KxNj?C$LD7&uW9fBo5ReShm@VyBa<1_H0y zGigH4YQRdXoA!I4E2Hge@AhQ1n(V2WbYnGznes*}@=sZ- zB6mOXnFPjeyF=OI@_xw}67vWnMhC|V;_-dsv>NF&R4dlW9p*xu85?CIW0vvH3DZ*B z1Q_wT4vv@4#aBBwoS{(;@(;o8JmTZcSP1n>rGFa#H9I)HV6eST;dJ**rck=vgzRHV)^=SqRDFu8qf84fpEeb&(T3S@e_Fvyd)~?-%|F8X=mN z+UJx|YUP`sPNT@MPhe(sijRKrN!fP3rUc9IyJs{!2OYTBIRE)Kfw_%OIU#CmeK$vB$kW zzV)xQfB`PXgbV4eio=;;|07y=Datl-*GH?Lah)=K2&C`VnwJYQaJSP9Jah&pvC!Rc z4BjU~vQh8UdB`wO(fle18^gMS;leTbFDTt!$yvY?+F&@DRM~xD!Ywjt?n>Umwp@39 z{azP#gLKX7DnkAhb#j**Q`edS3GhHXIkltYk4KaqRms%9zO+Yoc#Z;nj&tAY{giYw zS0{6Z#3>ux_b{kbB1E8KC6padV=Jd>YaKiu>^#A4K>{7Go!GAn36zcB>Dfk`nU96? zSNHJZQ!0Ry%xJUZ*x!XOm z9g~p7()nET$>zTy4%E8S222LocVy4)(jXO^m*HH!$OWN}m1@){pNxyc^-eMxMVPr8 z#mG!11gMlqpzY!iaRPPQW}(LodTg-sdg@{2}qyKkiDP_&p~b@{5%dy*I)Z@_C_bV<>8}WK7F7glJ_AUW7aLs zpkH^vOPwr+ldJ_u+PLA42dj9C_}#_+*kj!fuNP?%*}t`@>{+HN^wkOkuioFH-w)M{ zC4RvxMIqZUO)S9hOtT>9=E~z7TA|-Lcnl}3Jr7WcJ`BLFOLRG3(4hG;J2af!k1)ri zs`4hv8ZioAz66Hw@#E4@A=<1&WR@xf1*C>oDA&g!^v%fboi9)?Dqx_^@~b4^TFv7I z-u;BeI$R?R6r1Cl4V`nw7`r4Dr)A5>pXd=lBwrEf#VY(q3{=F;ieKbQ(Eajy&J49_ z@9VekRte6(tgUA&av{mbw|#8p*n#~5%$+z2q%BPFREbO^JNLmo3IU+pV7lqAb>Nl}+OsK8$@v?6^-;P*o?Sfr*ll z>Z`F`-hO-URJVV-JpX@@_ZCcfblci)aDo%u-QC^Y-Q6KL1b27$V8J0lf=h6B*Wm8% zewugfwfCxh&JQ^CeJLtM)zkFTvwQTkG4AW8pKWH}OD5Q$XXuzLM|9DfVmRh_#2FAN zoZBLfvsucbJzgjtzUtFrwpZ87;Hb?h*G)8=FB(eCGNsC!@c8`-HjjqxPHG|YbH|3Z zV`u;S$`)$DIDe(jBb|jxlRjRZ*qvL;dLvEh#3zL4eS{{x<^91|K+VaM*z z>7D@!&S=>kP~QF5nJCMy^|Ie7zo~`rG@)`Os74fwC@pqBb-z2jVEhM7z{T6){@|a_ zHsQd0CZ)^hll?W@g06`G;NHuRDB-a6Ifgx#Dy3j2N<$45%g4;+ zRGpaWup#=s*es$%UgR3*y#GbNvASB;$7e-Y*=9I5#`+a3ExpEhL|i4l^8Q_&a&2s+ zS7pnZm1@m&`qJ}#u+aq|O{d%UsZxU)OFww&ymB0a$uzQ02n2@YYLub&xb{BtR*|5?<&W;JUo9{NB zT19>{@3E2pW1UOkSs-QqiRC^`C@dNN3H>wp`nG*syuC!4eRv#1;!vkg%g8DQ9(A)v zh|?9FJyxFDRF0AMG!X_Keb^~pd&2udhs8*b{hKm*s_w~bb;gR>_)M{uPX?=3hVIJ- zML%SDdKDyIYm~0v{HJ%0q=~n5dJ+A{t{wRINa^YFIhaGgUd|-+VOXWWHuTX-I%o5_ z1PJF^v&Tf)r6#iG8}AjbRc^8AR^{Wg@#bOatqA3Qv$uDV9>Vm_5?7H7GQ3OTK23`l+@+Y^hWIP=~Ujf99oh-{dx z__P%ULb$>hXjp3WR1ktBbw3!r0#5P{0uky3`juBEHa!X)v{%lvWkS*!h6oBK-x7Ky zHVp2}LBRob#euUxq(9JEK1}ekA6DWwkw2DbQOy?o!S&#uPcSDx<$aLgVxtx&>DDgf znVtTrjKzVL=Q(AiNzJ~;fSZQK7Z=6zAqCsap1e=+_}E@=bHbN4^9ty$G!ED=h^$u^ z<@Akr#uQnr znzkTum!=_pVv&|6lLm6tj$Eto;vT;8^mqxRxF0D|oc3@Eo+K#N2z8ad1ZD+(^SBxi zq0yTzU(uAyrQ>3fyH(AUgf9Y0>HI($K zQk$8nEmc(`DL(KRaHaF*YGoZuZa8oiJ!RDAN62bzWxkOr9nLvLthhhK?Wx>Y;RxOK z1?!PE{{HGREdcv>IB1=K{vMu(c<1V*3TimC^=cL6!Y<}lckuGmRX>5(lE)r(PhkEG#x(fXe@e7{BLxs4R06e<=q1!J*zJQ7BAzIc9{pF4Km8DL_G{#|aVVqsA{jQsjX z<|ne_GOpj90keS4nnznf^z)y9a=kQi%RiEYd+O!#X$F4Q#8rF?YV6obW{8+(3Cfx~ z2?k*tDUY=W6G!V*!#*h4z_SsQg}A%SKmXD*GeEtCWD=cxfIpVlJMn|q4s(26ZiA4t z%W#dWk+Qd7pqglsCL}z3C&6gT!p(pqb{99yL27@o^K#{S7b1x3Apdq2V(TmuQdZ4j z)n#ER?@T0wlNg=GnO}VL+gVJPn0sYP$UWWx(>H}RxxdDe?}CVBU_Ouqw^^Hfqjm4*1_<#67NTba_y|tu75hdQ@9cKISt~Q10zE?S%}LKM zCK9>d7=lJyEOYVqeSK8@me=Xg*|EDGTaj}L5y>vg3@BvAv72-zZ8S7>i51#;x(k)w zBR}oYYSWpJL&Uj$T`muRTltjnq)-}*JhaJw;(_x<;W`6vwP7^R57 zl9AdE&>hR&4p#rTNnS!mq0_+}f{;e1iGz!pGhT>qWqj+UjDZiBF>DfRN_2NGw}`np zrQ0{Vu5e~NU-Z76mX*|Jg!Sld6C->vZ@ff*e1|+i`DXDa%}*(r`5?;WoI_%nYv>C+ z1zQPO+TgWCJIyIhs#vpV{$lvgV*!3C_C1LMdu_VplwBQ|>7DbCQ&Sax4f^<0m-!JO ztw_O;_2%2WTkpJWbIUR<3d?|3PvGJI@W?5@8644pb zzUDw@-zy$e=+PeRzg3H4mqGox(ko&>Dw^e0>ivZ*FDN=VTq6dAXyx_0|9Sa4AngYD zu>=epbpl9{;TTkt@x=-#D#)CM-hS6(9Cy4ox!XMZ6s8{{<%ZX+l^pC|Y$JDsaPTM1 zW+7E3|2{y1BqIi@PZL^gfhu}0nXbDINViYk_j>4@I~@?>>hyB#Q3aM(z5uv64?MZZXb7OWl?;f z{)4T6hhwil=V$`Dg{-!#a3MFVHr@U`!71r=X?k@HF!h+{syY0)byDrL702%{f#;Hpw#aKSe%uOm7T_k{!6jP#tc`h@lpp{?6#mKi2e7eNcE)dw z8sVxp$49|k)c zXDX+X2?AKv&TpCfnoQd?o<1H8y2UpYloIi3sW-{Qk4IM+g>EwIot6Uf$MkWCIG9SE zeC!7*OGR_SkTcjuK(b5JEu?ThO)x_L`gHmjR$#ls{N-sHDEqmLS@iKelCGIg(~ zhH8vW9-9xpp$-p{-nUJ?m%*LHl~f6+(-YJIMX{#rwnJ&w_|L*?FAez)NAs1{aDH#b zQg`caoXj&SMe?p0F`b5_ZXGW{yl>;-Fe*@_WF9+DGxtee40x1Q^l^VPMcz|k&)%D6 zAsq?jR-MKJoZffAd}v;xGwKBIIv)C_tsqu(eWd+7rg>q*c7JgU7cPy^>|}O{@W-Pa zJ5guVdOedno%e;wxT-&fp39M;?ELW)NQo2pN~LoZ=mo~Y5@~kcHb1BjrY@cR)Luz_ zykI(H4@~SH{EnsU!e76dr4H6N%BFJPVlqyW_(gvuH}F+%hgDZ$!q(=(M**sf? zbYo=Pxvb<8IhO$4ehGx7FGV^#>}+Fc#Jmm_iy~PIIhlP82&35ML@ZUp6; z5V(X}?)WwY@WCEVS;M0OP|z;Ia$q8e%^0ym&mx#acbZ6Es5c0_EWByKL)@V76WHC8 z%}~_PPnSfjQoTTSu`y`$!iEd#nTvQD*`ZY;=T8zO7b3ohuo?1MAx>+LZikBG=Haeh z%cgd&#~zlK{01kSdk-5jxr_mKV;Vgw^+p3cnE}2j_{w=ZL&lpGxUA7Q_=@tc5cg?s zlo>CT+Fq4q=q5_)-5-jsdzBb*S|G6}K& zHpQ!hPZ_=H*P^{1gsy2rsQ>ZY6m+ax72B=v?onj5xFLMYDV|nqzA^OKxZd{>3B+u= zY#ol8^$g!1?+_D1ek^H!&+U%p#^cxZ+jTu%zU~=plc0lOM9L5FlU}D)u{;iD2Z`RD z%!#)@c3j)~@pH^*yild_U$sPI>#gtTy-Q;NV2lw*3fe@3Rarc#)#Lo8MG}+&wqynY zd%9+!(1$P*1OxLDXn#ux+&art3X5z39qjMd_7k9$iRv;ANK$auz5r-MD*)*vIFE2U z*RdbV)pSI13!bqU3qRLEOyU!GkoAuVoaN9j_MIa62B>Rs-s!%j9w z0SN-1Z&vS3hJ@VdN#ijpjQybZp{0TwAz+(991%v3G&F0U;&AkpTb0lDyC zU!YS&S)2QC0;BicHUgkhm(%upEfdZnLApXMzI6wGo{GrEC{f|qkSaHU8YLP$z2wtN=30gsT~QwGvYHq|9pgH# zsM_)B(U;sdam$qSJ{?`t)UCy|eBn!U>}vJOZWbuFaV1Z?dk?fCVi}IvluQ8uA-k6y z(hQ7kq$}uP)dk@WR#9x&9CltMtV*(3@Gbeo7*|J>zOMj!KrKiWJB`l!6T{M|79tNO^M1T8F;X#d+3<-v<>f@TY^Nc(F| zz0DG?HjWJwG3ePZU>W5*7^fh-+aQ$mVVI7RZ9FZ5R6de!)5_=f~ zZGwc*Di_>bZ9pl63KxuDU`R~NiQmt+zv&E!tWUz@p&`026l)VUR>E0AGxr%LK#{n| z>xl<1TG#;9`5XG@ZXsmoa1>r8U4n(~gM?t{t;fyQGpQ~t=JkwN|NStI&yJ&kire>w z4hT>8mnU506JQV%kBe~*!lGnb+}P(zq@3(3u#77L753pAIamuP(4=oyYaSHQv_sb> z--Z;=u`gu_Bp|7XR+~ZyCJ-2~b3}4LNCKR^W5@XAEol#<_B(w@2&E>@thd%KhlvQ0 z1MQFxLvQ*Vk*r&J3=QU;dGG6B@&s?1b(DT^UuAp=z_O3Bxlxcx_&S_33dh`Z?xhXX zC}g^g-+xIOaOTM9Q)TwR8C^fXcSd49k!T*hbsIB$?E3b^qKpe($l}YIRFm zcC1w_s3cO+B30+@dj4w{YTjv(K{~|`qdwKZ;#*P}leJ7zMa`>WV(|-CV^FsL z>z|=E(yHHEpD+;I3>F98t(H|(u7PY>s!N;0#uMb(s@&6^&9(tbMqsxw&(N)3ZR+Df zaX_OwK|G7u$(p2pAy zSS-!Jw-3Y4%|%33Wpn=suRVSJi3&bdIhMIQWHdmVT6Z%^v`LzWZp%It+<`rmx)RB5 zB_v;Sv*cW1I!B!-23u?PG<&c=ru&CpPdWO|ddf5Eg{dY26-~pN!Ry|R0_tpPsc?T$ z%$V14pXJ12y5@r$Yz=KjnKe3QFy0g5V1)Vir9vK8q^%`(q?)t%8tGRgi6mzZn0~ed zzDMu7TsaiVL_FBUSb(*|IS#`9Uz4R0>UdWqz2ND@PagZ>@-Qx(8vJIi@`!=9$VhKLj!r`Q z>LX^|m{w>K8ZkSAG<;<41)T>69ye|F{7$}qOF)K)UCG|B^MLMKw3lR%Y~1$C{;fKW z_x|{;hAi5_c@k-O&?W|H*g>%|%X5(aYM!qI>zS}l&%dN{pDJ!B&ETygknJAw5#X`Q~g^cFk`CFs*IR2Q$(MRz$#Ilag;a)B}nhD5sxJYV;wqc?LAj zLj#|AYBmp*p zC_20&c<;7}lp;9Ho{3pV)a-aiFkH|E(Mis79u1RUczhyW*vu0AY&~30r)Gi|qfh-Mz$;fwXN_Jr5utp&!6#?1rds)YfXcNg z7W}ZuY^LR`LTsh=yTECOf3--f$L76}Svvak;c3Cd>?ZiJ#>iPrsOKOJBeyOs7-nh( zy|8G55Zae7lS6`n;djP$L*auq7wE?1VOiI)3RE!#=RxPLG7Zav>!=(?uTYcT575f* zU6MV^JAzxLb>dIf5Z!|bqcr=$YM?%y9|9#cM-6H-Hn2yQvO>kgD~?{XOVr)(&JPOa zj^ktNglx(Aq9A#0qf~b*;ULrZUCTQ!jZip5ld?{*DlwZH&T!liA}c2ABdjDV^xpTl z&+R#Hx1Tc1)(Fm(q-3FE%Cl4Oy@zDeqU#RUY-d92cW*@8;7cjI8HIO~&g1G_3ve22 zmzrX4j@4>~yJp7tO2VK8bgOCgV1k0_AiKkBYAvvYaBNgeur203^X;aKT&r_AJ!Qa> zvYGLyeI-%^!GJ>G-0*iIj%`=5Z@W#+{)2Fi3)>yjyqwj=ujKPqkch;=4hS`>8Lm$rjvqAa#aQ0`O?8Am<#?2HwT55#3$Iekf z`Ct_IYsJ&qtol*&UW79t9AXVjsQ|W^1j|Az#dDjq06zTaPns`c}@3JW|*^ zb3A;7l;m$woFci3w3)KYIA>Rqb8OlH@+&%g*%3wv&B4)Mi05P9b=hKg>;p~}|3C|i zK~KxU`Pru?9XhjJ9Z-oPr^P1b6H`$QrhO@fpL5urSd6#V*{ryy+h@KCHA36(b4dZu-|qc> z0gTuw!KXV*z4p!NiBR|(0c&z=^%;NUa@MQi?xm%x#{pYeei(hOyET`#LW^L;yl)#{ z?pEAFzE#hlKWNzPTYg5Nz$AZ9)v>Dv^Arv2o#1u(bJp?;&{XMMrG>mWQHLIJRdR`{SR-IHS@oMi-&&gCH!#JyRq~@V`jl7C?;yu3j{^1wuU|~;pqh7QP>Sy3|Lh*@(J91)A)+u>QDwf zGG8oK&@w#%oomVK3YYHM_v1}naGv$Z)M6N)Z&^~^v+qJF=V+yQ>1u5x+iWe`^KIV- z{TgGvZ|5>*XB+YF$dOAI)V0*5yIC%nI_5Ly)4fWgejcv-g_E;hLJxf-RKqYUC=YY6 z0(1Ch6shYpsiFX1yHVr;=%`T&^s${?>8-pOXd4{qb{=KkYhTJaxnivm84oL^?#Q`*Kg)9c20-vLb^)xTqX~bpJQA}L7|MpG>g~?MQPC+z z{t?1u5?MRf?&Lz)p1ybsD4dehu+Oc1HH!iY;WG?ppaoz5VkKsy9e`~0J+`yRU*x^E z49{@jGhm4a2WN^C&b+J@*_ymJUfi6nY3sMV0zCN**~~`eCxeUf@^w|mH8zehoVip5 ze13_$BfPgIyC^Gg!_{J4uDzCp-MG(h9vVTNDE6zY2HFkFw0%Iae$9_mTAd7ejK&`u zLdVN)6}W!Gyi!-k^NKUjrkDX74~bgS?aSov*i|nqKg+z|#AL2%yJ1c5SwUr|+b36e zRCpQJ^`BGWPl6BO@N4TMT(Lm_D8m;C)lOTS)+7T)vNyRcDi{MUJGxfX_SFfriS`K` zYmqR-DuA~j){SYZA7@&!FSB1vQ)CV3%gTmetkx}K>MA?rwK+6t=*HGhSl0`$d5F0n ztZGd4e6)IN8rny7Rwd0VzEZQKaU^Kbd>DH9ZNsKLbU;AmxopMQ4>uN+JTIt%7rf2}xusgz^YfRwJJnWz0!q9rFQ3vhkc?;QjsQj}qmXV%Uu0 z9EIuJRt98Xj{yE%gWl_*_J;~#udu6mbr!2-z_(?OHd3-QJqb<_w;W`LQvl+?&u%N3 zG9h3?p;9vcS<8(F)b4G%{_2qd#q8-N#c5iIXJl0D*QiV3h=B_F@Gm!nD`4CJXb+TZ zpFX0+PqkBxqko6$Qc_wuqRy&G-LqbodNumTF`$(|b{#1c*;boow2oq;j_$qGk+`d^ zW|AI*YkwB#2lLW?j;pE$CrKn43J~ztZzP+!lMyK$V-&W1iriy1lXBKF?A11^mZFe4 z!J7D0&P}~RoKy;0+0-QZlymKOwLWV0#;0;L z)9Z}E$Y1#8xBBXY99`mlxEzXX&k7cgB-xYqE~F@^@y9=g>b8#;ZLT%sq5?b;oiW+1 zf_wB@Bpt7O5>GcabTj*v@jZ8Eh5*KeG7idi|4Rbwn=@TT_TOCaV|8f7*k#kZ}D`$8C~bjcudSrrN}4?arG@uE;I&G zUXnE>i{5wG&u{_z&m%^7%Ta#mGBFcmWKzmD{1_fdF+Pc=P|#G=@~LzoR5e@zBJ8_n zgHO1;=JhrL^=z57W6Cn>cI9-x*ymE=_uQiDeK?K9NJ`07cxcq-<2V=IZ9mplz!lna zJeE0ab}TD*c1Z}lQCO_r2EcDS2X*k%{;E@Ob{@eLRE45-=&V ziM`4;HA9v8bI0X$&{;H{e`;P^Y|J0ij;M>pJ1yX6OTO;2EDRGA{p}FC=`%~(pn(d; zkU-*s-{zr|urimc%}eMfdEQ}5J! z@71`v`+eCCPdx1xb=WY&Y`)!MtTh;*dm+`yvEVagxla6`s` z*`uKKA&^FXO#;8;&hou+@*B@4vX;O9wV&I;a&3zDEjTFsOb0n{ltyKmQ9;Sjy@~l? z>HFT$_gnX!QKeNV_QyuwRbi_ItJ<4`YW=@%tCaOUvVUJK%KT|^H<2CowLu!EfO6_b zkD;s65?iwq1Nax~;ik9f*K&WIKTJTH-?*Gst6J#oJal`9heUWlPM;F$_LMjbQ>d~2 zgW3FFFW?uZT%xTtEOr_YK1b=+u4~u%&T%&BRw21@3z~Vm#2Bu}J5S?0SM^Z5Lu4>4 z#OYy|Ow*pdpYXnFG$eA`Y!&q4AzD|-I6%1>F|489K z`skPcq_jX}F+frO7bzp3k`4dAK|}eJpZ_l-0VM|;12P+Cmm}2=h;kKBz_{f2(R~4T zN4onrAf@qmIxP99_`f=-osbq13V8nqnGE({w~Q1Hx&eSI4*G+Dk+QL=!N9`4b^w2S zf?4X%GWp%@{q_EXK21L~G^AXm&-Vc--USMK(*QKcyZUnKRDr|@P_0D{@Ut$e%DDBb zG{|Ms2^aLeDsB#@de(US>{~CzGX(tF{eB&l6c&p5J{?j#0-CB!sd@f!e0 zv-tCU5PjKmuuDbcN9kh(!rt505Bl$aGyGqL?L5mSE=i|WahfbQm$P;?1>G~ zD=j)`cn}J)rV*?L13dkmzIOyDYOFdA;5EE?0##|YT*o_zcc1HnrE?3LljOOjwClf; z0XOQp3Lbs|>*OIIDoLZqRzdFtpzJT-5Jbn~@eBjX(UZ3E3>FE%4jQ0FZtj#Q4wQ@= zw0tY`J7y+c4SV)b+ruYLu^Nu;Ybfmj9YL-i`jUjF-I-tsAeVg(XG-GaT0fY$FAv*s z0ADwE5$+aHHZ8)yiCBA)2nZP9dz~;X2R7R*F?&@ByuZ}y0(`kEplE;O1Evj-zB2ue zk05$BfWNFm`<;IhPPq7ijD3ErXk1GK$k0m@0gCvq!1XqiD<>-|rmlHkk2UZ=f5^@( zvK&K6$x_tLXKfMr9}FjV(@kO%Zf?H4rOyU%r;nevDK0LV-C61hzDn|n=k6=W}Xb!Qx?FVO2kW9YpLd)Njj*GKH-;wDz1)DiV%W zfZ|XjWF0W_*g2gl2)hULa@Y7ygG6`OtM_BV*}g#WdNA)mZv@JzPqst%=Q}L!;~qVY z4do24@EhH)SNXSEw+w7~2+qUbZ9U*ylIhO8L)k?NlyV^GdCH+;0oXOQTz);BX+}oI zo5Ao8w6pvu`kFi{9Cn+Fs@ssCtpLlK{pK?VtDk}AVbSz&iZIK@54g7N(lLOaXSG{d z4DA9+?FkyhSTswc!Sw5Za3b8g1k-BwZUzz)JjSBmv%If{fcmUFp&&@WpaDasLc89^ zy)1}{-PXMb#ct?UA2_jV8<fB0{r(%?qUWlo}uVAyn+1y8hS#~*p5()HWHkRRwf zc7f`PB}|_;y5a6$XFz9=sSgya_+=JbCAN!P)ov}sL+7E0!9?hb6S^#DbnB309RQ7(9Q3BFS`>YN*9bl zs_ub;Rzd^s%aE7H{oJ?uIf+6bq%|W@3x0sBJ%Bgn)Quz9vZf6gYFC)DXUTMG z@i%Ue&u^^{t+_H!YRi-@4)xpw0?h_yTcq>`kg9Az6QUgFyVvyqvilVv5Tply*IQpG z0kJQ%qcC`)!)LV3d(Fl_z^dFf7bvjcSofa64%N_eQCk$wh5NQ_+o)9#yN%M&^$&e} z`h6A0m+tM_)(l12Ku|XPUnIC^+~AQA!UuA(+H)4*S6|zbO;g zj42963t)?WU8p=Y*bcJB9s5s1O#X;3DHy9|SV0m8%2~L}e}MBw%_5h#Oa7SS4(X zTc^MGv|Xjye)I^><;Y{Irr{#3&Nm_l>m42HcG8*`#a#O9ogud`Xa0@@zqc@5;Dkke ziQ$RcOBVXN=*e>Nciwy2M*LmiE zU#sFg5fQ~`k<5gBd$%RmD(Q*;`(b2U!oIyd+aFsw6DCHkXY+28C(EH%z{4&kf7GU& zUQ1;U-7SW)MgxSt#f}u^)sx?#v7o7`eS+tiELR$aWD>^^cqK!ksy0ALOPC*`K`A($ zhbnP!h9XFR4PGc2y(JVinl`U+Kzz4XxJLYMuTKl7f9sz)1Jkdq;4$ zcLS_Y1X93((67OEdH!9_W)0-3kflb?)wQ9{etivw=?cH%2+ms(aK%`jJ_Pveq$1y; z)U`1)RV_7i9YfesttJ=0Z(0uDK^Zj^T46&X0Yl5Z?&9Z)9(d!eN-uw2(POSWP5EtX zcxbp~f-yt#YX1aDzP(E82$;Ma(sw5VyEu^jeiZ-4POJy0%ScD5HWWL?BtyedNR*DQ z$|$nM@a*cfkEo|*Cy4o8$k8Z)EhM37QrcAmkc8)5(jd;bQ2>gM1&#)Q{x~%#Q03q$ z_Xnf?*%^-nqYuYd;?#5CL|9F%K8PEew;!L7V8ntkAd)Uti?_V7*U#B2`E91oN@e+- z5VH<-+Bp0NF?jOuh}H!vl6;^9t!fl;cRgT!rA$QPK7AoN$sE)esK}yihUdmN5qoUN zN0(U#Ipr6QBr8@1w}T;^fn~f{-6Fi4ol!)BNNCOi zQyXpi^J*l0T+K>D@FXb91k#-tMpR*$hJh*DlnSslj@AwUm!Amq6@Meawfg{r9B$1w zqddp)0B#T=P|U9gtfNHVk)+Oir_}mBBNo$>^rk;GIPZ@)fW*g}V?(bx*i{BtJ74iV zqEvMO^3>D%0p%(seei3fVC)d)|G1k5wdP|s+fgXuhrtRh*RrJf^)9O@2Y-%b&5nQf zz;ME-!ST6S^Zt$aQ8ElVB*75+qMC==qJVpwm|(U(i0xHQUMn-(T(%S;x*TxP%+VSfj!G zJec{=s%}0JjmSiM=q1H-M1PMs&H?SQE#+49pX@-e2P5KWnsQu4 z#N)2hjf%-v)-KA3;IA9oS ztkkd}02>?oV^QATy1PFZc-93YrqpXRX4hFLnx?l@m+@VcfRTL|>DQRVt0)Qh>gTMBAw^`puJHMJ_+3)P1_a;^kgzZwk?0da z?m?>`Zn_Y|bqC0b%HX7c<0az(f+WhOGA#CVq$tgX0{@b&Z$B8oQVMxyN8mG}CPk?U zv_)1S3F%70sk^O(1ER7)a6WuRc$7(V1UY(DxA{okGKLf3WY$5A^QgeH9GH6vgvSFc zBy&llKQ}Upg4a+=w>n~*^FN=|I(cAEfQl2mf?3*D!N7To5`2o5aIO}4gL~pE^@FI}V7YTm26DUo!kD*pIRMVFKM5iPd8~!#} zyMNjNtD-_jF9u(FchLv}cSE`OlizVgqm8kJKS-;*>Zcp~JYtc~>KK(ymPo{c2_{wk z?zmKeyB;Vf>8DS1aMJ!}WR8wy8 z5fcOP^=+0E{sDJ@Ic2)h833p%Z>OC^XkdfVkPy08G;JjOV7p}iXNv9V*fUjaUWz>& zBT20s64*Qn_cM2VRJc0D8bnAo2wBR!pOn>GtfP#RAlDTMZ<40H>=%~ z&LX})Y!K@w36d0P#0|;d;^&_QIiYT`MN&SBchz9tnp{%l-1AG^p zL}i9GMRek!9c~i?Hfgq35d%&T?vySt!I{rqhD9s6kf$7RBWx~#?}O2Y%Lt9l zL@jgZ_XShRKXiWYxoy{07D`kP|)c=l4?JR3FkZXRBb5p!|{;NQ2PQ2 z)#CMw03}_RBIxJB#&_?ewjHPD)ExvaZvi}~KaIe@KlT>w9`jWSC5@W~UZScA~k$OZ}8))5EBP+MPnXQYx}?$6TOHX{$&IuU%H z`*&c6g`{fjRY$<17>kdCk>+RotaYzGXwB*CDUm4vVRLF)8V+!}C2R&E=es zf${Z=C5zlcS9sf@zvpJJw*Z@7yMhJ}FU@0U(0-$SlUrhc*N$E}x4E6OM(O&=-!I$t zIwT~7jH)_i_K(|nA;9i0^q9EfGX5pECyO&b&oVUcNfjH_oPCLXq1Eqfr`7s^xSCIU zMEtd=YQ9LSf^Nm*fNsNd?Zm@EA!j3l5jO=N#}i*JuVh%TZydZUBs}|b;4?B03i!Q= zodsO3n(*RJs9OO%VGoW@n^-s#)?A&?O=46B+09{6iafBlGpuXMl-tN}MhJ}!5Y z6FWA*3m6!0G{3*SK3i~^^Gy;1qWc|ATbrg6&xbVM!6}mtY?fm@ji*bi0 z-uTyhf zWhp}LRU`VpPH=ZJn)nT$6W!k4J~%j-kez+rT>>dMT5vfJekzqVM(O{3IJQ1AGP0C6 zAS?{-?G+^@B_-tM#x^oCYVx=yAI}xYHI82qM*=@ij~-iJCjg9lB!GR=1R4!`a7f7R zfdL5|G9ajfM@jypwXWtegGI;=w5;OT`l_m^#3UrQ29+!v!c3Avl6@;p58oj%P*H<7 zHVj|WLapqAjV%J>9Y5Oo`cGSj!m~5%t>v*nES^TzQ2UK)F6xUmBZy#xsZ;y=%Ec2_#$`SlQ4V^`e&Z1KvR* z>;Jfy(b-xgG5#cbp%ncT&=~`!(ZvgUA0J!)&lv}Dvb7TJutEMiv^URs&7q4Pt*<+P zGxyi*_iEcm0*}rV{+~w&G-JaztD41HQ_k3!I`wjtoHh+&vm?4FAR~H$HzwAR%S6%`8Gl%#gO>Wyu*aBP)avAAth- zKi_7vlrc0oOesi+evl`d8QU|AX%3pSKF;_s9Fh&W`>SPdbI# z%!U|R*qqCG4-P2Af%Nn9bN8aSPa`EKj|2h__EF=6mI(jiPN}{$ty&oA%=WDR=>)(u zb#3)UAz@)zl@DRQ&~XXKG~tHlc$zdki?{SFlD;M}fD0 zorE?u9O|DwK8uVQ=F)nea4$?MJevhf3S;6|id&@;DYP&%Adlw)w!~AcF2yPNfl(&{ z7{3i)pKjCHZLt9}Gh4u78oa85A2*3Yp&u~(a(KS8-9`~szZlWNguuQFu{T5jE@g$c z6@~+`f-yBKEE^6M<1Gf*&BnIQ3c8;gPNAv)(Sn2guSgk>PY!;F!9jj}dk)XJV9RgE zj7eW7Cz8T;3v?LJYOpE09KKcjeY)0$qf+t~tyI9@c`#MLveSlalT@I~f?B$~`cq>F zKf-aUsrAmQS69HS+%a~hM1cmRQBeZR9NaTQr~YdUKzGNsKBNB_9-&YOP~vC?sy0?~ zoaIxly(UO*bSby;x1;$MDVT_m)8h0RS{SaKw!7imD)=y4x@^B}S75k>xh_82NerAp z`{pbVvooVJHJTv#&}d_{@3lNEx|5QbU)-0Cp=_a?Np5m6hII`5bv>ih;=6ZCZsH_8 zS&8X|#i*a4qaugA#P#Z>aBy>GLpc+z-Tr% z3}#4qq?K@<0XRimTHH3IUjr55-rriQ=b1p7!4#`q#AZE85r)~RqXNxfx|at$4G?l+ zpEb?~@~5I4%xADj`?af@UIJexciZSK@3nbMn^Loy5JrQlJk2)S83j2HS@Z4D=@u*+&5l%J6W8+6 z2KM3M>A<fL|wm-9@lg*7gu`y#xfpmAZ49Hx!6lRiNxd=bE zlSL6OHvMbH2sKGGFZXu><*lk2h0gFE8pudU{s2JA_+o=-FoUI}nw^gsa{LLC z)7dWfZ?sBnC<#6BtfmQ=n!==337+fEhbO3f%}kAiKdub!9?#_VCPCW+aV8HJ3l49* zBCFY9_xC`C@e-B?@M6$Vu$w9f6b#zTl z=fuJjp`+&@Q zJ<8Jm{U+SsnRerg39*CKoJLcP4|!rb>M-`t*o}(f+o-q9UbR8>I+#e87bFk}n&Yu@ zAvqQxWs$u@UeJ+wf7Rr;?P8qC6H^O1?ovT2gmgyf%~@TYt@Xv^i=)vp1g)<|U)^aJ~J`~v|+P`uW(>4ebTewdzh{jqpa7Z(mDG4WWSu*o7_PjDf1(0Lpl zYn&=kU+lgK7SYl$?eC(+{r2%AKANWA2D55*+=e7q$SG{=!3xN!gu-|KM5kT%`{=eM z^JAre4>J%scdCv-7up~=kj9gLpT359H^TU0KmLBP6HRfsI6E-8pUH2@TgY^!J{U!M zJ6Q|GXZgH$(vv(;yiuj(w>f{S)6Zj7Hdl=f>CN(qOXhFv`vmP@lZK<;^3hu`?6OP# zBTx)DG8rUS=7spyocFiQe}neEj-PQxSxw0Hgc5LIT7N82G=vnSkLPWtQvm#CzEXjB zsd7?WM$p*uA zs}UyqQ}4j+G(hII8!+|VdTN;p@5pBVNI`4oC)ry!-x)e@cjoJ|2{k1ym%9%YL3$0 zGe_7O1IOPv{eRQ7xXP**&k;-n(|t^LolJ$u7&zyVF>r$5emr#yT6>g4vCY(vujM zh{N*_8=ICDNEKaiC0#{LJ zR_iDSck%9Ni_ zv}jS|la4ua<}{8SJ8CT58!+pl<@w5NaTfpF#Zb8UF!%-p8bZxNys0W8d0D;I9N4!X@4xpRwr$;tKK=UQgVCdLEBqF^cJ9m;8wL*@g5G`lAV-cI#$Ig09-pwV zFeIwuwInDwSf6}*4GU{fG;p6h7E6Ma63)y8Wv@=0I-i#K`v<6f2J0RXo^N*&L?cqi z9(FTKUEjkC;w&y?t=q>BW~ZuZFQeryLuHDFI?2|%F)qLdjCPn zfb-_di=xGf=?N5hP5GeZ_c`N^=XK=0Yz-bSdp4Ze58?4fM@1t_9UJcr^Sgt`!+qJ~ zU-!V-fa2u9ssq?MZL1#B6&r5*KFnDvCm#O%Vbp!7ZaNV{VYBw9wV40bd_;#vyTk)$ zqQQU$81&;Hc>6kct;{*Q^DKVo_XDmTyK1blkEI8W0&3UXU6}UNG@Z`h78D*usue-q zUUgBSc?A@%To~E&E3sx=Yn~r^0x#^nfP*UzV#kafxP8@j?=G)r+55|4=(M58T{O4N z``Db18hnI{doLPGX<=%9dNSL*+(zx(SZM`*ulcp*cs$QzKo7dt**Kcd9i$PYfyGog=s=N+tF^ABEn`DHxw{Qt0X#|}K)vzHb}10Q`9FTV0J{Cxco zuY?90$F@!)8~a~;_8D$iF|Ic4+Mz?oju}$^dt=Et z_)wcTVFC^xI%KR#rAEzK=-I26j?vw>XOFSgJ+yAq2CY>2^pml;5|XvCK(R4wcqbv7 z5>thX6hY<6RZy`~MU+q#1O=8ocyC?3a#icEVAivFfgWofD%oz1uSBy1jDN?j39_iZ`B}$aAxes$_56hP=!-{3h zWR!?RZ7b>x^$+KR`RvOtk~g%0J1e}%@JJk9dsv6OaSDKfB@1fL3DfyIOH^1C_AK3l z=`T!2bV#(ZHa*#kW=Ef|`k-FVdg;zvOk@oHc>ND-nYhKJ0*=D%wRNwdbi<4_jCnp5 zeY6P6K3V2;*rTO@<9u$v4E+U%mmf}fwpoKBrROI-QLubLheDGRdSBds5kC+986k&4 zjJ4^Zm}xt_ExLW!%~)ffQFTVy6>wcgb+tP`UuFq88iG-^M%ff_H6E;ifs+R!M*+v3 za#=+LoFJrS`}Q5!vv(gJAM|L7O=Ok-ihyGX<>#M&jzx)O?c8EHk6le6frHZ%YzlMP0=~g1nTNNL_00dNh5^qh~kDWVrChsf* z)KKlrFO>$!nKPHM=HKIv3OE|j{(bvZgVtX)=qn*QIvS7m>yN!koHS_I5ECbTkNCKF zy!-ZBShi#-KKC?` zNXw;*7q##^e=dEZgQwJu_pB0!js%4z6qEfPe#Gj-;Pp4(#8c1Op6|mN^ym#yN}ht% zl6D=cIeruIjv=FxI*T3UprYuH7C|BKIK_DqPs= zO-8}JWBYb2UA#mIp=G#v^QQ4m#wc8*C|b5|r388hE$FO?bcW(~YTM3c>@$twu_1%; z{0lE2S1!kf%7@;BapN&@eERv#_*+k&vp-1IofRG@k^a8d_c}=jeS-<=KaQg+T&*xF zwXB31?P{QCm7+S7jpK`~T^kb-HzF`;&?KB#e!}LY;C^}i@ce@3QNB_6bo*jf#<%Of zjnR8X8*9>&Q2|G4b*qKJzYWgN_$UgC@twxw`l;*28pl`)xNXz7;g`X`XfbFFdfqCw zs)(oOK8=82w&b1X`kCwaw%fOQV)HaET{8l*2jKl9@9W9U9U%hF!vq}bTlL4DJqqbz z8%55U^K9L^4SV-dz&)nxJy-Bw5pcZMX3m_6Z@!tJ+W_|9JxQ@Mbm-7D5lJH8GDlKk z0z$X`fROd7&E=H{CM!u!XhI=J;bszV$%2mS^|E?2B>CmQ^S}7u+%;ooPC_p##ao>^ zb&WOuF3t+LWDCp!Y%p(E4~nrLe~eGZe4;&06i+X_{E}{9U%q?^dz6q13JyXbV_Vfh zaALd!b&-}YU8=orO`6`1+I8#b&rVii@3XOEF+nw$W1<}!rtt5ete?@&X1EP zTlJ=$QzConPd{VS$oDa4)~rm6H*)#XMf~yGZy5Q`yO{FBkGOU%gI20wC&l#Nr(yP= zGxh6x1!dmwRsSVLCE?QcOIY*i8jR~O4r5!6#UC&Ifz8u5BlNs&Z)dhV*)Z&yV*n;7M z>wjEtQ@oK<4NBqhpC8Y#cq8u4O*c-sbc}P0-9g@NisNK2t z8JCzChmc$QaP0aL+=xD_exmE9zThv9Q(_<@UbQK%hhY4n-FRl>YIIpL9}g^;hh~4x zNBjAU@YL#!_-5Y`9K0Te$hdgKCuZ@SDOi_n+_(v+PM^l&#Y-^u)6Xz|!Z$c{_=x(= zsr%=4t5&bZ#EIYGyYDAq+xG3cvzFYtbqmW^ti-;3`!Rj`481MGxc-{I0AoHGi!Z+X z8f(|BPZ1payLs~#eEHSa_~_#?n4#*VkdV~K5hh|da^xsZoH&6QGycTSQ-9I>HZ{Pn zUB8Z>ewvC;#(s*?AC1AJ$v@!y1$7?@S%&S@(xpo~UYIiE!MB;CYo9dE+l*ZF4xuXX$O?NO~-bz_a^kYxeK zhtRHFyRd)XK5W>q0pCyj79p1})8LtE{+&Fdvn}67h>Ca5Zgzgit=(9z* z9cq0H7Q-+*zSF^?A9L*vDacT^Oc!6x+(pj?OX-35PD#vV?FFb7+~45bm)p8`K3MR89TYb_5f&l$C^LY961YRZW;x8@2a$|g0f9*OkQem zboEg^ak$`lB;CA*`1OAw`pc&g_0B_xnD!xJw=G2cnf-{rb_VfRP9y%5y05p*LiDV+ z5Hr3D;^w@H#3L(}kvX3MFAWJXNZS8C61Km9xP2cZZp&jxyrTB!A}H2&pPht7`=7^( zBV#aa-%uR7zT9{>YlO!}W5eas_;l|E^jtX;kN&d&| z>UGRIdIE2+-;PGp7o+9O75He&L2SNs9SKo;uFbsGiTiWMucYxizcs8~Vwobz>z zYR?QAJQ)4^_0i+e=(*zlmMvG?spp%!nzKWCw2v8L+nWYK1L-3;+&5_1%9UaBymNtr_N=v+4nK5D%)a5xRe zFVfrPX=GWzQJA%E(;8is2+I~6jFsvEHcmCH$)oBa7^v2^X8EcdlIeGn+PU z)DN;&>a!Gaix&KiC5skmuSmZE{j~=#ATZ;-+rqy?cvv{*&YqK!w1r`lKm9lb3+B$l zP4(Iud^na@6XWjcH2Ed22OB;3wK zg>^*t1g2#gmC;!oEn!1v^Us^LFmnV&+q}2tAu`l@=E?ef>+5Wh zj$rttJ?|T{N|tL0K4VGn2-N+Wcz7qGzkLhQ zKa51&-gQWfVFD!MeM^k>iBY!@zi$y@=f95lm1B?;c0T!y43HFm3rUw(&=9z!^js(s zZtOOevra-%431oyqr^(`bA#i>_g`J2UuT8wSI=O~ZY9KauEg9^`*0~D+d(;coU|+oNigDrnNUG4kinkJ`0sp;gP4N@y+DA%eSi z@6mNx`*!Vgwz^`)ilI}-4to5&Da_8-*AFcocmVb4*3~`x-m0a^PHuYR3KS@SQl(1j zFgvz~_V3@1)~#EiTD58@Uc9*eE+*|-xpI}>7w?U5wcSHqAJoT1LCMb_G{)6aV3a6P zTz5!DMn<7rxw5!@J3@a($T}uZr!#mn#?P7FLBVPK3sgex>OJ7&$E=hl5vSfT#=Ip9 zIsQ!+Zd%YO5$EfhfX;OyQL#w+d={3leaR^%Dp#&-tnn-|E#P=_a}q;N6HrP0_!nP# z5l=kzq)u@0*W9`I>dP4(f?g9hRIAAit^BndhEE`SRs;PuHB;vr~Gc?j?Mb!S^3HKna$ycFSXfAJ@Xy8lAdy!RSxM zXyIe4v}ZyW8yPfnDlz@s3(sTPpEGQh-)Bt6lwYRei?6@J&?ko>pQ_kwv3}h;M61e* z72#$j&}Yw@Wph3i{-)<%cmdp zv7XL3U)g+U@JIuzqSYL}e{lIhV<|mslohX89QjJ7pBpcuaEgGS+_tmZa{fM!a}+!T zWk(4Iw&{;`=m--_9SgmMaa#|g&z#NJcO@+O zjSRx?D_ppcv6KvxbTJDt1>D)Q=X9Irv(LZKA&)GRfA~>}v)GjAlEAjvPqLC3(a4oE7pDF=MK`XQMC0A}-`Apz*$QjbuB{(P?K^b9*AvF;X6LsP zzR`yA;>$1VeNZGU{`+tA?%q>>$IKajCV%NY1daYaC188>>S?z$Y1&j*3f5rO9A>k$ zSE^O7o@QXJIex0b;Eel?8aK9C8aHWz2U@g1m#$s)^_e|y9`fe3-gu>0V{aovV8@Re zhcFxC-RK_Yee#47gjK3k(MH5Uq&!X*j*H~nEEOtNL?0#a-+uQURI6bAD}w0fl#s`spp{8jQJQ9aZg3A%)c1pM>mKlo|L zPdK>xV7eY}HZE&*uBG>#=#{;Aa)J^EZ5yWe4?S&r{G6AMm!31t$3N?pHA68gwybC@ zrH9vq^Vr45%Ge4|ASvWLVtyQhgfqwV^WCRl2?Vt6i{Q8ZK+yOd2zqZ3f`-pT(1^bg z^x+l+y*LX2%^!tVZqBD?c0VP=BjMOGB<%V{?bNkn*j$g59v3HwPKd*_qwDa;kqx*N zOF=@z^n#zN8%ku$g?<&PV_K^&*xs!l{_4;LKR(bBQ(8WN`JLKhf8Xv{)}tMss#O`K zbLD`aF$D!fC^wwFisu&X!r^O~@Ioo!V>w3q^r_SEQS~InRgRoFbjRcH*IvblH{a0Z z?YBl?*sv$erawxXY#@8KY-r!Uo!+i_v!+D-6S59Fp^qIq=2SQ;uMsmBihy%HzB!5^ zca47V^$Sqvndh2C8G!NEw5-D@Q#^6>!X=??tw=O1Z#%O#ZTjh_pV1vGo=T<#+-22p zo;P={&KmjblTYEr|2?lBcBiy3qt}KWGC`4@2L-~pWuew%7!n+E>gOYod9?$%zR$kKPK6&Ma??K9@Z-b01g z!kNNeJqjyk#VJ*~6xw&{XzU>^{5>q&wro?^F8%ZgCLVqCQ9RVGn-*>M;CHiUq(#e? z=-a|S5ZrV zcp4*jjl?IXKSAfwosp|-F8e}G{oJz~&*J;u-{a8Q^w*0S3>)=ngkE3wLJ}XE7ElDv zdTEx*uwwe|hvi13hb zn^`ysZ6SXd+g|95z~iaXri!s-1$D|PD4EbixVuSG^;68xUn2hKK6nRbgMX`@N}zoW z|3M?+U7?;5{uZ5RYT2t~E%7&aV)ld0F{DO$ zT*3w>gk8IK9TzWN)b&i!qD6G(IQY@nU-X+(osI@bFvudrzM} zgVwED8GEn|_wB8olM#_odTd@;Sh!B8!q?0nkX)B}=|pf*QMMbgf^|3qWbfWRPI=(i z&f@jFmjv9ABS$cQ{`@QzCf{)dL1jB2XQi(Ceoq!|EQWZa=S}@h4yoj8{$)L#1V8^I zJlWwUisrTc9BAIWxelN7WHK$_H~}-gH#u|W)Ms?r(xvFrtGCWJ$VT^Hix#3zUB@Xo z*@*Ab^+7$y*GHp1#Nfvs!`984^*$J9-c>d9`#(C+*jl(7$g3V!*0`^6BYiz5eg9o@ zZD<8wB?LJKpfinW6Wo^56mUukHlQtGSj)i!2OK88;RDv%=WpBy#h=rsW7W!4y64T4 z;JIPp++h^-zW%<*Q#=p4zS$KY?)wl!XAec?E|n2nK#64=r#$vXEXH>nj}5|E34ylp{#I9coub^NAbRUX<$KHT< zxf*Oq7+ctY_FJ?Xd^-$>PomNdqmp@;=G!6N% zu$x%Bc0H!g_!G;PE!T-iiWMuW>!D5^JLto*E3-J5&Kk(8kFQY}P zmMEwM7sDpUeB}Im8-xv}g9i@kEQTD++q=&r`0UFsv3$*Hd^LU?+O})wu`#Om8tj>( zaa%&+)_>qYZ4k$fS|5i*8Z>B-DVH@vRG8GnR$5>7gkHODKhZ^%DpgUYY+0*4WgHLp z(+@xBY^wK4Wyd+;8gy@fCx3eq{lDvv3hgRb4TmI2F-e&F`dn8lx5*ko`M_-9KW3Pf?WW7ZD+*PKg-E@A6$TdfK&imfJt znrLsLGuTUBVj?3Gr8~kj@0=|XViL6XQMh|aV(9pvQ;?L90KfY?!ME$<>VcRsk?)nO zD12K!5APglpN)wj`;c^Qldyw2K}48_dwI&IOqSa}5aWZbrL2l(NQ zMwRhst#T=SRD2T5*nI}Ot#sU)Bf3l5I#FZ{x3XeTD z5HCFcKV6vpv2x`~`h7+eJ^l2PDUl`IzFfI-=+oz63>`8Uk3T+0?~me(6ey4%4?WZs z|9kFPwXf&&erwdIuFnUlSGNv&bbm;{Hqo(T2XyY#QSZA}t(tiC)tB-7|DHqt{(VuW zPHnvW(u-=F_S!2{tY}dky4k3a^^Q*=J?y}pGiMII{PL@mv4)N)s?IZ=8TS%Qy2Ia} zmEy{26}$uUAXmMY;Nu^ZY~xzIsXzL)QIyr|{x2=+l7$>|%QUPMg;upvql9P+ZQ8U! zrAo&ax=syWddd!C9a!NrV$6TkmnCn#Yy!9IQa8e2IBA4U!CYlx3O9gC^I|Ar4f z{1DAsJfIqq@wj~XGR~YneNQGb`M05gUA}Z#$9mf=oQvl0VeH$x7wgxp$5)?!fdl*Y z8+%A5I5-;~eCQ$6SK^GJBDS(l5@syTEal3V(_x&};8-|o^U#@7wR%+y9y$bNm5Ad# zXUXPGoACRuzv++{dtMk%BPeGO>UOJ(r>8%St{-)US77?zPHtbmjoGiy#>E5aCxtFr zxhR@G(G-9tjeEhO9uhi}o6@b5W9*_Zv$a9em4C=Z_+ zU05L+Yk12iA@MX*^?UyFC?+8mE6;94XjH1G@==YcAyu29L4lISn#}WU``V~eByY;; zyAd6WNt;h1)(LxMqmI)DFcrUWFJk=}N}&=K7KT%&PGR-xRTwd11jdg21g1=@j$k{2 z$@(lkrGo46FHi^B%5+loq0t$ugj$M_OZjUm$;%{LU=ki|5P=f;lB0{tlqsX`0@C0(qv4Z_6PRt+N}rY)>94njKWY%CY4ybW{sW?Zp!2z@Xh!M z_(FZh$D>DM!C!yfQ;q7o0VeL4J!>Z3fA?LRW#qf>;oZ01#v88<$Fom9iC=#HImJW8 zhZn1QWJ(PUOjmgAxA@S{p@u_ghtpD7ra)%xq{K@)lPWyhewgSr1dbR+ZS$Q zV*iN<3rX8^O<~pSv1TaTps=yV4E2@EK3j&!FzX^JqvR@*3w65H(Vjm`Vk2U)dcx`y zFX7>phjDb3b&qeM%7yfNZPvuPEjX^w-u8S3-M*H|dAh7{^P>HU?Spa!IrP#AcLC~_ z@$b?f-o@?D?PnJ+Z}`;dqZ%k?oh0ghOS*B2H@>mLbGdl?D)wACoKla|%IXy=i>{?> z>E*1#5Xu)DR?uVQ2*W3LUATdh>bd9$)+_XmF~m{0_c(d-B)vg6^ zjxpne-6j|+S-DCTJ=fne|N9?Gmnp3uVw|+p(s;}X%t8+*D}GxNK3+BGof*Hzo>op( zz;I7fVjXihzufP?{ig2k#dm#&LFm~~{Pg%w__F<%_@?JK_<7jRm^X4BcFfyhbB;EO zqWd1X4-b!j7&%K?pCaJK!5jGX`Ckzok#_LAUw|J5eK*Le2hS_U3o}N{FqX0o!yY-` znl<+Sy&uQc9z)En7;O4wlhyHeet~{y`)XS~uU$sSSvaTNaLc5GBwRm}Jm$p}9AAIj zcx453ni&Ebn8Pf&g>YBmldlkb+xJx0%-++_F*Munlo16$c zi_qxQwt=sY4_X$hlBuvsThuO`2W9i7hOh3wd<%OohCB3MvhHFCq@%=ICYaoUNlvmF zCdMp>GiT0nX}Fn?o|7j}VCvML&B|0voAx{A&6}r1+mR!Ob*F7c>-;WVx_ImyVxEX^ zp6bX}v7feAK5F^2HGkylWz!2Ye0`G8vuOl6G%JCgJ$v3W31>1b;A+&WiTm$wicVcR zqtAf;+5_|ai!bVF15#7!Iu9Gx%r^Mu+izjSJ8$EEFTSAX2J6hMdOw4X$0i zroF%0x4R}R&|NU&V&wHm+&ptrPtv__&OR&~vkbpK@jJ$M9glye{F81x8=;7+)42|Y zO&^BfBGwx$hZi2k`d`)?OQ{sATnx>hZEh^3W!sP2j8|DFAV&cDecR8fnBwpL>z%)F zeB*H(`{!6n2b?)&8ka%UwpEQK=b>QAS;V@qCG14vm^*9PE;9-f#vEE^J6kL5h#o-- zXW{OJPY~aR&EOSmeSB_)@b-aM=|*W9AW5;efJo0DLa^e@I#wLU5>O#`QB+dJm6XmC zjHV^>rJTN)gan+qaa-A~Ydho4pFgh?on)r7+MJu}^Ltet%Fa~5eXza44qo$;6^8(y z&6O)x!Amc_EbUScf^8pr?Nd)9(|6uSW5(dUQSamR5hL)@%P-^UXP?2KA%oGXbt|1> zH>2<~=g*x(!GeYGP`B=Q;>jnKAbbI@zA+qcz5A|CJAW^ocu$8thMYEH@uEfe^0Uuz zV1IIUK!!QmnmC+X&&0i9;{3U@S~OaN->WmCItk}3{ND@DZcnGRaqNqE!o7t;wjhD4UXuRQvE)d&7L(}(EAN5*6OAKP*D z)Kz0mGPJ?QeHz0zXZl|Aq&U|$lFBzJkBY4-rfJNYxVZNsHvY5`VVBZ3pcrykyIXDK zF6w+XI}^;}S+Rxvx!Er4yMXY^);*jVfkM#Uet0bTO69Zq`-Qs*)qwM<+r(JNI$n7# zvalw^0I{wM+x<6)iI2gRNb@nuUMQM94+;c1|IBuVU_W0J${wgLNwU$fPTF%J4Dk*G z9P6{2H^Wk*IQW;t0n9yPTuw+x)ca)p(W84e{nx}bWBbt1(3B2M!gZG~hv@8(e9sx- zKtadx|NIOShw(d2@HOYFJAUE>PMkccUz^_NUq;HDIB`;^aSv6WmB=O6!a9)S=wDXt zUTaZ2g8PWSu^$mfr|J8gw>CG}0bY11aWKXN1nKQ;&-e>mFM%6XV5W72ouVZ`gNW6*#B82!O0tY5np-iEOh zDr{x!jSHbs+7gIy@viM1)8A!OF#jgJZi5C7#?;?`$1l_Dmnl=H;$3xp3s_{3q#>O+ zeq3KaRyw|_0=3-6oONs08Lu)1DA4>B7nl6R;OBMJfyMJ;9@Px{0)oDXj2terZf(Gt%4R<~E(Oxsw4z76aqGr4r&5>9VB zt?&KJ!sI3Ee@O4)G$%N3FsgN|W-JMJD_#LgFch;s^+l!#%%%P@yLS^4kfa2VCvzg~ zOqxa&lb7TR%%SUdSHitz5AaKwQh@AM0?tt)6VB4cO`A3ymo8n_%M@fw zmoC?T+4$SDXD{Z>`zxhGk?W@aF$0@7Z^4WiGxg^QLnN8vf6YJt;ONn#SiEGZo-V*1 zF)^|D>#zCvN7a>_k89pv3$Sa~?i3+LVK;xl-#B#WFb=D-b?bI@{K@0|c>I5Z%QVuVQKs+bbqaug(9Ozwas%V{Rhi$RCKibv{+J z>A%Kz@wIC6Bqgenye5T%XZ^8wM|Sv8rn@kHhKDd@FUW`{>d=cF`cfqSK zzJyuRXXp%mb?etNwsIb3FnsR$=TWp+G5w&MGiw&!dUFK2s(ruo{0lgHMpXcAg!7Zx z@*JH%ciwpAJi2jbc^4UE%NC66HnJ(+N$C17U!gpjHfyRqQTECMEnDdM_@@0iBV8g9 zRxDdKZ%!E_*Q{AH{nr{x)jeZAmd;rirn!IbUaJ#pGM>3aiIUn2ml61osZpZ_UVQ0g zJ^sNGUPF!noI88AvBvXIk5b$`c~egjuzK=pV{Iw{*#gk^g|^66EdBjRkJ^gyE7A!S zX0>FNNS{FW$B~z5PzIITR<>Rt*s!lQFSS9=LYeQ`FWaariq^Cp+C>lN`XARLE-LdK zJ14iC#DOITj3w(7u38uc%1CyzyB)9KY`VA#FK<6{`KIo3Hrzeii?ZqBHf64<2J7eT zgR;5Y(NWq^j3gm5X3Rjm5`_Fr zyLN5S^}){Av2&-M6r51Fak_&}9Xp^?r;cdTrWJDJNDkjUaq<+hWzUArojdC31lqT6 zhupc?At1N{j(3|dVFDUAZhTLB-vtY1`9YaO_{NPkY@Jo_fIyA0K$vRrh0`J_zisN{>G9 z@6;V$nHcwN3nnu0tp8r_bQ8Q)e*m zTu<{?!Mp2yzHH<4ggzr-@^(Rgi(K=thB1lyd%oA!P2 z(j_#gUmyGS@7HackdP}VQ#LtK7vbOZmg$8Dnm5y7m3*HODf{;wK;C?LaplTYT)1#i zd+PlBd~x<{>QpG^FpP7@&Rr<4>R5g!Kf?}BiiOb7WC2&Wa3Ne&>-X*3kINw;X|g_Z zqHlf|y?mEc8$`y|WgWH=*zS4isi*P%_mfbhNYO0SFP;FS-deV7rAo_8ml7pP7<+vMe~M}n|wPihAn z?Roj~W!h2X>16uJUbsjRwC&IyIdkPgvlb8NIp9Wp^pT#V@}0Nd#;Et-$M@fUs~;e2 z0J9-%4%P3~D_8U&;6sNG;kT(%b;^3q$I+u#FDzTN60g1S1}avng!>vd)`=&afe&9^ zpp2Hh>Y>~4zWa>5q~+YXvsmV&V(0fWCZUp3S2l04-XzGG3*^`2U+X zLiO*@U!VY5wP|hhH%^@JjTZh4TjBP$;B{eq|L?#4idldDsfC;+JfC&y)ffdqL__QAdpSB;ur?o20-cjqEc)t1N1Zs?eSIv*%UE^c;)))<+A|2HZT_fOpyaUjr z{7AH@@IKn8za7fIhbBb_8}Fua@Y)Xiyk`P_**h6O@0)@td#B?1?X;O&qvB)n%c0d6 zyK_E1+OZHHY+sCbw=Khnt;;Zc+j6|TbroLRyc#cTT7&;>_y_-6w-(R*vlhcvufr27 z*JJSV^%%5t0|qYMgnoZ-LZ1by%u{8~CQROb9MN$ZALBv+$KH9?^C6c*w3mf-6z8C& zP%2cgki&YL-Y*k;CWPM#$0a2O*;r&bcKjGhl`3U(yf!<1*=9O-{zA%UPM$igL_-ne z&Ye8ou6%_GXw>LFoH~74HAGin*|O!T9hDr_QnF+TJskogp;oL|iN%YT=rMFc#^7~k zR8G^T&G78A&*Fm*M&Yr?9!r^oUAS{3CB`Fc`xl70c2wCESJ1XtDB>v6ybZyYyiN zvvKO=DLoLGo+LhKc?0l)&lI#*uU^HGLx(YW(j+Wi__tNTQL1z)eXcy2Cigex6V6yl zhD$e$!PEgtY(MwH3+lI~_E?(3`TpL0`)!^4WasuB`o5s2m0~_j?Slzzm^@^gx_3VP zWUQV%+%z`99Nr^Ut5rkKM;^{p=2LT6K|c2Q5xu>9-gxOFY9SJB7U z2W_5dqZ+8j3NylvhvCwZOU6<%+`f8;J2M7=-l%S)yE*j2RqI?0rR%$9EK=T5d2#=s z`|W-wMeVAuS7GYlso3=MrgZ1joGZt!V9A&zm@;q*cFx$TK5V>Ug#r}|pu-y-j3q(v z92h2fG2$#D?M z`aK?h<;s=Nv1123*rhXKRNLg}(PR3(|1xZc*ee`{kvn&8J*1+0_a1mpwfDaHW}==S zu1}x7+5>m5q@MRQqE0SB_>uWam?hhGvYAu7slOC!>TjYJa(vCd?ELpaT&x$qUy>Ua zZ&+{7tzG*MR>h0s{h6V|yz;{9qK8FI|d)>ftoOR>m8+QYTCQq z^%uU?p6U zzgbtZ>Ve2H0u0k;)~Be$6If_fv41a)71TA zD}=FV(PHSUu3z1{3=_&aRjO9ioe50N!~6SQgpHnq3lHM#?)2F;OVurfVzr7HOQ|Hr zCSvo9&BjtHP5L&`4YoT$$vP#`a6m(2DLvWqXGgtW^;|Y*8U1h~Qc_hAU`z2<+I#W+6jIKW#qw9^v!uJ>I?3wmp2F;ewwM5aX zu1&8lxCpiuqN0(c8qKR6XP}5a*0#h$K5oFdYzDV zF2}5qgxL67h%rjEDzOP^lS--PFskLNhXUCOBiKJ1viY$DtLLd@o$7@^UqAI;fG*it z{Q1{kzs{lRmz?T#PP4zc_?mwMSp1M%6|T#zmian&pk?t#{@?&S+q5il209+E#7^mw zB}(A8-+tF2jO@s}ze!`&&|9lt=gMW~HPd=9eJ7?l6jS^>$H5&rdQ^8@(^Gc(^eJ6O z5(-NS(p$GU)j=Y%De>01b0@4?y#~jQ9Yd75*BC;1{`>`Su2T|nDeeHf)H=lYdB4e*AF?e*N_~%$_|*dohCTN1uNEJiaG~ z^R02dH%sVMHKk{zSZ&v#gBC<3N|sEiu*{*@qv!3PHEXoc|4xaw?7+69-=2?k->t*>UFQ)S5vxNe?LkjtlOauT|Ks<|*tfeCRq4k~ zn~uoALlF6-D$fo^%=@n(=~{BuL06D;emml3JchWLs?2#5@e7Bkb@nrsYGQ97Y0G0s z+|(cOn+G6v>p&!&ou+o^8XJvQVsO==SFl8tzYo2Nd52%a>a!D!cT*{uvpj~?c@0n1 zeF4wZ{~w-j@C=GtBrNgw@kNh{P4L!zJ@8(mhcK$qgZQ{n7kt*F6TWQR0ppsq$M~jg z@lCTfnD{^&eBZJ)Cbw>hAKSLX&+S{{*N!dlTbCA?-nBXY?A8o3d#KV&m4{X7+Y~eV zH^r=h_hZ?U58$mfmEh-OSoMAP-G^$`s-t%8T6&pYw?>T`p>CZzT9BE@nIi}4)vKE_ zw1ag}9kmUIVwhl^SEELCv}xTMJ9qBFEF~2FUbqma&m@ntBlNh@W421QlT1?&Fa)w~ zo7ULAV<+a!ou@NSp1*KGwJVaZV{A+ecJJAZg=$~RSFF_85X%|k(%C58xBr0po<&%+ zcnK<2tc0pnWTI(?tk$eqGfm-nt5~t3&MHY~gS9q_@b8h75QFeTa}gDCC{?Jb!sfMh zywl@mh&KJ&2%Aid_rm$Bept987*VnIuPMiw&YnHnYP6gu;F6Xv9~yl4um|hmK98Vl zw{D1vjKt4B{;0(a2SifXMMp*J6UBx(MGPka4-31ArAwCJtP*W)+qK1j#~wwIqD9f^ z!Ol7(<(-7b)a3pqdj7fE77y&Kz=wN>P962+G%iuqjJ?M#!@PqHf~5`2&|lX zKk|s4Z;j{E6`_f|M$C#ie8dR!?cdL4vXsoi{Xh6nS9Kpg=CbR5H*$E{Vcfo+HpvbJ z3k8yofBFuF$g7cv3`^VFPA^r?qIWJCPrlOm(D|LtC{!hNEQ(){pZ1b<9?@A(dhKes z&;AqpqjvY&I=P9vpy#dat8MYnsE71#67Et|OCpKMA@=P<+yPY%?L+*DBWzV08)Tit zs4yg6+KTifD*Q3s*h!Jyg_~%0L9HmeqdkK`tQ36%+q@AjTp8FiW0jQO?1Zw9kjym~@ zp-Zi$x}%`Q6gqm0cAeT7pz5kVk35XNefwx}W_o@xT(W1+9y;8SuL<94*suZmscj$b z-3$Hu^+i+FE-`%6Zrp=q{htn*%G zd^l>?u5G;I*-(r>*!975g6z!cGwHmhyoL{U?TU9szK0=CJb{v>N@c1!L19%@-A6CH z^b+2D`z>@<6<%htPC5gn3Y9 z$3Ms)T|exKPH%T|SqP}`U&KqRBZs zznwe@uf6^{nmzD<&aUVP&L>w&?dzd#-7#kDrx^R$XBha{qiOb^5$O7_QM0BN$(E&1 z2Bub^h_?KE9rYXY=g+u6<@urbe)RD{sMnxD%KmtrIVjX}mV3^x$L|{U^ivo=aU$Lu z`99jVYlp%`3fm2TWaY&Bi5{^JKmG_~KmQDm4;`Y%O4_O@xxR{e-~4x)N(ptp@z|Y# z!t=$KUqbgDJ*@7Fg(0z?3McvA{?~S6DU|}{3cxRW`g0UJ)v*0(_P6HsUPn zGDmP4fQ){IutVG0&_^6g!_cA-PZ zj`(`QcpN-%KxcB?zHJ-&4j6#qJ^Zj!qJ+nJ{n~Z)*HtM% zy>wWht%;!dV84Cqw)z{P2MJSva!wYC(uDW~CE9%TM4AN(7DUmaMRnt!qBeVu9Psz^ z&r;)(o=c7!OtA8Br?|7$b7u`^$c(p0nM$$4eV9TwG6Jt1r%T{DFi&JbuNc$s+7eiV zp|rPdtIw+Yi~~r|srw~VUC$^j@RJY}6pR8&SQal?T#rQyR=?c6bx@UG-|(vf64IbF zNP~3e25F@c1f*djA+hNW=?0bV5)kR`?(Xi8?rzWGci-=OKkswS%=zogJahhGnBf}s zzGAIwt#5oj-*to!<`zAW51|KRCso+)a^oKP=NhvVXD)OG=Dt$a&3uu^(`)_l95;l= z({cvUn}aVM9M0Q6${uhKY&vM9DtBYm^lr4qL7}5bV}V3iS62@a*85hoUJV|6_eWb% zQX^?na3C2sXJpwH5pnlRm*dPvRON<^jiz#FI$SsP>SF|A*-z0v6#TnBJ&=QItoak3 zFXUY`>-L#3iyF3F?*vliA_X{Ua6^M>_MbaPw2sSmEBf&0?BLLHcTnN;hvQ?ru@UzE0YWWk5M z80`Xz{a!zCV^7&herSI`@l6Sh=wtnhd5pgrsk!ckT=eqT4#}he?wiJx@(^Z&hWd}k zqF!1P!&SXb0YjhJU3RN@-S&xBBi*oaq>qUujtv$96#2WFgg-&%a6x%rK!ya$F58js z8np8Fg>+q|1yCR2sKqdU{>F_WST}ES95`c5;Ufb~%4r+g=E?f?u ziI3zoo8%lQM$;_+IAwLV6%m>x00$93wf&sfPFkJ}wlqW-LemrKYou{)=@KD`#H1L` zfE~(eGQ8F~noPaA$DQ=|*1mlKUFi~kSMnTHMUETx@UgGQGcxpY9I<}f?4#S=Gfc(v z_w`(+!VU-X@%3e>%}6yv!hb3H2nMvhYLZJDdR^ORIVLv&*qP9@ULMQ(EGgcgnFG#G zX37#HUHkUpmr|KODV593I6W|VU{4k5jBUHjIc}w4gu}g$pU$+7q>;y<`s3qsT43d> z_R^KYUx&y;*OIz_iy&XI#QS2N?N}NBp~sh8`0veYPX1Ip8OS0$7y06;LC5>R=b(=- z&P~mqPhu-cdR9-sD6GJ?mM_(dtwSr1_e~pF?bQ85kiXU+w_cER5h|EU{7&{z`~La) zN7FQ2wR{z&2*3WU$`zq~znr)>i~M`lSLnP|o5y96idNqy+)Iuw1-$7~2Ex8o_YU4Y zyR%bYo7p%19*>;Gr>n7xr?FdNPNPon_jFA-u$J(1{(JvtnMSb*bUKjzdBgNgv6B5u zi<|qD)C*a8Zv&qrhLvjP2O68$?t-qe_*m=;nJp@+L8M>(tyGlcU81AzJTmFheu`DD z+5tV%?-UDPM_H;9ewr*7mKgSY7+0})I*BpKYrDayny;1H)e+xzy($Xx&G7xgz;~+s z_myM+VA6U{YWCKl13ZWHBcETrmg7ikAn7`%#E>YZ3zVwt-u5RKE8l|leeRgOHV3A% z(k0VX%EGnY8yJ?emn|JbdS~=qK!S2|&bq+hS-zrYjZjdZ4$_-RT<0C3fHxDnyVe$i zNUpZLOO%@?g;}!~gMvfzL0}9F^sbdS$XXr)fZDgvI#EZ*} z8gB8+56q;sfmd5+iyGX$Wq;&$kHb(RMs1V+Fec$R2QOT+qM10zf)z~#;I>0TvBwY1UrlNkm-5vXm_ryN9Tlx(7 zt=Uhj$2OqE%z znkN%bZ-Xt2hK&eH8fPOPL@4hi@!M-S%9mORl57^);;nkwk{&mI#+;pRMBr`Of}^4l z&$}60w^rT^EQ-cTa9uqsEm;)S)LA#X5b~C;0BNyjfd3s~I`*Jg*RS5ioh+B?aTEXQ z?k~-t%kmEJ+sv3n*%F0C0{y5VVYHj{Kt~sHqW;&CF-fg zU^qh(>A`+ySM^B ze}6h@H0i?$5!`<2opvwXNcFS}e&~x8I~q!;z~}LJc0tCQCV{F?={%6&S?qeL!<5fE zH2rXoXqOrPEMTtG(Yk{nM;CiWl=L;*=Uz2PQZYf)KZ_~mod^4ZQAv*91w`1}mF@BY? zgokDMWcGcaajDp6`P{Yn(5aoxxXy-cdbleM1(IdI<F=jkpWWO% z-n|Pft+>9qbAGn^$au1;I#);e%j`CIklcjlDR_(6$~-gJiuaA_NrA3q81tdSd)iXJ zvp~eb3r#HM%O%?6evs&Kcv0?szg?w z1JPVBs9Nd(MNVJuu1~fl7q=oh^`>#elt5afA;NLdT|wd50CIi9IG;wD5vU7m3O`sN z9bGJM1670DrJtQvuK#Y|$AgGjM9NX(7}`-CItjmTe3{YOcPUcI4wRQ?ymz{ux7rvy zCKIK89^ta_Cz0~mA9Ea-NJypFdKBw#&UXl1?IDj(wK2rXcO{bGz4Rv1pzT!MWgGn^ z)5`r$K4PJ4JiAM6aDFSFkMPwpTmXSmB=)Mc(V*2lZ?)j{)%cn(vi-IC`?a7`_RB`< zzi4k1NhL_{NE>IAamYqr{k?(`0_51<_`N=Qz&WV7q$82OhLJzgd420pg?!`ZGsgsy z&!%}b+r``Uu5yBoSu5SN1;~!SY{#lv`s#gE4>8;0t6}R&iyJ)(%pP~vE&2|5>M?AhC zaxKyn0-|vnsM5Gxg4q z%9kEngTmHp!@c9R^7kwC=UU@9PG+z~mx2ZxnnR-fX0q#qhG@xpEQc6Ys_1KO_Fhl{ zzy|HKgKi&4KGK|9NqiwwVPs~Z+$>ck0)t_I&Ei)1Ou*=X6@ZB0>fx5PH`tss4U=U< zBSf#fBCwqA%&5duL_R#@$k8y{qs{f#<+b$RZ$G$aSrP5F+4t*yiAS=sXOO#_7KrYD4T_Iu|FzqX@6%^XilDBeYxP~>) zOgm6vibT2Vz#EP`6NK8X?k`Q_jt0co#W{{wZ(4wSF^76e_U zjWg`6pGsyK7((=0tvx4VwVa1{yIr`Pbyd$(m0QymIYP}SN@n~!J*wpGqbXK+90bUX>KOrJ|z1J>}!>{ME6wlyJGBtu) zx?e5V%4Tz&M0bvH31q0qpQ1*+uYQT+1G|8=e6tTZdKi?uTG@Ka$m>c-#H`y{Xf{>C za`xOo_eDpxj9STAKea5x3HuN>8zB->x%{B3wp#GfwfMf(UJE^5Hi>$FdkN|pNGhrZK5a#>O;6@?qi#IdwU`|d0-C%oOKOJ86uw5%4Aq9_5=M{7vJJUCO~9^riffX31Coj0d9 zo-)(=qTNXg?UPsBv|>X;lP>G;Sa(vtQ|^yle`+_pBWBfny#E|&EHIns)c@#cjI^R( zoRL>X&Zf5` zcE{Ap-dV!)vN0izq;OETU+m3fZkmqmGBQ7dYy!?yC#V)XpU=9zIA{Yq1aA)Gp`FV} zqM#T&`2x1nZZ=eQkqRNu|CX`Y^JkjP;QEt?W3BxcK4N27~l8uJvb|^)AhG zetp@s07W4Mk*kDMH3Tt+HUp@Y5^}Bb$_g*(EB`f~HY@%+Ze}TQatdalFrE<7LbsVa zwt9(=cU;9-cWy~$tE@euJ~!uzKdn}x-q*#)8Ey@?@I!?8TrY?zC@6rF(b4W;9vSrG zd{iJW_PBq(03C7{6xu4`?4gT8K5Su5R&K4PTP7vpeY?Q?)MfUGvP}SJ3`>IsIM6S3 z93D)k`=kVnx8KVpq-%tjlOeM?xHWq&qEe)T7V?TEhB3-p>RS|k z#A%4Iq?Wji@ptX0pqx-2WopfpFWuT@buNW73gM#CvCLt>FEy-B@ls~%k50vxm*`|d z=J_?Z|7PY7rV&2%D9~&zl4H2SWqGeZCouVSoc z5QZ@cWCJ=;33vRSpR%LPXj0SL>TTZeCC6VrdZ%7!oiV@?zR~l6Rn+%KNOfa$HyZ z^oz2ryQF>uIR2Qyh0-q>vfX%SuRp)rzCk0hK3W#po36ph8A=i<$&i>hrt(xPF_0MZ zRk2y*4)0*Yt3i12N<4A0Kru`DPt*{b=IhNgc8f0P@GUz|xnFcOqvGppA<%T}xAKEQ zHNV{p4zXg>>M*6Dwj|;EH+cYg(h+R2P3zh}>1DyPpOD=8NpffVQlw;%a^=siRt|yR z92`b@_qh1>=1^)EU~Z4eQrqy5k0Bv>6MZ*@2cX({B>||r^EnFkP^ypGVA2uur-TCc zH90&i691oxIUmuI;fvgUudF2Q1obe!KgN0&$RBp~tM6T4lb2IPQXGDWcnV9Z`O$G7 zTvKx^lH#+0DW2Hl+;S=!>Sf7wYw?Z%%0$nn>&P|(#|N~nRlDmlpy+Dry494s9?*_4=Qi=fF;oJ4Rm<{0K4)`0e?#} z6#x5%PIouwvqB1B_E0Z1%x%=|M;5Ln%-L+rN|7Mw!;u^K` ztzP$+ExYJqy)&EX=(_CZ@_xoadCEui!+aoM)>;W07{_~^dt{KpvNRCC%rvahG8DUK zlqX7((Gzj@gg6o_YX)^4!CdQpL`Z0h*|Q(Cm&}JbT!0Oe-tnr0OQE1<;MY1=c&-1S z%2(0M^X-Y=~h_Kp5{pPxUe zpcd?oyUsX*WV6)~pq-}5GAsm-h0tsa`{heN;DnWgb?q}DV_IfyTCl>;(s~ zm{@3T+&$bLx)4eEEk%B}Kxm1M+iNYwrBi%@0Xh5TA>Y;2wQC`F(b76qSq#it=Cz*5 z3lR+IVG{+Kcz`_$%2fS!uZ5~`4&2#omzH`qi?**yKj?n;7tqfGnu|DAebHK%Gxm&OYoECjuLjWt>M6*@pw ziD12}qZQMG3J{Xf;VFNPK^mH?oRht4jsvgIg>!J*KdI)(0K7p?z)*S&J|NSu=f_P_ z(@8HwU`R+?8-{S`qb+lPfA}vU#j+qV3N4}J!`*d>+qF&o!_983aX4Z0N`6_pMXQj(H8=kv5}NYf@iLlT#yVypgK z`uY2>0+W>3#P5&FmDg(#!lN~NM~R0VQsOuIhug6-#rcK4?|9&w+0XkYg1!7|WKM zRn4Ce%f1QrD`F}EbqgM5BN=`klb73LQd626$-#q@JfAx3WV2nmeGAURcX(QCi|v=d z)l5*~l;ST+WLf4|Tw(4)+Mz#%G$F0uF(4LT{J-SuXdz`N=c$o;}B6G|wu^ zXUuR%87CA=j!W)wJg^iUZZ_u=!-dD4+n!Y$t#n0T9NfZ3`qa6=JB#O&H_I z$cU*&X2t2oo;8wp8K=dp%2HK~KE2!5R6g71XhhtclT>XeZs3yFdK>)8WKut7Ew=$R zOY|?3Upuk8J(QNL?e69gnbPov#}D0;LR_{|PC@l4Uw%N6*U&u{rDPgxsgWK!qK;uc zU#-;SP}x{B#;e?foZ?e(yGU$Y=nS}&XAM0LOMv@##y^RHYlT{8+!a8{uAz%%d?-~( zZrZjN^^>IRA%kZRJP)0(wtK!Lg2ccVUQm4TDl10pbMPysV9=emniZ?VK55ur$v8Jf zeDc=ov-1J(;`wm8nA5TsVmTZo!%_?P-9k80HR{{%DvfT=3mhVFTzGQ%TllAQ>0*d> zyOVORbs#=iIG@z4#q*(vT#4b-P{s(Nf1U`*fI~zdpF4iqoDvLjhC5K> zbj_qxo_onxgves_a!n(%&bAl4@GYOzKOHZYpvtVcYk_rN)SHjJ>XE2oCesW(L)W*r zL}p`cI+`vB>Lm&F)85Ou?VX(lE#3$>pER31?m1I7-L6jn|FLoG9Mg5+Q_e^>UH)`) zA0rQJaXdNDoVYYRj??sqrD4f~9?6QVI}}5*Hcrqn2D}+OB&{nwa0KC8FB_1Ji}(&1 z{7r23*yk+jJ%w-gINTL%iQ_oh-hrFJ=V~sCD}^O0unNl}QRY=r|DZQRk!Mi~X29LG zt=Wpai&kqg+=zT*rc{!_hgUc;`(~BoGeX?Lj(6zj&U&{An=E-tBAZ>{4)Ef&NP9=8 z27%VlFJi#ll5^n}pBA`+HkjfC)F}97UeD18f3m52`1j$ORJz;~*t9A>sr{8q;ur(O zOAylV;~qDp_+{&n7a~gUeDpG-3k_Av?RduA2Xz=0mt|vG*T8Eg>A-$fh`>P%YM}r5 zaC0-qH5;@z!ra6(k|5|JR5=b|{Kyj7(- z<0>kV+^Xl6v?4-Z?w8nJpJD$q_yxew8{>5^r?R#l#q0l`Xq9)@dr&#fn2-u z488QyAM$aYWnqIjM-jC8EM&5oPqQ{On94pw^N(f{)0x(U>X>xnmV{@cS=kYLf5XC` zH6V>?mB(Jx)q;fpj0b*EzSvww*a(dvdA=y7tU3<2+o0t_?Z*Ha@GV-V+2i}i#Ri+# zip{^V%diuvn}6j~UYc&x7)4QDagj>qoV1d-r@0d_JZt3nK4LaV_3=#gROoYzO!(72 zB!b+cBnY>1-Dv6l`<5-)`{-zH0s;PQ_qAiIq}R!)sL4PDT>Z+PSf=Y{{AFbarivw$ z-w`#{P@TF6`I=?A2#FU1AQjZryunIsl&!Hc?KbYcIQ}!qHT}x&1TM2L7-V89CcpMb zvTm2)8LTf7ZU>S+SAhG!mZ)>vJ!N?RYAk9p+`J1W)5 zPOGny(zXn*sESyo5^Y)Lz)5k$G0)crcOMJ4l}bhO zFlvtxSDT`73pxAP?xBYTQG}mrTYqJ6C7WjbqxX@J0WnXb^)T(J2hfo7KTk0DCON47 zMYVCSC!U=nLjD!H5**ut5Smuoo!+GLzyy1F+@)2nHf&+B?v1fjsHT~yUKA3KgD}{1vu6v+K<8<@O(R;tzU&RrOkPO&)p7<>}0py zkcE7UIxr(7FVnM0dlQ)#*Y$n;UqL9dxTx!`*qTrI~}8O zL)6LwME|lbawG!3725p#IQ00H@3=aXdPRRJBLp+rs^Vg%?TShRVxsX8`M~;C)Afl- zUwtZqNH?#n>7Zl^vcGBxsUbTufF7QQt$q|-Fal9MDd5{Sb2sobp*tUgPa`jptSJ6)o%lqrS5Kdm$} zU=SrdgyUGZcven$wmtverpgBZqdBHK4jVN?+0t-eTv*>sOX0=x6Qw~-F*yB7a#9X4MJF*1hxj!l zUL`h!BvaSL@lXt77*^Gv&6)yde44J;#V3YZJUPN?49PPwai0Q%KGD6M8EYAqy|7|6 z!?^5P>M@s^kFmXv6aW3^pSKu!=1~f??c3#>LuFTFR3|c!Us0u{UE%n8F|_#vhoSf~ z{E|br%w{9a9;n^Wafnz);E0@XE!_sF;PO+aBM)TV*WM%*9CCbULcV-HsAC(9u*iYS zWIkrShnLf>m)vshw)E*=*RhgerqOUN(z6B)0!_O3I)*b%!C=8oN+iIKEFcj7{YPq+ zoYFkI7fLc7jjLm{#jzG!dn9z$34K8L^j}vqQ*RkzLy$A3BdYo9j7^+Sd;{*Dy|Ds3 zOsEfk2H+`n`RGi{lUf#ajF_`Ivc4Haz`+a%6+!oNiPNz`Z20La8`~NNOjR{}Pb57Z zQ}qK7z$Y9GQ0*lX@K`22$gCN)ML}5h3Rc1fbUqLg66N7G^pm!G>r|F?Xh+J8Eo+VXr{&1bg;p)>CyDIBP27*Lxgz2 zMw4Wzb{ciz{n`-y!l{Hd5QFey6sz-VNU}4!8#jA>@R~KAhao@aTR^e+k16x2>i;!Pv24S;5%xY$-S93E z#4>jOe=OYm|4twJj&`}ezCKjV8}#&=aZ>}YWue5QvoPXOkw|82F8$A~d5jgk2IfFAJvmGFiI zZ4B_}fMs*-KbFl%ECG(C$t2EagJ-b~^r3BrD`qlnIAXZIDvV(veS3Vz=jRh(%Ubnl z5Dg{&-v&_y$-RCXO&t1X;c=*;o^lPe8@uo##St|vC2^N32@k-QC_4FHt|fs$nIz#; zseUwtPz6l(h#q{oqJc-x=G5!|{U0MB0%cSSoX`Gs*TB!#g`fdU>C>qVPXv+Tu+Kw5 z6^poF1a=etWAXe~ln`n>S)9<`uEMw$FQ?A@qScc?29ARu)%iJjpM|VL$K$mMFDWOF z)PB6VyT$+KZ=cFI#&l&yk$9EUpR&WDD1Wjs26F=HET%F&2T^Q|2=?Q6NBhsGO_y>+ z7JT~A*(>Yc%~_lzE+49h$&P3dz%8!HJ`;`n-;XZiFf)08|H~nLgN*MtLE%EndE8vg<1>Qz_;Db@3xcb}Gcgk*+2GOme;y9h zdH<0Ci&Wnh^8>jc{FMGc_T%OLFJEIVR4(XWO3BA*?|*2y2C`w!IILRjCeg925l*HwuJ7eXbw_j$D%NQ$J13nkjgB`u)gO+|RY8>nw*+`MX4 znDIlCycW9;4&lgPEKmj!t8jn_daYS$esp;mP~~Xj`QE@_6#_N(w&MBsliSbL-rlk1 zpu%V=)w$fCrMSmyLmD8j+Zps?%IZKGU$l)9vuJR44+AmX-bcDK$M-MFhQr3%8XOA? zi#B7;CVbKjK{fs>A2{;wymr(mQbm$0dtvzE?KD(*{D|t7*jt-n(g@zeX@=?Jb`Ev=J1c(A*P}KJ9w<%z;Gt&lbX-fHf9*deu2!xw-4}t6PH> zS~2nFd&Fx6lABM~V4w6(nNu~VI31Pq=Ec5WF927Hyng!7%V#se~~5Ql?*-p|!ML4n~N z8*s))R{@@B=YMGo&cmTD{fPyY^biIPn(t z|Lj^5qnl|=E#JmAI;b(qf8Q~x!y8jzcJB5I@Gy(9Z9Eq3WC&qzqI8;n{V}#aviVuk z$M?CSyK`GvvmJu(K_+OP!IX}` zv%+gNIPSfR-mZ^9mU)|tRC&LSQzOz+?Ti3PC#+UXEeVZG^ zQtEcz$KkE3Wt`(d4ZWE*fK_&lj9@#Iim@6TeQM#IvmB<{F;bz?xc*XA-FlB^QPYL+ zy?^H5EIms!Ehd@6o3NAXXJK3^0V+=5Oy*6EL3Do_bkK5J&IdASRxY;(;Q&3Pe<6qM zdbbxa@Mh>VxPU?t|-kNrTT4?YVhAmb|32?skc|-k;)|l1i^9$e9 zqKFc|*LMN%^Gk?g$NT;={PTm@wn}eVrV|T7GSv&8K;<;9t~ZTRn}c>oItk}+he*C! zFZ8V{zN1>y_$eM(q61AAM=`u?ixPVIopO1Rb+~PZ2P4_Jzp#7Y<$4H9ZA=Bd#`R1( zN5sylF~9Dl!&I)cPW{PSYo<8J%iJT7A+2s3R44N|9pG`=&Wa7?3fHHetAgpu#<4yH zeB5%4ZKIcy6H`UHT_Dn547L!!HXIiY19_xc>%-o)wQI|X-!E$&55w_voLba(`BR)$ zk=r}64tW_B)1GvO5@Ztb7g7lck%HuIb014n=s(6fRnKGaE{Ybeg zU#L@g@+-?lcIs$7L}P@c&C1Z1=4YuWVUn`mYsaePc$)E)^mN=QC{?>3*;h8#{K7w; zY?atG9oB!zuRw1KOOF)sAazd|J36loap)j2Rc8;t?tSR|;3B^e*lCTZpCf3DyC6c! z2$u3kvY=)t0K{aB-qm9T=b4UWn-(ETpb}LU8iMULAaX;%(kw;)SW>o2EkhicseG%% zmJ8N+o&5^=j#;gB?Ez%f#-eL%H@{%5NDF}&rAnSEns%*S&o?JZT-7D_OA?@m&KNpw z@j(o8)auHXODvb#2UleX{2i2g(m&Q3OzC=(C@Ed+>bD5R7W%C%ybn2UiOSk;Cz|s_ zI5m`Nsysj14DeyQTx+Q=(s+*r^X@w`f12|KiKf*mJEfWsD<_02uEpj&h;=^v{gSHz z`nhhh!OX~SH_POHao2Bq&3x-`7_e4dBk;|;_tlAO4gT`nGI;f^v*K1)sypw|tm5qH zme;p5nkZ7#c_5(-w)paskFfFD2t2E6wbaj1a`0PfO*xw5eT5aY`&(pt)+qY*ye0@V zBY<%{o^BKW2b3nyx^@lsLR>HK%{VaH)~Am^DIA|_p_b_cPBhlEm2R<~*Q=*X9=A*x znIuvvyf4IFfc>Hj6K1JPk|Uvi2ACAo$pktqRK-mU4Sx!C$t7^M;OV-h7wM~}E8c^2 z-bjtD@!mom&sZx+UFB)~v=KhQjQfiqDhgYLws-p8wH5Lb{ z-J6#m1Y;@#!$P{^`*@12mrU;(R$dzw&W80(wS_q%kV8m8hrjV|tQm{mN{b>GkNso~ zPa79qv?k&HjCN(i_}yQS74U|rQ@=46E^JV2u;Q+tD}=W}yj~8ez$?-FYhSUr%3<`P zBG{-ckegbGF5tdDCZ*Lek$GimUkw12Slc$clBF|4!>rAc6y4i1#-<$(R8LUW+&-mT zN@m$CpB8?Gcr-#B1Co;JNW%7~EDbvTm`yvcy~_Bp`jk2&a$Jqtw959xK`=zHOGjt1 z`_EyBq_a(?+(Z4ZQ~}zPn+2lFoTH_Vj}D!kY+|U|$PvZ^30&2GA8sFU=v_-?#tMwO zjq~bjE~L)uSa-lbjf7Mz)ZR?s)JP!u{xB;DXvx<=cGst<2$-r)k8U(@5*flnv_0(`oN@!cB1jSjw-^KV}^9~Pv*&r?uO4&6;J14{tKO^)}e z%EA*y$y_(q<-c`Du6&PlTOMCmWN?>r;S9B`+D+6kW-gQc5&B^$yR7eQ4c2f-hKO?H zbq1fn+_CxmnxUS=9bEe(Wz5Ud4j#WGJ?e+^O-@iS0#XKf@KQQs%KVQ?pEejl>!Q4x zT&~oiWjA+H41>A0#=frzHv3LA+>aIcIDyx0%v#OQ@jw-~^N=rS0u-5NYEO>?Xmkyo#alYQ~-)hM{W^C=YKa3Y@&#A8w zpXq@tvnJ z^OsGAy1)Zd>#xY2YQpdU;IW1LR>?KUF9F$!<<<$-dUi|Fjk_`o~-+wLH(h%FM8r~8av|xwZ zmeGvUamP8eSt82{1sUF6&!HobK|Ic*<7(8V9qB(r@Ysn5ym7V&=8-3{+WUfQDfww= z*pR}LTX_7!4A-P!?f=w7M%bRj&7X(@y2OZd?q(%l`3n?qxw& z;Z(kWS436%OHA=;I#;mPK^Yg_BrI4y;`H)BEQ_u*`JDW&1nb|OGon`CvfF-W!;7Kc`fnBmC{o^`SP^US%hv-+w z4-Vh?;5M&5tj-S`%hLr+#+KylgAYzMf+8L!>aC}ccfJuZI8&KckkQ7Y6Q!3@k)xGF zs7aRk3q7n%>w6*!xwOV9cC@D$x~*Fih~ID(tYh9x*N^i}7z~&vJO23H-B7v_)wGL+e6Z$IoE&krW@ zU|7sm%Ej`hq^3(nte8F~k^SqLaid^O_myqOwz%Jt-YkO>o^fb~ldLSg!bn%trL2|6 zkaUqwLy|OEllct0dw!w+?fzszA}Ns*&oOu7NGE046qe z#t_SkGLaKfV7-bE6X`OgOL0A@K@G!LzkEzjg14*_nbi%7P95O2QwR!b;VtIKp0yrn zn~^Hg%;{THpkDw8=;xTVO)HINE3%?AIPE}ts2{IeRz_64(cUYBbj>d6S3bsVoT zTx}B{LS_zgR}7r-n4fk++igJAx5g0hDP)JED^;5Z^G3RuIfqU$TN6&@s?SV<8`9#B zJ3pmP^Za5FpAR!*eqUVPoW)r{Ts2+I_Kx+=-MUSUYkiNloQUg;mO{FGS(q5sxntmM z<(C56rza325fYidWF1f0XlD`66#pJ@S6|9z+u$q=dDiIR(g3+S_)6l6l};ETx>23z z@&VL___)~bWLOr=UKtM%LJRc5fV`EcXteG5#O+~I(dH*;TjC-Q&RHSkYT1XMl4|oU zRN4`TQZjT{^i5_8k7)W8s6rr=nN0tsmw}7I2Z(?qiC6%M`zX{9)C5I}plf#Z4fG>(Y(b#NBm6K;Mugvxu()&KAu9O2mJZ^R zxgrQpTw7oG9K0bhl|6BELdnE>(=+%>{r9(j?WGWiVGpESCqy#ge9jQUoTRwj0YdWx z67--}eZ+Wt`gwoOdPmg5TI_;b=9$ZWE5?rzgq5@hG@kny6VUCw7J4KUqsX6xlX1lt zbZHRSHbSCKPJ`8Zqz^`r3TF~G+augBjOroU>H1d%`W9bv7QF&P6lB{Kj5%KIwD*QJ z^q`6uti%QWrI3ms?ErSpuq+kX&5oJY>l~z^%cZALwkbTIN_M2c+N?7g*Y zi#di%3kn`{J|u^DbK_mLLW*y&{V4P7{$8<63k%gm4FgAK zJ1A_a=__1Qlh+IxBwQiI9g{`qq1YA~AHR{uIQ2X~Iy?>5$0kAnV{=E66ll7%{mq&*vMSo-^w>bTN1u=8DMy$(jCDX(LGDv@!Y zCM2iJEU$cSpj(D1XwEz(0A$JELo6FBDfAHS4kNri2E1q?4g8 z==o4zLIXLAQ?#1pKtZl?NiC!4VA83O{la;T4Xg?7*P!FHE`%F z(P0xKQBR%K8a#cd6|E*FNaRb=VCj_aU8voNF0LbaXy% zu$Jx7u9K7lKHT=J_J%&IiRFjuoYb>J1zA}Zw;G|$jw{0*eg;VOg&y2l8-~LdWq>A5q341#N zE%FI^FIUQ+41Y+U0(ITKrf=tSU}Ja(U253lcOmu6pAL$v9=`~Qa4BpYtwOk|EO?=t zI_`i0fi*GBcsVxe29}X;ce!p15QJg`*tf0@1*~iz$SIb&DP7527?VfdF+2-j#5b}w z{P2Q%r)V*}?d3=2fG!QDSaVz&>L6~>6T;ZuAajqw`y_@P+r+_Xg#cJsrdp;|UyI?= zwfDUuZ-9jYZr6%6Gv|ZlgVBS7JkeOwT&D^D0vga<^DYx{=%v`YX-}stE<^ z1tPpZmoH*N)h4eUR@*s|U(w;lMGwr6iIHE<143T?m#D=)R=>)iE2os{v*Pccb^1## zbyT;;idfJ|`9ewR9MeHfVy8w6AuwXI>SP6o`V){J!e7vAZ#1-szZ&$FL0u1Kwnb2aSVGK7buwVMo&a$RyZ90R4% z|D()~^Md~+EU3PwGKZ>64~ zawa7yqp}s<*NJ5Q+;2jA5qzvPxf+|)ZJ+*4dM8(wcwpO!Lbyz6e*(2?6Lr~(;TUE( zzMGHab($IF`0nC1Kjt%Wva(GhfoVuiL*U@Nrx8Ue|KVn3@wHI{uc2lG>e$u;v z_BOh8Mv+G0Yul3W-Qn1@Rjx-3yc1fg)h$w5?e)b{39|Nb5=EUUa-Tq@J(obbk3GIMCkIuf^j$YPhE)+CDdjIANwDwd8TX$6bq@YC85eE_@ilvED0ZYGRx8wWCW^{7yTg~V<;GSFh24aaW5q+cBh&+NUkxnBh32<3iG4Qg^vOk$Uaii<_@orE_@^!=i@n6aTDU{RVfr z2vB9@iG|a3g~uC+NXZ1%o3b~@?e>LkPhKR`f1ff+;78QwX*+YY-{_eW82Bv~e$G>MTFDxRC-dz(+63Crn|##^%uJz3 zlJlJ@vD8L)EOQ5;B=S55Zu=dT;jt~>>sl|X%vw#Edp$V)v?r;*HX({)+Hm_B2Lcr{&dc-6 z%Ujm7b~mqL$Qp7xh#(3|*2uN`$c{oi#G&xl7QE#2jd4f4lL&rooK`cpzOGTGB%8 zC(af~EFM$K1YLI6IGU_x>tAmW*naBkCz4h#Iq0-tB(i8C*{xL^)7IVGN4luJ-LBt; zSk(sz>DZ%tlD!jf7;%G9!HUU@AgU5e7<Wx_%86s$8Zb_YHkFsCjnO^;Mdg%yTms z<#0TD4qwn3g55Cj-R%2@AD)0phgd~0gHl3sJ&hBzN5@>4?DrCf+#*TZ;C6z?z=+3z zLuegCr9 zpWzY8Z9+6e4C1$+sG0k_B8u7muH&=;yJcNYiid`AedcSX{O{&YA_R2(=T!#~ zm!G~;2|&t6wn)tO3~gt!jL83dpgqWBsl=xw3Z)Cw@Xm`LMn1F5!OpIiVl2V6(gooL zQnHsu--DAcUrL-^!DxQYLx0ZsXxpm_vY!Q|JBg8SdFa`T2ne z8AscVXWLb!gncr2y9W`?O~rVl>`sf@K3_rToN6!`5)cmnz*FaY6E9NT_*I)|II@R^ zKd~1Km`NNBdiNC zxPkb*_Vmm)<`jV^_Q6>wtd>JRX?-4ee~TdzbQ&`}MQOX+3Mxm~J*%K?Wwn}Bx+~fJ zE#KuSgWT4iB;z8+M}G+uDAe2geaanS+OtB$f8GSKd47`y{UgZTLDUqIo-2~Ke@Qd~-Lw*;301&X@`cZ$2qyRZBC6K}qxH<7!X+tWH%Qy`@kfco}IN*PCH3xH3KC@Hy-xLz}yv^tWu z2NSnfo7Xwg9Qs<*nfpl-3ryZ#0oY9Frd1!1o{QxhaqmHxou*aDc~nXh<~22yXQ(xRHRshH@$XjjziPrs+1na>4tSks1i*aK z_qpGG<*m{Asa~sN&7cl4f6oz%Gsc&dW5@|#=z%`a zr6dv`UhOAeH*t9)>v;Y`9mVInFZu`)@S;%8_K8YTd0AfQiyDOT)2=OM z4r4tt6*Sk`Y=4f+hfq*fyd=B?@Vx4Fp++hj$#^)B;Y>^EA-`F!Oq=g+e_Lys8ksP= zBECAO8a!VbauJKz##c2T)kLZ2l@Xm8?Y0{`Ne5+d(+7o9M6XJ4@7-$-6Si z+dS9Q&c>g>!B!4uh^&PKKhi+Wcix3lTVN}JSS?AFI=1e6kJR}^qbb}pRWKh79|FJn zP~vWB{2bt@^W{h--0bxA2mlh2xqEH-f-!`^h$5iy#$}2tYZOw?KR*$|Zy?PB4TXHn zy=*g*wdOON|1>^23;2y}!tE?QcTRO}t!CHo#cV%<@71MO8)z@#wxQKFVTOs{YSqsT z5=^wvf70L$mq=y#c3Xt^W-L`=AL0$iOAoRYf~Nd4mddF}@JIXf-}AUx%YM-Wwu92W zvL{!;73@gV++%)>PRm3S{R2`DDEM)-8%MQWy}w^!>r7w$SFzj?oCYaYbRxSWq!rRl zhb}FNxNcKY2XINrV+mQOgZ#vv+4siYP)D?Deq(Bs6d~v4@VKYlB4pm1O@DRJO(*#_ z0A8aEyv7F7t9*)?k6aOG>>aXwTHDD~d1OLTu8)9Q`v7YS07p+P zCwT$scu(d#o~G5pJT=Arn`ko?(=MDEVZxNa`_J?cb`Fb24@3U^dVwYew-LsR%WV~n zYf{tpBKaw*#KZEZ3V=fCuke@HZmVhkEoclJB5N*96?8i_V_2M;ejghT6?fauQiJPwQut(6Nb`yAuB zU3;)|*8sg=-;ZCd$VX!`8u$?q7wChTo?Mlzjpf@Jk)r|2YML*%cj0iAh)PU7ufzaPd{DXYl0(NQt|IdgtD zMEtl_nQc6^(QQ9s=xbHa{cC`lIVvAH_btWrJ^)D}nENQDZxO9k@UJ;&yr-*W%t;Pii0AXgz{4q-2<78EOW9zpUMMDE z!j${YzoNfY8l#VkTom*9?d0@WW!en%)+1x$DJe5=?mR#F72%xDNg#Wt%z|EZ%kBPi^&69 zAG_oHjPAg0gJY?4w3Uw=8AV^m>a+hzh4A<^>U_cqyMw0BZaIjrzPHg~|NYssKt@xaluZFOG^ful21L%#R)i_##*5umFp!0EY}d*J->OW=jv81ZzO6BP)-?0n_A z{pn(_IeeQTg7VInirjMXSe?bBt%!||f4A?#{+8Z&|E5NrpMTXbShiMr>#x>R%G9Oa zC|b#@x2sGUjpyru9#H9w)%kdSb-10u2}aiLoFB(;& z{}>&*J$qY!y>TiSjwA35t18fXMDK{hYmh_HK_53;a5ZrX!n8Se5{Wg4fgZyjnE04a z*$gkeaH+*KRqsyEVx{O(h1mBHfgbSwh2mdmB`^VVRump@>1)7jyIKOd_>@VQ`nQp6 zTf4x#pIc;?K7n!}Xd*`-)~C-x(u{L=Kfj6&!ITq;PTm!+)8YEHm4co^mEhbz{`G*` zf)U>;y{LsJNey$7skSRi*`@(|xx@(AOMyDFqk(RdW5@e8(Q7Pdle05(aqpAaY#HXX zkGvAPg>m{8t-4>`Q-$9SU{snlb1eD3kXG#wdEH7Hq3$;Q)pw2)B^;*H=6VajZj$=B zkLdN~HFFgH;w;pKW(YJ%Bypjfp+@8gqG-{M4lLDM$d(H468EhASyB|=QO=MWBA6ez z)hl#ZYIt1vY86L%rV_!%Wjls-bL=_i*}3pj7Ye!-RyI)=6C|su&kcUpeu74w5&&8K^ev0owt4z;3+yKnlobn25WcwnWfY@y+{;XFf+@YWlex zMzr<29fW<_8(pP5RPAs(ZIy>V19JKuvnuTICJK2n?=rgf-~G2PA^KtBkwmdO8Si~XzHcFN!u?yGqo?vyT1W3hZtIAwze1(r`R<(o~z)< zPN)%2>$jGj*XdV`oi|^%P&P(#dnRYEdcSyyvBQlkG>;>?RA?OfI8X`R8O`)vQ$k0L zk-S>-^Br;MQOt6Lmc)oV*Y3EFt+U@YPN>(Q4_+$~iBTkC5Jd^{*cU{$na~AtO`ZPf zKaI#kNs#rsU9#r-(U5Pv2@G=EJCl)))*MiE>-a8#D7h+kQQ107tLo7Q!#R$V-3MAx z4A%I}N4gBdJNK1jYS?fORFN==*u@j8M;Lq^&L+f;lu^92k!;PrY}Zf|V&UgP3vlo@ zBD)-qid%srU*Mx3m(!ZE*1v-2(IC*rE4@Mjp?z5EHAjc)4 z2(PuS>95@0ry4!x4|_v!!geW@$W1gkGiiP&ztccTor7(D(uzL%bAc(*?p|nXC7$TG z%ci?D=E)q1vXT6hV>#_xuezpgp{Z9;$?Z5UC$>;xZE zmdtNoPhc+q=oZOwmKL}O-PEL$d&HYlb!1vaWCj|8-eqDaiCTR=bt>g zKidRzs4t@3$UPn|dl<$r^rTK1t@Jy3a=F}GrJ5_6FK~P!Wd}<<5+&glNX0Z1LgAyAGBltu}65w*Gw;TTqE*MF$;rk5^ z|0Cp#9a}FWp)a{p{Pi83KtnR+OyCklttgxQjl@@+kkz=Vl;Fz+xctS&*}BP^VW3l{6~o2)V5iE(`w zLuI%uk1R!;M#J(G2@fmR_iP;SF(KA0M?Z##RQJ)Jryk)wfnHF~@8vZzr~U%S-klCpj*{QI>iRP`8rCp@Q?!i^=d`Cquh*u& zIvwLeVB<(F`}2u(LywB#H=nd2Eg@tY3t&f&=+?dtNYk5MLM@0Ee{z>xcVhM#X!qp5 zWc~bH-*NA;YUZPn^Y7QG@JPU6K*oG|L^iCMdG*xb_x>r$prS#ZOqU>7s-+E7JjEMv zN-?idA4_s8Rqz6-N)En@w_8pc&nV^$Iev?6x&uv?g!+R>yZ6a-t>fnli{4AaG!`9# zl%QuAp}XUGWRq9GX=4Kg+s5dcep!0U1I-_D!+PT+*yad%*L0b5z1$jz$V@IheVZg5 zya*HWqEXQ>17NI>@Xkpw9hX;sD4Opce~Ld1*MEsa!PW?B24exQaeQRQ10#s%3j@Jw zbZ321CjJnf8(R5S`kq0`pqx$aSO18+!?`1q=EnAD;Na`J<8a7tw8jP*A(RYRv8}}p zCn_ajFHM7}GYL(EC!x{<)Dkt^2{(N2<+H_W+ywBwEfa!(;c- z^9rYC@F`RBY^PkIVA1p1Kg;cUMTGqna{dse`is_k}WnnyD z?FmPm<`m>=Hd@JB&!POb`W3VX;@n#)p@DRkt47aM{qf_y_??SS{fvWbd#evy! z9+|QH2pIb)*BW;zguLYKG4z2;aU(!GGM^2xDIZj&-^r9ej< zS5dpW))OD{jHqw8dbN;ES~*UI;H{$+*Rya7^)uBJ8%CoF5Bcx`b}B3LMuhNXzHRyz z#p8^-KzE75Aly5omYslU)WzqJy+@u+epv&HR1Bt`l=-Wy`QZ$tUEmb{=*zJ=g38tv zoFDW8PulFDOq?u!5eLWRVzwPFh0xpm^9*M2m+%9_76jn-jH!eT(D2*p_Yt z6P{EpR{|RQC+&d<&Zf`zSC7;by!UF$GN|000+;Q9O;Y4YbpS*Atd!0N)@_b`^1PkSNmKRr7E2h%BCbQaDe}SjY->b|2$3b7<-a3-hJ^rx+0q*AQxX6P)_nQv=3JSqXO*}UV@Ir)4OVxiV_caW-nx|ztn zdsaP=m%QnXN&JFHg83w<^kCa#6}Fx*U_tqsWJNhIeoFJlh7_9F&G%S^UyiK&!9HRiKW<-0PDJQ( z716$)F5tQQ@yP_1@Xdew{-m@3Klqa{)HgC^d5^5k>&xwi}2G!RqqkP)X4lVm;_1AQT2 zGd^a|E)+Ri1F$h%xvSIIL2ob3V^~TtxubEgqezN&5^Moh1MZdxbfyjc85|aIIyK>5 zW-4LX)&XNEXg9m5SU~A?;ak%B(9^J*ENlu=Yh%jqltuSr|EzuQ1k<3@PsuvoquHkO zL+(_z%lu2B@6TN-p+H>hHoqfoJ?-p^4&UTn_AEpE+l?JU5be(Mi~li!*j%6yWSd|6 zL{W_8&quUkO`LsHn-vtRBu(BB3 z%6yLayY>u2j1duoN*sg2fE0yNhJ=J6C6!tpJd6i_QTmHhIt^_}LA>Nxr0w7R`u_FW zEw9ovjglSg#DWS<>yoY0X~3z#zDT{>KYb6V=TkzSTxA*PK_>1wU@M1UXZnmXV~|f$ zXS$D`ZFr5^8oazR>LQf$X140YQ%W8$I>nLg0LO0LJu6EaJGwGcU#HI`?;Qgn=k#H7neaZ9Kt%?~Mpr*8v zgCFPDK$!DE&VZu}YTo%RkHzMP@5`I~m(JgtR^P>)qqHgAA>Pr zh+nh99vhna0##8tRN1)`D^#QFoeH68Nuua6P_jFS=|234?B{M%$#VY?_3E;dh1*&J z!^Et%6sUUpSK}f3!Y3M(-k&@?Oe*$fvmFzm=?QYfjh|ND=T_i*9EkS?u`u%|rZPK_ zs1$X|QU58p@VJbP|9aglRJ>`>J)8rHEOznv4)^eQXvTKagbf4IEFR-aKTU4a&>lLA zoEXu;oYP()-QHK1G(0>Z#RGTEM4%5G>ov}_sca$MRP(K;LV3=94F5PywbMFH(?P({ zT=)!-%|ET5$uUCh-+>8CYxu5wiYZpx6y}DticSKmi{%Ps)SH#bAN~ue*ssFw4#u-l z$@eU@Hm5x@SH-_vagU(431z@)l}{J%erBm7V!zXy8Bi+vyz1)~wCv@k0y&(n4?t{n zduV`i)O*~n2xLC=d)#Co&EGZKGM!NRe-R}S(njchZR2*DRIRb6!eo!6)W>$?(uDp; z#hM#ui%DCw$lK~A@mK&us1qkDfmo(pfIB^0b4ba}+;obHXnFn>^Jr2N0II$r_5rY4 ztJm(Cfc4b?82@P2F$tL4dv?F?v|D5kV3qUk_0jNH@3%IPeL88h?vKHLY+YVnE41b! z;~yu*p`nfadQ9tiupWy;njUD1)$O6hgqLCt@3;W3TW6k4kj6>Lz(|Ds(c-BoebRLB zHdC$T>+sf%+n)Iz$LoPr%kCMj+lJ3!7gL)@1f1o{!m{6Wj$sE$aT7knZ}U8&=h0#Y zfzsyRREq^izlgtC_58hN%->^KuZii>4Xr7w^d{5ZKVvr1MRhIaJa#tI2wwm}XU$t& z&s=X_M-SJH^gpG7MZ9jj_JAc#6tY?4kKvj4{N!Kd-*yYmI$zSt`096k8MG@-fR71N z{D{cSQbPB$2@%HTO<)iq!9un;9%$V*G0dYf#cC7x%$VdY_IE>#4+zTJPZ70GiR)4wPDW+KKE7a7rNo?sWvhn#Rq3&k zuXKiDrX>nEJrAnacG)JoopAMstpgq3FHCSrI#1aaPW+)syOWO`uERUvGmI{_ComSj z=Ov!r^=dCzTL|2tl*xM|Mrm?@M8*%G;TizknqoYC1H8ri6MWPVAYXjA&)N49?(5QBa+jL^Q0~6I1#5wdS2`*3{G4bOFho@Mq zCpB!9&Z6yGSpc!)KJQTt$q0tw7^nEV;e)#}Bi#rvLwRq;g;V!hmgkXCaCf5J?vu|L zV`b@uy-NA_WPA@k|C_r zk@=Cn@5PsupF!bmyzQ7<|4aVNnq)HwkwqDiGpB7r?86A)*-XYI(dw2IOUIx(y(}g| zM!D8;@1_bfBQtzG7jG?rx4|Dix1M|zai`TD#-Hr1@gn|QUDl)~KO4vZ>!Q~kkG=J* z;a}t*_A38xCaf`!`o;)z+-{34KU4Crf4_LXG@IcWe5~(x3o;{mME&Ks#Q~EpoNpTj6Z2 zldD<`!2`8_u`EZk*~v%?N^wQ%ixRVIih^gr-ZfCJx=@=x_MZ8Ed>QrRH0*oS zH(}kuDp#+yUW2)Qdt%H8i(`Y=j(^LNn#sbT*BF|N;WrDhyDn(ceuC6S`!9HEg@e4; zf4w~Fh0Bq|%GHl5NuEN92hH-4F43TMr!4(3=6<@B)Rg_23fOFHsJ*k%zVeL-*Uxrp z{V^QKLbr@x0-AmIt-*JFb}6t6embu(AR*&JL|qv3;la}wwkbg29pl) z2$r!^YHQ2&sm4S9N5m3xiPT>R@st>Tm z(7p`UiHm9BP*^}>WZ=qBLNW@`f;`~(;^c%VJvA_lqkf23o6mk-X`)!rP54L-QHZEy zL_PgP_A6lOn#m!mHIwFy`Gs%f0%X(g*XBiKtHr@7NhpK2VZ-I95TqK!-z~Q&@dbUj6_VT z{H)vBkk=`=sj_sVGVn{#r`W>SiD7PdPvb|P3)p2ZCY00p#!Rk9x%;*~{a3)71~^F8 zyM_^cm@wJqWKc+$LA=D_S$5A+sHUdR@q+E~>utHlXJ1WEe?*jO6nDBj*P+W4c@)@M3_ViFxrEC0oCsTUB(X9W8qB*mEnfx=2>ue2nVR<0rFM1 z=7o@KYo&!u*aSxzy8RP>NME(zo+F*PSu0_EYQg-K`TR9*g5s{2O{de{Mc|c}BHeZScK0rL9G$s2b1S?DAplP#$>_>dQUS^Kw}4GO}A} z5^bhHDKHHb@S9=VxRUY~@>^w5lf&-UtO{r*3s1g1P`A!om_Q4P1zqixBM*^>Pmpfc zIzV=a*41PIo`rEdi^o?(0=gU7AF9?}eD^{dFU!74(y;_TfT0M5dh>a|kNk%K6;{K& zhWIF(cGNn0BUNMjt**;Vr2e)`4IW=w^>~QoD3e|qOi`g9f#!I)${4$T#ux%q-ia_N zKj*!2$LT&)0`6uS=qA6nDqAs|Qn$W8ZijH5&rmu9Vvb@VAPUbbt_5B6fa~DWA*K&I zQA-VOu_yCFMBiKZD$BfS6?5iON%UK?9?k}{-Rwi!svqo z^Y4=t4LUv}1l)FmUlvL5V}^D5+P>Ue_G?x`TR`0-QJD&JpeQ8NWHS-y-|viK#qm9T zY3(NaC6{XJx6rd6wQ-q^@@rX5G<{vJk#yc8*99uW&ZKW?PG_S-=W9@&vMOWykSJ9i z(8CP1b>%YgGx%(yaI8|Hb*tQn8PZJWS@~0%)!VysA_gSVcP9L5#jiKqvFk-0*LgTo zS9Wc$Zg|Q_aX6V{V?(@j>6|*c>h)RlVH@n4q`0{-UZ%z+cJp zq47?7;7JfAWYs9!NFpe6?$^{PIXly{tr{9xpY^Lr2+=+9RgfM-2iy?*$U!#FB z-Z3j5SjEegt^mIm^>HQuyh>uQI6RBKkgE#8UPW>I%tT)!8y%=)dU*jbPb*uY4kzI4 z_F&ffw4?Mw)kTCmK8)$F@U`0~&Ff9X`XZ~Ia|Whg-p@;J_Z+hmr5f(6Px;na1|1R%G>~q)#7APwPn2(16HbBbsyF+aH1Gsld>JTO-+7su?RaC#TrR){n0I%nUO zhVzwqTFZgz%RBLsvSGpP8=8B6vfSD-Fw3dk@ z+D(L2Zft1pFJX&EmpBLlLMDDv)#JIXR3*?rAgY^)MTxMkF`(@Jh6QafFY53PvgJXB zK;+{-gZ$@oTb8l+<}0;68h&gLXeJprn`0x-vyBPO!6^@KUeHfXZGw6aCc$C~6gbqo zu>8@QVJq&dZ1?3+<~kkg&WT=pepPk=rrf@XoNo^_-z|6kCfPaU zPz!C?L`$I3=-&a1W(z(;&x6U&huMS4f=KI-v#uv){QDm7MwNWgPjSnB0WA~%ji}J6 zHutY_6Evr!Q#dih3i2rk6}(oaG^4P^{ry%u|W|(r* zo>+eMKrOM`1tpHuA`=LZ<2@5%HMq_HTjxwkM)ZL^Yca2RDtLnLq==xbzZpK!s{o7u zgS_1^Pu|JeJ}xz~dmt4p|0Q`b?5kWI_V~3}tkzVfIjE1_bFkpjfgwMM~5 z;d@t9)bk$=@ZXA^%Gu{2*RyjzCtPvr;fCPaG<;&hEgVrb&EgJ)CsJ*;EU0<=okQHS z=Vin#xm+5q&ep6Z_Q0alHnxjcnAP*iWWd~~0M{t5_D9Q0{p#eHb>ui(=4>(5!yn_7 zPT5AMr*7Y<&6%2Wk`woiRgbD@A#g@?kWzIc`U|$MN_{#m)>7kqs3p#b3(=sg-?|26 zC5t!V#fqu#W#C{UdAc@>eo?^Pd9Kq+QMAXw;%5{BW(Gh}p+PkvoVT}>C$bygyf(`Q zifEtbQj89E3MGQIH10lMJZQV0R$wcC_jQyu!l}ZBv6p6;fz!M;`-A@IG2}606H>^u z8-|Q~O*?P4k6`(l&F5UR_u7CqjFbzE=Er_wN<58{30Lt0O5t?>O)RnrVAL)d88M8V z^Z5e>?44}quBr5|JMx6`ux%9>B-~%Rzmis1Y@HD3(FF?<)e+YgbgFqFG4zyt*`Xop1M6bkN?IDpu`w>Y8){xV)Wj)$ zHF6luCMU>pb5MqhjYP45{fC-RPO5$7lN|zvxm{@4zeJFCbu1;*qO0yuxpW(ZoZoJq(OV9dGk9XX zi=&JZr0s)j5V5{3yUUGrS_3TrxaT`ch9gy1BJcBAgtO6NqKC^;*alzwJruXY>a6$r z5m=>cgv<-dU9Vl!<;u4Iv}>z&++E%Wwso30i6~BNL_I>4cds+Kgx9iCR&UUnvjsgX zP|R&W^@ybE9+7_{@M-rig!Mkx^}vmc1BlKa2(%mI;iq8f7(BIn*k0O!wxwm;=CDt| zu*kS(esQBP{>HmiFGNfVW?Xu!ykIdQ7>vaK;3N;g{BZSYi$U)?#rC#*Dnmir%a%x? zC&o-9(Y99Ow}oiXg6g|rVxRa7tTp2Wo!{Q?5H;Ru5NQiLmr&V$}!l@e2VFTS*v)Mw8D0aR&)5TcS&3oq=le>icxGwnC}s4h0>0o5aK#ab7CYQzTmRy8>MEOg%N zjj=aI&%G7qZ%3!vzTti&jY-?Uw7c$;PS$xUfWBevy$9#2fW!1aCLXAtO-b7yO&)B| zEN^nzH&))XqBLhDOyA#4ji{8x`Sg(>M6n^?0SRbpXQ(A15xfVJbJOyFxOjlIq*>S32a(yDmLWqt~u$mEp z5&Ci-noDp1UKk6U;VTo1+z_QiC7_wz)X! z$NnrH;29noMh^J1&V4(nA1pbtTxF&r&%`2JqLB0q$`yO}BiqL)n7RSJx-oGKns2Kg zc?tjRtNy-xMdkKFP;UCa?D4V<=O0Z(V)E{7D~!9~r`^67%Hjn9ZJQW-pTXp#R#hdRVxv|OJx zUvoxTYh*9wQ}M@Qha~nP!hv&{(@zs|0M{bv~tyK0htLyh4ff!Dx4L6-9Kz80g5ULM(0061pzInvB_k zm?ur$D#KGV>73!sGIJvp(=P+%^G38>9h57tPdx22d%gB*mA_J%jKAbIYd$7?;h`H> z;}8Cx6IX7duENNu=WW}q#r|8Zrg>9$FXV3b3Q=zEW5K7moX)%tPZQR8G&5*gv?ey+ zL7WaBKf>x`O-_ez>>iStE}9Ky`~q12<*o(^nt7ur5@d9%GE^vGA;(?{(pdh0f8i@o zQ?vVM>)kPs6GkAXi6Z^ZlQ4{5lU&+02^h_8Ek0r^vG~0>9F9uZR_ux5j$EVHC=-E3 zoXk5uhEFsY`6=f=g2CFi&v{`JMDU`_Oj7h2b8?Tt7B$8ed0yoW9k(I;s!VXVB~w%Tk?6&EeG$G1e>{ z+5wGwq*$&O$+eU_a8&aVKUgUyZSc5)@uawFgOq!xm#;G8=e2$Q!KvP)3lo66)W@2d zT4@xklbTByD$J_1{RV7^B^&@bw4*$a1#>{q}f`t2%QV6 zUR(lL9^fAAdwX1-?@k$qE_J5@R%~sQ#yEqA7stxtV4cjIAVR_aRMcki%dNPy_adH@ zZ}#pumaR*5-BT7w>?{1|7>LQ`k6q?kFlxamrkj1WT{VV?h4P%e*=eN`3UaGTi<7xt zSv6e09vBD&33$d3D1?BXcOp4Niam{vSUL~d_$^(1)o8$f(fvCz!IdU@0fU5Z^#*io z#-%SxGu>yQ?cikIn=?%#Cb6t``T0}9UnVIi76*aP=K}ce(U&oElkd8XsR(N={d2v4 zeENusDDq2Uy&QF6XUTit`anBN*6&3GI`?DHU|zGCqwgy5(Jk}&#@qKI#iPaX(0-ux zM}B9~xF3^jyDBHJJiQ+EzX@O7h`y+|(d~ZR8PlfKT6!DYKh|k(EH>{k1(iD5GG>eI zx2w2?%I<*j3@85MsOVd&Mo_YtMq*NGUzM3>1YC8tU@uSwlR@)%4 zXZVm^&6(*bD#kquW6?~agvCT=@S)LbPImm*I16!g8CYLkWrzG3&lS?z{qp?6VFX9_ z$UAUdaKxf-c+u=UT@?La^Td;7z?*}91G4cH%^qt`b1%$l+)-B3w@#9;wA$u5XWw^+ zhtF-)f0B80?gvPmb1yIN&vrWq_I}U7GWaknAPFb()>2)q|0`jma_)a8+YG13G<6y) zlBvHTFM}{lS}k9WytaP7Yg2NBo6)x5^=&k(6hs>g3#jK; zNv}$=3YGFnI0w!`-@_3fg z0*zf(pcX;um4l0kymiaJQvJL8f}1INjr>p)B+E$j?;a1%!`=)V_Y#F@pCIKsO(fgE z(3xz_P*zaQP$nrd4%r4ZPfUc|wi~v3qiz`<${2Nl{`V1<6DP83r5%vrM3iviQKmHw zE$zrIDTmwEF}_~oRg}Z(TP>N0pInXXV03qWEs&^CsYqj@d-HaNpM$1}Ie&?0c=51z z1xF#nk|iO!{ewa1-6-z zcz%AS;p9}O@Y#UT1KTA10mM|zIMMO(KL}0y)pA`#+v5gFdF)c>Mx^r6l9NAX^ExI2 zti#X4KbT6p-n1$-lNOBqlmLok$@*b;dQJ{AKsx*maFx`Ix;*|RuK?kg!yBm3s^3|s z+s1DgJwR&H78i8ldhI3EnpC*V`D=Ly;5pAUAvY=C-nQ3?{B8pSGo=A?g4=Dt(C9Fb zvAHQOPfkwW8RJ{mJlgorJXzeDSijo0|25){%lF!*+^D^&txL5);u4@M`~}9OYMO?= zQo`m$0&Xwm|0qDj0&YbB{8oO=?@WZ4+vfX92S|g$@0f#F(4$OBR`#fwuFdm9#~45Y z0-}`EfIlvY)u^rPOw8-Gt}ikeY&AymGXGW$cr^Y32}ou>rxn$7SXkKNfT!ETo2{^$ zX0<|@!?3G?O8|i>CnXiuhB;Lox6JwRXgHlMze%Z1SmgD7`;hnb_4UxiemISV31T;w z-2M8fV3O=~wKs-CM#lVfdy)d|gbo!nq@|^Y0ErOw-HLnS&Pck7$szge**aUI=wsPv zj)2DDLT#;RCBUN_iN>Wb(kNBtbXwt;Q&gOFR+?x34O#?joXkRA$KQb+z%Y=Jpar}& zX#jd>2%uY)bhYq1{%7*xnX29fz6&n_9;3oSjnQ0^G+D@>J4)Ge?3GWN0917%UMK)y zklI;%TZqN=0?ND970Ue1%#`e=!7HwV~v6o?Cr3cdo&E~$AUe)53zNh^a($bB!aO$9J9X`-ez z|K>dhY9wm=>CAe+ZjsMHW|T11&Wi))>+jCu9V~*c*txOSJr|7Z1}8=r7IaSA&qO_E zErA#QIIU?T_zJYt=j0QA{Ktkn9B~41zZXMW9Fl6}Ql0=tSdZ5_4Z3;lxvWON9KE-S zpo|AJM`b0B;9pp+TaPzKZkxUE5LZ+Hu99Bz36NSG-nt%4QhAb!^P&1v@;NMiaPGWG zdsHr5{;&Q+Lq{jW;=I}h(WiIW!4OKZ0{DTohgs3ASuNX|E7y8q{pCwn+zTMKnYS+gws-71LC8@)XBg-nwy5V)}`{e)5y8epKVtFuMTc_3+95 zw_s!%-?e$Uc6HX{Yc}i}z}%dB^xn_)t{BmCgvG_d*_qQZ(R%XU8-#JVs$+ThFR^1; zk~%9g$D%TaDb{jS-ukYy_A!O5%WGfp53zI$>RqcoPS(41wRv@?R)UkkfXl zv{|>_yuoc(o|xa|XBW-f+v}Ye#QpZX53{l`TUVf7@(%b^9W;Mgy^tj2<}TQ_VNiB; zbv=xA?#dMbwqV+C`n=19nG~ev4vGQ({@2A)e79Wf2kA8oJbb=&0%8;GyX%u`2*f9yH$_>xH=^Yg7X3KkwLZ;OE_$OG3)3fj(OCHk>XM7(Vj>1 z<*d2)hJuNyW5it_ z&>KkHbrl>OK5}0@__+U<7wa?jvQSlD+A2pqWfW{RKNhTwtt{%tZ>9AtpEIP=RXu`6 z6Mp{uFb9j6j~St}(V4L+Vg>?qVK=m2(SduZnI#A~f6T@NQ>QPwN-;>@aRLRX0`ZXd zoB(Fy-&$w8U_*4_sV=Bqtry@s2cLdg%qYM|mtA)4l|i2X?gPy2LU9Pk{CoiNG5oXp z6dw513|1d-B?TZST}{$c_Monco*PM+N{II)@+SP&3;lkv5sF!1I@jcA;Q0qrY>Od` z6F3_}F2s+Pzf(!hHUJ6A%BM|^dZ#!_F=L}dnDrL)c|JHI_rIU{rI30D(2uZqb%FOz zOP07^ttSNCf!iqL2D6s@h(!W-?F|6go=`xzyRBC+g?fBefr6UKa)t$UTn*i#ZU2q# z`h^~^&EyA3_PzvW3fOAUJz;*oEM-8EmoK(qwBhb(6Pi$Le!y*}Ad-zk;ki)`;D$Ry zv2r90zad=~UbzLLYig@;Q@v;jq7(xYru%q^LN4cs-LSfZlf#j@DzZ(nQcEG)79Pld zV7`R$`-_pQwRza$Ut<}*TScpnmt2tdsISs2Pe##|Im045mppO@5O%Y-!&8nQ=8c32 zZ?Di6-}(YS7&G|}nr3qX1U$&;#zmYXvVQ;6&)}eoN5k+4n5ujqm4kv%Px4iIom%7E zO;e_&!FT&+x|=TCa7ydCFm==s@hSj2X7}y&>`il5XrwUnp3_2hI+$F6EaY>Fu>MjV zS5&S9p2~0eC^ivgBp*ObH&nN6jhR*9>VZQl>~Q~1YhdyRypisC2k^%Phn&%2eA#G^ zF5E^^yno&9fTRgHNX5ad-}BMXNCBYgLSDfLKp9y9i!~qvcZ8?^F0O_K!Q(;VPGRI! zgTHq_dfxKbGz+nE`=LHbT1&xi^o;m??L0eV*c_>SF{$5ivcErgJql!ewHzbE$L*57Ix*qJiDm z!{<-&XiwW0tzK5F!ODZIrXudHBGqxndC$Guf zYf)&Kuu4BfUZ)WrczvH|5s4fSo4%B_7DmXvmZv~<^^L6DH{hJ~bnh;+AvfOJWR(umR}-Q6uIAR#3PlG5$>kNx62b*E;1d77cZ+DXD51HbOS`6**I(s zxp`Qb4{+FW5+4{oJxKFdL`ypp5wD}2CtkimvJkZX-c+xLJ7$LbNaR`ctL4EcTCOBU zVZ!Kg0Yq8V@o&Kd#E%4o8GJd1png(&0hBhN==%{>bW64uy<2G|Ce(9Qjbw*V0<%F; z1n$BJZ6cbslyw)4n!X?TPVzB@&GC3T7IZf{I*Q&Yp|QqnF8~s1H@=k}U>tVfxRn|} zuIkeJib&xQKi2{shbGHCta|!+t{R#|f}_Ogb5k_T1~nQB>N=B5EXfB$(61>R*fS<% zWTmpp1DQ`-8%9!XsrEvW1gk!hEHYM%hE5A2b+cz@2JY50p5j*Qy*Pv5NaS2H zlM->_Jh*S)_XJhofrKJbG`n$eINxl}DZliFW0mJgyEjJx?S6wE`o@(G3y1rP)5 zSOl!zQ6l90YRfYJB|!4F6iNWq(;8+pE9Kq9AUo)NAnNw5{7bMlLZC!ss^hVkj9oB0 ztA%psxCWUaUuUe^V1?~0WYrxd=nOLKOm)?@En{ABWIhGIdC-*`X=OjN?Um)l6&rLJQ<8mh~X3dcWco zMZtz@@NSpE_bV>yL1VOQ=bSB71I3Rvk%kT0kJMN>FdTn8$l5+x&lstB78-G(U@pOU z94-X+o@4p=x2|=+X}4dGsMsnW<=J96Pz2I)2>@Dw&EaF`VMJ)|+7?HY=wOmz?yq(a z0UM&pq=krzM6Ud2og0V58NAxMqd)x_3UzPj! z*;SB{WlPQ&Mdr4vOXgbB>y<2wH=G)EiWAz{j6aDkH9_?b29dk9-VCvKPZ~8b1uwlO^Jtspbe?%(nnu=>nKOc*x7G%Rh~i=A zO9wF|0tVcq$h~+!pAKL(Y^Jy5Kw+4`b=qJA~YO?poXQb{{rXjd!1VK0W!@u%U+?b zcrdmK=Wuc{=RpZ=G9O`^R>(AGGb&5Xi%o)gDHmHw1pFfvmRO;z6AWp^{Y42yzSkMX z&E>+`a&B~ zl6_pKx7<()lYd&I_^r+>>&Ea7GaaIecj2}opQhVW+_frsai(d8Oi2EiR19}Cgh@yW ztO(mhT&pRuzQ8F@zy)DbwkpqtYo`KsexRARqFzsF^iv6PDE8XD}I#0*;#(*)9BWWL*rrduTaFXN-SpWQhPs=M83tZqqxuC zmS{^A+;3cm)qy`vc_d0ZVhSdWblyCWOIUQoR6oFVoG_4A__5xv3Y-^(xMaoMzk|=S zurnTBA`8q0WV}T4637<7K;-&kHs@rQsp{h5g7IF!u_GB%7Hv$)ib6oXR@6mA+yuRt z_Y8shRO~S#@{1~O;zd$UnZO;AdL%u{a)ho}4I57TXa6EPfJf#nr&9ZXRO1hINZ?OG z{jojk6UTHaF3nX%ld>7#`8wiWNRj})AT>b6=8=#VY_SX(#?!HV5;liFeD3KJ;30e$ z>V%3@nuUqeQ~%=XK~G!uJZVEn_J@x=1lIug>1*U_TkIJ-LHLSlYz}djc8tVQ-wO=S zAojXPeESu&ZQ_Wz41HFWoRcbiDr0cMt{7PS@Vo8*0lL&6b|vUk;vYCg4BmJyM}== z@^XNW5gd=*+-gXmZgWJTJH$F_=st<;;{jjdNdf&U`gNkS(rP!&&guZiGxggSsh;#}81e}tO-?Jq+I7pz z#^BDt|L*233k0{6)(al=2>zfMJUgl9HLpkz(+)VIKV?xe0A!|q)lr&G!MxsSg*!U! z?DDec;fVhCU~om}e}~5TmB^Q-5^89he?`EG(T}Y#PZHcY^XDrL+>`~pH|wuu5=8zT zBf-iH-FWIYf6wk81F~!2!FWf25CaMZzm%s;AO2@|0|XyRt~OjxG;L8+l&WePCvw_> zmSTPv@CTK5%i63ugT%%yn>X|Et)f(oE-L?(b$uipB<+J~N2Cxl4&Dr*4h~Ay@(dck z3gmZ2q5i?&(|U)Kmi9rGBLQ{Kl9iP}?f(D;cw#hu5F zxoa=w>%Ar8IG)FMMP1T6cG}5*J1wK56rp7%Dh&grQELYeGbM0W_5a`;G&V-AS!%(q ztd^Ktf@r!K-_-pH_c^{#J8&IM`IJ37I~xxIq4M?p=B|HvvwAmGYvF0=4q@_fb(f(F zS4*$u4Qijoxn0pRKg&60{j#CSS#~y6Om(ANs3;Z^y!uSq%eLkFqDScHB`^f9wEW5d zoncS}+3gwt^)^PXp}~Jq^o9N(2<+=J#a%e(26%*}^4PW(X3mM3WoH`?l7$WRcY^YNQqL-ADV-#ez;0!SgupyUFHA`b4FfV!hLPcnckY#KPIZkM%k8OoFA` ztRTnEU~G7&L%@{v^~rexSr;8hCok}v@c)tm_3^TZ28VO3c$YmyPR<9f4xtgP~k zuZRKEl_d$2lPY9{Jc_uLJee|U{RHc-)s{Yhx`&)e1zK1_k5udjrkLbY4n9b^`xt4A znA;2wlmH7FW`KH+bC|jPn&pC{$sB(PIoB!t-R#4vQYf|N z{ZRV?nu~NNM(+{O3;fGB81DPW_UuoukK?yhQoqh3ZuH<0acCrub&& zRUe<%*2zk2HpT8VYO;f3Euc(gIygAEh|>a4hPt$p+5)oBrC=1o$o2oMl7)xMadGtY z3@U4{;^>U(So(WMqgW3KtSOuA?R0edz1kG30f`n9)2RlI9uOEm7UPZ!-`#vSHSqZC4Cz$ZBlZbg5% zerb=$gp_s(_E7K>D^yS4);+4v>`3WN%gi|P8MXeG5`po?4(-k9RNNTzb$9JC`9;Uj zD&q|mSDCVAiHE3(INlSl{58uZwN`$2iJkcd*PS&0|7&H6Z1_nc!~-nvnh=nH|9QYl z!!Ek@3D>YkH`R2%MUuXePZ+K!&-V`>iHV?JZ@V_GU|;Fa;I&ptPkBZrYA&)I=s8sY z>uSElHhueMdhiCshfw;?=IsaPFah*E{?TK*eDro_yzmlZmcX5vvZDRpc4#$d4IVg6 zvZ{pc?WRV}uVLx?sd(_`TbG%$_W#vn#5ISBl>K7y`y+uV9)V?h(#4kmlG zy%b(bu6b4TEG!DJ!YiVUi&@ zc~SCR_&6S;V6TeP4{x-1xHd6BDixu9X z?$d)J4H+W~%0dkpk!+M4TJvLiJ>rP~quJHiZ>DuRkhkPBs|B8rIOb3V9(E! zk7w9E1Q2JQ@AXA0B3Zz9;3k)6)atA4e#nJz_Bqp+C4z`U8>&&WeE!6B`(p>O#0WsQ z>44&JvP>CH0t~eHTpla_K_bH_y8`zwO{Y{q9xfvbUfM)+qxD z2L4!?Bi8?Ho;!w2@p1GAmwbiqKMEBj)smD6l6~L5QWxo!KO6_54K464DolL3zM%gd zqMdV}!b1$@f!7K)M1zw=Kq92Me4R=7JJ4w`fsr0QJb*Moc*j@IHa>tuepfPr=99L^ zdg8Oml(yoIM^qlv__R!`uHD_;KWAoxOH;s7NjA^$lnJT>zs(Y&M_hko-$V_{*OKEI zRkvp#cZ*jz8Oh*qeaFeUQ}i>d!nd{DpxQz+y5lb`tNJ}RDUVU0!67RBCwk5Fo~P=r zf=EO1r@PxjgUwiaGUJKm=A#)-8$%=^9oWlmw^v8+T!2;`<>RlmBN{PMLo9NC!fG&K z@=Gl~Q)j~ZV<^W3pEKe>A?OqQ4mRpQy=`G95MW-w(cbW+Zk7JgGTgwQ|<&N5?MG8HVC@RVJUM;;=uDf5A^WQlZmSo| zmSQC&lWJVGl`+iP?I&ijVpvr2hgO?O4#5XM3f0i)iZr!>`*NOg2WgqdY|MPeBKxwCzvlz?mn1um%XhV75RX!cv^Na-Ajg z1MtK^oej%Z6TdkANeicKtO{6l%CmfxWjutdN^NJ$#f{=H2Gt`s!0NAj^j*yb#gOjc zoH`3!Yh+u8PnaUSjw<2X#fQ7;_6*-au)DUqHu!Uv#7;x4i5hBD?R0^6$yu{2b7>M+ z_0!(gUeCxP!p-NZH{uv`MAD<9j@j!o;Hnf7SzyOrFN0}_C4;eIK|k;k7R z0f^xdN*Ths{Py43;F7oYAL;JjV?oJ#f0O5tRxF;oM9KwNuv%GUfMqR24hvufw5tY; z3zx1$JXtc3iD#i#dyp#NVS_KbQH#ON&87X6v=i{DVd2f*8n{pCZCu?q0%k`mpZCpk zu;3DbaiLB+>Algf2pj`qFpqBz0+^?S)x zNBO=Zt}Tg*@>^Gr=~W}%gGY|zSc=*5kEc60CBKL(rnepR#eJZULn&IU7X(QuZjZjT zE^GeO@H-nL`wORXTb^R_u_B4dfFGRfgbiS!qmL(l>Nv{r>rY_T%uf&)RT_wl;RLQTQyQivL0EK@38 z_i<6vg0v~BT;>A86M)g>42N0pX0Ny9hUa&Kpz@=OowB0J>m>gHEJJ47W(Iujc5^c^ z%xu2Xr8lisD4k8#N&AAi^iaST`+S<-@avrARq`|z8_cYpeymis54hB#Bw_2{=$TJ5 z?k}?A;zRA;N>Y}9D>!@tLqXOq1za->d48Rc1P zcNEj!hP5oB6QJXAtBrLKBIC*qZt1@*K(rxX7f0*mpS3;G!<#GM{>z?(|erzPLm5}ScUcD8c~ z{q#%VOxPu$@AU5t$L&BJ!b)LBN6_bL60fl0 zgm)ipD^|uNuvOR(#RN+49HX#1n9?h_Aa$kiyBvIhks%`>na+ThC6B|ALa*2c_5|dG zG2krPO8zBz=45u2t_wUzVHJe3z`S()mHW71qejHY6>y5uMp6&Mqn}LSX*(fK4h|dU zqQEhH%tepTz)KLISV4LV7Fs4 zA|oqW%!#kWfFM90TDgKXlp7q+m*2HzQLFUy=~KSv>_THfW8*CEJw%a}tRuy&SkIP4 zOi1r3X@75UwT9uNn2?~TLN0y{KMWZa1wlj52EXmMd^5CHH8(2${{Ds15NHHiddd=Z zyqD>ou-8%7N(=Ebonjp>0>ry)(x4|L6r3JO25T1TjeW z#L{2Ch$>B-mLMPl;fh|hRbGP$P1RgOI!On9&qa1QdV|gjQUt9cDX}jn4dXDp5*v#t zNq707>SThd>8Vmfeb*H!zup~5HAeU`G$3DkzfNLWU$^+6pOT$g&ja(<1cukag@#bo zJqV9gQrDY<2scr$70GJ+i_5%>(O@8R(S&(d@v&nIoy}Ir-VJXh0HVd#o8RGfL!42sG<4AaD@pl)@x0G|D+-(%UT-;fJ4Ev5fU(%igSl(*tV{ z3E9MH8x-4UacY_j-zCjH{-oFu*iCaVS4Qt461@%sR)sltAsWhpDv%~7N;gsoQRa(`m|Dt@<8 z>m$J2w|+Og5S*V(seN}Tr&e~qy4m!8h61LIqbZTxLf5UOyoynhGJBc6vM}Z|u1%zX zkA@JaDciG7U6uWTb|Xd~RDWK9&8`rWHN1<;X2sJ+glL(EH#}$dAqh!A&v14n6T^v& ztNKuOi02k$cAt#+!=2FW*a!p!>z3M*mzn9)pO){Tlg=3;dQ~|VW11?2cllhEbiKRQ zrsLnKP{kRSFdfSpxj@;Zj>%G71CFJ((FmP%V<6dZwk&<@rKZvP`egeeZ;12$9594E z1`crRwq2pPDp5vXP){?3Ju5HBezmVnqlryth0`~XlX7AQKrK)tAyNc#$ZE(h@v*pw zWW`T=Yrnr%RKBn+V_o|)Dr)^jku_i*D0(KZT!=@5OXEO(mWeUa;{*A=p8**o-<5%* zv{Gx#rS92m&y=nPh`w^@qM^rS%}~T=ZBjbquS4mdsH}=!Xuk;YfigedeW&OYY(FQ| z#n|!saaVEHI=0!)xZ$YYJqb*;&noF!x?ZxZyiYIy{EdUwfF!YYA4EcKbu&2MxM&T8Rn@^fT3Q694~CT z$1R&xecw5PI)Ms*wIylsc0VQMv>4Rl3?A-h@T8NZ;ljfwtG9J%+8+)Wv|lrNUYkww61hg1cutrO1^kd zT~)@788npZbO&V>x0K_>34=KsP>CmP#TZ0c!I(-a$Dl3 ziYRV2-V=9_s6SIRS|y9*@&byYT~sRbtV0^N4crMbYzIF`a9g0@@WOE7y-l_%m7EVW zdcWDI$$ogfVm%W?%A6TW<$gI*B!p$?7mm2%ka6=T>>!I52%+>zRyqYB^T zDwQu96db*~FT^fGy4$9Z`OSl4@6?{ELf{#QYV|q0<1$uZTL3}PmF31fEpg^*(020y*x8sJH$yTv_ z)A;gAIaSo>buc31h{Q}USTyp$i_=Dwh~ZhUun!2Kr;-I8l;o$}$8jF?2#Xk-gziGABeSDbj|UuCZ2+f zNpyvJHJ-!38$&wWz5;R8-JPWWNQwvjytwRaX884cBzyk@<6N024FqJ7kj8^Hi^4i{HRgv*+B%|`I2*7z+RGtzoLS;p@(bp zw$jhvjH15ZRJ_G9CMB4}5ms|i_|eBD^mWS5WP&5sMp;`>s`o!#@5Dbf5TY#DNOw*K znO;!`@g6Bs$O~Gg>v!60_&r;oeefqPhdP`eOK{n25jARU@5fc(*qzQ&>3?9IKOMSC zRwoi54WQG>6hW=Nt~!oD(#W|E0ba!v)7^Rd)+vs>5t5L?=p;_cM;(v8cLE!1Ghhb1 zea_YI3MvF%+XX7Q1WDDu!GBYro*V3I-OH$(~5$HBSR84OULq8<1d4 zdfCjBrnAJX$}RJBF5$n6a)@wrJR?(0f0D;yu=)g9C{QgDx9bNj8HlfV2mLItd;f#5 zWfS)gC+gF0XV+FW?@v+%TP1jYn@L{A73r##aI97uxBHN@o3EHd_?R_20fd1sN2>Wrwi-2G4X+|@pf^!lU_w}42St8L`(4Hri zo4(WL(gF?}Zy51!+IA%SeXg1zPsTI?3E9#jwP~8(0eP`b$;&Xo$+1dvG2+gIdr1)C z{S+A@1#j8?i-RiS>XpZ0@PY7g8SD}kPh^ch_kfFnI9~|4=5gu zzCDdTLhC&HwSVTJ>cK9nw{2tcKVY${->}BW5lR)wqPgPmru2j@wUDU9GyV2aR76 z&Imd38$@#+eSFuN`O#vgh+GeuPb~VY(K`$ef$XbBLM%p1V5O1jKO7G`q} z`H3*hIPbM$!eu-|I_7uq?4m#lk*NhtfpJ}?4CNQKRqV#IY3{-%KCYSGT$Z{!(~$7 z(2<9~eJTy!Rr?X%*!eV;{nIpqGk>O6^LrXBtF+up1m93=`vt>tI89pgUR+Q#0S>3uMl+cWEG zL*;qu!wmS1rOFjsxTqAvAIJy#L5h$E@DXm1R?fBfG)@_#qOIhooB^b6wn;gjLD@$8 zGu<6>v(SGHbztZ>=NKH$kE2pwfUygEoY+^BCeNMf$j%| zRX0FGeM}Fh2y!KeS(I~DY4s-5v|fU={tyJh10YIR2LR4R36F$HGyvt~^Ar+~>DQS5 z2UtI}@Z-rkle^kfq}y8*jB^s1uBQ6DdwfAS#OmMKD8Kw=SK~8LT~Ky+2{;Ym{^dSl zURgWq06>Jt5EpodbbpH)oIaDGI|L`}j5{4A567ML;>cfI8UgfQUW486D`E!Wwpv}6 z-#~(=WY=f0z_TPKCSRCB*zN69g0Ss&erbFyQbNre5Ou8sO%d$PLw7O1t+DL4L+L`# zP%sXK|GfmZgJo5DlD9ghoq&mzU%rlfJsCaox1&qsvk29fDeudLpj`xgMG}j$B@mY+CwOk!47U)#uv;+!A{X;4JY(;liOTd1_F9(pebxzQhpmIID&idqqm5P5X8JjrbZqfbu3QYj;m6s zeAa}WcNIOc$)=cTMD0skAbxfF^GjE^8MM1LY}8ifaaip_pIV$)S~3bJVEt66NON3m zJFg99i4h>6#zmuV0-<*mBx@?*AgAd1TPL#D#h!2%qDlR~R#x(VstQ=GL3aR|6cNay z8$ZK~PF|Rrezl8ulfJTjExZCSZ6`Qc2D-2w)2FUHSwO9&|NHj*2A>0{R}z4hN4E=r z(si33%}7*KbXyo6q~WUDInN{4fsr2o#2Qcf>*FnII5q|{iUPtqz>io?=Es7%p@KJe z33jMc#QbY40~eR7ND2`>z=TnQRnnqWYM=|PohCR0GX28pQiFOYjeIp~&z+Lmmmo=T z0#VqjZSFAkusVv`B8|Lor4&wakz`$O?rBFdt`EPaannS*L&)k(&t$HJ_(fHFu-db;Y?^N>`B#H`V&_LaLzIUN~j5g z-@fTUMo+B0wJws&0+huh0pPRn4WQmMJ+Dt@zIJ5f);bdje+SCym%nB!wmre&%K##) zL{L;=_FA;*_p|4o(sm1V*`l3*9w{8P0501slPACCG~pb_vr+?fXuA1Gn)>PB&(ph= z>%v+OMKVxO2=~6+tu36g1+Vwvtm(Ts^2-{RpBlQLYmikcDJTaJ-fu(V2FEnqwp)*r zOR{!|qvcHEuEfKuihbq0#&-TwsVpWz045@JDPUadFHS+JNGyQI2YR9&r8lIDfQ}n# zB?i+fbt726oq?hmE{Hx6hU(X7t=LW%>sC6fbYOwez6N5bfjY+@T76z8Q_u3iCJm>b z;n|ZgD9$N7!S;s?0*3QJ)?g?rJnG8G}}wZOo@{7tX~ zbj1_po!beI&}T>@Pv;6@tRP~HZqd>zFEMIuA$?G1HO0cmKjt^u<&k+tREB;EHEgKT zLk09y0=L;f{$>4+$lcmrEVXOA=f1ZfICZF1pg})1^+0@x!lhd3aeJlNpn{H0N((P6 zmPY!cOcd!wo)#J-_z?JS9cUb}J~dkhhb?COwDUtKUdvTGkuc_>U@krcQT6uN?&|95 zv3W%)sAWpUs3m4JFc)j|G_8T4#CwXas2%Ivpiu*#;$ zf8V`CTW+rUC<2jX9UhOXZkjIl8d?0zP})(YA1_dIa&khCjkg2x^O;W0l0JhnA*CP& zbGuDtyY9b3p77A_!##6$y?|9(neP5#Di;Da9kpl*kwS@(IdG!FI|qSBun@0oB4?)* z_0|zlad0Sy*GS=z31k6lBXl%0hb{1m8HXi3#y1jIOe4|j+HZk)e0f;vbn(xqS=mM0 zGuN#`?&c#48uRBqC2oC%PmO@wVf5x2=32%p>G*)H_r!1Wd5d>F*%KGJC|XH1D`1Xo zv)BNOV(vT%26R!;!7ETt&OYQ7lu-srbGnKF&TyB|t6xE-^$rxv z(I2+OT^_qQ0mMZgx%)zPgL;nBD(DH#ovsF*s64?^n??F8>T}Tt)CMrB&f|M2Ds6b- zDRA1YFzp6jwKcGQ`g_U9^$~A)7F9kmik4a)u4#wb%3-Q`Ml2vh3$~+27Df!=g=^rA zkK4@fQfXEdrhNVe^vl)SlJ4&NHSk~WBqCXlcD6KDi81kIuxDR!y6@G2Y|)R!!;N89 zO7DGt;ln;!tH^0hRgt`KA2?mnD*XiivQ5|uV48_wH+&w{iGYOq5oRN`l5khh#GtTg zI$k+Ww*@U@V8`_W4MbFW>#bMO{V9#IGS9*@gzyS{|&cjF3PXWRkCcJz0m zs;z{3D`phr5Jc8G0S^c7ABH@l8R*E@pnKOt|uu{8j_kf^5QjTk}XIdtgy&>>XhH} z%ELKJIO}K+D)hk5njn?~PCl$CiOrx+E&ca+ZU~&3RLKx#1{Hbnj2RvQhO2k?$|UQ6 z1+Ngrx-0&yf2jYzT@E@|?s@cz8B~`EUu@+%tq&0R6j-wx)MYUsjqd&M2w&fc__xyX z@9;R?M-^b@%_i zF<`@26ch8q5J2M^k(hlA4wk&<&94tTA3V(;1U)-Qe@+&}OM&W1c-f=oC#Bf0pH&q@ zaLCu*J-tz@HcJ$?=HaCI*}Y+qF&x>jQ}}h!iHVUXB9^!Y=?wYC^TZ zj-m#AiKc>P1fupt&;vpq#346m1q{mX=>Qr1to9N2%TRdZYz?4e@PvoXEth+m{-xj` zD>P$gXICjTYGnt7xf+9T7fZ{z$~+9?hm0y(Cbj@e+;%K!7FC?{Y_8h{CJZ@B%F^-jY8kPxNLAY}J)bT! z;sB-p)5+nJ1)8!7$?PRYcz04o8eyQ&C^`bN0F4?OU3mosjS_pbOfUz@0L3e^i$oQc zg>yQ6ownq$i!%|I%ZI5v=G-m^cX<%aCTCk}R^HDUqcg6(2*y>inNt6@R_nW9I<;2qsWD{DSXdNrhqZnk|?YXSsW)wDJ}M4 z*9P~Lk%-@Dee^EX&7=4$&n=IR8( z%tH_hDHt1PweXVbm3*tV{v4iq1@;RoD}S)s@&`d!tGUPuKu-=(9B@ZspDGYgP?Q#m zzF@C{pMSkzJ!irjV8dH!=-sHT-U7*j#`=Z&#S&SAfqIjNBkcSDT>Y8mgDe7E=Oz;@ zGQF=uY(|-RX&%jY1De2xDuAt*2vHd8a03zawjxjGBqEbTrQ5C^YzUeD5TNFTdJ9k7 z_lotJ0QIY9Hk~DoM1Z9}LTSh~K8!~|$0dW^Ee?<8eJ+mGH3_pq!-abCv(W|tyu00Csy4G>SxgRQ;;%sud;Cp=i4ddev4nB!H^2dG zMT$~2OFAW}n;j3|`whAY|2GpFptfOMgG-v3;qR9d(O+#3F6aeNgb)p+rT;2NnI-*x zj4;HSJN%Eb^#8A0zT4O9B|i}gq>6rd;~GnT1g`LkZKV4jWh3Jg~~o?;iXR0^2V@6$6n`P{J}Z zjr3CPROe2x;CtqQ0?0N0(IX2sarNQm@q}=v$*N)E;3UPwKu$Oi$@T9}#XfRwWPu+> zRUIZ*I6#0*G7B_x6S~3N1~L&JT3cJ6tTZ`3`kTcML|5IrzX36?YDi7SjrG~c3XZRz zr~8{Wn>`B#zsu?wlUaM>Y;5XrcPs9F_{&&W_EK}b%QEn_viFkbpN0zRdvTUD_IDc> z%|QD9S|{doVM`W4*_WnZ0q$ku>rU93fgADkmmrP67p~tdKlg*()ePuO zvw4s`K}nt&$bSCRHSRSi!hs9RZ&d}tSNok4&0g+B#p!^RpU$oMCk6m50KV?s&M)0C z0L-F#U<#bb<^&bsc=~p90q%NXfuk30zO6l%heZGz`Fp$g^bB&yajB`6BCp!gionj+ z|4}~PDV?~>1Z>EMfJN;GFFyzHOu*bFD55&qThwm>6;8Tf7q=+u17Ry0NNcdJRp~@T z7JYMw!AabAV+h`Hc-I9o0f)BN!)k1@k;K&Q-#tHLLtU0hLDg+F23c>9gx&AtrTts* z>A)aELM<&p&A<1PVACdf3@)iZssRoh2`_2D05HQtG@Ta^eA zH~gFu6B6(*mZ%pLh?W=`DcS$G3A&owKn8U!9d9ZLK} z;ha_N-$~993#F9jMw@~+z5tQ1pbc$_Fa?wSiz*t6F`$9d4ASFhGehcr=>o<8+C}RY~$!NrQ2;iOS$Nbkd{NmcZdE%^N z1m0Gi?Ck=0u*!Kd-b$jlm&j)Q*ZO$TF^e&f0Tx(c z;GYdFZsyfp+OYf2#qKw~b>0|4zvkeN?z$$V`_EW2K}O&w)=y9*j}O)3tvZExd>9cv zQu;u=25|Z6pC>{tTgJC<0G2-&)BEQ3atWTM_oFkWK`vnMYKGZgp~WZFAm;HZePmZB z4s=>nAR?b^8EfY5M{)(bal9p*XGS){a7?qe?J9 z@|@u@z#ys>D4BQB-_ae%Dn$&T(~kE5ENA__b!mm=rMz5RsGtcQuP5+1++`eiXq~g7 z+A^xYcj326sFM>3yKt}~QYEC(7ZRvm17 z8^NdNvJlctXp)^z%~=!KbK7`Xp*5O%w~pXTNmMM6=kC=9D>%X1@85^XVLHF}?`tNl zeu(hz>I=_VLyGIi0H0mmx7LJTpyIzS@SQ&|lqLf5Pn(Q<(VOUBEx| z`@mXpTTnAWxNULP1p;pHzkcOd3l-Xov;}8ewjsD2^0%0;8BOqubY<--sUK3=@-OZ1KZ}(JX+l@4hQ~H51mJcikp^a|4m6^Pv?nqyIJ~4$jkP_`bj$(#Fl_$P_ z-DQj#u{0LD%vQDpPceJ<>320e(*0hmmga8rEk0iNSkWBc*<9_~YJKa&Ux)0;pk7f% x$Yx6f{pwlzZfYS3zp7hRI literal 0 HcmV?d00001 From 033e3b7e85fc1310868060a47ec02e5d710c78ee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 11:42:57 +0200 Subject: [PATCH 102/903] Small title adjustment to the Home Assistant Alerts integration (#76070) --- homeassistant/components/homeassistant_alerts/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json index 0d276c6f3ae..7c9ddf4f905 100644 --- a/homeassistant/components/homeassistant_alerts/manifest.json +++ b/homeassistant/components/homeassistant_alerts/manifest.json @@ -1,6 +1,6 @@ { "domain": "homeassistant_alerts", - "name": "Home Assistant alerts", + "name": "Home Assistant Alerts", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts", "codeowners": ["@home-assistant/core"], From 786780bc8ca3d25de59b3f7a2161adc8ab697161 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Aug 2022 12:35:24 +0200 Subject: [PATCH 103/903] Use attributes in limitlessled light (#76066) --- .../components/limitlessled/light.py | 160 +++++++----------- 1 file changed, 62 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 5073e3b4a7d..801f104bd3b 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -2,9 +2,11 @@ from __future__ import annotations import logging +from typing import Any from limitlessled import Color from limitlessled.bridge import Bridge +from limitlessled.group import Group from limitlessled.group.dimmer import DimmerGroup from limitlessled.group.rgbw import RgbwGroup from limitlessled.group.rgbww import RgbwwGroup @@ -56,7 +58,7 @@ EFFECT_NIGHT = "night" MIN_SATURATION = 10 -WHITE = [0, 0] +WHITE = (0, 0) COLOR_MODES_LIMITLESS_WHITE = {ColorMode.COLOR_TEMP} SUPPORT_LIMITLESSLED_WHITE = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION @@ -104,7 +106,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def rewrite_legacy(config): +def rewrite_legacy(config: ConfigType) -> ConfigType: """Rewrite legacy configuration to new format.""" bridges = config.get(CONF_BRIDGES, [config]) new_bridges = [] @@ -151,13 +153,15 @@ def setup_platform( # Use the expanded configuration format. lights = [] + bridge_conf: dict[str, Any] + group_conf: dict[str, Any] for bridge_conf in config[CONF_BRIDGES]: bridge = Bridge( bridge_conf.get(CONF_HOST), port=bridge_conf.get(CONF_PORT, DEFAULT_PORT), version=bridge_conf.get(CONF_VERSION, DEFAULT_VERSION), ) - for group_conf in bridge_conf.get(CONF_GROUPS): + for group_conf in bridge_conf[CONF_GROUPS]: group = bridge.add_group( group_conf.get(CONF_NUMBER), group_conf.get(CONF_NAME), @@ -176,22 +180,22 @@ def state(new_state): def decorator(function): """Set up the decorator function.""" - def wrapper(self, **kwargs): + def wrapper(self: LimitlessLEDGroup, **kwargs: Any) -> None: """Wrap a group state change.""" # pylint: disable=protected-access pipeline = Pipeline() transition_time = DEFAULT_TRANSITION - if self._effect == EFFECT_COLORLOOP: + if self.effect == EFFECT_COLORLOOP: self.group.stop() - self._effect = None + self._attr_effect = None # Set transition time. if ATTR_TRANSITION in kwargs: transition_time = int(kwargs[ATTR_TRANSITION]) # Do group type-specific work. function(self, transition_time, pipeline, **kwargs) # Update state. - self._is_on = new_state + self._attr_is_on = new_state self.group.enqueue(pipeline) self.schedule_update_ha_state() @@ -203,29 +207,34 @@ def state(new_state): class LimitlessLEDGroup(LightEntity, RestoreEntity): """Representation of a LimitessLED group.""" - def __init__(self, group, config): + _attr_assumed_state = True + _attr_max_mireds = 370 + _attr_min_mireds = 154 + _attr_should_poll = False + + def __init__(self, group: Group, config: dict[str, Any]) -> None: """Initialize a group.""" if isinstance(group, WhiteGroup): self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_WHITE self._attr_supported_features = SUPPORT_LIMITLESSLED_WHITE - self._effect_list = [EFFECT_NIGHT] + self._attr_effect_list = [EFFECT_NIGHT] elif isinstance(group, DimmerGroup): self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_DIMMER self._attr_supported_features = SUPPORT_LIMITLESSLED_DIMMER - self._effect_list = [] + self._attr_effect_list = [] elif isinstance(group, RgbwGroup): self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_RGB self._attr_supported_features = SUPPORT_LIMITLESSLED_RGB - self._effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] + self._attr_effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] elif isinstance(group, RgbwwGroup): self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_RGBWW self._attr_supported_features = SUPPORT_LIMITLESSLED_RGBWW - self._effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] + self._attr_effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] self._fixed_color_mode = None - if len(self._attr_supported_color_modes) == 1: - self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) + if self.supported_color_modes and len(self.supported_color_modes) == 1: + self._fixed_color_mode = next(iter(self.supported_color_modes)) else: assert self._attr_supported_color_modes == { ColorMode.COLOR_TEMP, @@ -233,59 +242,26 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): } self.group = group + self._attr_name = group.name self.config = config - self._is_on = False - self._brightness = None - self._temperature = None - self._color = None - self._effect = None + self._attr_is_on = False async def async_added_to_hass(self) -> None: """Handle entity about to be added to hass event.""" await super().async_added_to_hass() if last_state := await self.async_get_last_state(): - self._is_on = last_state.state == STATE_ON - self._brightness = last_state.attributes.get("brightness") - self._temperature = last_state.attributes.get("color_temp") - self._color = last_state.attributes.get("hs_color") + self._attr_is_on = last_state.state == STATE_ON + self._attr_brightness = last_state.attributes.get("brightness") + self._attr_color_temp = last_state.attributes.get("color_temp") + self._attr_hs_color = last_state.attributes.get("hs_color") @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def assumed_state(self): - """Return True because unable to access real state of the entity.""" - return True - - @property - def name(self): - """Return the name of the group.""" - return self.group.name - - @property - def is_on(self): - """Return true if device is on.""" - return self._is_on - - @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness property.""" - if self._effect == EFFECT_NIGHT: + if self.effect == EFFECT_NIGHT: return 1 - return self._brightness - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return 154 - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return 370 + return self._attr_brightness @property def color_mode(self) -> str | None: @@ -294,33 +270,17 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): return self._fixed_color_mode # The light supports both hs and white with adjustable color temperature - if self._effect == EFFECT_NIGHT or self._color is None or self._color[1] == 0: + if ( + self.effect == EFFECT_NIGHT + or self.hs_color is None + or self.hs_color[1] == 0 + ): return ColorMode.COLOR_TEMP return ColorMode.HS - @property - def color_temp(self): - """Return the temperature property.""" - return self._temperature - - @property - def hs_color(self): - """Return the color property.""" - return self._color - - @property - def effect(self): - """Return the current effect for this light.""" - return self._effect - - @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return self._effect_list - # pylint: disable=arguments-differ @state(False) - def turn_off(self, transition_time, pipeline, **kwargs): + def turn_off(self, transition_time: int, pipeline: Pipeline, **kwargs: Any) -> None: """Turn off a group.""" if self.config[CONF_FADE]: pipeline.transition(transition_time, brightness=0.0) @@ -328,40 +288,42 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): # pylint: disable=arguments-differ @state(True) - def turn_on(self, transition_time, pipeline, **kwargs): + def turn_on(self, transition_time: int, pipeline: Pipeline, **kwargs: Any) -> None: """Turn on (or adjust property of) a group.""" # The night effect does not need a turned on light if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT: - if EFFECT_NIGHT in self._effect_list: + if self.effect_list and EFFECT_NIGHT in self.effect_list: pipeline.night_light() - self._effect = EFFECT_NIGHT + self._attr_effect = EFFECT_NIGHT return pipeline.on() # Set up transition. args = {} - if self.config[CONF_FADE] and not self.is_on and self._brightness: + if self.config[CONF_FADE] and not self.is_on and self.brightness: args["brightness"] = self.limitlessled_brightness() if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] args["brightness"] = self.limitlessled_brightness() if ATTR_HS_COLOR in kwargs: - self._color = kwargs[ATTR_HS_COLOR] + self._attr_hs_color = kwargs[ATTR_HS_COLOR] # White is a special case. - if self._color[1] < MIN_SATURATION: + assert self.hs_color is not None + if self.hs_color[1] < MIN_SATURATION: pipeline.white() - self._color = WHITE + self._attr_hs_color = WHITE else: args["color"] = self.limitlessled_color() if ATTR_COLOR_TEMP in kwargs: + assert self.supported_color_modes if ColorMode.HS in self.supported_color_modes: pipeline.white() - self._color = WHITE - self._temperature = kwargs[ATTR_COLOR_TEMP] + self._attr_hs_color = WHITE + self._attr_color_temp = kwargs[ATTR_COLOR_TEMP] args["temperature"] = self.limitlessled_temperature() if args: @@ -375,28 +337,30 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): pipeline.flash(duration=duration) # Add effects. - if ATTR_EFFECT in kwargs and self._effect_list: + if ATTR_EFFECT in kwargs and self.effect_list: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: - self._effect = EFFECT_COLORLOOP + self._attr_effect = EFFECT_COLORLOOP pipeline.append(COLORLOOP) if kwargs[ATTR_EFFECT] == EFFECT_WHITE: pipeline.white() - self._color = WHITE + self._attr_hs_color = WHITE - def limitlessled_temperature(self): + def limitlessled_temperature(self) -> float: """Convert Home Assistant color temperature units to percentage.""" max_kelvin = color_temperature_mired_to_kelvin(self.min_mireds) min_kelvin = color_temperature_mired_to_kelvin(self.max_mireds) width = max_kelvin - min_kelvin - kelvin = color_temperature_mired_to_kelvin(self._temperature) + assert self.color_temp is not None + kelvin = color_temperature_mired_to_kelvin(self.color_temp) temperature = (kelvin - min_kelvin) / width return max(0, min(1, temperature)) - def limitlessled_brightness(self): + def limitlessled_brightness(self) -> float: """Convert Home Assistant brightness units to percentage.""" - return self._brightness / 255 + assert self.brightness is not None + return self.brightness / 255 - def limitlessled_color(self): + def limitlessled_color(self) -> Color: """Convert Home Assistant HS list to RGB Color tuple.""" - - return Color(*color_hs_to_RGB(*tuple(self._color))) + assert self.hs_color is not None + return Color(*color_hs_to_RGB(*self.hs_color)) From fe6d6b81e312fe9877e866f83edc2d05d2a9cd96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 00:38:31 -1000 Subject: [PATCH 104/903] Add support for switchbot motion sensors (#76059) --- homeassistant/components/switchbot/__init__.py | 2 ++ homeassistant/components/switchbot/const.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index e32252a7615..7eec785233f 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -22,6 +22,7 @@ from .const import ( ATTR_CONTACT, ATTR_CURTAIN, ATTR_HYGROMETER, + ATTR_MOTION, ATTR_PLUG, CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, @@ -35,6 +36,7 @@ PLATFORMS_BY_TYPE = { ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], + ATTR_MOTION: [Platform.BINARY_SENSOR, Platform.SENSOR], } CLASS_BY_DEVICE = { ATTR_CURTAIN: switchbot.SwitchbotCurtain, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 9cc2acebbf8..a8ec3433f84 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -8,13 +8,16 @@ ATTR_CURTAIN = "curtain" ATTR_HYGROMETER = "hygrometer" ATTR_CONTACT = "contact" ATTR_PLUG = "plug" +ATTR_MOTION = "motion" DEFAULT_NAME = "Switchbot" + SUPPORTED_MODEL_TYPES = { "WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN, "WoSensorTH": ATTR_HYGROMETER, "WoContact": ATTR_CONTACT, "WoPlug": ATTR_PLUG, + "WoPresence": ATTR_MOTION, } # Config Defaults From 320b264d03c2c391eca87656798fbbf8c78a915e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 2 Aug 2022 12:40:14 +0200 Subject: [PATCH 105/903] Use `SourceType.ROUTER` in Tractive integration (#76071) Use SourceType.ROUTER --- homeassistant/components/tractive/device_tracker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 19d30aa9856..f92a8e71df3 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -57,6 +57,8 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): """Return the source type, eg gps or router, of the device.""" if self._source_type == "PHONE": return SourceType.BLUETOOTH + if self._source_type == "KNOWN_WIFI": + return SourceType.ROUTER return SourceType.GPS @property From 48a34756f0e93d9d8bb2e84a15ebfe016c6c5871 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 13:03:34 +0200 Subject: [PATCH 106/903] Remove Somfy from Overkiz title in manifest (#76073) --- homeassistant/components/overkiz/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index a7595065224..6e6e57f12e5 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -1,6 +1,6 @@ { "domain": "overkiz", - "name": "Overkiz (by Somfy)", + "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", "requirements": ["pyoverkiz==1.4.2"], From d69d7a8761b5b335bc8f26e04de319897743c0a0 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 13:11:06 +0100 Subject: [PATCH 107/903] Fix typo in new xiaomi_ble string (#76076) --- homeassistant/components/xiaomi_ble/strings.json | 2 +- homeassistant/components/xiaomi_ble/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 48d5c3a87f7..9d2a0ae40d8 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -12,7 +12,7 @@ "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "get_encryption_key_legacy": { "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 836ccc51637..4648b28cc93 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -27,7 +27,7 @@ "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "user": { "data": { From 404d530b5fa45de8e9cd4f9efbf9267dad0cc5c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 14:13:07 +0200 Subject: [PATCH 108/903] Handle missing attributes in meater objects (#76072) --- homeassistant/components/meater/sensor.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 84ef3a2e2a9..a2753a42307 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -43,14 +43,18 @@ class MeaterSensorEntityDescription( def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert elapsed time to timestamp.""" - if not probe.cook: + if not probe.cook or not hasattr(probe.cook, "time_elapsed"): return None return dt_util.utcnow() - timedelta(seconds=probe.cook.time_elapsed) def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert remaining time to timestamp.""" - if not probe.cook or probe.cook.time_remaining < 0: + if ( + not probe.cook + or not hasattr(probe.cook, "time_remaining") + or probe.cook.time_remaining < 0 + ): return None return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining) @@ -99,7 +103,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.target_temperature if probe.cook else None, + value=lambda probe: probe.cook.target_temperature + if probe.cook and hasattr(probe.cook, "target_temperature") + else None, ), # Peak temperature MeaterSensorEntityDescription( @@ -109,7 +115,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.peak_temperature if probe.cook else None, + value=lambda probe: probe.cook.peak_temperature + if probe.cook and hasattr(probe.cook, "peak_temperature") + else None, ), # Remaining time in seconds. When unknown/calculating default is used. Default: -1 # Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time. From cfe6c8939c889f702df2dadeb881ccfe822a1fb5 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 2 Aug 2022 14:49:46 +0200 Subject: [PATCH 109/903] Add Open Exchange Rates coordinator (#76017) * Add Open Exchange Rates coordinator * Move debug log * Fix update interval calculation --- .coveragerc | 2 +- CODEOWNERS | 1 + .../components/openexchangerates/const.py | 7 + .../openexchangerates/coordinator.py | 47 ++++++ .../openexchangerates/manifest.json | 3 +- .../components/openexchangerates/sensor.py | 139 +++++++++--------- requirements_all.txt | 3 + 7 files changed, 133 insertions(+), 69 deletions(-) create mode 100644 homeassistant/components/openexchangerates/const.py create mode 100644 homeassistant/components/openexchangerates/coordinator.py diff --git a/.coveragerc b/.coveragerc index d529cdbd9ca..3c587e11cf6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -850,7 +850,7 @@ omit = homeassistant/components/open_meteo/weather.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py - homeassistant/components/openexchangerates/sensor.py + homeassistant/components/openexchangerates/* homeassistant/components/opengarage/__init__.py homeassistant/components/opengarage/binary_sensor.py homeassistant/components/opengarage/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index ce31d6e8dd3..16523cafa81 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -766,6 +766,7 @@ build.json @home-assistant/supervisor /tests/components/open_meteo/ @frenck /homeassistant/components/openerz/ @misialq /tests/components/openerz/ @misialq +/homeassistant/components/openexchangerates/ @MartinHjelmare /homeassistant/components/opengarage/ @danielhiversen /tests/components/opengarage/ @danielhiversen /homeassistant/components/openhome/ @bazwilliams diff --git a/homeassistant/components/openexchangerates/const.py b/homeassistant/components/openexchangerates/const.py new file mode 100644 index 00000000000..2c037887489 --- /dev/null +++ b/homeassistant/components/openexchangerates/const.py @@ -0,0 +1,7 @@ +"""Provide common constants for Open Exchange Rates.""" +from datetime import timedelta +import logging + +DOMAIN = "openexchangerates" +LOGGER = logging.getLogger(__package__) +BASE_UPDATE_INTERVAL = timedelta(hours=2) diff --git a/homeassistant/components/openexchangerates/coordinator.py b/homeassistant/components/openexchangerates/coordinator.py new file mode 100644 index 00000000000..0106edcd751 --- /dev/null +++ b/homeassistant/components/openexchangerates/coordinator.py @@ -0,0 +1,47 @@ +"""Provide an OpenExchangeRates data coordinator.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta + +from aiohttp import ClientSession +from aioopenexchangerates import Client, Latest, OpenExchangeRatesClientError +import async_timeout + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + +TIMEOUT = 10 + + +class OpenexchangeratesCoordinator(DataUpdateCoordinator[Latest]): + """Represent a coordinator for Open Exchange Rates API.""" + + def __init__( + self, + hass: HomeAssistant, + session: ClientSession, + api_key: str, + base: str, + update_interval: timedelta, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, LOGGER, name=f"{DOMAIN} base {base}", update_interval=update_interval + ) + self.base = base + self.client = Client(api_key, session) + self.setup_lock = asyncio.Lock() + + async def _async_update_data(self) -> Latest: + """Update data from Open Exchange Rates.""" + try: + async with async_timeout.timeout(TIMEOUT): + latest = await self.client.get_latest(base=self.base) + except (OpenExchangeRatesClientError) as err: + raise UpdateFailed(err) from err + + LOGGER.debug("Result: %s", latest) + return latest diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index 43c45b6b665..a795eaf8d5e 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -2,6 +2,7 @@ "domain": "openexchangerates", "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", - "codeowners": [], + "requirements": ["aioopenexchangerates==0.3.0"], + "codeowners": ["@MartinHjelmare"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 318cae4ae0e..337cd3050ac 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -1,32 +1,28 @@ """Support for openexchangerates.org exchange rates service.""" from __future__ import annotations -from datetime import timedelta -from http import HTTPStatus -import logging -from typing import Any +from dataclasses import dataclass, field -import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import CoordinatorEntity -_LOGGER = logging.getLogger(__name__) -_RESOURCE = "https://openexchangerates.org/api/latest.json" +from .const import BASE_UPDATE_INTERVAL, DOMAIN, LOGGER +from .coordinator import OpenexchangeratesCoordinator ATTRIBUTION = "Data provided by openexchangerates.org" DEFAULT_BASE = "USD" DEFAULT_NAME = "Exchange Rate Sensor" -MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, @@ -37,10 +33,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +@dataclass +class DomainData: + """Data structure to hold data for this domain.""" + + coordinators: dict[tuple[str, str], OpenexchangeratesCoordinator] = field( + default_factory=dict, init=False + ) + + +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Open Exchange Rates sensor.""" @@ -49,75 +54,75 @@ def setup_platform( base: str = config[CONF_BASE] quote: str = config[CONF_QUOTE] - parameters = {"base": base, "app_id": api_key} + integration_data: DomainData = hass.data.setdefault(DOMAIN, DomainData()) + coordinators = integration_data.coordinators - rest = OpenexchangeratesData(_RESOURCE, parameters, quote) - response = requests.get(_RESOURCE, params=parameters, timeout=10) + if (api_key, base) not in coordinators: + # Create one coordinator per base currency per API key. + update_interval = BASE_UPDATE_INTERVAL * ( + len( + { + coordinator_base + for coordinator_api_key, coordinator_base in coordinators + if coordinator_api_key == api_key + } + ) + + 1 + ) + coordinator = coordinators[api_key, base] = OpenexchangeratesCoordinator( + hass, + async_get_clientsession(hass), + api_key, + base, + update_interval, + ) - if response.status_code != HTTPStatus.OK: - _LOGGER.error("Check your OpenExchangeRates API key") - return + LOGGER.debug( + "Coordinator update interval set to: %s", coordinator.update_interval + ) - rest.update() - add_entities([OpenexchangeratesSensor(rest, name, quote)], True) + # Set new interval on all coordinators for this API key. + for ( + coordinator_api_key, + _, + ), coordinator in coordinators.items(): + if coordinator_api_key == api_key: + coordinator.update_interval = update_interval + + coordinator = coordinators[api_key, base] + async with coordinator.setup_lock: + # We need to make sure that the coordinator data is ready. + if not coordinator.data: + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise PlatformNotReady + + async_add_entities([OpenexchangeratesSensor(coordinator, name, quote)]) -class OpenexchangeratesSensor(SensorEntity): +class OpenexchangeratesSensor( + CoordinatorEntity[OpenexchangeratesCoordinator], SensorEntity +): """Representation of an Open Exchange Rates sensor.""" _attr_attribution = ATTRIBUTION - def __init__(self, rest: OpenexchangeratesData, name: str, quote: str) -> None: + def __init__( + self, coordinator: OpenexchangeratesCoordinator, name: str, quote: str + ) -> None: """Initialize the sensor.""" - self.rest = rest - self._name = name + super().__init__(coordinator) + self._attr_name = name self._quote = quote - self._state: float | None = None + self._attr_native_unit_of_measurement = quote @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self) -> float | None: + def native_value(self) -> float: """Return the state of the sensor.""" - return self._state + return round(self.coordinator.data.rates[self._quote], 4) @property - def extra_state_attributes(self) -> dict[str, Any] | None: + def extra_state_attributes(self) -> dict[str, float]: """Return other attributes of the sensor.""" - attr = self.rest.data - - return attr - - def update(self) -> None: - """Update current conditions.""" - self.rest.update() - if (value := self.rest.data) is None: - self._attr_available = False - return - - self._attr_available = True - self._state = round(value[self._quote], 4) - - -class OpenexchangeratesData: - """Get data from Openexchangerates.org.""" - - def __init__(self, resource: str, parameters: dict[str, str], quote: str) -> None: - """Initialize the data object.""" - self._resource = resource - self._parameters = parameters - self._quote = quote - self.data: dict[str, Any] | None = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self) -> None: - """Get the latest data from openexchangerates.org.""" - try: - result = requests.get(self._resource, params=self._parameters, timeout=10) - self.data = result.json()["rates"] - except requests.exceptions.HTTPError: - _LOGGER.error("Check the Openexchangerates API key") - self.data = None + return self.coordinator.data.rates diff --git a/requirements_all.txt b/requirements_all.txt index df7bb65f9da..11136a0702b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,6 +219,9 @@ aionotion==3.0.2 # homeassistant.components.oncue aiooncue==0.3.4 +# homeassistant.components.openexchangerates +aioopenexchangerates==0.3.0 + # homeassistant.components.acmeda aiopulse==0.4.3 From fbe22d4fe7242bd67c17003ab9459d896b91485a Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Aug 2022 10:10:20 -0400 Subject: [PATCH 110/903] Bump AIOAladdinConnect to 0.1.39 (#76082) --- homeassistant/components/aladdin_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/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 008b8f81c89..5e55f391aa6 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.37"], + "requirements": ["AIOAladdinConnect==0.1.39"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 11136a0702b..3f8351a97be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.37 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 196cca0c3ba..f6dfbe5e4ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.37 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 From be4f9598f965007b6f09b7278ec5fd6298d85203 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Aug 2022 16:28:41 +0200 Subject: [PATCH 111/903] Improve type hints in blinksticklight lights (#75999) --- homeassistant/components/blinksticklight/light.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 621bd01f874..8373d2990a1 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -1,6 +1,8 @@ """Support for Blinkstick lights.""" from __future__ import annotations +from typing import Any + from blinkstick import blinkstick import voluptuous as vol @@ -57,15 +59,15 @@ class BlinkStickLight(LightEntity): self._stick = stick self._attr_name = name - def update(self): + def update(self) -> None: """Read back the device state.""" rgb_color = self._stick.get_color() hsv = color_util.color_RGB_to_hsv(*rgb_color) self._attr_hs_color = hsv[:2] - self._attr_brightness = hsv[2] - self._attr_is_on = self.brightness > 0 + self._attr_brightness = int(hsv[2]) + self._attr_is_on = self.brightness is not None and self.brightness > 0 - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if ATTR_HS_COLOR in kwargs: self._attr_hs_color = kwargs[ATTR_HS_COLOR] @@ -73,13 +75,15 @@ class BlinkStickLight(LightEntity): self._attr_brightness = kwargs[ATTR_BRIGHTNESS] else: self._attr_brightness = 255 + assert self.brightness is not None self._attr_is_on = self.brightness > 0 + assert self.hs_color rgb_color = color_util.color_hsv_to_RGB( self.hs_color[0], self.hs_color[1], self.brightness / 255 * 100 ) self._stick.set_color(red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2]) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._stick.turn_off() From 67cef0dc942b3ab0d3a653eb4a989e4691ea3fe6 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 2 Aug 2022 11:29:32 -0400 Subject: [PATCH 112/903] Ensure ZHA devices load before validating device triggers (#76084) --- homeassistant/components/zha/__init__.py | 5 ++++- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/device_trigger.py | 9 +++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 70b9dfd9b46..0a7d43120f7 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -35,6 +35,7 @@ from .core.const import ( DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, + ZHA_DEVICES_LOADED_EVENT, RadioType, ) from .core.discovery import GROUP_PROBE @@ -75,7 +76,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up ZHA from config.""" - hass.data[DATA_ZHA] = {} + hass.data[DATA_ZHA] = {ZHA_DEVICES_LOADED_EVENT: asyncio.Event()} if DOMAIN in config: conf = config[DOMAIN] @@ -109,6 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].set() device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -141,6 +143,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload ZHA config entry.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] await zha_gateway.shutdown() + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].clear() GROUP_PROBE.cleanup() api.async_unload_api(hass) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 4c8c6e03c79..dfa5f608cfe 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -394,6 +394,7 @@ ZHA_GW_MSG_GROUP_REMOVED = "group_removed" ZHA_GW_MSG_LOG_ENTRY = "log_entry" ZHA_GW_MSG_LOG_OUTPUT = "log_output" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" +ZHA_DEVICES_LOADED_EVENT = "zha_devices_loaded_event" EFFECT_BLINK = 0x00 EFFECT_BREATHE = 0x01 diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index cdd98110f83..4ad8eccea1d 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -16,8 +16,8 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError, IntegrationError from homeassistant.helpers.typing import ConfigType -from . import DOMAIN -from .core.const import ZHA_EVENT +from . import DOMAIN as ZHA_DOMAIN +from .core.const import DATA_ZHA, ZHA_DEVICES_LOADED_EVENT, ZHA_EVENT from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" @@ -35,7 +35,8 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) - if "zha" in hass.config.components: + if ZHA_DOMAIN in hass.config.components: + await hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].wait() trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) @@ -100,7 +101,7 @@ async def async_get_triggers( triggers.append( { CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, + CONF_DOMAIN: ZHA_DOMAIN, CONF_PLATFORM: DEVICE, CONF_TYPE: trigger, CONF_SUBTYPE: subtype, From a1d495a25b1c1c85ec7539b02bb4920aa89e2ec1 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 2 Aug 2022 11:08:33 -0500 Subject: [PATCH 113/903] Bump Frontend to 20220802.0 (#76087) --- 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 45331491aa0..ed9b381ee9d 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==20220728.0"], + "requirements": ["home-assistant-frontend==20220802.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 860d1538727..81182a7d8ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3f8351a97be..70169ffa4f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -842,7 +842,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6dfbe5e4ae..bdfa620a64a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 From f043203b566fbd834c4450b2e3ccb7acf31ce3d5 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 17:20:37 +0100 Subject: [PATCH 114/903] Add optional context parameter to async_start_reauth (#76077) --- homeassistant/components/xiaomi_ble/sensor.py | 20 +------------- homeassistant/config_entries.py | 7 ++++- tests/test_config_entries.py | 27 +++++++++++++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index b3cd5126967..fef9b334e75 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -181,25 +181,7 @@ def process_service_info( and data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified ): - flow_context = { - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "title_placeholders": {"name": entry.title}, - "unique_id": entry.unique_id, - "device": data, - } - - for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): - if flow["context"] == flow_context: - break - else: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context=flow_context, - data=entry.data, - ) - ) + entry.async_start_reauth(hass, context={"device": data}) return sensor_update_to_bluetooth_data_update(update) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b0c04323005..638aa0b8110 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -640,7 +640,9 @@ class ConfigEntry: await asyncio.gather(*pending) @callback - def async_start_reauth(self, hass: HomeAssistant) -> None: + def async_start_reauth( + self, hass: HomeAssistant, context: dict[str, Any] | None = None + ) -> None: """Start a reauth flow.""" flow_context = { "source": SOURCE_REAUTH, @@ -649,6 +651,9 @@ class ConfigEntry: "unique_id": self.unique_id, } + if context: + flow_context.update(context) + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain): if flow["context"] == flow_context: return diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 3e7245ed73a..66f51508441 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3273,3 +3273,30 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): with pytest.raises(config_entries.OperationNotAllowed): assert await manager.async_reload(entry.entry_id) assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + +async def test_reauth(hass): + """Test the async_reauth_helper.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"} + assert flows[0]["context"]["extra_context"] == "some_extra_context" + + # Check we can't start duplicate flows + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(flows) == 1 From 17fbee7dd37e7939baf7ff34fb408285ffff5042 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 19:05:09 +0200 Subject: [PATCH 115/903] Refresh homeassistant_alerts when hass has started (#76083) --- homeassistant/components/homeassistant_alerts/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index d405b9e257d..60386e3d080 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -14,6 +14,7 @@ from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import __version__ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.yaml import parse_yaml @@ -100,7 +101,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: coordinator = AlertUpdateCoordinator(hass) coordinator.async_add_listener(async_schedule_update_alerts) - await coordinator.async_refresh() + + async def initial_refresh(hass: HomeAssistant) -> None: + await coordinator.async_refresh() + + async_at_start(hass, initial_refresh) return True From 9f31be8f01e0382df2ab7362b2d9b0df122f2d71 Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Tue, 2 Aug 2022 19:22:29 +0200 Subject: [PATCH 116/903] Fix capitalization in mitemp_bt strings (#76063) Co-authored-by: Franck Nijhof --- homeassistant/components/mitemp_bt/strings.json | 2 +- homeassistant/components/mitemp_bt/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json index d36c25eafec..1f9f031a3bb 100644 --- a/homeassistant/components/mitemp_bt/strings.json +++ b/homeassistant/components/mitemp_bt/strings.json @@ -2,7 +2,7 @@ "issues": { "replaced": { "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/mitemp_bt/translations/en.json b/homeassistant/components/mitemp_bt/translations/en.json index cd5113ee02f..78ec041405b 100644 --- a/homeassistant/components/mitemp_bt/translations/en.json +++ b/homeassistant/components/mitemp_bt/translations/en.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced" } } From cf849c59a4cf15893742a0a9e460b591b1d9ccd6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 09:11:50 -1000 Subject: [PATCH 117/903] Bump pyatv to 0.10.3 (#76091) --- 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 dec195fddee..5717f851b81 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.10.2"], + "requirements": ["pyatv==0.10.3"], "dependencies": ["zeroconf"], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 70169ffa4f4..253dee5445a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1398,7 +1398,7 @@ pyatmo==6.2.4 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdfa620a64a..8a8adbdf846 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -971,7 +971,7 @@ pyatag==0.3.5.3 pyatmo==6.2.4 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From a628be4db8c5aebd09a097f0ab8e71b8fd672414 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 10:38:01 -1000 Subject: [PATCH 118/903] Only stat the .dockerenv file once (#76097) --- homeassistant/util/package.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 18ab43967ec..49ab3c10f8c 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from functools import cache from importlib.metadata import PackageNotFoundError, version import logging import os @@ -23,6 +24,7 @@ def is_virtual_env() -> bool: ) +@cache def is_docker_env() -> bool: """Return True if we run in a docker env.""" return Path("/.dockerenv").exists() From a0adfb9e62564615c1de8ea619634ddc3e102936 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 21:38:38 +0100 Subject: [PATCH 119/903] Fix serialization of Xiaomi BLE reauth flow (#76095) * Use data instead of context to fix serialisation bug * Test change to async_start_reauth --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 2 +- homeassistant/config_entries.py | 7 +++++-- tests/components/xiaomi_ble/test_config_flow.py | 3 +-- tests/test_config_entries.py | 12 ++++++++++-- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 092c60e9713..725c513914f 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -260,7 +260,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None - device: DeviceData = self.context["device"] + device: DeviceData = entry_data["device"] self._discovered_device = device self._discovery_info = device.last_service_info diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index fef9b334e75..dcb95422609 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -181,7 +181,7 @@ def process_service_info( and data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified ): - entry.async_start_reauth(hass, context={"device": data}) + entry.async_start_reauth(hass, data={"device": data}) return sensor_update_to_bluetooth_data_update(update) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 638aa0b8110..7c2f1c84ff9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -641,7 +641,10 @@ class ConfigEntry: @callback def async_start_reauth( - self, hass: HomeAssistant, context: dict[str, Any] | None = None + self, + hass: HomeAssistant, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" flow_context = { @@ -662,7 +665,7 @@ class ConfigEntry: hass.config_entries.flow.async_init( self.domain, context=flow_context, - data=self.data, + data=self.data | (data or {}), ) ) diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 0d123f0cd54..32ba6be3322 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1033,9 +1033,8 @@ async def test_async_step_reauth_abort_early(hass): "entry_id": entry.entry_id, "title_placeholders": {"name": entry.title}, "unique_id": entry.unique_id, - "device": device, }, - data=entry.data, + data=entry.data | {"device": device}, ) assert result["type"] == FlowResultType.ABORT diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 66f51508441..b923e37b636 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3286,8 +3286,14 @@ async def test_reauth(hass): await entry.async_setup(hass) await hass.async_block_till_done() - entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) - await hass.async_block_till_done() + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init) as mock_init: + entry.async_start_reauth( + hass, + context={"extra_context": "some_extra_context"}, + data={"extra_data": 1234}, + ) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -3296,6 +3302,8 @@ async def test_reauth(hass): assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"} assert flows[0]["context"]["extra_context"] == "some_extra_context" + assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234 + # Check we can't start duplicate flows entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() From fbf3c1a5d4572fe94db0ac53310690f4e491f290 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 22:05:36 +0100 Subject: [PATCH 120/903] Fix Xiaomi BLE UI string issues (#76099) --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- homeassistant/components/xiaomi_ble/strings.json | 12 +++++++----- .../components/xiaomi_ble/translations/en.json | 14 ++++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 725c513914f..a05e703db6a 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -189,7 +189,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Ack that device is slow.""" - if user_input is not None or not onboarding.async_is_onboarded(self.hass): + if user_input is not None: return self._async_get_or_create_entry() self._set_confirm_only() diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 9d2a0ae40d8..5ecbb8e1b88 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -11,7 +11,7 @@ "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, - "slow_confirm": { + "confirm_slow": { "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "get_encryption_key_legacy": { @@ -27,14 +27,16 @@ } } }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 4648b28cc93..2cb77dd2c07 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -3,17 +3,22 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful" }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "confirm_slow": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -26,9 +31,6 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, - "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." - }, "user": { "data": { "address": "Device" From bf931f1225ee82b143b4472ac94281941c1c3d9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 13:46:43 -1000 Subject: [PATCH 121/903] Handle additional bluetooth start exceptions (#76096) --- .../components/bluetooth/__init__.py | 33 +++++- tests/components/bluetooth/test_init.py | 110 ++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 39629ab6d85..c91563d7729 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Final import async_timeout from bleak import BleakError +from dbus_next import InvalidMessageError from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -26,6 +27,7 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.loader import async_get_bluetooth +from homeassistant.util.package import is_docker_env from . import models from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN @@ -341,13 +343,42 @@ class BluetoothManager: try: async with async_timeout.timeout(START_TIMEOUT): await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + self._cancel_device_detected() + _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) + raise ConfigEntryNotReady( + f"Invalid DBus message received: {ex}; try restarting `dbus`" + ) from ex + except BrokenPipeError as ex: + self._cancel_device_detected() + _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + self._cancel_device_detected() + _LOGGER.debug( + "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ConfigEntryNotReady( + f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" + ) from ex except asyncio.TimeoutError as ex: self._cancel_device_detected() raise ConfigEntryNotReady( f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" ) from ex - except (FileNotFoundError, BleakError) as ex: + except BleakError as ex: self._cancel_device_detected() + _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a47916506df..edc5eb024a6 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice +from dbus_next import InvalidMessageError import pytest from homeassistant.components import bluetooth @@ -1409,3 +1410,112 @@ async def test_changing_the_adapter_at_runtime(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() + + +async def test_dbus_socket_missing_in_container(hass, caplog): + """Test we handle dbus being missing in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "/run/dbus" in caplog.text + assert "docker" in caplog.text + + +async def test_dbus_socket_missing(hass, caplog): + """Test we handle dbus being missing.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "DBus" in caplog.text + assert "docker" not in caplog.text + + +async def test_dbus_broken_pipe_in_container(hass, caplog): + """Test we handle dbus broken pipe in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "dbus" in caplog.text + assert "restarting" in caplog.text + assert "container" in caplog.text + + +async def test_dbus_broken_pipe(hass, caplog): + """Test we handle dbus broken pipe.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "DBus" in caplog.text + assert "restarting" in caplog.text + assert "container" not in caplog.text + + +async def test_invalid_dbus_message(hass, caplog): + """Test we handle invalid dbus message.""" + + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=InvalidMessageError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "dbus" in caplog.text From e09bbc749c06f55de0437ff882f9c2e17065c438 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 3 Aug 2022 00:28:23 +0000 Subject: [PATCH 122/903] [ci skip] Translation update --- .../ambiclimate/translations/hu.json | 2 +- .../components/govee_ble/translations/no.json | 6 +++ .../here_travel_time/translations/no.json | 6 ++- .../homeassistant/translations/no.json | 1 + .../homeassistant_alerts/translations/no.json | 8 ++++ .../homekit_controller/translations/no.json | 4 +- .../components/inkbird/translations/no.json | 1 + .../lg_soundbar/translations/id.json | 2 +- .../lg_soundbar/translations/no.json | 2 +- .../mitemp_bt/translations/pt-BR.json | 2 +- .../components/moat/translations/no.json | 13 +++++- .../components/nest/translations/ca.json | 8 ++++ .../components/nest/translations/de.json | 10 +++++ .../components/nest/translations/en.json | 8 ++++ .../components/nest/translations/fr.json | 5 +++ .../components/nest/translations/hu.json | 10 +++++ .../components/nest/translations/id.json | 10 +++++ .../components/nest/translations/it.json | 8 ++++ .../components/nest/translations/no.json | 10 +++++ .../components/nest/translations/pt-BR.json | 10 +++++ .../components/nest/translations/zh-Hant.json | 10 +++++ .../components/nextdns/translations/no.json | 10 +++++ .../openalpr_local/translations/no.json | 8 ++++ .../opentherm_gw/translations/id.json | 3 +- .../opentherm_gw/translations/no.json | 3 +- .../components/plugwise/translations/no.json | 3 +- .../radiotherm/translations/no.json | 6 +++ .../simplepush/translations/no.json | 3 +- .../simplisafe/translations/no.json | 7 +++- .../smartthings/translations/hu.json | 2 +- .../soundtouch/translations/no.json | 6 +++ .../components/spotify/translations/no.json | 6 +++ .../steam_online/translations/no.json | 6 +++ .../components/switchbot/translations/no.json | 3 +- .../unifiprotect/translations/hu.json | 2 +- .../components/uscis/translations/no.json | 8 ++++ .../components/verisure/translations/no.json | 15 ++++++- .../components/withings/translations/no.json | 1 + .../components/xbox/translations/no.json | 6 +++ .../xiaomi_ble/translations/ca.json | 3 +- .../xiaomi_ble/translations/de.json | 6 ++- .../xiaomi_ble/translations/en.json | 6 +++ .../xiaomi_ble/translations/fr.json | 3 +- .../xiaomi_ble/translations/hu.json | 6 ++- .../xiaomi_ble/translations/id.json | 6 ++- .../xiaomi_ble/translations/no.json | 40 +++++++++++++++++++ .../xiaomi_ble/translations/pt-BR.json | 6 ++- .../xiaomi_ble/translations/zh-Hant.json | 6 ++- .../components/zha/translations/no.json | 3 ++ 49 files changed, 293 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/no.json create mode 100644 homeassistant/components/openalpr_local/translations/no.json create mode 100644 homeassistant/components/uscis/translations/no.json create mode 100644 homeassistant/components/xiaomi_ble/translations/no.json diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 92ea617393e..421d692bbba 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "K\u00e9rem, k\u00f6vesse ezt a [link]({authorization_url}}) \u00e9s **Enged\u00e9lyezze** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", + "description": "K\u00e9rem, k\u00f6vesse a [linket]({authorization_url}) \u00e9s **Enged\u00e9lyezze** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a visszah\u00edv\u00e1si URL {cb_url})", "title": "Ambiclimate hiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/govee_ble/translations/no.json b/homeassistant/components/govee_ble/translations/no.json index 4fd1e1d0c9d..28ec4582177 100644 --- a/homeassistant/components/govee_ble/translations/no.json +++ b/homeassistant/components/govee_ble/translations/no.json @@ -9,6 +9,12 @@ "step": { "bluetooth_confirm": { "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" } } } diff --git a/homeassistant/components/here_travel_time/translations/no.json b/homeassistant/components/here_travel_time/translations/no.json index e4282933051..4af798a78fc 100644 --- a/homeassistant/components/here_travel_time/translations/no.json +++ b/homeassistant/components/here_travel_time/translations/no.json @@ -41,8 +41,10 @@ }, "origin_menu": { "menu_options": { - "origin_coordinates": "Bruk kartplassering" - } + "origin_coordinates": "Bruk kartplassering", + "origin_entity": "Bruke en enhet" + }, + "title": "Velg Opprinnelse" }, "user": { "data": { diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 675c02a6b66..0932c2e75a6 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU-arkitektur", + "config_dir": "Konfigurasjonskatalog", "dev": "Utvikling", "docker": "", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant_alerts/translations/no.json b/homeassistant/components/homeassistant_alerts/translations/no.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/no.json b/homeassistant/components/homekit_controller/translations/no.json index c383dddb763..70b74d51f64 100644 --- a/homeassistant/components/homekit_controller/translations/no.json +++ b/homeassistant/components/homekit_controller/translations/no.json @@ -18,7 +18,7 @@ "unable_to_pair": "Kunne ikke koble til, vennligst pr\u00f8v igjen.", "unknown_error": "Enheten rapporterte en ukjent feil. Sammenkobling mislyktes." }, - "flow_title": "{name}", + "flow_title": "{name} ( {category} )", "step": { "busy_error": { "description": "Avbryt sammenkobling p\u00e5 alle kontrollere, eller pr\u00f8v \u00e5 starte enheten p\u00e5 nytt, og fortsett deretter med \u00e5 fortsette sammenkoblingen.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Tillat sammenkobling med usikre oppsettkoder.", "pairing_code": "Sammenkoblingskode" }, - "description": "HomeKit Controller kommuniserer med {name} over lokalnettverket ved hjelp av en sikker kryptert tilkobling uten en separat HomeKit-kontroller eller iCloud. Skriv inn HomeKit-paringskoden (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret. Denne koden finnes vanligvis p\u00e5 selve enheten eller i emballasjen.", + "description": "HomeKit-kontrolleren kommuniserer med {name} ( {category} ) over det lokale nettverket ved hjelp av en sikker kryptert tilkobling uten en separat HomeKit-kontroller eller iCloud. Skriv inn HomeKit-paringskoden (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret. Denne koden finnes vanligvis p\u00e5 selve enheten eller i emballasjen.", "title": "Par med en enhet via HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/components/inkbird/translations/no.json b/homeassistant/components/inkbird/translations/no.json index 3cf7f2b76c8..28ec4582177 100644 --- a/homeassistant/components/inkbird/translations/no.json +++ b/homeassistant/components/inkbird/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" }, diff --git a/homeassistant/components/lg_soundbar/translations/id.json b/homeassistant/components/lg_soundbar/translations/id.json index 74d380f3a1a..3f6d9ea8f81 100644 --- a/homeassistant/components/lg_soundbar/translations/id.json +++ b/homeassistant/components/lg_soundbar/translations/id.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi", + "already_configured": "Perangkat sudah dikonfigurasi", "existing_instance_updated": "Memperbarui konfigurasi yang ada." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/no.json b/homeassistant/components/lg_soundbar/translations/no.json index 41eef0e4c16..58d4c11916b 100644 --- a/homeassistant/components/lg_soundbar/translations/no.json +++ b/homeassistant/components/lg_soundbar/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert", + "already_configured": "Enheten er allerede konfigurert", "existing_instance_updated": "Oppdatert eksisterende konfigurasjon." }, "error": { diff --git a/homeassistant/components/mitemp_bt/translations/pt-BR.json b/homeassistant/components/mitemp_bt/translations/pt-BR.json index 634f5dd71fd..991a749a729 100644 --- a/homeassistant/components/mitemp_bt/translations/pt-BR.json +++ b/homeassistant/components/mitemp_bt/translations/pt-BR.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade do Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE foi substitu\u00edda" } } diff --git a/homeassistant/components/moat/translations/no.json b/homeassistant/components/moat/translations/no.json index bce03ad33d7..28ec4582177 100644 --- a/homeassistant/components/moat/translations/no.json +++ b/homeassistant/components/moat/translations/no.json @@ -5,6 +5,17 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" }, - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 791be9975eb..33d0179208b 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -96,5 +96,13 @@ "camera_sound": "So detectat", "doorbell_chime": "Timbre premut" } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuraci\u00f3 YAML de Nest est\u00e0 sent eliminada" + }, + "removed_app_auth": { + "title": "Les credencials d'autenticaci\u00f3 de Nest s'han d'actualitzar" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 933231fd1e8..675b9087b6c 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -96,5 +96,15 @@ "camera_sound": "Ger\u00e4usch erkannt", "doorbell_chime": "T\u00fcrklingel gedr\u00fcckt" } + }, + "issues": { + "deprecated_yaml": { + "description": "Das Konfigurieren von Nest in configuration.yaml wird in Home Assistant 2022.10 entfernt. \n\nDeine bestehenden OAuth-Anwendungsdaten und Zugriffseinstellungen wurden automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Nest-YAML-Konfiguration wird entfernt" + }, + "removed_app_auth": { + "description": "Um die Sicherheit zu verbessern und das Phishing-Risiko zu verringern, hat Google die von Home Assistant verwendete Authentifizierungsmethode eingestellt. \n\n **Zur L\u00f6sung sind Ma\u00dfnahmen deinerseits erforderlich** ([more info]( {more_info_url} )) \n\n 1. Besuche die Integrationsseite\n 1. Klicke in der Nest-Integration auf Neu konfigurieren.\n 1. Home Assistant f\u00fchrt dich durch die Schritte zum Upgrade auf die Webauthentifizierung. \n\n Informationen zur Fehlerbehebung findest du in der Nest [Integrationsanleitung]( {documentation_url} ).", + "title": "Nest-Authentifizierungsdaten m\u00fcssen aktualisiert werden" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index cd8274d635a..e0c0b8e67a5 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -10,6 +10,7 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", + "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { @@ -25,6 +26,13 @@ "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)" }, "step": { + "auth": { + "data": { + "code": "Access Token" + }, + "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", + "title": "Link Google Account" + }, "auth_upgrade": { "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", "title": "Nest: App Auth Deprecation" diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 16990b93193..7cbed195e0c 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -88,5 +88,10 @@ "camera_sound": "Son d\u00e9tect\u00e9", "doorbell_chime": "Sonnette enfonc\u00e9e" } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Nest est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 228f4f5e745..f453fdef150 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -96,5 +96,15 @@ "camera_sound": "Hang \u00e9szlelve", "doorbell_chime": "Cseng\u0151 megnyomva" } + }, + "issues": { + "deprecated_yaml": { + "description": "Nest konfigur\u00e1l\u00e1sa a configuration.yaml f\u00e1jl \u00e1ltal a 2022.10-es Home Assistantban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 OAuth alkalmaz\u00e1s hiteles\u00edt\u0151 adatai \u00e9s hozz\u00e1f\u00e9r\u00e9si be\u00e1ll\u00edt\u00e1sai automatikusan import\u00e1l\u00e1sra ker\u00fclnek a felhaszn\u00e1l\u00f3i fel\u00fcletre. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Nest YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "removed_app_auth": { + "description": "A biztons\u00e1g meger\u0151s\u00edt\u00e9se \u00e9s az adathal\u00e1sz kock\u00e1zat cs\u00f6kkent\u00e9se \u00e9rdek\u00e9ben a Google megsz\u00fcntette a Home Assistant \u00e1ltal haszn\u00e1lt hiteles\u00edt\u00e9si m\u00f3dszert.\n\n**Ez az \u00d6n r\u00e9sz\u00e9r\u0151l int\u00e9zked\u00e9st ig\u00e9nyel a megold\u00e1shoz** ([b\u0151vebb inform\u00e1ci\u00f3]({more_info_url}))\n\n1. L\u00e1togasson el az integr\u00e1ci\u00f3k oldalra\n2. Kattintson az \u00dajrakonfigur\u00e1l\u00e1s gombra a Nest integr\u00e1ci\u00f3ban.\n3. A rendszer v\u00e9gigvezeti \u00d6nt a webes hiteles\u00edt\u00e9sre val\u00f3 friss\u00edt\u00e9s l\u00e9p\u00e9sein.\n\nA hibaelh\u00e1r\u00edt\u00e1ssal kapcsolatos inform\u00e1ci\u00f3k a Nest [integr\u00e1ci\u00f3s utas\u00edt\u00e1sok]({documentation_url}) dokumentumban tal\u00e1lhat\u00f3k.", + "title": "A Nest hiteles\u00edt\u0151 adatait friss\u00edteni sz\u00fcks\u00e9ges" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 6a45ccaee8c..1d69f9f48bd 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -96,5 +96,15 @@ "camera_sound": "Suara terdeteksi", "doorbell_chime": "Bel pintu ditekan" } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Nest di configuration.yaml sedang dihapus di Home Assistant 2022.10. \n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Nest dalam proses penghapusan" + }, + "removed_app_auth": { + "description": "Untuk meningkatkan keamanan dan mengurangi risiko phishing, Google telah menghentikan metode autentikasi yang digunakan oleh Home Assistant.\n\n**Tindakan berikut diperlukan untuk diselesaikan** ([info lebih lanjut]({more_info_url}))\n\n1. Kunjungi halaman integrasi\n1. Klik Konfigurasi Ulang pada integrasi Nest.\n1. Home Assistant akan memandu Anda melalui langkah-langkah untuk meningkatkan ke Autentikasi Web.\n\nLihat [instruksi integrasi]({documentation_url}) Nest untuk informasi pemecahan masalah.", + "title": "Kredensial Autentikasi Nest harus diperbarui" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 8979631dec0..6771cd00431 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -96,5 +96,13 @@ "camera_sound": "Suono rilevato", "doorbell_chime": "Campanello premuto" } + }, + "issues": { + "deprecated_yaml": { + "title": "La configurazione YAML di Nest sar\u00e0 rimossa" + }, + "removed_app_auth": { + "title": "Le credenziali di autenticazione Nest devono essere aggiornate" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index ce6663ec22e..89ba7bc8b7c 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -96,5 +96,15 @@ "camera_sound": "Lyd oppdaget", "doorbell_chime": "Ringeklokke trykket" } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Nest i configuration.yaml blir fjernet i Home Assistant 2022.10. \n\n Din eksisterende OAuth-applikasjonslegitimasjon og tilgangsinnstillinger er automatisk importert til brukergrensesnittet. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Nest YAML-konfigurasjonen fjernes" + }, + "removed_app_auth": { + "description": "For \u00e5 forbedre sikkerheten og redusere phishing-risikoen har Google avviklet autentiseringsmetoden som brukes av Home Assistant. \n\n **Dette krever handling fra deg for \u00e5 l\u00f8se det** ([mer info]( {more_info_url} )) \n\n 1. G\u00e5 til integreringssiden\n 1. Klikk p\u00e5 Reconfigure p\u00e5 Nest-integrasjonen.\n 1. Home Assistant vil lede deg gjennom trinnene for \u00e5 oppgradere til webautentisering. \n\n Se Nest [integrasjonsinstruksjoner]( {documentation_url} ) for feils\u00f8kingsinformasjon.", + "title": "Nest-autentiseringslegitimasjonen m\u00e5 oppdateres" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index d96387aee82..973a3cf3b69 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -96,5 +96,15 @@ "camera_sound": "Som detectado", "doorbell_chime": "Campainha pressionada" } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Nest em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.10. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de Nest est\u00e1 sendo removida" + }, + "removed_app_auth": { + "description": "Para melhorar a seguran\u00e7a e reduzir o risco de phishing, o Google desativou o m\u00e9todo de autentica\u00e7\u00e3o usado pelo Home Assistant. \n\n **Isso requer uma a\u00e7\u00e3o sua para resolver** ([mais informa\u00e7\u00f5es]( {more_info_url} )) \n\n 1. Visite a p\u00e1gina de integra\u00e7\u00f5es\n 1. Clique em Reconfigurar na integra\u00e7\u00e3o Nest.\n 1. O Home Assistant o guiar\u00e1 pelas etapas para atualizar para a autentica\u00e7\u00e3o da Web. \n\n Consulte as [instru\u00e7\u00f5es de integra\u00e7\u00e3o]( {documentation_url} ) do Nest para obter informa\u00e7\u00f5es sobre solu\u00e7\u00e3o de problemas.", + "title": "As credenciais de autentica\u00e7\u00e3o Nest precisam ser atualizadas" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index 11a4823d77a..77f271518a5 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -96,5 +96,15 @@ "camera_sound": "\u5075\u6e2c\u5230\u8072\u97f3", "doorbell_chime": "\u9580\u9234\u6309\u4e0b" } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Nest \u5373\u5c07\u65bc Home Assistant 2022.10 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Nest YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + }, + "removed_app_auth": { + "description": "\u70ba\u4e86\u6539\u5584\u8cc7\u5b89\u8207\u964d\u4f4e\u7db2\u8def\u91e3\u9b5a\u98a8\u96aa\u3001Google \u5df2\u7d93\u68c4\u7528 Home Assistant \u6240\u4f7f\u7528\u7684\u8a8d\u8b49\u6a21\u5f0f\u3002\n\n**\u5c07\u9700\u8981\u60a8\u9032\u884c\u6392\u9664** ([\u66f4\u591a\u8cc7\u8a0a]({more_info_url}))\n\n1. \u700f\u89bd\u6574\u5408\u9801\u9762\n1. \u65bc Nest \u6574\u5408\u9ede\u9078\u91cd\u65b0\u8a2d\u5b9a\n1. Home Assistant \u5c07\u6703\u5f15\u5c0e\u9032\u884c\u66f4\u65b0\u81f3 Web \u8a8d\u8b49\u6b65\u9a5f\u3002\n\n\u8acb\u53c3\u95b1 Nest [\u6574\u5408\u6307\u5f15]({documentation_url}) \u4ee5\u7372\u5f97\u554f\u984c\u6392\u9664\u8cc7\u8a0a\u3002", + "title": "Nest \u8a8d\u8b49\u6191\u8b49\u9700\u8981\u66f4\u65b0" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/no.json b/homeassistant/components/nextdns/translations/no.json index fb4d6616587..a7685d0caa2 100644 --- a/homeassistant/components/nextdns/translations/no.json +++ b/homeassistant/components/nextdns/translations/no.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Denne NextDNS-profilen er allerede konfigurert." + }, "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", "unknown": "Totalt uventet feil" }, "step": { @@ -15,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 serveren" + } } } \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/no.json b/homeassistant/components/openalpr_local/translations/no.json new file mode 100644 index 00000000000..92e6841ef94 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR Local-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2022.10. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "OpenALPR Local-integrasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json index 5f87fd0ed4b..c41035d1800 100644 --- a/homeassistant/components/opentherm_gw/translations/id.json +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", - "id_exists": "ID gateway sudah ada" + "id_exists": "ID gateway sudah ada", + "timeout_connect": "Tenggang waktu membuat koneksi habis" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index 5cf2e9732e9..cc29595fdc4 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "id_exists": "Gateway ID finnes allerede" + "id_exists": "Gateway ID finnes allerede", + "timeout_connect": "Tidsavbrudd oppretter forbindelse" }, "step": { "init": { diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index d9fbb15b2e8..ad95ab8e4ee 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert" + "already_configured": "Tjenesten er allerede konfigurert", + "anna_with_adam": "B\u00e5de Anna og Adam oppdaget. Legg til din Adam i stedet for din Anna" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/radiotherm/translations/no.json b/homeassistant/components/radiotherm/translations/no.json index fc05e672cbe..4ba3f323973 100644 --- a/homeassistant/components/radiotherm/translations/no.json +++ b/homeassistant/components/radiotherm/translations/no.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av radiotermostatklimaplattformen ved hjelp av YAML blir fjernet i Home Assistant 2022.9. \n\n Din eksisterende konfigurasjon har blitt importert til brukergrensesnittet automatisk. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Radiotermostat YAML-konfigurasjonen blir fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplepush/translations/no.json b/homeassistant/components/simplepush/translations/no.json index 5c2e447b098..af15a931acc 100644 --- a/homeassistant/components/simplepush/translations/no.json +++ b/homeassistant/components/simplepush/translations/no.json @@ -20,7 +20,8 @@ }, "issues": { "deprecated_yaml": { - "description": "Konfigurering av Simplepush med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet." + "description": "Konfigurering av Simplepush med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Simplepush YAML-konfigurasjonen blir fjernet" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 7e6875a2590..a0335228b2d 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk.", "email_2fa_timed_out": "Tidsavbrudd mens du ventet p\u00e5 e-postbasert tofaktorautentisering.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "wrong_account": "Oppgitt brukerlegitimasjon samsvarer ikke med denne SimpliSafe-kontoen." }, "error": { + "identifier_exists": "Konto er allerede registrert", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Autorisasjonskode", "password": "Passord", "username": "Brukernavn" }, - "description": "Skriv inn brukernavn og passord." + "description": "SimpliSafe autentiserer brukere via sin nettapp. P\u00e5 grunn av tekniske begrensninger er det et manuelt trinn p\u00e5 slutten av denne prosessen; s\u00f8rg for at du leser [dokumentasjonen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) f\u00f8r du starter. \n\n N\u00e5r du er klar, klikk [her]( {url} ) for \u00e5 \u00e5pne SimpliSafe-nettappen og angi legitimasjonen din. N\u00e5r prosessen er fullf\u00f8rt, g\u00e5 tilbake hit og skriv inn autorisasjonskoden fra SimpliSafe-nettappens URL." } } }, diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index 47698ef528c..6487e12962d 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -30,7 +30,7 @@ "title": "Hely kiv\u00e1laszt\u00e1sa" }, "user": { - "description": "K\u00e9rem adja meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k]({component_url}) alapj\u00e1n hozott l\u00e9tre.", + "description": "A SmartThings \u00fagy lesz be\u00e1ll\u00edtva, hogy push friss\u00edt\u00e9seket k\u00fcldj\u00f6n a Home Assistantnak a k\u00f6vetkez\u0151 c\u00edmen:\n> {webhook_url}\n\nHa ez \u00edgy nem megfelel\u0151, friss\u00edtse a konfigur\u00e1ci\u00f3t, ind\u00edtsa \u00fajra Home Assistantot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", "title": "Callback URL meger\u0151s\u00edt\u00e9se" } } diff --git a/homeassistant/components/soundtouch/translations/no.json b/homeassistant/components/soundtouch/translations/no.json index bdaee03da54..3761bc55ad7 100644 --- a/homeassistant/components/soundtouch/translations/no.json +++ b/homeassistant/components/soundtouch/translations/no.json @@ -17,5 +17,11 @@ "title": "Bekreft \u00e5 legge til Bose SoundTouch-enhet" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Bose SoundTouch med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Bose SoundTouch YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Bose SoundTouch YAML-konfigurasjonen fjernes" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json index 54e3ca1f8b4..d8172e1a9bd 100644 --- a/homeassistant/components/spotify/translations/no.json +++ b/homeassistant/components/spotify/translations/no.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Spotify med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Spotify YAML-konfigurasjonen er fjernet" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API-endepunkt n\u00e5s" diff --git a/homeassistant/components/steam_online/translations/no.json b/homeassistant/components/steam_online/translations/no.json index 1b30669fad4..08defe9e2be 100644 --- a/homeassistant/components/steam_online/translations/no.json +++ b/homeassistant/components/steam_online/translations/no.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Steam med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Steam YAML-konfigurasjonen er fjernet" + } + }, "options": { "error": { "unauthorized": "Begrenset venneliste: Se dokumentasjonen for hvordan du ser alle andre venner" diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 1d7836f6776..6c8d877551f 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Switchbot-type st\u00f8ttes ikke.", "unknown": "Uventet feil" }, - "flow_title": "{name}", + "flow_title": "{name} ( {address} )", "step": { "user": { "data": { + "address": "Enhetsadresse", "mac": "Enhetens MAC -adresse", "name": "Navn", "password": "Passord" diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index cbef293c7e9..645ec1f8c90 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -16,7 +16,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ipaddress})? A bejelentkez\u00e9shez egy helyi felhaszn\u00e1l\u00f3ra lesz sz\u00fcks\u00e9g, amelyet az UniFi OS Console-ban hoztak l\u00e9tre. Az Ubiquiti Cloud Users nem fog m\u0171k\u00f6dni. Tov\u00e1bbi inform\u00e1ci\u00f3: {local_user_documentation_url}", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ip_address})? A bejelentkez\u00e9shez egy helyi felhaszn\u00e1l\u00f3ra lesz sz\u00fcks\u00e9g, amelyet az UniFi OS Console-ban hoztak l\u00e9tre. Az Ubiquiti Cloud Users nem fog m\u0171k\u00f6dni. Tov\u00e1bbi inform\u00e1ci\u00f3: {local_user_documentation_url}", "title": "UniFi Protect felfedezve" }, "reauth_confirm": { diff --git a/homeassistant/components/uscis/translations/no.json b/homeassistant/components/uscis/translations/no.json new file mode 100644 index 00000000000..a4db40941ed --- /dev/null +++ b/homeassistant/components/uscis/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasjonen med US Citizenship and Immigration Services (USCIS) venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2022.10. \n\n Integrasjonen blir fjernet, fordi den er avhengig av webscraping, noe som ikke er tillatt. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "USCIS-integrasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/no.json b/homeassistant/components/verisure/translations/no.json index 0bd5529c51d..195f8aadd3d 100644 --- a/homeassistant/components/verisure/translations/no.json +++ b/homeassistant/components/verisure/translations/no.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ugyldig godkjenning", - "unknown": "Uventet feil" + "unknown": "Uventet feil", + "unknown_mfa": "Ukjent feil oppstod under MFA-oppsett" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant fant flere Verisure-installasjoner i Mine sider-kontoen din. Velg installasjonen du vil legge til i Home Assistant." }, + "mfa": { + "data": { + "code": "Bekreftelseskode", + "description": "Kontoen din har 2-trinns bekreftelse aktivert. Vennligst skriv inn bekreftelseskoden Verisure sender til deg." + } + }, "reauth_confirm": { "data": { "description": "Autentiser p\u00e5 nytt med Verisure Mine sider-kontoen din.", @@ -22,6 +29,12 @@ "password": "Passord" } }, + "reauth_mfa": { + "data": { + "code": "Bekreftelseskode", + "description": "Kontoen din har 2-trinns bekreftelse aktivert. Vennligst skriv inn bekreftelseskoden Verisure sender til deg." + } + }, "user": { "data": { "description": "Logg p\u00e5 med Verisure Mine sider-kontoen din.", diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 40ba24111bf..488a43592a5 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -29,6 +29,7 @@ "title": "Godkjenne integrering p\u00e5 nytt" }, "reauth_confirm": { + "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", "title": "Re-autentiser integrasjon" } } diff --git a/homeassistant/components/xbox/translations/no.json b/homeassistant/components/xbox/translations/no.json index 4736fc91bf0..130905fc3d7 100644 --- a/homeassistant/components/xbox/translations/no.json +++ b/homeassistant/components/xbox/translations/no.json @@ -13,5 +13,11 @@ "title": "Velg godkjenningsmetode" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Xbox i configuration.yaml blir fjernet i Home Assistant 2022.9. \n\n Din eksisterende OAuth-applikasjonslegitimasjon og tilgangsinnstillinger er automatisk importert til brukergrensesnittet. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Xbox YAML-konfigurasjonen blir fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index 1c24ab7172e..873bcb3f3bb 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -6,7 +6,8 @@ "decryption_failed": "La clau d'enlla\u00e7 proporcionada no ha funcionat, les dades del sensor no s'han pogut desxifrar. Comprova-la i torna-ho a provar.", "expected_24_characters": "S'espera una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals.", "expected_32_characters": "S'espera una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals.", - "no_devices_found": "No s'han trobat dispositius a la xarxa" + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index 0d3e2c542d2..6fc2c74af75 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -6,7 +6,8 @@ "decryption_failed": "Der bereitgestellte Bindkey funktionierte nicht, Sensordaten konnten nicht entschl\u00fcsselt werden. Bitte \u00fcberpr\u00fcfe es und versuche es erneut.", "expected_24_characters": "Erwartet wird ein 24-stelliger hexadezimaler Bindkey.", "expected_32_characters": "Erwartet wird ein 32-stelliger hexadezimaler Bindkey.", - "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindungsschl\u00fcssel." }, + "slow_confirm": { + "description": "Von diesem Ger\u00e4t wurde in der letzten Minute kein Broadcast gesendet, so dass wir nicht sicher sind, ob dieses Ger\u00e4t Verschl\u00fcsselung verwendet oder nicht. Dies kann daran liegen, dass das Ger\u00e4t ein langsames Sendeintervall verwendet. Best\u00e4tige, dass du das Ger\u00e4t trotzdem hinzuf\u00fcgen m\u00f6chtest. Wenn das n\u00e4chste Mal ein Broadcast empfangen wird, wirst du aufgefordert, den Bindkey einzugeben, falls er ben\u00f6tigt wird." + }, "user": { "data": { "address": "Ger\u00e4t" diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 2cb77dd2c07..be75cc007b2 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful" }, @@ -31,6 +34,9 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." + }, "user": { "data": { "address": "Device" diff --git a/homeassistant/components/xiaomi_ble/translations/fr.json b/homeassistant/components/xiaomi_ble/translations/fr.json index c8a1af034cf..4c9b9b980ed 100644 --- a/homeassistant/components/xiaomi_ble/translations/fr.json +++ b/homeassistant/components/xiaomi_ble/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index b03a41f60a6..3962e890d4f 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -6,7 +6,8 @@ "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", "expected_24_characters": "24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", - "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." }, + "slow_confirm": { + "description": "Az elm\u00falt egy percben nem \u00e9rkezett ad\u00e1sjel az eszk\u00f6zt\u0151l, \u00edgy nem az nem \u00e1llap\u00edthat\u00f3 meg egy\u00e9rtelm\u0171en, hogy ez a k\u00e9sz\u00fcl\u00e9k haszn\u00e1l-e titkos\u00edt\u00e1st vagy sem. Ez az\u00e9rt lehet, mert az eszk\u00f6z ritka jelad\u00e1si intervallumot haszn\u00e1l. Meger\u0151s\u00edtheti most az eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1t, de a k\u00f6vetkez\u0151 ad\u00e1sjel fogad\u00e1sakor a rendszer k\u00e9rni fogja, hogy adja meg az eszk\u00f6z kulcs\u00e1t (bindkeyt), ha az sz\u00fcks\u00e9ges." + }, "user": { "data": { "address": "Eszk\u00f6z" diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json index ea45a7ba9c3..5d01dcab709 100644 --- a/homeassistant/components/xiaomi_ble/translations/id.json +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -6,7 +6,8 @@ "decryption_failed": "Bindkey yang disediakan tidak berfungsi, data sensor tidak dapat didekripsi. Silakan periksa dan coba lagi.", "expected_24_characters": "Diharapkan bindkey berupa 24 karakter heksadesimal.", "expected_32_characters": "Diharapkan bindkey berupa 32 karakter heksadesimal.", - "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "reauth_successful": "Autentikasi ulang berhasil" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "Data sensor yang disiarkan oleh sensor telah dienkripsi. Untuk mendekripsinya, diperlukan 24 karakter bindkey heksadesimal ." }, + "slow_confirm": { + "description": "Belum ada siaran dari perangkat ini dalam menit terakhir jadi kami tidak yakin apakah perangkat ini menggunakan enkripsi atau tidak. Ini mungkin terjadi karena perangkat menggunakan interval siaran yang lambat. Konfirmasikan sekarang untuk menambahkan perangkat ini, dan ketika siaran diterima nanti, Anda akan diminta untuk memasukkan kunci bind jika diperlukan." + }, "user": { "data": { "address": "Perangkat" diff --git a/homeassistant/components/xiaomi_ble/translations/no.json b/homeassistant/components/xiaomi_ble/translations/no.json new file mode 100644 index 00000000000..6c63f53c6aa --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "decryption_failed": "Den oppgitte bindingsn\u00f8kkelen fungerte ikke, sensordata kunne ikke dekrypteres. Vennligst sjekk det og pr\u00f8v igjen.", + "expected_24_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 24 tegn.", + "expected_32_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 32 tegn.", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sensordataene som sendes av sensoren er kryptert. For \u00e5 dekryptere den trenger vi en heksadesimal bindn\u00f8kkel p\u00e5 32 tegn." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sensordataene som sendes av sensoren er kryptert. For \u00e5 dekryptere den trenger vi en heksadesimal bindn\u00f8kkel p\u00e5 24 tegn." + }, + "slow_confirm": { + "description": "Det har ikke v\u00e6rt en sending fra denne enheten det siste minuttet, s\u00e5 vi er ikke sikre p\u00e5 om denne enheten bruker kryptering eller ikke. Dette kan skyldes at enheten bruker et tregt kringkastingsintervall. Bekreft \u00e5 legge til denne enheten uansett, s\u00e5 neste gang en kringkasting mottas, vil du bli bedt om \u00e5 angi bindn\u00f8kkelen hvis den er n\u00f8dvendig." + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json index 21c251bf0eb..b0776a217cd 100644 --- a/homeassistant/components/xiaomi_ble/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -6,7 +6,8 @@ "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", "expected_24_characters": "Espera-se uma bindkey hexadecimal de 24 caracteres.", "expected_32_characters": "Esperado um bindkey hexadecimal de 32 caracteres.", - "no_devices_found": "Nenhum dispositivo encontrado na rede" + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma bindkey hexadecimal de 24 caracteres." }, + "slow_confirm": { + "description": "N\u00e3o houve uma transmiss\u00e3o deste dispositivo no \u00faltimo minuto, por isso n\u00e3o temos certeza se este dispositivo usa criptografia ou n\u00e3o. Isso pode ocorrer porque o dispositivo usa um intervalo de transmiss\u00e3o lento. Confirme para adicionar este dispositivo de qualquer maneira e, na pr\u00f3xima vez que uma transmiss\u00e3o for recebida, voc\u00ea ser\u00e1 solicitado a inserir sua bindkey, se necess\u00e1rio." + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json index 81f7e2050af..13d0a986b79 100644 --- a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -6,7 +6,8 @@ "decryption_failed": "\u6240\u63d0\u4f9b\u7684\u7d81\u5b9a\u78bc\u7121\u6cd5\u4f7f\u7528\u3001\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6cd5\u89e3\u5bc6\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", "expected_24_characters": "\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", "expected_32_characters": "\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" }, + "slow_confirm": { + "description": "\u8a72\u88dd\u7f6e\u65bc\u904e\u53bb\u4e00\u5206\u9418\u5167\u3001\u672a\u9032\u884c\u4efb\u4f55\u72c0\u614b\u5ee3\u64ad\uff0c\u56e0\u6b64\u7121\u6cd5\u78ba\u5b9a\u88dd\u7f6e\u662f\u5426\u4f7f\u7528\u52a0\u5bc6\u901a\u8a0a\u3002\u4e5f\u53ef\u80fd\u56e0\u70ba\u88dd\u7f6e\u7684\u66f4\u65b0\u983b\u7387\u8f03\u6162\u3002\u78ba\u8a8d\u9084\u662f\u8981\u65b0\u589e\u6b64\u88dd\u7f6e\u3001\u65bc\u4e0b\u6b21\u6536\u5230\u88dd\u7f6e\u5ee3\u64ad\u6642\uff0c\u5982\u679c\u9700\u8981\u3001\u5c07\u63d0\u793a\u60a8\u8f38\u5165\u7d81\u5b9a\u78bc\u3002" + }, "user": { "data": { "address": "\u88dd\u7f6e" diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 4e719e63ae1..47ae51b89f0 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -46,10 +46,13 @@ "title": "Alternativer for alarmkontrollpanel" }, "zha_options": { + "always_prefer_xy_color_mode": "Foretrekker alltid XY-fargemodus", "consider_unavailable_battery": "Vurder batteridrevne enheter som utilgjengelige etter (sekunder)", "consider_unavailable_mains": "Tenk p\u00e5 str\u00f8mnettet som ikke er tilgjengelig etter (sekunder)", "default_light_transition": "Standard lysovergangstid (sekunder)", "enable_identify_on_join": "Aktiver identifiseringseffekt n\u00e5r enheter blir med i nettverket", + "enhanced_light_transition": "Aktiver forbedret lysfarge/temperaturovergang fra en off-tilstand", + "light_transitioning_flag": "Aktiver skyveknappen for forbedret lysstyrke under lysovergang", "title": "Globale alternativer" } }, From 0dbb119677c15d0a430bc6683ebd377a790e916a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 20:34:46 -1000 Subject: [PATCH 123/903] Bump pySwitchbot to 0.17.3 to fix hang at startup (#76103) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 41d0d7efda6..f01eae4a938 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.17.1"], + "requirements": ["PySwitchbot==0.17.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 253dee5445a..511ec048104 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a8adbdf846..00abfb8c115 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 6006fc7e30dbeb974de8684ddb17f20ef7aa9772 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 20:35:41 -1000 Subject: [PATCH 124/903] Bump aiohomekit to 1.2.3 to fix hang at startup (#76102) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 2de2a915d41..ac1be576906 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.2"], + "requirements": ["aiohomekit==1.2.3"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 511ec048104..db7a13b7357 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.2 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00abfb8c115..0fb93106711 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.2 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http From 98f0b24c42cf8557019bd4d749d5b27c13e96524 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 3 Aug 2022 09:41:00 +0200 Subject: [PATCH 125/903] Fix deconz group log warning (#76114) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 8019d0df2df..6384ebfcd5f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==100"], + "requirements": ["pydeconz==101"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index db7a13b7357..5cdc66dec32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1461,7 +1461,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==100 +pydeconz==101 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fb93106711..24b0f4b52e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==100 +pydeconz==101 # homeassistant.components.dexcom pydexcom==0.2.3 From 1ba18f8df6fafbe17d83d439c079437fae68d448 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Aug 2022 09:56:13 +0200 Subject: [PATCH 126/903] Improve type hints in vesync lights (#75998) * Improve type hints in vesync lights * Adjust import --- homeassistant/components/vesync/__init__.py | 2 +- homeassistant/components/vesync/common.py | 31 +++++++++++---------- homeassistant/components/vesync/light.py | 16 +++-------- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 10aa49514e5..55addd81066 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -21,7 +21,7 @@ from .const import ( VS_SWITCHES, ) -PLATFORMS = [Platform.SWITCH, Platform.FAN, Platform.LIGHT, Platform.SENSOR] +PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index e11897ea9ae..752a65ff051 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -1,7 +1,10 @@ """Common utilities for VeSync Component.""" import logging +from typing import Any -from homeassistant.helpers.entity import Entity, ToggleEntity +from pyvesync.vesyncbasedevice import VeSyncBaseDevice + +from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity from .const import DOMAIN, VS_FANS, VS_LIGHTS, VS_SENSORS, VS_SWITCHES @@ -48,7 +51,7 @@ async def async_process_devices(hass, manager): class VeSyncBaseEntity(Entity): """Base class for VeSync Entity Representations.""" - def __init__(self, device): + def __init__(self, device: VeSyncBaseDevice) -> None: """Initialize the VeSync device.""" self.device = device self._attr_unique_id = self.base_unique_id @@ -65,7 +68,7 @@ class VeSyncBaseEntity(Entity): return self.device.cid @property - def base_name(self): + def base_name(self) -> str: """Return the name of the device.""" # Same story here as `base_unique_id` above return self.device.device_name @@ -76,17 +79,17 @@ class VeSyncBaseEntity(Entity): return self.device.connection_status == "online" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.base_unique_id)}, - "name": self.base_name, - "model": self.device.device_type, - "default_manufacturer": "VeSync", - "sw_version": self.device.current_firm_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self.base_unique_id)}, + name=self.base_name, + model=self.device.device_type, + default_manufacturer="VeSync", + sw_version=self.device.current_firm_version, + ) - def update(self): + def update(self) -> None: """Update vesync device.""" self.device.update() @@ -100,10 +103,10 @@ class VeSyncDevice(VeSyncBaseEntity, ToggleEntity): return self.device.details @property - def is_on(self): + def is_on(self) -> bool: """Return True if device is on.""" return self.device.device_status == "on" - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self.device.turn_off() diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index 8727a770112..57329c0973e 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -67,7 +67,7 @@ class VeSyncBaseLight(VeSyncDevice, LightEntity): """Base class for VeSync Light Devices Representations.""" @property - def brightness(self): + def brightness(self) -> int: """Get light brightness.""" # get value from pyvesync library api, result = self.device.brightness @@ -141,10 +141,12 @@ class VeSyncTunableWhiteLightHA(VeSyncBaseLight, LightEntity): """Representation of a VeSync Tunable White Light device.""" _attr_color_mode = ColorMode.COLOR_TEMP + _attr_max_mireds = 370 # 1,000,000 divided by 2700 Kelvin = 370 Mireds + _attr_min_mireds = 154 # 1,000,000 divided by 6500 Kelvin = 154 Mireds _attr_supported_color_modes = {ColorMode.COLOR_TEMP} @property - def color_temp(self): + def color_temp(self) -> int: """Get device white temperature.""" # get value from pyvesync library api, result = self.device.color_temp_pct @@ -169,13 +171,3 @@ class VeSyncTunableWhiteLightHA(VeSyncBaseLight, LightEntity): ) # ensure value between minimum and maximum Mireds return max(self.min_mireds, min(color_temp_value, self.max_mireds)) - - @property - def min_mireds(self): - """Set device coldest white temperature.""" - return 154 # 154 Mireds ( 1,000,000 divided by 6500 Kelvin = 154 Mireds) - - @property - def max_mireds(self): - """Set device warmest white temperature.""" - return 370 # 370 Mireds ( 1,000,000 divided by 2700 Kelvin = 370 Mireds) From 1ee4445a7b8a5bda428b34f8746bcb97bf914ecb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Aug 2022 10:15:41 +0200 Subject: [PATCH 127/903] Improve type hints in azure devops config flow (#75909) * Improve type hints in azure devops config flow * Improve --- .../components/azure_devops/config_flow.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 8fba3378886..a654dc2be61 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the Azure DevOps integration.""" +from __future__ import annotations + from collections.abc import Mapping from typing import Any @@ -19,11 +21,13 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize config flow.""" - self._organization = None - self._project = None - self._pat = None + self._organization: str | None = None + self._project: str | None = None + self._pat: str | None = None - async def _show_setup_form(self, errors=None): + async def _show_setup_form( + self, errors: dict[str, str] | None = None + ) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -37,7 +41,7 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def _show_reauth_form(self, errors=None): + async def _show_reauth_form(self, errors: dict[str, str]) -> FlowResult: """Show the reauth form to the user.""" return self.async_show_form( step_id="reauth", @@ -48,9 +52,9 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def _check_setup(self): + async def _check_setup(self) -> dict[str, str] | None: """Check the setup of the flow.""" - errors = {} + errors: dict[str, str] = {} client = DevOpsClient() @@ -69,10 +73,12 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): return errors return None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: - return await self._show_setup_form(user_input) + return await self._show_setup_form() self._organization = user_input[CONF_ORG] self._project = user_input[CONF_PROJECT] @@ -115,7 +121,7 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="reauth_successful") - def _async_create_entry(self): + def _async_create_entry(self) -> FlowResult: """Handle create entry.""" return self.async_create_entry( title=f"{self._organization}/{self._project}", From 651928ee0cd3f85db2526e2809c416ab824c1bc0 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Wed, 3 Aug 2022 10:31:09 +0200 Subject: [PATCH 128/903] Bump `azure-servicebus` to support py3.10 (#76092) Bump azure-servicebus --- .../azure_service_bus/manifest.json | 2 +- .../components/azure_service_bus/notify.py | 24 ++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json index 6cf5e2bf406..26ccea446f2 100644 --- a/homeassistant/components/azure_service_bus/manifest.json +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -2,7 +2,7 @@ "domain": "azure_service_bus", "name": "Azure Service Bus", "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", - "requirements": ["azure-servicebus==0.50.3"], + "requirements": ["azure-servicebus==7.8.0"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_push", "loggers": ["azure"] diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 0d48ff6b2d6..53873373011 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -2,11 +2,12 @@ import json import logging -from azure.servicebus.aio import Message, ServiceBusClient -from azure.servicebus.common.errors import ( - MessageSendFailed, +from azure.servicebus import ServiceBusMessage +from azure.servicebus.aio import ServiceBusClient +from azure.servicebus.exceptions import ( + MessagingEntityNotFoundError, ServiceBusConnectionError, - ServiceBusResourceNotFound, + ServiceBusError, ) import voluptuous as vol @@ -60,10 +61,10 @@ def get_service(hass, config, discovery_info=None): try: if queue_name: - client = servicebus.get_queue(queue_name) + client = servicebus.get_queue_sender(queue_name) else: - client = servicebus.get_topic(topic_name) - except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err: + client = servicebus.get_topic_sender(topic_name) + except (ServiceBusConnectionError, MessagingEntityNotFoundError) as err: _LOGGER.error( "Connection error while creating client for queue/topic '%s'. %s", queue_name or topic_name, @@ -93,11 +94,12 @@ class ServiceBusNotificationService(BaseNotificationService): if data := kwargs.get(ATTR_DATA): dto.update(data) - queue_message = Message(json.dumps(dto)) - queue_message.properties.content_type = CONTENT_TYPE_JSON + queue_message = ServiceBusMessage( + json.dumps(dto), content_type=CONTENT_TYPE_JSON + ) try: - await self._client.send(queue_message) - except MessageSendFailed as err: + await self._client.send_messages(queue_message) + except ServiceBusError as err: _LOGGER.error( "Could not send service bus notification to %s. %s", self._client.name, diff --git a/requirements_all.txt b/requirements_all.txt index 5cdc66dec32..80031eadf8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -378,7 +378,7 @@ axis==44 azure-eventhub==5.7.0 # homeassistant.components.azure_service_bus -azure-servicebus==0.50.3 +azure-servicebus==7.8.0 # homeassistant.components.baidu baidu-aip==1.6.6 From 7f83cba83ab0226e3dc9e261f941f65dff91f6be Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Wed, 3 Aug 2022 11:53:29 +0200 Subject: [PATCH 129/903] Bump pyTibber to 0.24.0 (#76098) To be able to add tibber sensors for production. --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index b07cebde680..0ac93c86668 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.22.3"], + "requirements": ["pyTibber==0.24.0"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 80031eadf8c..1c2079d6857 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1359,7 +1359,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.22.3 +pyTibber==0.24.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24b0f4b52e7..cfd8122bcff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.22.3 +pyTibber==0.24.0 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 34b0e0d062e96f5424267d79fe2c12e41fab9b9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Aug 2022 07:46:54 -1000 Subject: [PATCH 130/903] Bump bleak to 0.15.1 (#76136) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 40e63ec7180..f3828db5d10 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.3"], + "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.1.3"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 81182a7d8ba..3ff9fd3c114 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 -bleak==0.15.0 +bleak==0.15.1 bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1c2079d6857..e7e6999053e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,7 @@ bimmer_connected==0.10.1 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.15.0 +bleak==0.15.1 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfd8122bcff..18ef8e97b63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ bellows==0.31.2 bimmer_connected==0.10.1 # homeassistant.components.bluetooth -bleak==0.15.0 +bleak==0.15.1 # homeassistant.components.blebox blebox_uniapi==2.0.2 From 84747ada668a2f07893e2b4093a586c4432b5e5f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Aug 2022 21:22:30 +0200 Subject: [PATCH 131/903] Use attributes in decora light (#76047) --- homeassistant/components/decora/light.py | 66 ++++++++---------------- 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 9dbc031d476..c6fae73bc28 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -6,7 +6,7 @@ import copy from functools import wraps import logging import time -from typing import TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from bluepy.btle import BTLEException # pylint: disable=import-error import decora # pylint: disable=import-error @@ -21,10 +21,13 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME -from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + _DecoraLightT = TypeVar("_DecoraLightT", bound="DecoraLight") _R = TypeVar("_R") @@ -110,65 +113,40 @@ class DecoraLight(LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def __init__(self, device): + def __init__(self, device: dict[str, Any]) -> None: """Initialize the light.""" - self._name = device["name"] - self._address = device["address"] + self._attr_name = device["name"] + self._attr_unique_id = device["address"] self._key = device["key"] - self._switch = decora.decora(self._address, self._key) - self._brightness = 0 - self._state = False - - @property - def unique_id(self): - """Return the ID of this light.""" - return self._address - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def assumed_state(self): - """We can read the actual state.""" - return False + self._switch = decora.decora(device["address"], self._key) + self._attr_brightness = 0 + self._attr_is_on = False @retry - def set_state(self, brightness): + def set_state(self, brightness: int) -> None: """Set the state of this lamp to the provided brightness.""" self._switch.set_brightness(int(brightness / 2.55)) - self._brightness = brightness + self._attr_brightness = brightness @retry - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) self._switch.on() - self._state = True + self._attr_is_on = True if brightness is not None: self.set_state(brightness) @retry - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified or all lights off.""" self._switch.off() - self._state = False + self._attr_is_on = False @retry - def update(self): + def update(self) -> None: """Synchronise internal state with the actual light state.""" - self._brightness = self._switch.get_brightness() * 2.55 - self._state = self._switch.get_on() + self._attr_brightness = self._switch.get_brightness() * 2.55 + self._attr_is_on = self._switch.get_on() From fbde347e6490292549fffdfe374561501dff633f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 3 Aug 2022 13:24:55 -0600 Subject: [PATCH 132/903] Move RainMachine utils to the correct location (#76051) * Move RainMachine utils to the correct location * Imports --- homeassistant/components/rainmachine/const.py | 17 ----------------- homeassistant/components/rainmachine/sensor.py | 4 +--- homeassistant/components/rainmachine/switch.py | 2 +- homeassistant/components/rainmachine/util.py | 17 +++++++++++++++++ 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index f386341c161..56c1660a0ba 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -1,8 +1,6 @@ """Define constants for the SimpliSafe component.""" import logging -from homeassistant.backports.enum import StrEnum - LOGGER = logging.getLogger(__package__) DOMAIN = "rainmachine" @@ -19,18 +17,3 @@ DATA_ZONES = "zones" DEFAULT_PORT = 8080 DEFAULT_ZONE_RUN = 60 * 10 - - -class RunStates(StrEnum): - """Define an enum for program/zone run states.""" - - NOT_RUNNING = "Not Running" - QUEUED = "Queued" - RUNNING = "Running" - - -RUN_STATE_MAP = { - 0: RunStates.NOT_RUNNING, - 1: RunStates.RUNNING, - 2: RunStates.QUEUED, -} diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index e57386fe0ec..797420b460f 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -31,14 +31,12 @@ from .const import ( DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, DOMAIN, - RUN_STATE_MAP, - RunStates, ) from .model import ( RainMachineDescriptionMixinApiCategory, RainMachineDescriptionMixinUid, ) -from .util import key_exists +from .util import RUN_STATE_MAP, RunStates, key_exists DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE = timedelta(seconds=5) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index aa91f529b5b..1fcdab49836 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -30,9 +30,9 @@ from .const import ( DATA_ZONES, DEFAULT_ZONE_RUN, DOMAIN, - RUN_STATE_MAP, ) from .model import RainMachineDescriptionMixinUid +from .util import RUN_STATE_MAP ATTR_AREA = "area" ATTR_CS_ON = "cs_on" diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index 27a0636688e..6bf15f2fb9c 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -3,6 +3,23 @@ from __future__ import annotations from typing import Any +from homeassistant.backports.enum import StrEnum + + +class RunStates(StrEnum): + """Define an enum for program/zone run states.""" + + NOT_RUNNING = "Not Running" + QUEUED = "Queued" + RUNNING = "Running" + + +RUN_STATE_MAP = { + 0: RunStates.NOT_RUNNING, + 1: RunStates.RUNNING, + 2: RunStates.QUEUED, +} + def key_exists(data: dict[str, Any], search_key: str) -> bool: """Return whether a key exists in a nested dict.""" From 1806172551236039306717edcc910ef3c17df258 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Aug 2022 21:26:34 +0200 Subject: [PATCH 133/903] Improve type hints in hive lights (#76025) --- homeassistant/components/hive/__init__.py | 6 +++--- homeassistant/components/hive/light.py | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index a1a784162e8..bd74ecbff11 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -139,7 +139,7 @@ def refresh_system( class HiveEntity(Entity): """Initiate Hive Base Class.""" - def __init__(self, hive, hive_device): + def __init__(self, hive: Hive, hive_device: dict[str, Any]) -> None: """Initialize the instance.""" self.hive = hive self.device = hive_device @@ -153,9 +153,9 @@ class HiveEntity(Entity): sw_version=self.device["deviceData"]["version"], via_device=(DOMAIN, self.device["parentDevice"]), ) - self.attributes = {} + self.attributes: dict[str, Any] = {} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to Home Assistant.""" self.async_on_remove( async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 69345c430c7..a340aee0764 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import TYPE_CHECKING, Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -18,6 +19,9 @@ import homeassistant.util.color as color_util from . import HiveEntity, refresh_system from .const import ATTR_MODE, DOMAIN +if TYPE_CHECKING: + from apyhiveapi import Hive + PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) @@ -27,7 +31,7 @@ async def async_setup_entry( ) -> None: """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN][entry.entry_id] + hive: Hive = hass.data[DOMAIN][entry.entry_id] devices = hive.session.deviceList.get("light") entities = [] if devices: @@ -39,7 +43,7 @@ async def async_setup_entry( class HiveDeviceLight(HiveEntity, LightEntity): """Hive Active Light Device.""" - def __init__(self, hive, hive_device): + def __init__(self, hive: Hive, hive_device: dict[str, Any]) -> None: """Initialise hive light.""" super().__init__(hive, hive_device) if self.device["hiveType"] == "warmwhitelight": @@ -55,22 +59,22 @@ class HiveDeviceLight(HiveEntity, LightEntity): self._attr_max_mireds = 370 @refresh_system - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" new_brightness = None new_color_temp = None new_color = None if ATTR_BRIGHTNESS in kwargs: - tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS) + tmp_new_brightness = kwargs[ATTR_BRIGHTNESS] percentage_brightness = (tmp_new_brightness / 255) * 100 new_brightness = int(round(percentage_brightness / 5.0) * 5.0) if new_brightness == 0: new_brightness = 5 if ATTR_COLOR_TEMP in kwargs: - tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP) + tmp_new_color_temp = kwargs[ATTR_COLOR_TEMP] new_color_temp = round(1000000 / tmp_new_color_temp) if ATTR_HS_COLOR in kwargs: - get_new_color = kwargs.get(ATTR_HS_COLOR) + get_new_color = kwargs[ATTR_HS_COLOR] hue = int(get_new_color[0]) saturation = int(get_new_color[1]) new_color = (hue, saturation, 100) @@ -80,11 +84,11 @@ class HiveDeviceLight(HiveEntity, LightEntity): ) @refresh_system - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self.hive.light.turnOff(self.device) - async def async_update(self): + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.light.getLight(self.device) From dd862595a3dc3c0715cb7527eb45ef5136228de1 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Wed, 3 Aug 2022 23:19:10 +0300 Subject: [PATCH 134/903] New binary sensors for Ukraine Alarm (#76155) new alert types for ukraine alarm --- .../components/ukraine_alarm/binary_sensor.py | 14 ++++++++++++++ homeassistant/components/ukraine_alarm/const.py | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/ukraine_alarm/binary_sensor.py b/homeassistant/components/ukraine_alarm/binary_sensor.py index 10d66e0cb64..3cfe79ef5fb 100644 --- a/homeassistant/components/ukraine_alarm/binary_sensor.py +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -18,6 +18,8 @@ from . import UkraineAlarmDataUpdateCoordinator from .const import ( ALERT_TYPE_AIR, ALERT_TYPE_ARTILLERY, + ALERT_TYPE_CHEMICAL, + ALERT_TYPE_NUCLEAR, ALERT_TYPE_UNKNOWN, ALERT_TYPE_URBAN_FIGHTS, ATTRIBUTION, @@ -49,6 +51,18 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:tank", ), + BinarySensorEntityDescription( + key=ALERT_TYPE_CHEMICAL, + name="Chemical", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:chemical-weapon", + ), + BinarySensorEntityDescription( + key=ALERT_TYPE_NUCLEAR, + name="Nuclear", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:nuke", + ), ) diff --git a/homeassistant/components/ukraine_alarm/const.py b/homeassistant/components/ukraine_alarm/const.py index cc1ae352967..bb0902293d4 100644 --- a/homeassistant/components/ukraine_alarm/const.py +++ b/homeassistant/components/ukraine_alarm/const.py @@ -10,10 +10,14 @@ ALERT_TYPE_UNKNOWN = "UNKNOWN" ALERT_TYPE_AIR = "AIR" ALERT_TYPE_ARTILLERY = "ARTILLERY" ALERT_TYPE_URBAN_FIGHTS = "URBAN_FIGHTS" +ALERT_TYPE_CHEMICAL = "CHEMICAL" +ALERT_TYPE_NUCLEAR = "NUCLEAR" ALERT_TYPES = { ALERT_TYPE_UNKNOWN, ALERT_TYPE_AIR, ALERT_TYPE_ARTILLERY, ALERT_TYPE_URBAN_FIGHTS, + ALERT_TYPE_CHEMICAL, + ALERT_TYPE_NUCLEAR, } PLATFORMS = [Platform.BINARY_SENSOR] From 842cc060f80a632032dacbe1e2eaa8ca6421eda0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 3 Aug 2022 22:33:05 +0200 Subject: [PATCH 135/903] Fix zwave_js addon info (#76044) * Add add-on store info command * Use add-on store info command in zwave_js * Fix init tests * Update tests * Fix method for addon store info * Fix response parsing * Fix store addon installed response parsing * Remove addon info log that can contain network keys * Add supervisor store addon info test * Default to version None if add-on not installed Co-authored-by: Mike Degatano Co-authored-by: Mike Degatano --- homeassistant/components/hassio/__init__.py | 12 ++++ homeassistant/components/zwave_js/addon.py | 22 +++++-- tests/components/hassio/test_init.py | 20 ++++++- tests/components/zwave_js/conftest.py | 60 +++++++++++++++++-- tests/components/zwave_js/test_config_flow.py | 18 +++--- tests/components/zwave_js/test_init.py | 22 ++++--- 6 files changed, 122 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 46592cbc20c..8535a0c3cc6 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -223,12 +223,24 @@ HARDWARE_INTEGRATIONS = { async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: """Return add-on info. + The add-on must be installed. The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] return await hassio.get_addon_info(slug) +@api_data +async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict: + """Return add-on store info. + + The caller of the function should handle HassioAPIError. + """ + hassio: HassIO = hass.data[DOMAIN] + command = f"/store/addons/{slug}" + return await hassio.send_command(command, method="get") + + @bind_hass async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict: """Update Supervisor diagnostics toggle. diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 7552ee117cc..610fc850e90 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -14,6 +14,7 @@ from homeassistant.components.hassio import ( async_create_backup, async_get_addon_discovery_info, async_get_addon_info, + async_get_addon_store_info, async_install_addon, async_restart_addon, async_set_addon_options, @@ -136,7 +137,17 @@ class AddonManager: @api_error("Failed to get the Z-Wave JS add-on info") async def async_get_addon_info(self) -> AddonInfo: """Return and cache Z-Wave JS add-on info.""" - addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG) + addon_store_info = await async_get_addon_store_info(self._hass, ADDON_SLUG) + LOGGER.debug("Add-on store info: %s", addon_store_info) + if not addon_store_info["installed"]: + return AddonInfo( + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + addon_info = await async_get_addon_info(self._hass, ADDON_SLUG) addon_state = self.async_get_addon_state(addon_info) return AddonInfo( options=addon_info["options"], @@ -148,10 +159,8 @@ class AddonManager: @callback def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: """Return the current state of the Z-Wave JS add-on.""" - addon_state = AddonState.NOT_INSTALLED + addon_state = AddonState.NOT_RUNNING - if addon_info["version"] is not None: - addon_state = AddonState.NOT_RUNNING if addon_info["state"] == "started": addon_state = AddonState.RUNNING if self._install_task and not self._install_task.done(): @@ -226,7 +235,7 @@ class AddonManager: """Update the Z-Wave JS add-on if needed.""" addon_info = await self.async_get_addon_info() - if addon_info.version is None: + if addon_info.state is AddonState.NOT_INSTALLED: raise AddonError("Z-Wave JS add-on is not installed") if not addon_info.update_available: @@ -301,6 +310,9 @@ class AddonManager: """Configure and start Z-Wave JS add-on.""" addon_info = await self.async_get_addon_info() + if addon_info.state is AddonState.NOT_INSTALLED: + raise AddonError("Z-Wave JS add-on is not installed") + new_addon_options = { CONF_ADDON_DEVICE: usb_path, CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key, diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 60fec517aa9..41b679e448a 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -8,7 +8,12 @@ import pytest from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import frontend from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.hassio import ADDONS_COORDINATOR, DOMAIN, STORAGE_KEY +from homeassistant.components.hassio import ( + ADDONS_COORDINATOR, + DOMAIN, + STORAGE_KEY, + async_get_addon_store_info, +) from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers.device_registry import async_get @@ -748,3 +753,16 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration): assert aioclient_mock.call_count == 15 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_get_store_addon_info(hass, hassio_stubs, aioclient_mock): + """Test get store add-on info from Supervisor API.""" + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://127.0.0.1/store/addons/test", + json={"result": "ok", "data": {"name": "bla"}}, + ) + + data = await async_get_addon_store_info(hass, "test") + assert data["name"] == "bla" + assert aioclient_mock.call_count == 1 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 7cf7ebd7ea2..1524aca719e 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -38,18 +38,56 @@ def mock_addon_info(addon_info_side_effect): yield addon_info +@pytest.fixture(name="addon_store_info_side_effect") +def addon_store_info_side_effect_fixture(): + """Return the add-on store info side effect.""" + return None + + +@pytest.fixture(name="addon_store_info") +def mock_addon_store_info(addon_store_info_side_effect): + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.zwave_js.addon.async_get_addon_store_info", + side_effect=addon_store_info_side_effect, + ) as addon_store_info: + addon_store_info.return_value = { + "installed": None, + "state": None, + "version": "1.0.0", + } + yield addon_store_info + + @pytest.fixture(name="addon_running") -def mock_addon_running(addon_info): +def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } addon_info.return_value["state"] = "started" + addon_info.return_value["version"] = "1.0.0" return addon_info @pytest.fixture(name="addon_installed") -def mock_addon_installed(addon_info): +def mock_addon_installed(addon_store_info, addon_info): """Mock add-on already installed but not running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_not_installed") +def mock_addon_not_installed(addon_store_info, addon_info): + """Mock add-on not installed.""" return addon_info @@ -81,13 +119,18 @@ def mock_set_addon_options(set_addon_options_side_effect): @pytest.fixture(name="install_addon_side_effect") -def install_addon_side_effect_fixture(addon_info): +def install_addon_side_effect_fixture(addon_store_info, addon_info): """Return the install add-on side effect.""" async def install_addon(hass, slug): """Mock install add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0" + addon_info.return_value["version"] = "1.0.0" return install_addon @@ -112,11 +155,16 @@ def mock_update_addon(): @pytest.fixture(name="start_addon_side_effect") -def start_addon_side_effect_fixture(addon_info): +def start_addon_side_effect_fixture(addon_store_info, addon_info): """Return the start add-on options side effect.""" async def start_addon(hass, slug): """Mock start add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } addon_info.return_value["state"] = "started" return start_addon diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index f107a5fd8e2..a8a2c6c7191 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -422,7 +422,7 @@ async def test_abort_discovery_with_existing_entry( async def test_abort_hassio_discovery_with_existing_flow( - hass, supervisor, addon_options + hass, supervisor, addon_installed, addon_options ): """Test hassio discovery flow is aborted when another discovery has happened.""" result = await hass.config_entries.flow.async_init( @@ -701,15 +701,13 @@ async def test_discovery_addon_not_running( async def test_discovery_addon_not_installed( hass, supervisor, - addon_installed, + addon_not_installed, install_addon, addon_options, set_addon_options, start_addon, ): """Test discovery with add-on not installed.""" - addon_installed.return_value["version"] = None - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, @@ -1443,7 +1441,7 @@ async def test_addon_installed_already_configured( async def test_addon_not_installed( hass, supervisor, - addon_installed, + addon_not_installed, install_addon, addon_options, set_addon_options, @@ -1451,8 +1449,6 @@ async def test_addon_not_installed( get_addon_discovery_info, ): """Test add-on not installed.""" - addon_installed.return_value["version"] = None - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -1533,9 +1529,10 @@ async def test_addon_not_installed( assert len(mock_setup_entry.mock_calls) == 1 -async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): +async def test_install_addon_failure( + hass, supervisor, addon_not_installed, install_addon +): """Test add-on install failure.""" - addon_installed.return_value["version"] = None install_addon.side_effect = HassioAPIError() result = await hass.config_entries.flow.async_init( @@ -2292,7 +2289,7 @@ async def test_options_addon_not_installed( hass, client, supervisor, - addon_installed, + addon_not_installed, install_addon, integration, addon_options, @@ -2306,7 +2303,6 @@ async def test_options_addon_not_installed( disconnect_calls, ): """Test options flow and add-on not installed on Supervisor.""" - addon_installed.return_value["version"] = None addon_options.update(old_addon_options) entry = integration entry.unique_id = "1234" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index a2962261ac3..202088bb481 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -432,10 +432,14 @@ async def test_start_addon( async def test_install_addon( - hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon + hass, + addon_not_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" s0_legacy_key = "s0_legacy" s2_access_control_key = "s2_access_control" @@ -583,10 +587,10 @@ async def test_addon_options_changed( "addon_version, update_available, update_calls, backup_calls, " "update_addon_side_effect, create_backup_side_effect", [ - ("1.0", True, 1, 1, None, None), - ("1.0", False, 0, 0, None, None), - ("1.0", True, 1, 1, HassioAPIError("Boom"), None), - ("1.0", True, 0, 1, None, HassioAPIError("Boom")), + ("1.0.0", True, 1, 1, None, None), + ("1.0.0", False, 0, 0, None, None), + ("1.0.0", True, 1, 1, HassioAPIError("Boom"), None), + ("1.0.0", True, 0, 1, None, HassioAPIError("Boom")), ], ) async def test_update_addon( @@ -720,7 +724,7 @@ async def test_remove_entry( assert create_backup.call_count == 1 assert create_backup.call_args == call( hass, - {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + {"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]}, partial=True, ) assert uninstall_addon.call_count == 1 @@ -762,7 +766,7 @@ async def test_remove_entry( assert create_backup.call_count == 1 assert create_backup.call_args == call( hass, - {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + {"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]}, partial=True, ) assert uninstall_addon.call_count == 0 @@ -786,7 +790,7 @@ async def test_remove_entry( assert create_backup.call_count == 1 assert create_backup.call_args == call( hass, - {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + {"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]}, partial=True, ) assert uninstall_addon.call_count == 1 From 72a0ca4871c7a639e3a6be73792aeb26ebe25dc6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 3 Aug 2022 22:03:10 +0100 Subject: [PATCH 136/903] Add homekit_controller thread node capabilties diagnostic sensor (#76120) --- .../components/homekit_controller/const.py | 1 + .../components/homekit_controller/sensor.py | 53 ++++++++++++++++++- .../homekit_controller/strings.sensor.json | 12 +++++ .../translations/sensor.en.json | 12 +++++ .../test_nanoleaf_strip_nl55.py | 7 +++ .../homekit_controller/test_sensor.py | 20 +++++++ 6 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/homekit_controller/strings.sensor.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.en.json diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index c27527d3638..5ea8205260e 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -88,6 +88,7 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.DENSITY_SO2: "sensor", CharacteristicsTypes.DENSITY_VOC: "sensor", CharacteristicsTypes.IDENTIFY: "button", + CharacteristicsTypes.THREAD_NODE_CAPABILITIES: "sensor", } diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index cddcbc59cde..ecfad477d00 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -5,6 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.sensor import ( @@ -27,6 +28,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -40,6 +42,45 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): """Describes Homekit sensor.""" probe: Callable[[Characteristic], bool] | None = None + format: Callable[[Characteristic], str] | None = None + + +def thread_node_capability_to_str(char: Characteristic) -> str: + """ + Return the thread device type as a string. + + The underlying value is a bitmask, but we want to turn that to + a human readable string. Some devices will have multiple capabilities. + For example, an NL55 is SLEEPY | MINIMAL. In that case we return the + "best" capability. + + https://openthread.io/guides/thread-primer/node-roles-and-types + """ + + val = ThreadNodeCapabilities(char.value) + + if val & ThreadNodeCapabilities.BORDER_ROUTER_CAPABLE: + # can act as a bridge between thread network and e.g. WiFi + return "border_router_capable" + + if val & ThreadNodeCapabilities.ROUTER_ELIGIBLE: + # radio always on, can be a router + return "router_eligible" + + if val & ThreadNodeCapabilities.FULL: + # radio always on, but can't be a router + return "full" + + if val & ThreadNodeCapabilities.MINIMAL: + # transceiver always on, does not need to poll for messages from its parent + return "minimal" + + if val & ThreadNodeCapabilities.SLEEPY: + # normally disabled, wakes on occasion to poll for messages from its parent + return "sleepy" + + # Device has no known thread capabilities + return "none" SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { @@ -195,6 +236,13 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), + CharacteristicsTypes.THREAD_NODE_CAPABILITIES: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.THREAD_NODE_CAPABILITIES, + name="Thread Capabilities", + device_class="homekit_controller__thread_node_capabilities", + entity_category=EntityCategory.DIAGNOSTIC, + format=thread_node_capability_to_str, + ), } @@ -399,7 +447,10 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): @property def native_value(self) -> str | int | float: """Return the current sensor value.""" - return self._char.value + val = self._char.value + if self.entity_description.format: + return self.entity_description.format(val) + return val ENTITY_TYPES = { diff --git a/homeassistant/components/homekit_controller/strings.sensor.json b/homeassistant/components/homekit_controller/strings.sensor.json new file mode 100644 index 00000000000..d7d8e888a98 --- /dev/null +++ b/homeassistant/components/homekit_controller/strings.sensor.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router Capable", + "router_eligible": "Router Eligible End Device", + "full": "Full End Device", + "minimal": "Minimal End Device", + "sleepy": "Sleepy End Device", + "none": "None" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.en.json b/homeassistant/components/homekit_controller/translations/sensor.en.json new file mode 100644 index 00000000000..b1f8a0a8128 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.en.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router Capable", + "full": "Full End Device", + "minimal": "Minimal End Device", + "none": "None", + "router_eligible": "Router Eligible End Device", + "sleepy": "Sleepy End Device" + } + } +} \ No newline at end of file diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py index 7e6a9bb672b..086027f2427 100644 --- a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -50,6 +50,13 @@ async def test_nanoleaf_nl55_setup(hass): entity_category=EntityCategory.DIAGNOSTIC, state="unknown", ), + EntityTestInfo( + entity_id="sensor.nanoleaf_strip_3b32_thread_capabilities", + friendly_name="Nanoleaf Strip 3B32 Thread Capabilities", + unique_id="homekit-AAAA011111111111-aid:1-sid:31-cid:115", + entity_category=EntityCategory.DIAGNOSTIC, + state="border_router_capable", + ), ], ), ) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 836da1e466f..c2a466d3997 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -1,8 +1,12 @@ """Basic checks for HomeKit sensor.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode +from homeassistant.components.homekit_controller.sensor import ( + thread_node_capability_to_str, +) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from tests.components.homekit_controller.common import Helper, setup_test_component @@ -315,3 +319,19 @@ async def test_sensor_unavailable(hass, utcnow): # Energy sensor has non-responsive characteristics so should be unavailable state = await energy_helper.poll_and_get_state() assert state.state == "unavailable" + + +def test_thread_node_caps_to_str(): + """Test all values of this enum get a translatable string.""" + assert ( + thread_node_capability_to_str(ThreadNodeCapabilities.BORDER_ROUTER_CAPABLE) + == "border_router_capable" + ) + assert ( + thread_node_capability_to_str(ThreadNodeCapabilities.ROUTER_ELIGIBLE) + == "router_eligible" + ) + assert thread_node_capability_to_str(ThreadNodeCapabilities.FULL) == "full" + assert thread_node_capability_to_str(ThreadNodeCapabilities.MINIMAL) == "minimal" + assert thread_node_capability_to_str(ThreadNodeCapabilities.SLEEPY) == "sleepy" + assert thread_node_capability_to_str(ThreadNodeCapabilities(128)) == "none" From 3388248eb52f5eb62cfe892f670e35dc058500c6 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 4 Aug 2022 00:58:30 +0300 Subject: [PATCH 137/903] Fix prettier on HomeKit Controller (#76168) --- .../homekit_controller/strings.sensor.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit_controller/strings.sensor.json b/homeassistant/components/homekit_controller/strings.sensor.json index d7d8e888a98..4a77fa1668a 100644 --- a/homeassistant/components/homekit_controller/strings.sensor.json +++ b/homeassistant/components/homekit_controller/strings.sensor.json @@ -1,12 +1,12 @@ { - "state": { - "homekit_controller__thread_node_capabilities": { - "border_router_capable": "Border Router Capable", - "router_eligible": "Router Eligible End Device", - "full": "Full End Device", - "minimal": "Minimal End Device", - "sleepy": "Sleepy End Device", - "none": "None" - } + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router Capable", + "router_eligible": "Router Eligible End Device", + "full": "Full End Device", + "minimal": "Minimal End Device", + "sleepy": "Sleepy End Device", + "none": "None" } -} \ No newline at end of file + } +} From 847f150a78a38b5623a2d71dfe835698237f1ac2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 3 Aug 2022 16:23:42 -0600 Subject: [PATCH 138/903] Modify RainMachine to store a single dataclass in `hass.data` (#75460) * Modify RainMachine to store a single dataclass in `hass.data` * Pass one object around instead of multiple --- .../components/rainmachine/__init__.py | 61 +++++++------ .../components/rainmachine/binary_sensor.py | 28 +++--- homeassistant/components/rainmachine/const.py | 2 - .../components/rainmachine/diagnostics.py | 20 ++--- homeassistant/components/rainmachine/model.py | 23 ++++- .../components/rainmachine/sensor.py | 90 ++++++++----------- .../components/rainmachine/switch.py | 62 ++++++------- 7 files changed, 142 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 3feeac7a827..c30ce81dc6d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from datetime import timedelta from functools import partial -from typing import Any, cast +from typing import Any from regenmaschine import Client from regenmaschine.controller import Controller @@ -28,7 +29,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,8 +40,6 @@ from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller from .const import ( CONF_ZONE_RUN_TIME, - DATA_CONTROLLER, - DATA_COORDINATOR, DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, @@ -49,6 +48,7 @@ from .const import ( DOMAIN, LOGGER, ) +from .model import RainMachineEntityDescription DEFAULT_SSL = True @@ -135,6 +135,14 @@ SERVICE_RESTRICT_WATERING_SCHEMA = SERVICE_SCHEMA.extend( ) +@dataclass +class RainMachineData: + """Define an object to be stored in `hass.data`.""" + + controller: Controller + coordinators: dict[str, DataUpdateCoordinator] + + @callback def async_get_controller_for_service_call( hass: HomeAssistant, call: ServiceCall @@ -146,9 +154,8 @@ def async_get_controller_for_service_call( if device_entry := device_registry.async_get(device_id): for entry in hass.config_entries.async_entries(DOMAIN): if entry.entry_id in device_entry.config_entries: - return cast( - Controller, hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] - ) + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + return data.controller raise ValueError(f"No controller for device ID: {device_id}") @@ -161,14 +168,12 @@ async def async_update_programs_and_zones( Program and zone updates always go together because of how linked they are: programs affect zones and certain combinations of zones affect programs. """ + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + await asyncio.gather( *[ - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ - DATA_PROGRAMS - ].async_refresh(), - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ - DATA_ZONES - ].async_refresh(), + data.coordinators[DATA_PROGRAMS].async_refresh(), + data.coordinators[DATA_ZONES].async_refresh(), ] ) @@ -250,10 +255,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather(*controller_init_tasks) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_CONTROLLER: controller, - DATA_COORDINATOR: coordinators, - } + hass.data[DOMAIN][entry.entry_id] = RainMachineData( + controller=controller, coordinators=coordinators + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -406,28 +410,27 @@ class RainMachineEntity(CoordinatorEntity): def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, - controller: Controller, - description: EntityDescription, + data: RainMachineData, + description: RainMachineEntityDescription, ) -> None: """Initialize.""" - super().__init__(coordinator) + super().__init__(data.coordinators[description.api_category]) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, controller.mac)}, + identifiers={(DOMAIN, data.controller.mac)}, configuration_url=f"https://{entry.data[CONF_IP_ADDRESS]}:{entry.data[CONF_PORT]}", - connections={(dr.CONNECTION_NETWORK_MAC, controller.mac)}, - name=str(controller.name).capitalize(), + connections={(dr.CONNECTION_NETWORK_MAC, data.controller.mac)}, + name=str(data.controller.name).capitalize(), manufacturer="RainMachine", model=( - f"Version {controller.hardware_version} " - f"(API: {controller.api_version})" + f"Version {data.controller.hardware_version} " + f"(API: {data.controller.api_version})" ), - sw_version=controller.software_version, + sw_version=data.controller.software_version, ) self._attr_extra_state_attributes = {} - self._attr_unique_id = f"{controller.mac}_{description.key}" - self._controller = controller + self._attr_unique_id = f"{data.controller.mac}_{description.key}" + self._data = data self.entity_description = description @callback diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 6ba374a28ba..d9448d68f9d 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -10,16 +10,17 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RainMachineEntity +from . import RainMachineData, RainMachineEntity from .const import ( - DATA_CONTROLLER, - DATA_COORDINATOR, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, DATA_RESTRICTIONS_UNIVERSAL, DOMAIN, ) -from .model import RainMachineDescriptionMixinApiCategory +from .model import ( + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, +) from .util import key_exists TYPE_FLOW_SENSOR = "flow_sensor" @@ -35,7 +36,9 @@ TYPE_WEEKDAY = "weekday" @dataclass class RainMachineBinarySensorDescription( - BinarySensorEntityDescription, RainMachineDescriptionMixinApiCategory + BinarySensorEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, ): """Describe a RainMachine binary sensor.""" @@ -124,8 +127,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up RainMachine binary sensors based on a config entry.""" - controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] - coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsBinarySensor, @@ -135,12 +137,10 @@ async def async_setup_entry( async_add_entities( [ - api_category_sensor_map[description.api_category]( - entry, coordinator, controller, description - ) + api_category_sensor_map[description.api_category](entry, data, description) for description in BINARY_SENSOR_DESCRIPTIONS if ( - (coordinator := coordinators[description.api_category]) is not None + (coordinator := data.coordinators[description.api_category]) is not None and coordinator.data and key_exists(coordinator.data, description.data_key) ) @@ -151,6 +151,8 @@ async def async_setup_entry( class CurrentRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): """Define a binary sensor that handles current restrictions data.""" + entity_description: RainMachineBinarySensorDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" @@ -171,6 +173,8 @@ class CurrentRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity): """Define a binary sensor that handles provisioning data.""" + entity_description: RainMachineBinarySensorDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" @@ -181,6 +185,8 @@ class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity): class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): """Define a binary sensor that handles universal restrictions data.""" + entity_description: RainMachineBinarySensorDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index 56c1660a0ba..f94e7011dce 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -7,8 +7,6 @@ DOMAIN = "rainmachine" CONF_ZONE_RUN_TIME = "zone_run_time" -DATA_CONTROLLER = "controller" -DATA_COORDINATOR = "coordinator" DATA_PROGRAMS = "programs" DATA_PROVISION_SETTINGS = "provision.settings" DATA_RESTRICTIONS_CURRENT = "restrictions.current" diff --git a/homeassistant/components/rainmachine/diagnostics.py b/homeassistant/components/rainmachine/diagnostics.py index e5e249d41f4..58b918c18ee 100644 --- a/homeassistant/components/rainmachine/diagnostics.py +++ b/homeassistant/components/rainmachine/diagnostics.py @@ -3,15 +3,13 @@ from __future__ import annotations from typing import Any -from regenmaschine.controller import Controller - from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DATA_CONTROLLER, DATA_COORDINATOR, DOMAIN +from . import RainMachineData +from .const import DOMAIN TO_REDACT = { CONF_LATITUDE, @@ -24,9 +22,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = hass.data[DOMAIN][entry.entry_id] - coordinators: dict[str, DataUpdateCoordinator] = data[DATA_COORDINATOR] - controller: Controller = data[DATA_CONTROLLER] + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] return { "entry": { @@ -38,15 +34,15 @@ async def async_get_config_entry_diagnostics( "coordinator": async_redact_data( { api_category: controller.data - for api_category, controller in coordinators.items() + for api_category, controller in data.coordinators.items() }, TO_REDACT, ), "controller": { - "api_version": controller.api_version, - "hardware_version": controller.hardware_version, - "name": controller.name, - "software_version": controller.software_version, + "api_version": data.controller.api_version, + "hardware_version": data.controller.hardware_version, + "name": data.controller.name, + "software_version": data.controller.software_version, }, }, } diff --git a/homeassistant/components/rainmachine/model.py b/homeassistant/components/rainmachine/model.py index 680a47c5d42..9ae99fe247a 100644 --- a/homeassistant/components/rainmachine/model.py +++ b/homeassistant/components/rainmachine/model.py @@ -1,17 +1,32 @@ """Define RainMachine data models.""" from dataclasses import dataclass +from homeassistant.helpers.entity import EntityDescription + @dataclass -class RainMachineDescriptionMixinApiCategory: - """Define an entity description mixin for binary and regular sensors.""" +class RainMachineEntityDescriptionMixinApiCategory: + """Define an entity description mixin to include an API category.""" api_category: str + + +@dataclass +class RainMachineEntityDescriptionMixinDataKey: + """Define an entity description mixin to include a data payload key.""" + data_key: str @dataclass -class RainMachineDescriptionMixinUid: - """Define an entity description mixin for switches.""" +class RainMachineEntityDescriptionMixinUid: + """Define an entity description mixin to include an activity UID.""" uid: int + + +@dataclass +class RainMachineEntityDescription( + EntityDescription, RainMachineEntityDescriptionMixinApiCategory +): + """Describe a RainMachine entity.""" diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 797420b460f..e2e602b945b 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -5,8 +5,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any, cast -from regenmaschine.controller import Controller - from homeassistant.components.sensor import ( RestoreSensor, SensorDeviceClass, @@ -17,15 +15,12 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import EntityCategory, EntityDescription +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow -from . import RainMachineEntity +from . import RainMachineData, RainMachineEntity from .const import ( - DATA_CONTROLLER, - DATA_COORDINATOR, DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_UNIVERSAL, @@ -33,8 +28,9 @@ from .const import ( DOMAIN, ) from .model import ( - RainMachineDescriptionMixinApiCategory, - RainMachineDescriptionMixinUid, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, + RainMachineEntityDescriptionMixinUid, ) from .util import RUN_STATE_MAP, RunStates, key_exists @@ -50,21 +46,25 @@ TYPE_ZONE_RUN_COMPLETION_TIME = "zone_run_completion_time" @dataclass -class RainMachineSensorDescriptionApiCategory( - SensorEntityDescription, RainMachineDescriptionMixinApiCategory +class RainMachineSensorDataDescription( + SensorEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, ): """Describe a RainMachine sensor.""" @dataclass -class RainMachineSensorDescriptionUid( - SensorEntityDescription, RainMachineDescriptionMixinUid +class RainMachineSensorCompletionTimerDescription( + SensorEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinUid, ): """Describe a RainMachine sensor.""" SENSOR_DESCRIPTIONS = ( - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_CLICK_M3, name="Flow sensor clicks per cubic meter", icon="mdi:water-pump", @@ -75,7 +75,7 @@ SENSOR_DESCRIPTIONS = ( api_category=DATA_PROVISION_SETTINGS, data_key="flowSensorClicksPerCubicMeter", ), - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, name="Flow sensor consumed liters", icon="mdi:water-pump", @@ -86,7 +86,7 @@ SENSOR_DESCRIPTIONS = ( api_category=DATA_PROVISION_SETTINGS, data_key="flowSensorWateringClicks", ), - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_START_INDEX, name="Flow sensor start index", icon="mdi:water-pump", @@ -96,7 +96,7 @@ SENSOR_DESCRIPTIONS = ( api_category=DATA_PROVISION_SETTINGS, data_key="flowSensorStartIndex", ), - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, name="Flow sensor clicks", icon="mdi:water-pump", @@ -107,7 +107,7 @@ SENSOR_DESCRIPTIONS = ( api_category=DATA_PROVISION_SETTINGS, data_key="flowSensorWateringClicks", ), - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FREEZE_TEMP, name="Freeze protect temperature", icon="mdi:thermometer", @@ -125,8 +125,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up RainMachine sensors based on a config entry.""" - controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] - coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsSensor, @@ -134,32 +133,29 @@ async def async_setup_entry( } sensors = [ - api_category_sensor_map[description.api_category]( - entry, coordinator, controller, description - ) + api_category_sensor_map[description.api_category](entry, data, description) for description in SENSOR_DESCRIPTIONS if ( - (coordinator := coordinators[description.api_category]) is not None + (coordinator := data.coordinators[description.api_category]) is not None and coordinator.data and key_exists(coordinator.data, description.data_key) ) ] - program_coordinator = coordinators[DATA_PROGRAMS] - zone_coordinator = coordinators[DATA_ZONES] + program_coordinator = data.coordinators[DATA_PROGRAMS] + zone_coordinator = data.coordinators[DATA_ZONES] for uid, program in program_coordinator.data.items(): sensors.append( ProgramTimeRemainingSensor( entry, - program_coordinator, - zone_coordinator, - controller, - RainMachineSensorDescriptionUid( + data, + RainMachineSensorCompletionTimerDescription( key=f"{TYPE_PROGRAM_RUN_COMPLETION_TIME}_{uid}", name=f"{program['name']} Run Completion Time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, + api_category=DATA_PROGRAMS, uid=uid, ), ) @@ -169,13 +165,13 @@ async def async_setup_entry( sensors.append( ZoneTimeRemainingSensor( entry, - zone_coordinator, - controller, - RainMachineSensorDescriptionUid( + data, + RainMachineSensorCompletionTimerDescription( key=f"{TYPE_ZONE_RUN_COMPLETION_TIME}_{uid}", name=f"{zone['name']} Run Completion Time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, + api_category=DATA_ZONES, uid=uid, ), ) @@ -187,17 +183,16 @@ async def async_setup_entry( class TimeRemainingSensor(RainMachineEntity, RestoreSensor): """Define a sensor that shows the amount of time remaining for an activity.""" - entity_description: RainMachineSensorDescriptionUid + entity_description: RainMachineSensorCompletionTimerDescription def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, - controller: Controller, - description: EntityDescription, + data: RainMachineData, + description: RainMachineSensorCompletionTimerDescription, ) -> None: """Initialize.""" - super().__init__(entry, coordinator, controller, description) + super().__init__(entry, data, description) self._current_run_state: RunStates | None = None self._previous_run_state: RunStates | None = None @@ -256,19 +251,6 @@ class TimeRemainingSensor(RainMachineEntity, RestoreSensor): class ProgramTimeRemainingSensor(TimeRemainingSensor): """Define a sensor that shows the amount of time remaining for a program.""" - def __init__( - self, - entry: ConfigEntry, - program_coordinator: DataUpdateCoordinator, - zone_coordinator: DataUpdateCoordinator, - controller: Controller, - description: EntityDescription, - ) -> None: - """Initialize.""" - super().__init__(entry, program_coordinator, controller, description) - - self._zone_coordinator = zone_coordinator - @property def status_key(self) -> str: """Return the data key that contains the activity status.""" @@ -277,7 +259,7 @@ class ProgramTimeRemainingSensor(TimeRemainingSensor): def calculate_seconds_remaining(self) -> int: """Calculate the number of seconds remaining.""" return sum( - self._zone_coordinator.data[zone["id"]]["remaining"] + self._data.coordinators[DATA_ZONES].data[zone["id"]]["remaining"] for zone in [z for z in self.activity_data["wateringTimes"] if z["active"]] ) @@ -285,6 +267,8 @@ class ProgramTimeRemainingSensor(TimeRemainingSensor): class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): """Define a sensor that handles provisioning data.""" + entity_description: RainMachineSensorDataDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" @@ -315,6 +299,8 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): class UniversalRestrictionsSensor(RainMachineEntity, SensorEntity): """Define a sensor that handles universal restrictions data.""" + entity_description: RainMachineSensorDataDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 1fcdab49836..ee6ac670840 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -7,7 +7,6 @@ from dataclasses import dataclass from datetime import datetime from typing import Any -from regenmaschine.controller import Controller from regenmaschine.errors import RequestError import voluptuous as vol @@ -19,19 +18,16 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import RainMachineEntity, async_update_programs_and_zones +from . import RainMachineData, RainMachineEntity, async_update_programs_and_zones from .const import ( CONF_ZONE_RUN_TIME, - DATA_CONTROLLER, - DATA_COORDINATOR, DATA_PROGRAMS, DATA_ZONES, DEFAULT_ZONE_RUN, DOMAIN, ) -from .model import RainMachineDescriptionMixinUid +from .model import RainMachineEntityDescription, RainMachineEntityDescriptionMixinUid from .util import RUN_STATE_MAP ATTR_AREA = "area" @@ -110,7 +106,9 @@ VEGETATION_MAP = { @dataclass class RainMachineSwitchDescription( - SwitchEntityDescription, RainMachineDescriptionMixinUid + SwitchEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinUid, ): """Describe a RainMachine switch.""" @@ -137,30 +135,27 @@ async def async_setup_entry( ): platform.async_register_entity_service(service_name, schema, method) - data = hass.data[DOMAIN][entry.entry_id] - controller = data[DATA_CONTROLLER] - program_coordinator = data[DATA_COORDINATOR][DATA_PROGRAMS] - zone_coordinator = data[DATA_COORDINATOR][DATA_ZONES] + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] entities: list[RainMachineActivitySwitch | RainMachineEnabledSwitch] = [] - - for kind, coordinator, switch_class, switch_enabled_class in ( - ("program", program_coordinator, RainMachineProgram, RainMachineProgramEnabled), - ("zone", zone_coordinator, RainMachineZone, RainMachineZoneEnabled), + for kind, api_category, switch_class, switch_enabled_class in ( + ("program", DATA_PROGRAMS, RainMachineProgram, RainMachineProgramEnabled), + ("zone", DATA_ZONES, RainMachineZone, RainMachineZoneEnabled), ): - for uid, data in coordinator.data.items(): - name = data["name"].capitalize() + coordinator = data.coordinators[api_category] + for uid, activity in coordinator.data.items(): + name = activity["name"].capitalize() # Add a switch to start/stop the program or zone: entities.append( switch_class( entry, - coordinator, - controller, + data, RainMachineSwitchDescription( key=f"{kind}_{uid}", name=name, icon="mdi:water", + api_category=api_category, uid=uid, ), ) @@ -170,13 +165,13 @@ async def async_setup_entry( entities.append( switch_enabled_class( entry, - coordinator, - controller, + data, RainMachineSwitchDescription( key=f"{kind}_{uid}_enabled", name=f"{name} enabled", entity_category=EntityCategory.CONFIG, icon="mdi:cog", + api_category=api_category, uid=uid, ), ) @@ -193,12 +188,11 @@ class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity): def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, - controller: Controller, + data: RainMachineData, description: RainMachineSwitchDescription, ) -> None: """Initialize.""" - super().__init__(entry, coordinator, controller, description) + super().__init__(entry, data, description) self._attr_is_on = False self._entry = entry @@ -299,13 +293,13 @@ class RainMachineProgram(RainMachineActivitySwitch): async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" await self._async_run_api_coroutine( - self._controller.programs.stop(self.entity_description.uid) + self._data.controller.programs.stop(self.entity_description.uid) ) async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" await self._async_run_api_coroutine( - self._controller.programs.start(self.entity_description.uid) + self._data.controller.programs.start(self.entity_description.uid) ) @callback @@ -342,10 +336,10 @@ class RainMachineProgramEnabled(RainMachineEnabledSwitch): """Disable the program.""" tasks = [ self._async_run_api_coroutine( - self._controller.programs.stop(self.entity_description.uid) + self._data.controller.programs.stop(self.entity_description.uid) ), self._async_run_api_coroutine( - self._controller.programs.disable(self.entity_description.uid) + self._data.controller.programs.disable(self.entity_description.uid) ), ] @@ -354,7 +348,7 @@ class RainMachineProgramEnabled(RainMachineEnabledSwitch): async def async_turn_on(self, **kwargs: Any) -> None: """Enable the program.""" await self._async_run_api_coroutine( - self._controller.programs.enable(self.entity_description.uid) + self._data.controller.programs.enable(self.entity_description.uid) ) @@ -372,13 +366,13 @@ class RainMachineZone(RainMachineActivitySwitch): async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" await self._async_run_api_coroutine( - self._controller.zones.stop(self.entity_description.uid) + self._data.controller.zones.stop(self.entity_description.uid) ) async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" await self._async_run_api_coroutine( - self._controller.zones.start( + self._data.controller.zones.start( self.entity_description.uid, kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]), ) @@ -426,10 +420,10 @@ class RainMachineZoneEnabled(RainMachineEnabledSwitch): """Disable the zone.""" tasks = [ self._async_run_api_coroutine( - self._controller.zones.stop(self.entity_description.uid) + self._data.controller.zones.stop(self.entity_description.uid) ), self._async_run_api_coroutine( - self._controller.zones.disable(self.entity_description.uid) + self._data.controller.zones.disable(self.entity_description.uid) ), ] @@ -438,5 +432,5 @@ class RainMachineZoneEnabled(RainMachineEnabledSwitch): async def async_turn_on(self, **kwargs: Any) -> None: """Enable the zone.""" await self._async_run_api_coroutine( - self._controller.zones.enable(self.entity_description.uid) + self._data.controller.zones.enable(self.entity_description.uid) ) From 1ff7686160ee24f6e413c521a912beb017002719 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 01:15:56 +0200 Subject: [PATCH 139/903] Use attributes in zengge light (#75994) --- homeassistant/components/zengge/light.py | 86 ++++++++---------------- 1 file changed, 29 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 6939ecb276b..2d4ba4614e6 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -53,49 +53,28 @@ def setup_platform( class ZenggeLight(LightEntity): """Representation of a Zengge light.""" + _attr_supported_color_modes = {ColorMode.HS, ColorMode.WHITE} + def __init__(self, device): """Initialize the light.""" - self._name = device["name"] - self._address = device["address"] + self._attr_name = device["name"] + self._attr_unique_id = device["address"] self.is_valid = True - self._bulb = zengge(self._address) + self._bulb = zengge(device["address"]) self._white = 0 - self._brightness = 0 - self._hs_color = (0, 0) - self._state = False + self._attr_brightness = 0 + self._attr_hs_color = (0, 0) + self._attr_is_on = False if self._bulb.connect() is False: self.is_valid = False - _LOGGER.error("Failed to connect to bulb %s, %s", self._address, self._name) + _LOGGER.error( + "Failed to connect to bulb %s, %s", device["address"], device["name"] + ) return @property - def unique_id(self): - """Return the ID of this light.""" - return self._address - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness property.""" - return self._brightness - - @property - def hs_color(self): - """Return the color property.""" - return self._hs_color - - @property - def white_value(self): + def white_value(self) -> int: """Return the white property.""" return self._white @@ -106,19 +85,9 @@ class ZenggeLight(LightEntity): return ColorMode.WHITE return ColorMode.HS - @property - def supported_color_modes(self) -> set[ColorMode | str]: - """Flag supported color modes.""" - return {ColorMode.HS, ColorMode.WHITE} - - @property - def assumed_state(self): - """We can report the actual state.""" - return False - - def _set_rgb(self, red, green, blue): + def _set_rgb(self, red: int, green: int, blue: int) -> None: """Set the rgb state.""" - return self._bulb.set_rgb(red, green, blue) + self._bulb.set_rgb(red, green, blue) def _set_white(self, white): """Set the white state.""" @@ -126,7 +95,7 @@ class ZenggeLight(LightEntity): def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" - self._state = True + self._attr_is_on = True self._bulb.on() hs_color = kwargs.get(ATTR_HS_COLOR) @@ -135,37 +104,40 @@ class ZenggeLight(LightEntity): if white is not None: # Change the bulb to white - self._brightness = self._white = white - self._hs_color = (0, 0) + self._attr_brightness = white + self._white = white + self._attr_hs_color = (0, 0) if hs_color is not None: # Change the bulb to hs self._white = 0 - self._hs_color = hs_color + self._attr_hs_color = hs_color if brightness is not None: - self._brightness = brightness + self._attr_brightness = brightness if self._white != 0: - self._set_white(self._brightness) + self._set_white(self.brightness) else: + assert self.hs_color is not None + assert self.brightness is not None rgb = color_util.color_hsv_to_RGB( - self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 + self.hs_color[0], self.hs_color[1], self.brightness / 255 * 100 ) self._set_rgb(*rgb) def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" - self._state = False + self._attr_is_on = False self._bulb.off() def update(self) -> None: """Synchronise internal state with the actual light state.""" rgb = self._bulb.get_colour() hsv = color_util.color_RGB_to_hsv(*rgb) - self._hs_color = hsv[:2] - self._brightness = (hsv[2] / 100) * 255 + self._attr_hs_color = hsv[:2] + self._attr_brightness = int((hsv[2] / 100) * 255) self._white = self._bulb.get_white() if self._white: - self._brightness = self._white - self._state = self._bulb.get_on() + self._attr_brightness = self._white + self._attr_is_on = self._bulb.get_on() From e2e277490bb2064a40f684975b42c370cc26716e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 4 Aug 2022 00:27:38 +0000 Subject: [PATCH 140/903] [ci skip] Translation update --- .../alarm_control_panel/translations/uk.json | 1 + .../components/ambee/translations/ru.json | 6 ++++++ .../components/anthemav/translations/ja.json | 1 + .../components/anthemav/translations/ru.json | 6 ++++++ .../components/google/translations/ja.json | 1 + .../lacrosse_view/translations/ru.json | 20 +++++++++++++++++++ .../lg_soundbar/translations/it.json | 2 +- .../lg_soundbar/translations/pl.json | 2 +- .../components/lyric/translations/ja.json | 1 + .../components/mitemp_bt/translations/hu.json | 2 +- .../components/mitemp_bt/translations/it.json | 2 +- .../components/mitemp_bt/translations/ja.json | 7 +++++++ .../components/mitemp_bt/translations/no.json | 2 +- .../components/nest/translations/it.json | 2 ++ .../components/nest/translations/ja.json | 9 +++++++++ .../components/nest/translations/pl.json | 10 ++++++++++ .../components/nest/translations/ru.json | 10 ++++++++++ .../openalpr_local/translations/ja.json | 1 + .../openalpr_local/translations/ru.json | 8 ++++++++ .../opentherm_gw/translations/ja.json | 3 ++- .../opentherm_gw/translations/ru.json | 3 ++- .../radiotherm/translations/ja.json | 1 + .../simplepush/translations/ja.json | 1 + .../simplepush/translations/ru.json | 6 ++++++ .../soundtouch/translations/ja.json | 1 + .../soundtouch/translations/ru.json | 6 ++++++ .../components/spotify/translations/ja.json | 1 + .../steam_online/translations/ja.json | 1 + .../components/xbox/translations/ja.json | 1 + .../components/xbox/translations/ru.json | 6 ++++++ .../xiaomi_ble/translations/ca.json | 5 +++++ .../xiaomi_ble/translations/de.json | 8 ++++++++ .../xiaomi_ble/translations/hu.json | 8 ++++++++ .../xiaomi_ble/translations/it.json | 14 ++++++++++++- .../xiaomi_ble/translations/ja.json | 8 +++++++- .../xiaomi_ble/translations/no.json | 10 +++++++++- .../xiaomi_ble/translations/pl.json | 14 ++++++++++++- .../xiaomi_ble/translations/pt-BR.json | 10 +++++++++- .../xiaomi_ble/translations/ru.json | 14 ++++++++++++- .../xiaomi_ble/translations/zh-Hant.json | 8 ++++++++ 40 files changed, 209 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/lacrosse_view/translations/ru.json create mode 100644 homeassistant/components/mitemp_bt/translations/ja.json create mode 100644 homeassistant/components/openalpr_local/translations/ru.json diff --git a/homeassistant/components/alarm_control_panel/translations/uk.json b/homeassistant/components/alarm_control_panel/translations/uk.json index b50fd9f459d..de28faca361 100644 --- a/homeassistant/components/alarm_control_panel/translations/uk.json +++ b/homeassistant/components/alarm_control_panel/translations/uk.json @@ -29,6 +29,7 @@ "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": "\u041d\u0456\u0447\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430", + "armed_vacation": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0432\u0456\u0434\u043f\u0443\u0441\u0442\u043a\u0430)", "arming": "\u0421\u0442\u0430\u0432\u043b\u044e \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", "disarmed": "\u0417\u043d\u044f\u0442\u043e \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438", "disarming": "\u0417\u043d\u044f\u0442\u0442\u044f", diff --git a/homeassistant/components/ambee/translations/ru.json b/homeassistant/components/ambee/translations/ru.json index c229c2d6020..11b3cbbf9d2 100644 --- a/homeassistant/components/ambee/translations/ru.json +++ b/homeassistant/components/ambee/translations/ru.json @@ -24,5 +24,11 @@ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Ambee." } } + }, + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Ambee \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e Ambee \u0443\u0434\u0430\u043b\u0438\u043b\u0430 \u0441\u0432\u043e\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0435 (\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u044b\u0435) \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u044b \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0431\u044b\u0447\u043d\u044b\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u043f\u043b\u0430\u0442\u043d\u044b\u0439 \u0442\u0430\u0440\u0438\u0444\u043d\u044b\u0439 \u043f\u043b\u0430\u043d.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Ambee \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ja.json b/homeassistant/components/anthemav/translations/ja.json index b55e8b2b030..2081a17914d 100644 --- a/homeassistant/components/anthemav/translations/ja.json +++ b/homeassistant/components/anthemav/translations/ja.json @@ -18,6 +18,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Anthem A/V Receivers\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Anthem A/V Receivers\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Anthem A/V Receivers YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/anthemav/translations/ru.json b/homeassistant/components/anthemav/translations/ru.json index 0f343609e4c..e55ad7100e7 100644 --- a/homeassistant/components/anthemav/translations/ru.json +++ b/homeassistant/components/anthemav/translations/ru.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 9507f32812d..7ab3209ac1c 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -35,6 +35,7 @@ }, "issues": { "deprecated_yaml": { + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fcyaml\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_track_new_yaml": { diff --git a/homeassistant/components/lacrosse_view/translations/ru.json b/homeassistant/components/lacrosse_view/translations/ru.json new file mode 100644 index 00000000000..931b4c32274 --- /dev/null +++ b/homeassistant/components/lacrosse_view/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": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "no_locations": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "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": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/it.json b/homeassistant/components/lg_soundbar/translations/it.json index c9f58f15aa2..30a7b328038 100644 --- a/homeassistant/components/lg_soundbar/translations/it.json +++ b/homeassistant/components/lg_soundbar/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "existing_instance_updated": "Configurazione esistente aggiornata." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/pl.json b/homeassistant/components/lg_soundbar/translations/pl.json index 1b84366cfa4..4a6b3d077df 100644 --- a/homeassistant/components/lg_soundbar/translations/pl.json +++ b/homeassistant/components/lg_soundbar/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119" }, "error": { diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index 5394e978c27..bd8b4b93442 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -20,6 +20,7 @@ }, "issues": { "removed_yaml": { + "description": "Honeywell Lyric\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Honeywell Lyric YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/mitemp_bt/translations/hu.json b/homeassistant/components/mitemp_bt/translations/hu.json index 7e6b52a25ee..c970c4b4bd8 100644 --- a/homeassistant/components/mitemp_bt/translations/hu.json +++ b/homeassistant/components/mitemp_bt/translations/hu.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja nem le\u00e1llt a 2022.7-es Home Assistantban, \u00e9s a 2022.8-as kiad\u00e1sban a Xiaomi BLE integr\u00e1ci\u00f3val v\u00e1ltott\u00e1k fel.\n\nNincs lehet\u0151s\u00e9g migr\u00e1ci\u00f3ra, ez\u00e9rt a Xiaomi Mijia BLE eszk\u00f6zt az \u00faj integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1val manu\u00e1lisan \u00fajra be kell \u00e1ll\u00edtani.\n\nA megl\u00e9v\u0151 Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom \u00e9rz\u00e9kel\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant m\u00e1r nem haszn\u00e1lja. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "description": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja le\u00e1llt a 2022.7-es Home Assistantban, \u00e9s a 2022.8-as kiad\u00e1sban a Xiaomi BLE integr\u00e1ci\u00f3val v\u00e1ltott\u00e1k fel.\n\nNincs lehet\u0151s\u00e9g migr\u00e1ci\u00f3ra, ez\u00e9rt a Xiaomi Mijia BLE eszk\u00f6zt az \u00faj integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1val manu\u00e1lisan \u00fajra be kell \u00e1ll\u00edtani.\n\nA megl\u00e9v\u0151 Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom \u00e9rz\u00e9kel\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant m\u00e1r nem haszn\u00e1lja. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", "title": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja lecser\u00e9l\u0151d\u00f6tt" } } diff --git a/homeassistant/components/mitemp_bt/translations/it.json b/homeassistant/components/mitemp_bt/translations/it.json index 0fe7ab58919..68738725251 100644 --- a/homeassistant/components/mitemp_bt/translations/it.json +++ b/homeassistant/components/mitemp_bt/translations/it.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML esistente di Xiaomi Mijia BLE Temperature and Humidity Sensor non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor \u00e8 stata sostituita" } } diff --git a/homeassistant/components/mitemp_bt/translations/ja.json b/homeassistant/components/mitemp_bt/translations/ja.json new file mode 100644 index 00000000000..11212382f1f --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "Xiaomi Mijia BLE\u6e29\u5ea6\u304a\u3088\u3073\u6e7f\u5ea6\u30bb\u30f3\u30b5\u30fc\u306e\u7d71\u5408\u306f\u3001\u30ea\u30d7\u30ec\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/no.json b/homeassistant/components/mitemp_bt/translations/no.json index 248f44b29af..de80b592212 100644 --- a/homeassistant/components/mitemp_bt/translations/no.json +++ b/homeassistant/components/mitemp_bt/translations/no.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "Xiaomi Mijia BLE temperatur- og fuktighetssensorintegrasjonen sluttet \u00e5 fungere i Home Assistant 2022.7 og ble erstattet av Xiaomi BLE-integrasjonen i 2022.8-utgivelsen. \n\n Det er ingen migreringsbane mulig, derfor m\u00e5 du legge til Xiaomi Mijia BLE-enheten ved \u00e5 bruke den nye integrasjonen manuelt. \n\n Din eksisterende Xiaomi Mijia BLE temperatur- og fuktighetssensor YAML-konfigurasjon brukes ikke lenger av Home Assistant. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "description": "Xiaomi Mijia BLE Temperatur- og Fuktighetssensorintegrasjon sluttet \u00e5 fungere i Home Assistant 2022.7 og ble erstattet av Xiaomi BLE-integrasjonen i 2022.8-utgivelsen.\n\nDet er ingen migreringsvei mulig, derfor m\u00e5 du legge til Xiaomi Mijia BLE-enheten din ved hjelp av den nye integrasjonen manuelt.\n\nDin eksisterende Xiaomi Mijia BLE-temperatur- og fuktighetssensor YAML-konfigurasjon brukes ikke lenger av Home Assistant. Fjern YAML-konfigurasjonen fra configuration.yaml-filen, og start Home Assistant p\u00e5 nytt for \u00e5 l\u00f8se dette problemet.", "title": "Xiaomi Mijia BLE temperatur- og fuktighetssensorintegrasjon er erstattet" } } diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 6771cd00431..73095bf0930 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -99,9 +99,11 @@ }, "issues": { "deprecated_yaml": { + "description": "La configurazione di Nest in configuration.yaml sar\u00e0 rimossa in Home Assistant 2022.10. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Nest sar\u00e0 rimossa" }, "removed_app_auth": { + "description": "Per migliorare la sicurezza e ridurre il rischio di phishing, Google ha deprecato il metodo di autenticazione utilizzato da Home Assistant. \n\n **Ci\u00f2 richiede un'azione da parte tua per risolverlo** ([maggiori informazioni]({more_info_url})) \n\n 1. Visita la pagina delle integrazioni\n 1. Fare clic su Riconfigura sull'integrazione Nest.\n 1. Home Assistant ti guider\u00e0 attraverso i passaggi per l'aggiornamento all'autenticazione Web. \n\n Consulta le [istruzioni per l'integrazione]({documentation_url}) di Nest per informazioni sulla risoluzione dei problemi.", "title": "Le credenziali di autenticazione Nest devono essere aggiornate" } } diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 55d9b9a0348..5509cd497eb 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -89,5 +89,14 @@ "camera_sound": "\u97f3\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", "doorbell_chime": "\u30c9\u30a2\u30d9\u30eb\u304c\u62bc\u3055\u308c\u305f" } + }, + "issues": { + "deprecated_yaml": { + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Nest\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.10\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "title": "Nest YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "removed_app_auth": { + "title": "Nest\u8a8d\u8a3c\u8cc7\u683c\u60c5\u5831\u3092\u66f4\u65b0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index c7a5c88b120..a0b879ccc90 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -96,5 +96,15 @@ "camera_sound": "nast\u0105pi wykrycie d\u017awi\u0119ku", "doorbell_chime": "dzwonek zostanie wci\u015bni\u0119ty" } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Nest w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2022.10. \n\nTwoje istniej\u0105ce po\u015bwiadczenia aplikacji OAuth i ustawienia dost\u0119pu zosta\u0142y automatycznie zaimportowane do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Nest zostanie usuni\u0119ta" + }, + "removed_app_auth": { + "description": "Aby poprawi\u0107 bezpiecze\u0144stwo i zmniejszy\u0107 ryzyko phishingu, Google wycofa\u0142o metod\u0119 uwierzytelniania u\u017cywan\u0105 przez Home Assistanta.\n\n **Wymaga to podj\u0119cia przez Ciebie dzia\u0142ania** ([wi\u0119cej informacji]({more_info_url})) \n\n1. Odwied\u017a stron\u0119 integracji\n2. Kliknij Zmie\u0144 konfiguracj\u0119 w integracji Nest.\n3. Home Assistant przeprowadzi Ci\u0119 przez kolejne etapy aktualizacji do uwierzytelniania internetowego. \n\nZobacz [instrukcj\u0119 integracji] Nest ({documentation_url}), aby uzyska\u0107 informacje na temat rozwi\u0105zywania problem\u00f3w.", + "title": "Dane uwierzytelniaj\u0105ce Nest musz\u0105 zosta\u0107 zaktualizowane" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index e071637713f..71721cda936 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -96,5 +96,15 @@ "camera_sound": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0437\u0432\u0443\u043a", "doorbell_chime": "\u041d\u0430\u0436\u0430\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0437\u0432\u043e\u043d\u043a\u0430" } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "removed_app_auth": { + "description": "\u0414\u043b\u044f \u043f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0438 \u0441\u043d\u0438\u0436\u0435\u043d\u0438\u044f \u0440\u0438\u0441\u043a\u0430 \u0444\u0438\u0448\u0438\u043d\u0433\u0430 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f Google \u043e\u0442\u043a\u0430\u0437\u0430\u043b\u0430\u0441\u044c \u043e\u0442 \u043c\u0435\u0442\u043e\u0434\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e Home Assistant.\n\n**\u0414\u043b\u044f \u0443\u0441\u0442\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0412\u0430\u0448\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435** ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url}))\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Nest.\n3. \u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e.\n\n\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u0443\u0441\u0442\u0440\u0430\u043d\u0435\u043d\u0438\u0438 \u043d\u0435\u043f\u043e\u043b\u0430\u0434\u043e\u043a \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Nest]({documentation_url}).", + "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 Nest Authentication." + } } } \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/ja.json b/homeassistant/components/openalpr_local/translations/ja.json index dbdd930cd10..2353cd9e60f 100644 --- a/homeassistant/components/openalpr_local/translations/ja.json +++ b/homeassistant/components/openalpr_local/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "OpenALPR \u30ed\u30fc\u30ab\u30eb\u7d71\u5408\u306f\u3001Home Assistant\u304b\u3089\u306e\u524a\u9664\u304c\u4fdd\u7559\u3055\u308c\u3066\u304a\u308a\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "OpenALPR Local\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/openalpr_local/translations/ru.json b/homeassistant/components/openalpr_local/translations/ru.json new file mode 100644 index 00000000000..180151e5fd5 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json index 16f4d55eac0..ca99567681b 100644 --- a/homeassistant/components/opentherm_gw/translations/ja.json +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -3,7 +3,8 @@ "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "id_exists": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4ID\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" + "id_exists": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4ID\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/ru.json b/homeassistant/components/opentherm_gw/translations/ru.json index e743830624a..448af518cc8 100644 --- a/homeassistant/components/opentherm_gw/translations/ru.json +++ b/homeassistant/components/opentherm_gw/translations/ru.json @@ -3,7 +3,8 @@ "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.", - "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442." + "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." }, "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/ja.json b/homeassistant/components/radiotherm/translations/ja.json index 67f667bacc1..e3cf6571357 100644 --- a/homeassistant/components/radiotherm/translations/ja.json +++ b/homeassistant/components/radiotherm/translations/ja.json @@ -21,6 +21,7 @@ }, "issues": { "deprecated_yaml": { + "description": "YAML\u3092\u4f7f\u7528\u3057\u305f\u3001Radio Thermostat climate(\u6c17\u5019)\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Radio Thermostat YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index fd22d7dfef5..398fa3d63b0 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -20,6 +20,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json index 4844f358c82..2ddcba76929 100644 --- a/homeassistant/components/simplepush/translations/ru.json +++ b/homeassistant/components/simplepush/translations/ru.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ja.json b/homeassistant/components/soundtouch/translations/ja.json index 9bc5e427baf..a6417d9988a 100644 --- a/homeassistant/components/soundtouch/translations/ja.json +++ b/homeassistant/components/soundtouch/translations/ja.json @@ -20,6 +20,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Bose SoundTouch\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Bose SoundTouch\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Bose SoundTouch YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/soundtouch/translations/ru.json b/homeassistant/components/soundtouch/translations/ru.json index d987f817a85..318fe8abef6 100644 --- a/homeassistant/components/soundtouch/translations/ru.json +++ b/homeassistant/components/soundtouch/translations/ru.json @@ -17,5 +17,11 @@ "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index ae5f52b68a2..6f1c48c8572 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -21,6 +21,7 @@ }, "issues": { "removed_yaml": { + "description": "Spotify\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Spotify YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index 46c3eeb7d22..1524e2afc5a 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -26,6 +26,7 @@ }, "issues": { "removed_yaml": { + "description": "Steam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Steam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index f1c31b6c64c..530299b0b24 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -16,6 +16,7 @@ }, "issues": { "deprecated_yaml": { + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Xbox\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Xbox YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/xbox/translations/ru.json b/homeassistant/components/xbox/translations/ru.json index 5719a5d9d8a..ca47fd08c8c 100644 --- a/homeassistant/components/xbox/translations/ru.json +++ b/homeassistant/components/xbox/translations/ru.json @@ -13,5 +13,11 @@ "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" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index 873bcb3f3bb..6dde2ede685 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -9,6 +9,11 @@ "no_devices_found": "No s'han trobat dispositius a la xarxa", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, + "error": { + "decryption_failed": "La clau d'enlla\u00e7 proporcionada no ha funcionat, les dades del sensor no s'han pogut desxifrar. Comprova-la i torna-ho a provar.", + "expected_24_characters": "S'espera una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals.", + "expected_32_characters": "S'espera una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index 6fc2c74af75..c21a653c4dc 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -9,11 +9,19 @@ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, + "error": { + "decryption_failed": "Der bereitgestellte Bindkey funktionierte nicht, Sensordaten konnten nicht entschl\u00fcsselt werden. Bitte \u00fcberpr\u00fcfe es und versuche es erneut.", + "expected_24_characters": "Erwartet wird ein 24-stelliger hexadezimaler Bindkey.", + "expected_32_characters": "Erwartet wird ein 32-stelliger hexadezimaler Bindkey." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "confirm_slow": { + "description": "Von diesem Ger\u00e4t wurde in der letzten Minute kein Broadcast gesendet, so dass wir nicht sicher sind, ob dieses Ger\u00e4t Verschl\u00fcsselung verwendet oder nicht. Dies kann daran liegen, dass das Ger\u00e4t ein langsames Sendeintervall verwendet. Best\u00e4tige, dass du das Ger\u00e4t trotzdem hinzuf\u00fcgen m\u00f6chtest. Wenn das n\u00e4chste Mal ein Broadcast empfangen wird, wirst du aufgefordert, den Bindkey einzugeben, falls er ben\u00f6tigt wird." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindungsschl\u00fcssel" diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index 3962e890d4f..044f970038b 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -9,11 +9,19 @@ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, + "error": { + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "expected_24_characters": "24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", + "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, + "confirm_slow": { + "description": "Az elm\u00falt egy percben nem \u00e9rkezett ad\u00e1sjel az eszk\u00f6zt\u0151l, \u00edgy nem az nem \u00e1llap\u00edthat\u00f3 meg egy\u00e9rtelm\u0171en, hogy ez a k\u00e9sz\u00fcl\u00e9k haszn\u00e1l-e titkos\u00edt\u00e1st vagy sem. Ez az\u00e9rt lehet, mert az eszk\u00f6z ritka jelad\u00e1si intervallumot haszn\u00e1l. Meger\u0151s\u00edtheti most az eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1t, de a k\u00f6vetkez\u0151 ad\u00e1sjel fogad\u00e1sakor a rendszer k\u00e9rni fogja, hogy adja meg az eszk\u00f6z kulcs\u00e1t (bindkeyt), ha az sz\u00fcks\u00e9ges." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Kulcs (bindkey)" diff --git a/homeassistant/components/xiaomi_ble/translations/it.json b/homeassistant/components/xiaomi_ble/translations/it.json index 018829bfbd2..bf5ee87b949 100644 --- a/homeassistant/components/xiaomi_ble/translations/it.json +++ b/homeassistant/components/xiaomi_ble/translations/it.json @@ -6,13 +6,22 @@ "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controlla e riprova.", "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri.", - "no_devices_found": "Nessun dispositivo trovato sulla rete" + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controlla e riprova.", + "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", + "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Vuoi configurare {name}?" }, + "confirm_slow": { + "description": "Non c'\u00e8 stata una trasmissione da questo dispositivo nell'ultimo minuto, quindi non siamo sicuri se questo dispositivo utilizzi la crittografia o meno. Ci\u00f2 potrebbe essere dovuto al fatto che il dispositivo utilizza un intervallo di trasmissione lento. Conferma per aggiungere comunque questo dispositivo, la prossima volta che viene ricevuta una trasmissione ti verr\u00e0 chiesto di inserire la sua chiave di collegamento se necessario." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Chiave di collegamento" @@ -25,6 +34,9 @@ }, "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 24 caratteri." }, + "slow_confirm": { + "description": "Non c'\u00e8 stata una trasmissione da questo dispositivo nell'ultimo minuto, quindi non siamo sicuri se questo dispositivo utilizzi la crittografia o meno. Ci\u00f2 potrebbe essere dovuto al fatto che il dispositivo utilizza un intervallo di trasmissione lento. Conferma per aggiungere comunque questo dispositivo, la prossima volta che viene ricevuta una trasmissione ti verr\u00e0 chiesto di inserire la sua chiave di collegamento se necessario." + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/xiaomi_ble/translations/ja.json b/homeassistant/components/xiaomi_ble/translations/ja.json index d15f89bf9d3..597c0ecc3f2 100644 --- a/homeassistant/components/xiaomi_ble/translations/ja.json +++ b/homeassistant/components/xiaomi_ble/translations/ja.json @@ -6,7 +6,13 @@ "decryption_failed": "\u63d0\u4f9b\u3055\u308c\u305f\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u6a5f\u80fd\u305b\u305a\u3001\u30bb\u30f3\u30b5\u30fc \u30c7\u30fc\u30bf\u3092\u5fa9\u53f7\u5316\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "expected_24_characters": "24\u6587\u5b57\u306716\u9032\u6570\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", "expected_32_characters": "32\u6587\u5b57\u304b\u3089\u306a\u308b16\u9032\u6570\u306e\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", - "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "decryption_failed": "\u63d0\u4f9b\u3055\u308c\u305f\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u6a5f\u80fd\u305b\u305a\u3001\u30bb\u30f3\u30b5\u30fc \u30c7\u30fc\u30bf\u3092\u5fa9\u53f7\u5316\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "expected_24_characters": "24\u6587\u5b57\u306716\u9032\u6570\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", + "expected_32_characters": "32\u6587\u5b57\u304b\u3089\u306a\u308b16\u9032\u6570\u306e\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_ble/translations/no.json b/homeassistant/components/xiaomi_ble/translations/no.json index 6c63f53c6aa..ff428d248d1 100644 --- a/homeassistant/components/xiaomi_ble/translations/no.json +++ b/homeassistant/components/xiaomi_ble/translations/no.json @@ -9,11 +9,19 @@ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, + "error": { + "decryption_failed": "Den oppgitte bindingsn\u00f8kkelen fungerte ikke, sensordata kunne ikke dekrypteres. Vennligst sjekk det og pr\u00f8v igjen.", + "expected_24_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 24 tegn.", + "expected_32_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 32 tegn." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Vil du konfigurere {name}?" }, + "confirm_slow": { + "description": "Det har ikke v\u00e6rt en sending fra denne enheten det siste minuttet, s\u00e5 vi er ikke sikre p\u00e5 om denne enheten bruker kryptering eller ikke. Dette kan skyldes at enheten bruker et tregt kringkastingsintervall. Bekreft \u00e5 legge til denne enheten uansett, s\u00e5 neste gang en kringkasting mottas, vil du bli bedt om \u00e5 angi bindn\u00f8kkelen hvis det er n\u00f8dvendig." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -27,7 +35,7 @@ "description": "Sensordataene som sendes av sensoren er kryptert. For \u00e5 dekryptere den trenger vi en heksadesimal bindn\u00f8kkel p\u00e5 24 tegn." }, "slow_confirm": { - "description": "Det har ikke v\u00e6rt en sending fra denne enheten det siste minuttet, s\u00e5 vi er ikke sikre p\u00e5 om denne enheten bruker kryptering eller ikke. Dette kan skyldes at enheten bruker et tregt kringkastingsintervall. Bekreft \u00e5 legge til denne enheten uansett, s\u00e5 neste gang en kringkasting mottas, vil du bli bedt om \u00e5 angi bindn\u00f8kkelen hvis den er n\u00f8dvendig." + "description": "Det har ikke v\u00e6rt en kringkasting fra denne enheten i siste \u00f8yeblikk, s\u00e5 vi er ikke sikre p\u00e5 om denne enheten bruker kryptering eller ikke. Dette kan skyldes at enheten bruker et sakte kringkastingsintervall. Bekreft \u00e5 legge til denne enheten uansett, s\u00e5 neste gang en kringkasting mottas, blir du bedt om \u00e5 angi bindingsn\u00f8kkelen hvis det er n\u00f8dvendig." }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_ble/translations/pl.json b/homeassistant/components/xiaomi_ble/translations/pl.json index 2ca956019ef..7bb0c5da454 100644 --- a/homeassistant/components/xiaomi_ble/translations/pl.json +++ b/homeassistant/components/xiaomi_ble/translations/pl.json @@ -6,13 +6,22 @@ "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", "expected_24_characters": "Oczekiwano 24-znakowego szesnastkowego klucza bindkey.", "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey.", - "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", + "expected_24_characters": "Oczekiwano 24-znakowego szesnastkowego klucza bindkey.", + "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "confirm_slow": { + "description": "W ci\u0105gu ostatniej minuty nie by\u0142o transmisji z tego urz\u0105dzenia, wi\u0119c nie jeste\u015bmy pewni, czy to urz\u0105dzenie u\u017cywa szyfrowania, czy nie. Mo\u017ce to by\u0107 spowodowane tym, \u017ce urz\u0105dzenie u\u017cywa wolnego od\u015bwie\u017cania transmisji. Potwierd\u017a, aby mimo wszystko doda\u0107 to urz\u0105dzenie, a przy nast\u0119pnym odebraniu transmisji zostaniesz poproszony o wprowadzenie klucza bindkey, je\u015bli jest to konieczne." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -25,6 +34,9 @@ }, "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 24-znakowego szesnastkowego klucza bindkey." }, + "slow_confirm": { + "description": "W ci\u0105gu ostatniej minuty nie by\u0142o transmisji z tego urz\u0105dzenia, wi\u0119c nie jeste\u015bmy pewni, czy to urz\u0105dzenie u\u017cywa szyfrowania, czy nie. Mo\u017ce to by\u0107 spowodowane tym, \u017ce urz\u0105dzenie u\u017cywa wolnego od\u015bwie\u017cania transmisji. Potwierd\u017a, aby mimo wszystko doda\u0107 to urz\u0105dzenie, a przy nast\u0119pnym odebraniu transmisji zostaniesz poproszony o wprowadzenie klucza bindkey, je\u015bli jest to konieczne." + }, "user": { "data": { "address": "Urz\u0105dzenie" diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json index b0776a217cd..4702ca14bcc 100644 --- a/homeassistant/components/xiaomi_ble/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -9,16 +9,24 @@ "no_devices_found": "Nenhum dispositivo encontrado na rede", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, + "error": { + "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", + "expected_24_characters": "Esperado uma bindkey hexadecimal de 24 caracteres.", + "expected_32_characters": "Esperado uma bindkey hexadecimal de 32 caracteres." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Deseja configurar {name}?" }, + "confirm_slow": { + "description": "N\u00e3o houve uma transmiss\u00e3o deste dispositivo no \u00faltimo minuto, por isso n\u00e3o temos certeza se este dispositivo usa criptografia ou n\u00e3o. Isso pode ocorrer porque o dispositivo usa um intervalo de transmiss\u00e3o lento. Confirme para adicionar este dispositivo de qualquer maneira e, na pr\u00f3xima vez que uma transmiss\u00e3o for recebida, voc\u00ea ser\u00e1 solicitado a inserir sua bindkey, se necess\u00e1rio." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" }, - "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma bindkey hexadecimal de 32 caracteres." }, "get_encryption_key_legacy": { "data": { diff --git a/homeassistant/components/xiaomi_ble/translations/ru.json b/homeassistant/components/xiaomi_ble/translations/ru.json index a90da71d84e..3c0bf6cce78 100644 --- a/homeassistant/components/xiaomi_ble/translations/ru.json +++ b/homeassistant/components/xiaomi_ble/translations/ru.json @@ -6,13 +6,22 @@ "decryption_failed": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b, \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", "expected_24_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", "expected_32_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", - "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + "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.", + "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": { + "decryption_failed": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b, \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "expected_24_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", + "expected_32_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "confirm_slow": { + "description": "\u0412 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u043c\u0438\u043d\u0443\u0442\u044b \u043e\u0442 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0431\u044b\u043b\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043b\u0438 \u043d\u0435\u0442. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0442\u0435\u043c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043c\u0435\u0434\u043b\u0435\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0432\u0435\u0449\u0430\u043d\u0438\u044f. \u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0412\u0430\u043c \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u0435\u0433\u043e \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438, \u0435\u0441\u043b\u0438 \u043e\u043d \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c." + }, "get_encryption_key_4_5": { "data": { "bindkey": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438" @@ -25,6 +34,9 @@ }, "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." }, + "slow_confirm": { + "description": "\u0412 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u043c\u0438\u043d\u0443\u0442\u044b \u043e\u0442 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0431\u044b\u043b\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043b\u0438 \u043d\u0435\u0442. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0442\u0435\u043c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043c\u0435\u0434\u043b\u0435\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0432\u0435\u0449\u0430\u043d\u0438\u044f. \u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0412\u0430\u043c \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u0435\u0433\u043e \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438, \u0435\u0441\u043b\u0438 \u043e\u043d \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c." + }, "user": { "data": { "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json index 13d0a986b79..fdb720b2777 100644 --- a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -9,11 +9,19 @@ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, + "error": { + "decryption_failed": "\u6240\u63d0\u4f9b\u7684\u7d81\u5b9a\u78bc\u7121\u6cd5\u4f7f\u7528\u3001\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6cd5\u89e3\u5bc6\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "expected_24_characters": "\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", + "expected_32_characters": "\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "confirm_slow": { + "description": "\u8a72\u88dd\u7f6e\u65bc\u904e\u53bb\u4e00\u5206\u9418\u5167\u3001\u672a\u9032\u884c\u4efb\u4f55\u72c0\u614b\u5ee3\u64ad\uff0c\u56e0\u6b64\u7121\u6cd5\u78ba\u5b9a\u88dd\u7f6e\u662f\u5426\u4f7f\u7528\u52a0\u5bc6\u901a\u8a0a\u3002\u4e5f\u53ef\u80fd\u56e0\u70ba\u88dd\u7f6e\u7684\u66f4\u65b0\u983b\u7387\u8f03\u6162\u3002\u78ba\u8a8d\u9084\u662f\u8981\u65b0\u589e\u6b64\u88dd\u7f6e\u3001\u65bc\u4e0b\u6b21\u6536\u5230\u88dd\u7f6e\u5ee3\u64ad\u6642\uff0c\u5982\u679c\u9700\u8981\u3001\u5c07\u63d0\u793a\u60a8\u8f38\u5165\u7d81\u5b9a\u78bc\u3002" + }, "get_encryption_key_4_5": { "data": { "bindkey": "\u7d81\u5b9a\u78bc" From 22eba6ce1ba3611421c526b61432ac90592eff08 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 08:13:20 +0200 Subject: [PATCH 141/903] Remove attribution from extra state attributes (#76172) --- homeassistant/components/aemet/sensor.py | 3 +-- homeassistant/components/agent_dvr/camera.py | 3 +-- homeassistant/components/airly/sensor.py | 4 ++-- homeassistant/components/airnow/sensor.py | 5 +++-- homeassistant/components/alpha_vantage/sensor.py | 8 +++++--- homeassistant/components/aurora/__init__.py | 2 +- homeassistant/components/co2signal/sensor.py | 4 ++-- homeassistant/components/worldtidesinfo/sensor.py | 12 ++++-------- homeassistant/components/wsdot/sensor.py | 4 ++-- homeassistant/components/zamg/sensor.py | 3 +-- 10 files changed, 22 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index e34583148e1..42cc005dcdd 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -63,7 +62,7 @@ async def async_setup_entry( class AbstractAemetSensor(CoordinatorEntity[WeatherUpdateCoordinator], SensorEntity): """Abstract class for an AEMET OpenData sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 9aab33efd5a..e99f1ecf223 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -7,7 +7,6 @@ from agent import AgentError from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import ( @@ -70,6 +69,7 @@ async def async_setup_entry( class AgentCamera(MjpegCamera): """Representation of an Agent Device Stream.""" + _attr_attribution = ATTRIBUTION _attr_supported_features = CameraEntityFeature.ON_OFF def __init__(self, device): @@ -108,7 +108,6 @@ class AgentCamera(MjpegCamera): self._attr_icon = "mdi:camcorder" self._attr_available = self.device.client.is_available self._attr_extra_state_attributes = { - ATTR_ATTRIBUTION: ATTRIBUTION, "editable": False, "enabled": self.is_on, "connected": self.connected, diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index eeb0037c814..a1c9f8a3057 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME, PERCENTAGE, @@ -136,6 +135,7 @@ async def async_setup_entry( class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): """Define an Airly sensor.""" + _attr_attribution = ATTRIBUTION _attr_has_entity_name = True entity_description: AirlySensorEntityDescription @@ -159,7 +159,7 @@ class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{description.key}".lower() ) - self._attrs: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attrs: dict[str, Any] = {} self.entity_description = description @property diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 780e40ed2ba..decec74ee47 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -8,7 +8,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, ) @@ -73,6 +72,8 @@ async def async_setup_entry( class AirNowSensor(CoordinatorEntity[AirNowDataUpdateCoordinator], SensorEntity): """Define an AirNow sensor.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: AirNowDataUpdateCoordinator, @@ -82,7 +83,7 @@ class AirNowSensor(CoordinatorEntity[AirNowDataUpdateCoordinator], SensorEntity) super().__init__(coordinator) self.entity_description = description self._state = None - self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attrs: dict[str, str] = {} self._attr_name = f"AirNow {description.name}" self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{description.key.lower()}" diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index fb2c7f01fea..534383f0bbf 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_CURRENCY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -118,6 +118,8 @@ def setup_platform( class AlphaVantageSensor(SensorEntity): """Representation of a Alpha Vantage sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, timeseries, symbol): """Initialize the sensor.""" self._symbol = symbol[CONF_SYMBOL] @@ -137,7 +139,6 @@ class AlphaVantageSensor(SensorEntity): self._attr_native_value = None self._attr_extra_state_attributes = ( { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CLOSE: values["4. close"], ATTR_HIGH: values["2. high"], ATTR_LOW: values["3. low"], @@ -151,6 +152,8 @@ class AlphaVantageSensor(SensorEntity): class AlphaVantageForeignExchange(SensorEntity): """Sensor for foreign exchange rates.""" + _attr_attribution = ATTRIBUTION + def __init__(self, foreign_exchange, config): """Initialize the sensor.""" self._foreign_exchange = foreign_exchange @@ -180,7 +183,6 @@ class AlphaVantageForeignExchange(SensorEntity): self._attr_native_value = None self._attr_extra_state_attributes = ( { - ATTR_ATTRIBUTION: ATTRIBUTION, CONF_FROM: self._from_currency, CONF_TO: self._to_currency, } diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index b8d0589d007..bac402fe633 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -121,7 +121,7 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator): class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]): """Implementation of the base Aurora Entity.""" - _attr_extra_state_attributes = {"attribution": ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 841848621ec..680220371b4 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -59,6 +59,7 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): """Implementation of the CO2Signal sensor.""" entity_description: CO2SensorEntityDescription + _attr_attribution = ATTRIBUTION _attr_has_entity_name = True _attr_icon = "mdi:molecule-co2" _attr_state_class = SensorStateClass.MEASUREMENT @@ -72,7 +73,6 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): self._attr_extra_state_attributes = { "country_code": coordinator.data["countryCode"], - ATTR_ATTRIBUTION: ATTRIBUTION, } self._attr_device_info = DeviceInfo( configuration_url="https://www.electricitymap.org/", diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index 533328490c8..2433a9f678e 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -9,13 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -67,6 +61,8 @@ def setup_platform( class WorldTidesInfoSensor(SensorEntity): """Representation of a WorldTidesInfo sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, name, lat, lon, key): """Initialize the sensor.""" self._name = name @@ -83,7 +79,7 @@ class WorldTidesInfoSensor(SensorEntity): @property def extra_state_attributes(self): """Return the state attributes of this device.""" - attr = {ATTR_ATTRIBUTION: ATTRIBUTION} + attr = {} if "High" in str(self.data["extremes"][0]["type"]): attr["high_tide_time_utc"] = self.data["extremes"][0]["date"] diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 309b9a6a758..76d1b92b476 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_NAME, CONF_API_KEY, CONF_ID, @@ -106,6 +105,7 @@ class WashingtonStateTransportSensor(SensorEntity): class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): """Travel time sensor from WSDOT.""" + _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement = TIME_MINUTES def __init__(self, name, access_code, travel_time_id): @@ -131,7 +131,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): def extra_state_attributes(self): """Return other details about the sensor state.""" if self._data is not None: - attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + attrs = {} for key in ( ATTR_AVG_TIME, ATTR_NAME, diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index c32aa942625..8452841520b 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -20,7 +20,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( AREA_SQUARE_METERS, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, @@ -243,6 +242,7 @@ def setup_platform( class ZamgSensor(SensorEntity): """Implementation of a ZAMG sensor.""" + _attr_attribution = ATTRIBUTION entity_description: ZamgSensorEntityDescription def __init__(self, probe, name, description: ZamgSensorEntityDescription): @@ -260,7 +260,6 @@ class ZamgSensor(SensorEntity): def extra_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STATION: self.probe.get_data("station_name"), ATTR_UPDATED: self.probe.last_update.isoformat(), } From e6e5b98bc711c2b3f02df2622815f12a0d220155 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 4 Aug 2022 09:13:20 +0200 Subject: [PATCH 142/903] Allow climate operation mode fan_only as custom mode in Alexa (#76148) * Add support for FAN_ONLY mode * Tests for fan_only as custom mode --- homeassistant/components/alexa/const.py | 7 +++++-- tests/components/alexa/test_capabilities.py | 19 ++++++++++++++++++- tests/components/alexa/test_smart_home.py | 16 ++++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index c6ac3071d94..d51409a5a1c 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -73,11 +73,14 @@ API_THERMOSTAT_MODES = OrderedDict( (climate.HVACMode.HEAT_COOL, "AUTO"), (climate.HVACMode.AUTO, "AUTO"), (climate.HVACMode.OFF, "OFF"), - (climate.HVACMode.FAN_ONLY, "OFF"), + (climate.HVACMode.FAN_ONLY, "CUSTOM"), (climate.HVACMode.DRY, "CUSTOM"), ] ) -API_THERMOSTAT_MODES_CUSTOM = {climate.HVACMode.DRY: "DEHUMIDIFY"} +API_THERMOSTAT_MODES_CUSTOM = { + climate.HVACMode.DRY: "DEHUMIDIFY", + climate.HVACMode.FAN_ONLY: "FAN", +} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} # AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index ea6c96bbaef..10ad5f7ebd2 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -590,7 +590,7 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) - for off_modes in (climate.HVAC_MODE_OFF, climate.HVAC_MODE_FAN_ONLY): + for off_modes in [climate.HVAC_MODE_OFF]: hass.states.async_set( "climate.downstairs", off_modes, @@ -626,6 +626,23 @@ async def test_report_climate_state(hass): "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) + # assert fan_only is reported as CUSTOM + hass.states.async_set( + "climate.downstairs", + "fan_only", + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 31, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 31.0, "scale": "CELSIUS"} + ) + hass.states.async_set( "climate.heat", "heat", diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 0169eeff9d5..df45d90358b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2030,7 +2030,7 @@ async def test_thermostat(hass): "current_temperature": 75.0, "friendly_name": "Test Thermostat", "supported_features": 1 | 2 | 4 | 128, - "hvac_modes": ["off", "heat", "cool", "auto", "dry"], + "hvac_modes": ["off", "heat", "cool", "auto", "dry", "fan_only"], "preset_mode": None, "preset_modes": ["eco"], "min_temp": 50, @@ -2220,7 +2220,7 @@ async def test_thermostat(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") - # Assert we can call custom modes + # Assert we can call custom modes for dry and fan_only call, msg = await assert_request_calls_service( "Alexa.ThermostatController", "SetThermostatMode", @@ -2233,6 +2233,18 @@ async def test_thermostat(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + call, msg = await assert_request_calls_service( + "Alexa.ThermostatController", + "SetThermostatMode", + "climate#test_thermostat", + "climate.set_hvac_mode", + hass, + payload={"thermostatMode": {"value": "CUSTOM", "customName": "FAN"}}, + ) + assert call.data["hvac_mode"] == "fan_only" + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + # assert unsupported custom mode msg = await assert_request_fails( "Alexa.ThermostatController", From b7b965c9c9575e3717eb4bef39809c55c700b7d4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 09:41:07 +0200 Subject: [PATCH 143/903] Use attributes in yeelightsunflower light (#75995) --- .../components/yeelightsunflower/light.py | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 8b51dad40f7..29a6118fc03 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -49,42 +49,23 @@ class SunflowerBulb(LightEntity): _attr_color_mode = ColorMode.HS _attr_supported_color_modes = {ColorMode.HS} - def __init__(self, light): + def __init__(self, light: yeelightsunflower.Bulb) -> None: """Initialize a Yeelight Sunflower bulb.""" self._light = light - self._available = light.available + self._attr_available = light.available self._brightness = light.brightness - self._is_on = light.is_on + self._attr_is_on = light.is_on self._rgb_color = light.rgb_color - self._unique_id = light.zid + self._attr_unique_id = light.zid + self._attr_name = f"sunflower_{self._light.zid}" @property - def name(self): - """Return the display name of this light.""" - return f"sunflower_{self._light.zid}" - - @property - def unique_id(self): - """Return the unique ID of this light.""" - return self._unique_id - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - @property - def brightness(self): + def brightness(self) -> int: """Return the brightness is 0-255; Yeelight's brightness is 0-100.""" return int(self._brightness / 100 * 255) @property - def hs_color(self): + def hs_color(self) -> tuple[float, float]: """Return the color property.""" return color_util.color_RGB_to_hs(*self._rgb_color) @@ -112,7 +93,7 @@ class SunflowerBulb(LightEntity): def update(self) -> None: """Fetch new state data for this light and update local values.""" self._light.update() - self._available = self._light.available + self._attr_available = self._light.available self._brightness = self._light.brightness - self._is_on = self._light.is_on + self._attr_is_on = self._light.is_on self._rgb_color = self._light.rgb_color From d5695a2d8656d2f9cb4d549c80cad331c914af1f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 4 Aug 2022 11:30:37 +0100 Subject: [PATCH 144/903] Fix some homekit_controller pylint warnings and (local only) test failures (#76122) --- .../homekit_controller/config_flow.py | 60 ++++++++++++++----- .../components/homekit_controller/light.py | 4 +- .../homekit_controller/test_sensor.py | 8 ++- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 31677e37b20..d5ce8c37e7c 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,14 +1,17 @@ """Config flow to configure homekit_controller.""" from __future__ import annotations -from collections.abc import Awaitable import logging import re from typing import TYPE_CHECKING, Any, cast import aiohomekit from aiohomekit import Controller, const as aiohomekit_const -from aiohomekit.controller.abstract import AbstractDiscovery, AbstractPairing +from aiohomekit.controller.abstract import ( + AbstractDiscovery, + AbstractPairing, + FinishPairing, +) from aiohomekit.exceptions import AuthenticationError from aiohomekit.model.categories import Categories from aiohomekit.model.status_flags import StatusFlags @@ -17,7 +20,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr @@ -78,7 +81,9 @@ def formatted_category(category: Categories) -> str: @callback -def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None: +def find_existing_host( + hass: HomeAssistant, serial: str +) -> config_entries.ConfigEntry | None: """Return a set of the configured hosts.""" for entry in hass.config_entries.async_entries(DOMAIN): if entry.data.get("AccessoryPairingID") == serial: @@ -115,15 +120,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.category: Categories | None = None self.devices: dict[str, AbstractDiscovery] = {} self.controller: Controller | None = None - self.finish_pairing: Awaitable[AbstractPairing] | None = None + self.finish_pairing: FinishPairing | None = None - async def _async_setup_controller(self): + async def _async_setup_controller(self) -> None: """Create the controller.""" self.controller = await async_get_controller(self.hass) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow start.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: key = user_input["device"] @@ -142,6 +149,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.controller is None: await self._async_setup_controller() + assert self.controller + self.devices = {} async for discovery in self.controller.async_discover(): @@ -167,7 +176,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_unignore(self, user_input): + async def async_step_unignore(self, user_input: dict[str, Any]) -> FlowResult: """Rediscover a previously ignored discover.""" unique_id = user_input["unique_id"] await self.async_set_unique_id(unique_id) @@ -175,19 +184,21 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.controller is None: await self._async_setup_controller() + assert self.controller + try: discovery = await self.controller.async_find(unique_id) except aiohomekit.AccessoryNotFoundError: return self.async_abort(reason="accessory_not_found_error") self.name = discovery.description.name - self.model = discovery.description.model + self.model = getattr(discovery.description, "model", BLE_DEFAULT_NAME) self.category = discovery.description.category self.hkid = discovery.description.id return self._async_step_pair_show_form() - async def _hkid_is_homekit(self, hkid): + async def _hkid_is_homekit(self, hkid: str) -> bool: """Determine if the device is a homekit bridge or accessory.""" dev_reg = dr.async_get(self.hass) device = dev_reg.async_get_device( @@ -410,7 +421,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form() - async def async_step_pair(self, pair_info=None): + async def async_step_pair( + self, pair_info: dict[str, Any] | None = None + ) -> FlowResult: """Pair with a new HomeKit accessory.""" # If async_step_pair is called with no pairing code then we do the M1 # phase of pairing. If this is successful the device enters pairing @@ -428,11 +441,16 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # callable. We call the callable with the pin that the user has typed # in. + # Should never call this step without setting self.hkid + assert self.hkid + errors = {} if self.controller is None: await self._async_setup_controller() + assert self.controller + if pair_info and self.finish_pairing: self.context["pairing"] = True code = pair_info["pairing_code"] @@ -507,21 +525,27 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form(errors) - async def async_step_busy_error(self, user_input=None): + async def async_step_busy_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Retry pairing after the accessory is busy.""" if user_input is not None: return await self.async_step_pair() return self.async_show_form(step_id="busy_error") - async def async_step_max_tries_error(self, user_input=None): + async def async_step_max_tries_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Retry pairing after the accessory has reached max tries.""" if user_input is not None: return await self.async_step_pair() return self.async_show_form(step_id="max_tries_error") - async def async_step_protocol_error(self, user_input=None): + async def async_step_protocol_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Retry pairing after the accessory has a protocol error.""" if user_input is not None: return await self.async_step_pair() @@ -529,7 +553,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="protocol_error") @callback - def _async_step_pair_show_form(self, errors=None): + def _async_step_pair_show_form( + self, errors: dict[str, str] | None = None + ) -> FlowResult: + assert self.category + placeholders = self.context["title_placeholders"] = { "name": self.name, "category": formatted_category(self.category), diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index df691ac3f6f..d882f6790f7 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -107,9 +107,9 @@ class HomeKitLight(HomeKitEntity, LightEntity): return ColorMode.ONOFF @property - def supported_color_modes(self) -> set[ColorMode | str] | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" - color_modes: set[ColorMode | str] = set() + color_modes: set[ColorMode] = set() if self.service.has(CharacteristicsTypes.HUE) or self.service.has( CharacteristicsTypes.SATURATION diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index c2a466d3997..79ae29c7434 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -92,10 +92,12 @@ async def test_temperature_sensor_not_added_twice(hass, utcnow): hass, create_temperature_sensor_service, suffix="temperature" ) + created_sensors = set() for state in hass.states.async_all(): - if state.entity_id.startswith("button"): - continue - assert state.entity_id == helper.entity_id + if state.attributes.get("device_class") == SensorDeviceClass.TEMPERATURE: + created_sensors.add(state.entity_id) + + assert created_sensors == {helper.entity_id} async def test_humidity_sensor_read_state(hass, utcnow): From aa3097a3be267292812fa968848a39739727225f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 4 Aug 2022 11:55:29 +0100 Subject: [PATCH 145/903] Add a Thread network status sensor to homekit_controller (#76209) --- .../components/homekit_controller/sensor.py | 57 ++++++++++++++++++- .../homekit_controller/strings.sensor.json | 9 +++ .../translations/sensor.en.json | 9 +++ .../test_nanoleaf_strip_nl55.py | 7 +++ .../homekit_controller/test_sensor.py | 14 ++++- 5 files changed, 94 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index ecfad477d00..a6810c10d99 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from aiohomekit.model.characteristics.const import ThreadNodeCapabilities +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.sensor import ( @@ -83,6 +83,54 @@ def thread_node_capability_to_str(char: Characteristic) -> str: return "none" +def thread_status_to_str(char: Characteristic) -> str: + """ + Return the thread status as a string. + + The underlying value is a bitmask, but we want to turn that to + a human readable string. So we check the flags in order. E.g. BORDER_ROUTER implies + ROUTER, so its more important to show that value. + """ + + val = ThreadStatus(char.value) + + if val & ThreadStatus.BORDER_ROUTER: + # Device has joined the Thread network and is participating + # in routing between mesh nodes. + # It's also the border router - bridging the thread network + # to WiFI/Ethernet/etc + return "border_router" + + if val & ThreadStatus.LEADER: + # Device has joined the Thread network and is participating + # in routing between mesh nodes. + # It's also the leader. There's only one leader and it manages + # which nodes are routers. + return "leader" + + if val & ThreadStatus.ROUTER: + # Device has joined the Thread network and is participating + # in routing between mesh nodes. + return "router" + + if val & ThreadStatus.CHILD: + # Device has joined the Thread network as a child + # It's not participating in routing between mesh nodes + return "child" + + if val & ThreadStatus.JOINING: + # Device is currently joining its Thread network + return "joining" + + if val & ThreadStatus.DETACHED: + # Device is currently unable to reach its Thread network + return "detached" + + # Must be ThreadStatus.DISABLED + # Device is not currently connected to Thread and will not try to. + return "disabled" + + SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT, @@ -243,6 +291,13 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { entity_category=EntityCategory.DIAGNOSTIC, format=thread_node_capability_to_str, ), + CharacteristicsTypes.THREAD_STATUS: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.THREAD_STATUS, + name="Thread Status", + device_class="homekit_controller__thread_status", + entity_category=EntityCategory.DIAGNOSTIC, + format=thread_status_to_str, + ), } diff --git a/homeassistant/components/homekit_controller/strings.sensor.json b/homeassistant/components/homekit_controller/strings.sensor.json index 4a77fa1668a..ceede708572 100644 --- a/homeassistant/components/homekit_controller/strings.sensor.json +++ b/homeassistant/components/homekit_controller/strings.sensor.json @@ -7,6 +7,15 @@ "minimal": "Minimal End Device", "sleepy": "Sleepy End Device", "none": "None" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "leader": "Leader", + "router": "Router", + "child": "Child", + "joining": "Joining", + "detached": "Detached", + "disabled": "Disabled" } } } diff --git a/homeassistant/components/homekit_controller/translations/sensor.en.json b/homeassistant/components/homekit_controller/translations/sensor.en.json index b1f8a0a8128..acfa6ab2824 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.en.json +++ b/homeassistant/components/homekit_controller/translations/sensor.en.json @@ -7,6 +7,15 @@ "none": "None", "router_eligible": "Router Eligible End Device", "sleepy": "Sleepy End Device" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "child": "Child", + "detached": "Detached", + "disabled": "Disabled", + "joining": "Joining", + "leader": "Leader", + "router": "Router" } } } \ No newline at end of file diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py index 086027f2427..550c3a328d0 100644 --- a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -57,6 +57,13 @@ async def test_nanoleaf_nl55_setup(hass): entity_category=EntityCategory.DIAGNOSTIC, state="border_router_capable", ), + EntityTestInfo( + entity_id="sensor.nanoleaf_strip_3b32_thread_status", + friendly_name="Nanoleaf Strip 3B32 Thread Status", + unique_id="homekit-AAAA011111111111-aid:1-sid:31-cid:117", + entity_category=EntityCategory.DIAGNOSTIC, + state="border_router", + ), ], ), ) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 79ae29c7434..21112937939 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -1,11 +1,12 @@ """Basic checks for HomeKit sensor.""" from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.characteristics.const import ThreadNodeCapabilities +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode from homeassistant.components.homekit_controller.sensor import ( thread_node_capability_to_str, + thread_status_to_str, ) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass @@ -337,3 +338,14 @@ def test_thread_node_caps_to_str(): assert thread_node_capability_to_str(ThreadNodeCapabilities.MINIMAL) == "minimal" assert thread_node_capability_to_str(ThreadNodeCapabilities.SLEEPY) == "sleepy" assert thread_node_capability_to_str(ThreadNodeCapabilities(128)) == "none" + + +def test_thread_status_to_str(): + """Test all values of this enum get a translatable string.""" + assert thread_status_to_str(ThreadStatus.BORDER_ROUTER) == "border_router" + assert thread_status_to_str(ThreadStatus.LEADER) == "leader" + assert thread_status_to_str(ThreadStatus.ROUTER) == "router" + assert thread_status_to_str(ThreadStatus.CHILD) == "child" + assert thread_status_to_str(ThreadStatus.JOINING) == "joining" + assert thread_status_to_str(ThreadStatus.DETACHED) == "detached" + assert thread_status_to_str(ThreadStatus.DISABLED) == "disabled" From 726eb82758ba6ceec8e31611b2854e5f4c0cee72 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 13:21:37 +0200 Subject: [PATCH 146/903] Mark RPI Power binary sensor as diagnostic (#76198) --- homeassistant/components/rpi_power/binary_sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rpi_power/binary_sensor.py b/homeassistant/components/rpi_power/binary_sensor.py index f70581a8075..08535daf970 100644 --- a/homeassistant/components/rpi_power/binary_sensor.py +++ b/homeassistant/components/rpi_power/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback _LOGGER = logging.getLogger(__name__) @@ -35,6 +36,7 @@ class RaspberryChargerBinarySensor(BinarySensorEntity): """Binary sensor representing the rpi power status.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:raspberry-pi" _attr_name = "RPi Power status" _attr_unique_id = "rpi_power" # only one sensor possible From 88a5ab1e1eeec1975362c15102ca083d5e4b2489 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 4 Aug 2022 14:01:26 +0200 Subject: [PATCH 147/903] Bump NextDNS library (#76207) --- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/test_diagnostics.py | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index a427f930db8..3e2d3ebb3d0 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -3,7 +3,7 @@ "name": "NextDNS", "documentation": "https://www.home-assistant.io/integrations/nextdns", "codeowners": ["@bieniu"], - "requirements": ["nextdns==1.0.1"], + "requirements": ["nextdns==1.0.2"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["nextdns"] diff --git a/requirements_all.txt b/requirements_all.txt index e7e6999053e..8a1ef4cd991 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1109,7 +1109,7 @@ nextcloudmonitor==1.1.0 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.1 +nextdns==1.0.2 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18ef8e97b63..a67eb65f2e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -787,7 +787,7 @@ nexia==2.0.2 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.1 +nextdns==1.0.2 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 85dbceafff9..0702a2fa1d8 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -48,11 +48,13 @@ async def test_entry_diagnostics( } assert result["protocols_coordinator_data"] == { "doh_queries": 20, + "doh3_queries": 0, "doq_queries": 10, "dot_queries": 30, "tcp_queries": 0, "udp_queries": 40, "doh_queries_ratio": 20.0, + "doh3_queries_ratio": 0.0, "doq_queries_ratio": 10.0, "dot_queries_ratio": 30.0, "tcp_queries_ratio": 0.0, From 9af64b1c3b1a712d24fc7b86ed2cc5e1fa613f26 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 14:02:07 +0200 Subject: [PATCH 148/903] Improve type hints in zha light (#75947) --- homeassistant/components/zha/light.py | 37 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 9fc089e2241..88bd5299ff7 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -28,7 +28,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -390,7 +390,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" transition = kwargs.get(light.ATTR_TRANSITION) supports_level = brightness_supported(self._attr_supported_color_modes) @@ -567,7 +567,7 @@ class Light(BaseLight, ZhaEntity): if self._color_channel: self._attr_min_mireds: int = self._color_channel.min_mireds self._attr_max_mireds: int = self._color_channel.max_mireds - self._cancel_refresh_handle = None + self._cancel_refresh_handle: CALLBACK_TYPE | None = None effect_list = [] self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( @@ -675,7 +675,7 @@ class Light(BaseLight, ZhaEntity): self._off_brightness = None self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -755,7 +755,7 @@ class Light(BaseLight, ZhaEntity): if "effect" in last_state.attributes: self._attr_effect = last_state.attributes["effect"] - async def async_get_state(self): + async def async_get_state(self) -> None: """Attempt to retrieve the state from the light.""" if not self._attr_available: return @@ -843,7 +843,7 @@ class Light(BaseLight, ZhaEntity): else: self._attr_effect = None - async def async_update(self): + async def async_update(self) -> None: """Update to the latest state.""" if self._transitioning: self.debug("skipping async_update while transitioning") @@ -906,7 +906,12 @@ class LightGroup(BaseLight, ZhaGroupEntity): """Representation of a light group.""" def __init__( - self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs + self, + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, ) -> None: """Initialize a light group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) @@ -919,7 +924,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): self._level_channel = group.endpoint[LevelControl.cluster_id] self._color_channel = group.endpoint[Color.cluster_id] self._identify_channel = group.endpoint[Identify.cluster_id] - self._debounced_member_refresh = None + self._debounced_member_refresh: Debouncer | None = None self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, @@ -947,7 +952,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): """Return entity availability.""" return self._attr_available - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() if self._debounced_member_refresh is None: @@ -960,22 +965,24 @@ class LightGroup(BaseLight, ZhaGroupEntity): ) self._debounced_member_refresh = force_refresh_debouncer - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await super().async_turn_on(**kwargs) if self._transitioning: return - await self._debounced_member_refresh.async_call() + if self._debounced_member_refresh: + await self._debounced_member_refresh.async_call() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await super().async_turn_off(**kwargs) if self._transitioning: return - await self._debounced_member_refresh.async_call() + if self._debounced_member_refresh: + await self._debounced_member_refresh.async_call() @callback - def async_state_changed_listener(self, event: Event): + def async_state_changed_listener(self, event: Event) -> None: """Handle child updates.""" if self._transitioning: self.debug("skipping group entity state update during transition") @@ -1073,7 +1080,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): # so that we don't break in the future when a new feature is added. self._attr_supported_features &= SUPPORT_GROUP_LIGHT - async def _force_member_updates(self): + async def _force_member_updates(self) -> None: """Force the update of member entities to ensure the states are correct for bulbs that don't report their state.""" async_dispatcher_send( self.hass, From 8793cf4996b194680e6a2ef89fe814e7fc5b9930 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 15:57:00 +0200 Subject: [PATCH 149/903] Fix spelling of OpenWrt in luci integration manifest (#76219) --- homeassistant/components/luci/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 2d61852689a..b24c0234de9 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -1,6 +1,6 @@ { "domain": "luci", - "name": "OpenWRT (luci)", + "name": "OpenWrt (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": ["openwrt-luci-rpc==1.1.11"], "codeowners": ["@mzdrale"], From ff255feddae0879a923854ba91a25689f0461353 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 4 Aug 2022 17:04:12 +0300 Subject: [PATCH 150/903] Fix nullable ip_address in mikrotik (#76197) --- homeassistant/components/mikrotik/device_tracker.py | 2 +- homeassistant/components/mikrotik/hub.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index d02bf69b5ab..856521f019d 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -116,7 +116,7 @@ class MikrotikDataUpdateCoordinatorTracker( return self.device.mac @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the mac address of the client.""" return self.device.ip_address diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 66fe7226d9b..914911ee5cc 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -60,9 +60,9 @@ class Device: return self._params.get("host-name", self.mac) @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return device primary ip address.""" - return self._params["address"] + return self._params.get("address") @property def mac(self) -> str: From 63b454c9ed7354665a3f0efbb0c45b75754b1beb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 05:38:55 -1000 Subject: [PATCH 151/903] BLE pairing reliablity fixes for HomeKit Controller (#76199) - Remove the cached map from memory when unpairing so we do not reuse it again if they unpair/repair - Fixes for accessories that use a config number of 0 - General reliablity improvements to the pairing process under the hood of aiohomekit --- .../components/homekit_controller/__init__.py | 7 ++++--- .../homekit_controller/config_flow.py | 2 +- .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/storage.py | 19 ++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_config_flow.py | 6 ++++++ 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 37dd648dedb..b2ccad9a457 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -31,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice, valid_serial_number from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS -from .storage import async_get_entity_storage +from .storage import EntityMapStorage, async_get_entity_storage from .utils import async_get_controller, folded_name _LOGGER = logging.getLogger(__name__) @@ -269,7 +269,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hkid = entry.data["AccessoryPairingID"] if hkid in hass.data[KNOWN_DEVICES]: - connection = hass.data[KNOWN_DEVICES][hkid] + connection: HKDevice = hass.data[KNOWN_DEVICES][hkid] await connection.async_unload() return True @@ -280,7 +280,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: hkid = entry.data["AccessoryPairingID"] # Remove cached type data from .storage/homekit_controller-entity-map - hass.data[ENTITY_MAP].async_delete_map(hkid) + entity_map_storage: EntityMapStorage = hass.data[ENTITY_MAP] + entity_map_storage.async_delete_map(hkid) controller = await async_get_controller(hass) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index d5ce8c37e7c..eba531b917c 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -597,7 +597,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): entity_storage = await async_get_entity_storage(self.hass) assert self.unique_id is not None entity_storage.async_create_or_update_map( - self.unique_id, + pairing.id, accessories_state.config_num, accessories_state.accessories.serialize(), ) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index ac1be576906..5aff66fe757 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.3"], + "requirements": ["aiohomekit==1.2.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 8c0628c97f6..51d8ce4ffd3 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging from typing import Any, TypedDict from homeassistant.core import HomeAssistant, callback @@ -12,6 +13,7 @@ from .const import DOMAIN, ENTITY_MAP ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map" ENTITY_MAP_STORAGE_VERSION = 1 ENTITY_MAP_SAVE_DELAY = 10 +_LOGGER = logging.getLogger(__name__) class Pairing(TypedDict): @@ -68,6 +70,7 @@ class EntityMapStorage: self, homekit_id: str, config_num: int, accessories: list[Any] ) -> Pairing: """Create a new pairing cache.""" + _LOGGER.debug("Creating or updating entity map for %s", homekit_id) data = Pairing(config_num=config_num, accessories=accessories) self.storage_data[homekit_id] = data self._async_schedule_save() @@ -76,11 +79,17 @@ class EntityMapStorage: @callback def async_delete_map(self, homekit_id: str) -> None: """Delete pairing cache.""" - if homekit_id not in self.storage_data: - return - - self.storage_data.pop(homekit_id) - self._async_schedule_save() + removed_one = False + # Previously there was a bug where a lowercase homekit_id was stored + # in the storage. We need to account for that. + for hkid in (homekit_id, homekit_id.lower()): + if hkid not in self.storage_data: + continue + _LOGGER.debug("Deleting entity map for %s", hkid) + self.storage_data.pop(hkid) + removed_one = True + if removed_one: + self._async_schedule_save() @callback def _async_schedule_save(self) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 8a1ef4cd991..2893d0e943c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.3 +aiohomekit==1.2.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a67eb65f2e9..df4ee525846 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.3 +aiohomekit==1.2.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 78d3c609a9c..e72d9452e52 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES +from homeassistant.components.homekit_controller.storage import async_get_entity_storage from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_FORM, @@ -1071,6 +1072,8 @@ async def test_bluetooth_valid_device_discovery_paired(hass, controller): async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): """Test bluetooth discovery with a homekit device and discovery works.""" setup_mock_accessory(controller) + storage = await async_get_entity_storage(hass) + with patch( "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", True, @@ -1083,6 +1086,7 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "pair" + assert storage.get_map("00:00:00:00:00:00") is None assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_BLUETOOTH, @@ -1098,3 +1102,5 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Koogeek-LS1-20833F" assert result3["data"] == {} + + assert storage.get_map("00:00:00:00:00:00") is not None From 5d7cef64168673163986f6c3f31bb6a2cba32608 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 05:58:15 -1000 Subject: [PATCH 152/903] Fix race in bluetooth async_process_advertisements (#76176) --- homeassistant/components/bluetooth/__init__.py | 2 +- tests/components/bluetooth/test_init.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index c91563d7729..0b81472f838 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -188,7 +188,7 @@ async def async_process_advertisements( def _async_discovered_device( service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: - if callback(service_info): + if not done.done() and callback(service_info): done.set_result(service_info) unload = async_register_callback(hass, _async_discovered_device, match_dict, mode) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index edc5eb024a6..ba315b1f380 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -856,6 +856,9 @@ async def test_process_advertisements_bail_on_good_advertisement( ) _get_underlying_scanner()._callback(device, adv) + _get_underlying_scanner()._callback(device, adv) + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) result = await handle From 0ce44150fd1ef65ec832acab0a7e1a342c7d71b9 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 4 Aug 2022 12:01:58 -0400 Subject: [PATCH 153/903] Bump AIOAladdin Connect to 0.1.41 (#76217) --- homeassistant/components/aladdin_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/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 5e55f391aa6..febba16170a 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.39"], + "requirements": ["AIOAladdinConnect==0.1.41"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 2893d0e943c..903ac347c5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.39 +AIOAladdinConnect==0.1.41 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df4ee525846..2504705b55d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.39 +AIOAladdinConnect==0.1.41 # homeassistant.components.adax Adax-local==0.1.4 From 5e75bed9296e6131513da20d8da4349ebd5defc8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 18:04:54 +0200 Subject: [PATCH 154/903] Update sentry-sdk to 1.9.0 (#76192) --- 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 f6567fbd04d..0400424a7fc 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.8.0"], + "requirements": ["sentry-sdk==1.9.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 903ac347c5a..1a79d1ee3ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2169,7 +2169,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.8.0 +sentry-sdk==1.9.0 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2504705b55d..841af5b021b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1457,7 +1457,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.8.0 +sentry-sdk==1.9.0 # homeassistant.components.sharkiq sharkiq==0.0.1 From 8810d3320ca34e1a69db3ef748d9000200fe3821 Mon Sep 17 00:00:00 2001 From: MosheTzvi <109089618+MosheTzvi@users.noreply.github.com> Date: Thu, 4 Aug 2022 19:08:23 +0300 Subject: [PATCH 155/903] added Hanetz Hachama (#76216) added missing variable --- homeassistant/components/jewish_calendar/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 827a35587f8..085dbcd8c98 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -63,6 +63,11 @@ TIME_SENSORS = ( name="Talit and Tefillin", icon="mdi:calendar-clock", ), + SensorEntityDescription( + key="sunrise", + name="Hanetz Hachama", + icon="mdi:calendar-clock", + ), SensorEntityDescription( key="gra_end_shma", name='Latest time for Shma Gr"a', From 91486f2d61cf9f9f9abff349da0dee28bda2fae7 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 4 Aug 2022 17:41:47 +0100 Subject: [PATCH 156/903] Enable strict typing for HomeKit Controller config flow module (#76233) --- .strict-typing | 1 + .../components/homekit_controller/manifest.json | 2 +- mypy.ini | 11 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index 37dcdd7623f..cc1af9926bd 100644 --- a/.strict-typing +++ b/.strict-typing @@ -129,6 +129,7 @@ homeassistant.components.homekit.util homeassistant.components.homekit_controller homeassistant.components.homekit_controller.alarm_control_panel homeassistant.components.homekit_controller.button +homeassistant.components.homekit_controller.config_flow homeassistant.components.homekit_controller.const homeassistant.components.homekit_controller.lock homeassistant.components.homekit_controller.select diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 5aff66fe757..5f6b3f92220 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.4"], + "requirements": ["aiohomekit==1.2.5"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/mypy.ini b/mypy.ini index 8c85b550f71..26f76c24a02 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1142,6 +1142,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homekit_controller.config_flow] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homekit_controller.const] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 1a79d1ee3ef..48d2e47086a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.4 +aiohomekit==1.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 841af5b021b..08e47a7aded 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.4 +aiohomekit==1.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http From 8ca5b5d4a4f78531093ce081e061445f009628d1 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 4 Aug 2022 18:36:37 +0100 Subject: [PATCH 157/903] Remove icon attribute if device class is set (#76161) --- homeassistant/components/integration/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 5d0dde3e4de..b3b8a2a2b9d 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -223,6 +223,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): == SensorDeviceClass.POWER ): self._attr_device_class = SensorDeviceClass.ENERGY + self._attr_icon = None update_state = True if update_state: From b2dc810ea4d2d7a9a89c33c15169781f0661651b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Aug 2022 11:43:02 -0600 Subject: [PATCH 158/903] More explicitly call out special cases with SimpliSafe authorization code (#76232) --- .../components/simplisafe/config_flow.py | 23 +++++++- .../components/simplisafe/strings.json | 3 +- .../simplisafe/translations/en.json | 24 +------- .../components/simplisafe/test_config_flow.py | 55 ++++++++++++++++--- 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0b92871ccb2..7ae363c3be3 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -81,17 +81,34 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={CONF_URL: self._oauth_values.auth_url}, ) + auth_code = user_input[CONF_AUTH_CODE] + + if auth_code.startswith("="): + # Sometimes, users may include the "=" from the URL query param; in that + # case, strip it off and proceed: + LOGGER.debug('Stripping "=" from the start of the authorization code') + auth_code = auth_code[1:] + + if len(auth_code) != 45: + # SimpliSafe authorization codes are 45 characters in length; if the user + # provides something different, stop them here: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors={CONF_AUTH_CODE: "invalid_auth_code_length"}, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) + errors = {} session = aiohttp_client.async_get_clientsession(self.hass) - try: simplisafe = await API.async_from_auth( - user_input[CONF_AUTH_CODE], + auth_code, self._oauth_values.code_verifier, session=session, ) except InvalidCredentialsError: - errors = {"base": "invalid_auth"} + errors = {CONF_AUTH_CODE: "invalid_auth"} except SimplipyError as err: LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) errors = {"base": "unknown"} diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 16ae7111abf..618c21566f7 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL.", + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL.", "data": { "auth_code": "Authorization Code" } @@ -11,6 +11,7 @@ "error": { "identifier_exists": "Account already registered", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 70b0cc15383..245bb18351e 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,39 +2,21 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, "error": { "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", + "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "Unexpected error" }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Please re-enter the password for {username}.", - "title": "Reauthenticate Integration" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Input the two-factor authentication code sent to you via SMS." - }, "user": { "data": { - "auth_code": "Authorization Code", - "password": "Password", - "username": "Username" + "auth_code": "Authorization Code" }, - "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL." + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL." } } }, diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 6e6f99ad4bb..cf92ed94d41 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the SimpliSafe config flow.""" +import logging from unittest.mock import patch import pytest @@ -10,6 +11,8 @@ from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME +VALID_AUTH_CODE = "code12345123451234512345123451234512345123451" + async def test_duplicate_error(config_entry, hass, setup_simplisafe): """Test that errors are shown when duplicates are added.""" @@ -23,12 +26,27 @@ async def test_duplicate_error(config_entry, hass, setup_simplisafe): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" +async def test_invalid_auth_code_length(hass): + """Test that an invalid auth code length show the correct error.""" + result = await hass.config_entries.flow.async_init( + 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_AUTH_CODE: "too_short_code"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth_code_length"} + + async def test_invalid_credentials(hass): """Test that invalid credentials show the correct error.""" with patch( @@ -42,10 +60,11 @@ async def test_invalid_credentials(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], + user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth"} async def test_options_flow(config_entry, hass): @@ -80,7 +99,7 @@ async def test_step_reauth(config_entry, hass, setup_simplisafe): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" @@ -104,14 +123,29 @@ async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "wrong_account" -async def test_step_user(hass, setup_simplisafe): - """Test the user step.""" +@pytest.mark.parametrize( + "auth_code,log_statement", + [ + ( + VALID_AUTH_CODE, + None, + ), + ( + f"={VALID_AUTH_CODE}", + 'Stripping "=" from the start of the authorization code', + ), + ], +) +async def test_step_user(auth_code, caplog, hass, log_statement, setup_simplisafe): + """Test successfully completion of the user step.""" + caplog.set_level = logging.DEBUG + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -121,10 +155,13 @@ async def test_step_user(hass, setup_simplisafe): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: auth_code} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + if log_statement: + assert any(m for m in caplog.messages if log_statement in m) + assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"} @@ -143,7 +180,7 @@ async def test_unknown_error(hass, setup_simplisafe): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} From 02ad4843b8d2c3fdb87c9e98043c14a457ca41c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 07:44:22 -1000 Subject: [PATCH 159/903] Fix flux_led ignored entries not being respected (#76173) --- .../components/flux_led/config_flow.py | 44 +++++++++++-------- tests/components/flux_led/test_config_flow.py | 27 ++++++++++++ 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 61395d744b3..b245c0c2bc2 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -105,27 +105,33 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert mac_address is not None mac = dr.format_mac(mac_address) await self.async_set_unique_id(mac) - for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == device[ATTR_IPADDR] or ( - entry.unique_id - and ":" in entry.unique_id - and mac_matches_by_one(entry.unique_id, mac) + for entry in self._async_current_entries(include_ignore=True): + if not ( + entry.data.get(CONF_HOST) == device[ATTR_IPADDR] + or ( + entry.unique_id + and ":" in entry.unique_id + and mac_matches_by_one(entry.unique_id, mac) + ) ): - if ( - async_update_entry_from_discovery( - self.hass, entry, device, None, allow_update_mac - ) - or entry.state == config_entries.ConfigEntryState.SETUP_RETRY - ): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - else: - async_dispatcher_send( - self.hass, - FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), - ) + continue + if entry.source == config_entries.SOURCE_IGNORE: raise AbortFlow("already_configured") + if ( + async_update_entry_from_discovery( + self.hass, entry, device, None, allow_update_mac + ) + or entry.state == config_entries.ConfigEntryState.SETUP_RETRY + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + else: + async_dispatcher_send( + self.hass, + FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), + ) + raise AbortFlow("already_configured") async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 8abdb8e955b..3f1704f7e8c 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -695,3 +695,30 @@ async def test_options(hass: HomeAssistant): assert result2["data"] == user_input assert result2["data"] == config_entry.options assert hass.states.get("light.bulb_rgbcw_ddeeff") is not None + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, FLUX_DISCOVERY), + ], +) +async def test_discovered_can_be_ignored(hass, source, data): + """Test we abort if the mac was already ignored.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + unique_id=MAC_ADDRESS, + source=config_entries.SOURCE_IGNORE, + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" From b5a6ee3c567aa50633ef47d342af685fb75e5219 Mon Sep 17 00:00:00 2001 From: y34hbuddy <47507530+y34hbuddy@users.noreply.github.com> Date: Thu, 4 Aug 2022 13:44:39 -0400 Subject: [PATCH 160/903] Refactor volvooncall to (mostly) use DataUpdateCoordinator (#75885) Co-authored-by: Martin Hjelmare --- .../components/volvooncall/__init__.py | 228 ++++++++++-------- .../components/volvooncall/binary_sensor.py | 38 ++- .../components/volvooncall/device_tracker.py | 10 +- homeassistant/components/volvooncall/lock.py | 18 +- .../components/volvooncall/sensor.py | 32 ++- .../components/volvooncall/switch.py | 19 +- 6 files changed, 209 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 0f969d785df..cf385d320ca 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -2,8 +2,10 @@ from datetime import timedelta import logging +import async_timeout import voluptuous as vol from volvooncall import Connection +from volvooncall.dashboard import Instrument from homeassistant.const import ( CONF_NAME, @@ -17,14 +19,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType -from homeassistant.util.dt import utcnow +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) DOMAIN = "volvooncall" @@ -32,7 +33,6 @@ DATA_KEY = DOMAIN _LOGGER = logging.getLogger(__name__) -MIN_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) CONF_SERVICE_URL = "service_url" @@ -92,24 +92,31 @@ RESOURCES = [ CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL - ): vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), - vol.Optional(CONF_NAME, default={}): cv.schema_with_slug_keys( - cv.string - ), - vol.Optional(CONF_RESOURCES): vol.All( - cv.ensure_list, [vol.In(RESOURCES)] - ), - vol.Optional(CONF_REGION): cv.string, - vol.Optional(CONF_SERVICE_URL): cv.string, - vol.Optional(CONF_MUTABLE, default=True): cv.boolean, - vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, - } + DOMAIN: vol.All( + cv.deprecated(CONF_SCAN_INTERVAL), + cv.deprecated(CONF_NAME), + cv.deprecated(CONF_RESOURCES), + vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL + ): vol.All( + cv.time_period, vol.Clamp(min=DEFAULT_UPDATE_INTERVAL) + ), # ignored, using DataUpdateCoordinator instead + vol.Optional(CONF_NAME, default={}): cv.schema_with_slug_keys( + cv.string + ), # ignored, users can modify names of entities in the UI + vol.Optional(CONF_RESOURCES): vol.All( + cv.ensure_list, [vol.In(RESOURCES)] + ), # ignored, users can disable entities in the UI + vol.Optional(CONF_REGION): cv.string, + vol.Optional(CONF_SERVICE_URL): cv.string, + vol.Optional(CONF_MUTABLE, default=True): cv.boolean, + vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, + } + ), ) }, extra=vol.ALLOW_EXTRA, @@ -128,34 +135,70 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: region=config[DOMAIN].get(CONF_REGION), ) - interval = config[DOMAIN][CONF_SCAN_INTERVAL] + hass.data[DATA_KEY] = {} - data = hass.data[DATA_KEY] = VolvoData(config) + volvo_data = VolvoData(hass, connection, config) - def is_enabled(attr): - """Return true if the user has enabled the resource.""" - return attr in config[DOMAIN].get(CONF_RESOURCES, [attr]) + hass.data[DATA_KEY] = VolvoUpdateCoordinator(hass, volvo_data) - def discover_vehicle(vehicle): + return await volvo_data.update() + + +class VolvoData: + """Hold component state.""" + + def __init__( + self, + hass: HomeAssistant, + connection: Connection, + config: ConfigType, + ) -> None: + """Initialize the component state.""" + self.hass = hass + self.vehicles: set[str] = set() + self.instruments: set[Instrument] = set() + self.config = config + self.connection = connection + + def instrument(self, vin, component, attr, slug_attr): + """Return corresponding instrument.""" + return next( + instrument + for instrument in self.instruments + if instrument.vehicle.vin == vin + and instrument.component == component + and instrument.attr == attr + and instrument.slug_attr == slug_attr + ) + + def vehicle_name(self, vehicle): + """Provide a friendly name for a vehicle.""" + if vehicle.registration_number and vehicle.registration_number != "UNKNOWN": + return vehicle.registration_number + if vehicle.vin: + return vehicle.vin + return "Volvo" + + def discover_vehicle(self, vehicle): """Load relevant platforms.""" - data.vehicles.add(vehicle.vin) + self.vehicles.add(vehicle.vin) dashboard = vehicle.dashboard( - mutable=config[DOMAIN][CONF_MUTABLE], - scandinavian_miles=config[DOMAIN][CONF_SCANDINAVIAN_MILES], + mutable=self.config[DOMAIN][CONF_MUTABLE], + scandinavian_miles=self.config[DOMAIN][CONF_SCANDINAVIAN_MILES], ) for instrument in ( instrument for instrument in dashboard.instruments - if instrument.component in PLATFORMS and is_enabled(instrument.slug_attr) + if instrument.component in PLATFORMS ): - data.instruments.add(instrument) + self.instruments.add(instrument) - hass.async_create_task( + self.hass.async_create_task( discovery.async_load_platform( - hass, + self.hass, PLATFORMS[instrument.component], DOMAIN, ( @@ -164,93 +207,71 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instrument.attr, instrument.slug_attr, ), - config, + self.config, ) ) - async def update(now): + async def update(self): """Update status from the online service.""" - try: - if not await connection.update(journal=True): - _LOGGER.warning("Could not query server") - return False + if not await self.connection.update(journal=True): + return False - for vehicle in connection.vehicles: - if vehicle.vin not in data.vehicles: - discover_vehicle(vehicle) + for vehicle in self.connection.vehicles: + if vehicle.vin not in self.vehicles: + self.discover_vehicle(vehicle) - async_dispatcher_send(hass, SIGNAL_STATE_UPDATED) + # this is currently still needed for device_tracker, which isn't using the update coordinator yet + async_dispatcher_send(self.hass, SIGNAL_STATE_UPDATED) - return True - finally: - async_track_point_in_utc_time(hass, update, utcnow() + interval) - - _LOGGER.info("Logging in to service") - return await update(utcnow()) + return True -class VolvoData: - """Hold component state.""" +class VolvoUpdateCoordinator(DataUpdateCoordinator): + """Volvo coordinator.""" - def __init__(self, config): - """Initialize the component state.""" - self.vehicles = set() - self.instruments = set() - self.config = config[DOMAIN] - self.names = self.config.get(CONF_NAME) + def __init__(self, hass: HomeAssistant, volvo_data: VolvoData) -> None: + """Initialize the data update coordinator.""" - def instrument(self, vin, component, attr, slug_attr): - """Return corresponding instrument.""" - return next( - ( - instrument - for instrument in self.instruments - if instrument.vehicle.vin == vin - and instrument.component == component - and instrument.attr == attr - and instrument.slug_attr == slug_attr - ), - None, + super().__init__( + hass, + _LOGGER, + name="volvooncall", + update_interval=DEFAULT_UPDATE_INTERVAL, ) - def vehicle_name(self, vehicle): - """Provide a friendly name for a vehicle.""" - if ( - vehicle.registration_number and vehicle.registration_number.lower() - ) in self.names: - return self.names[vehicle.registration_number.lower()] - if vehicle.vin and vehicle.vin.lower() in self.names: - return self.names[vehicle.vin.lower()] - if vehicle.registration_number: - return vehicle.registration_number - if vehicle.vin: - return vehicle.vin - return "" + self.volvo_data = volvo_data + + async def _async_update_data(self): + """Fetch data from API endpoint.""" + + async with async_timeout.timeout(10): + if not await self.volvo_data.update(): + raise UpdateFailed("Error communicating with API") -class VolvoEntity(Entity): +class VolvoEntity(CoordinatorEntity): """Base class for all VOC entities.""" - def __init__(self, data, vin, component, attribute, slug_attr): + def __init__( + self, + vin: str, + component: str, + attribute: str, + slug_attr: str, + coordinator: VolvoUpdateCoordinator, + ) -> None: """Initialize the entity.""" - self.data = data + super().__init__(coordinator) + self.vin = vin self.component = component self.attribute = attribute self.slug_attr = slug_attr - async def async_added_to_hass(self): - """Register update dispatcher.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_STATE_UPDATED, self.async_write_ha_state - ) - ) - @property def instrument(self): """Return corresponding instrument.""" - return self.data.instrument( + return self.coordinator.volvo_data.instrument( self.vin, self.component, self.attribute, self.slug_attr ) @@ -270,18 +291,13 @@ class VolvoEntity(Entity): @property def _vehicle_name(self): - return self.data.vehicle_name(self.vehicle) + return self.coordinator.volvo_data.vehicle_name(self.vehicle) @property def name(self): """Return full name of the entity.""" return f"{self._vehicle_name} {self._entity_name}" - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def assumed_state(self): """Return true if unable to access real state of entity.""" diff --git a/homeassistant/components/volvooncall/binary_sensor.py b/homeassistant/components/volvooncall/binary_sensor.py index c8a0105128d..2aeaeff93e4 100644 --- a/homeassistant/components/volvooncall/binary_sensor.py +++ b/homeassistant/components/volvooncall/binary_sensor.py @@ -1,12 +1,19 @@ """Support for VOC.""" from __future__ import annotations -from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity +from contextlib import suppress + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, + BinarySensorEntity, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity +from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator async def async_setup_platform( @@ -24,16 +31,25 @@ async def async_setup_platform( class VolvoSensor(VolvoEntity, BinarySensorEntity): """Representation of a Volvo sensor.""" + def __init__( + self, + coordinator: VolvoUpdateCoordinator, + vin: str, + component: str, + attribute: str, + slug_attr: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(vin, component, attribute, slug_attr, coordinator) + + with suppress(vol.Invalid): + self._attr_device_class = DEVICE_CLASSES_SCHEMA( + self.instrument.device_class + ) + @property - def is_on(self): - """Return True if the binary sensor is on, but invert for the 'Door lock'.""" + def is_on(self) -> bool | None: + """Fetch from update coordinator.""" if self.instrument.attr == "is_locked": return not self.instrument.is_on return self.instrument.is_on - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - if self.instrument.device_class in DEVICE_CLASSES: - return self.instrument.device_class - return None diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index 4f9300fd021..ffed8005f36 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -7,7 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify -from . import DATA_KEY, SIGNAL_STATE_UPDATED +from . import DATA_KEY, SIGNAL_STATE_UPDATED, VolvoUpdateCoordinator async def async_setup_scanner( @@ -21,8 +21,12 @@ async def async_setup_scanner( return False vin, component, attr, slug_attr = discovery_info - data = hass.data[DATA_KEY] - instrument = data.instrument(vin, component, attr, slug_attr) + coordinator: VolvoUpdateCoordinator = hass.data[DATA_KEY] + volvo_data = coordinator.volvo_data + instrument = volvo_data.instrument(vin, component, attr, slug_attr) + + if instrument is None: + return False async def see_vehicle() -> None: """Handle the reporting of the vehicle position.""" diff --git a/homeassistant/components/volvooncall/lock.py b/homeassistant/components/volvooncall/lock.py index c341627eef4..da36ca2e573 100644 --- a/homeassistant/components/volvooncall/lock.py +++ b/homeassistant/components/volvooncall/lock.py @@ -1,4 +1,5 @@ """Support for Volvo On Call locks.""" + from __future__ import annotations from typing import Any @@ -10,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity +from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator async def async_setup_platform( @@ -31,15 +32,28 @@ class VolvoLock(VolvoEntity, LockEntity): instrument: Lock + def __init__( + self, + coordinator: VolvoUpdateCoordinator, + vin: str, + component: str, + attribute: str, + slug_attr: str, + ) -> None: + """Initialize the lock.""" + super().__init__(vin, component, attribute, slug_attr, coordinator) + @property def is_locked(self) -> bool | None: - """Return true if lock is locked.""" + """Determine if car is locked.""" return self.instrument.is_locked async def async_lock(self, **kwargs: Any) -> None: """Lock the car.""" await self.instrument.lock() + await self.coordinator.async_request_refresh() async def async_unlock(self, **kwargs: Any) -> None: """Unlock the car.""" await self.instrument.unlock() + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/volvooncall/sensor.py b/homeassistant/components/volvooncall/sensor.py index bdc1b57f588..41426ff878a 100644 --- a/homeassistant/components/volvooncall/sensor.py +++ b/homeassistant/components/volvooncall/sensor.py @@ -2,11 +2,11 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity +from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator async def async_setup_platform( @@ -24,12 +24,24 @@ async def async_setup_platform( class VolvoSensor(VolvoEntity, SensorEntity): """Representation of a Volvo sensor.""" - @property - def native_value(self): - """Return the state.""" - return self.instrument.state + def __init__( + self, + coordinator: VolvoUpdateCoordinator, + vin: str, + component: str, + attribute: str, + slug_attr: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(vin, component, attribute, slug_attr, coordinator) + self._update_value_and_unit() - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return self.instrument.unit + def _update_value_and_unit(self) -> None: + self._attr_native_value = self.instrument.state + self._attr_native_unit_of_measurement = self.instrument.unit + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_value_and_unit() + self.async_write_ha_state() diff --git a/homeassistant/components/volvooncall/switch.py b/homeassistant/components/volvooncall/switch.py index 63758ac010b..6c8519f12e8 100644 --- a/homeassistant/components/volvooncall/switch.py +++ b/homeassistant/components/volvooncall/switch.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity +from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator async def async_setup_platform( @@ -24,17 +24,28 @@ async def async_setup_platform( class VolvoSwitch(VolvoEntity, SwitchEntity): """Representation of a Volvo switch.""" + def __init__( + self, + coordinator: VolvoUpdateCoordinator, + vin: str, + component: str, + attribute: str, + slug_attr: str, + ) -> None: + """Initialize the switch.""" + super().__init__(vin, component, attribute, slug_attr, coordinator) + @property def is_on(self): - """Return true if switch is on.""" + """Determine if switch is on.""" return self.instrument.state async def async_turn_on(self, **kwargs): """Turn the switch on.""" await self.instrument.turn_on() - self.async_write_ha_state() + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): """Turn the switch off.""" await self.instrument.turn_off() - self.async_write_ha_state() + await self.coordinator.async_request_refresh() From 639a522caa7fa8f8801be50f2ae12fbba7ac77e4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Aug 2022 11:45:28 -0600 Subject: [PATCH 161/903] Add repair item to remove no-longer-functioning Flu Near You integration (#76177) Co-authored-by: Franck Nijhof --- .coveragerc | 1 + .../components/flunearyou/__init__.py | 10 +++++ .../components/flunearyou/manifest.json | 1 + .../components/flunearyou/repairs.py | 42 +++++++++++++++++++ .../components/flunearyou/strings.json | 13 ++++++ .../flunearyou/translations/en.json | 13 ++++++ 6 files changed, 80 insertions(+) create mode 100644 homeassistant/components/flunearyou/repairs.py diff --git a/.coveragerc b/.coveragerc index 3c587e11cf6..341494b1424 100644 --- a/.coveragerc +++ b/.coveragerc @@ -388,6 +388,7 @@ omit = homeassistant/components/flume/__init__.py homeassistant/components/flume/sensor.py homeassistant/components/flunearyou/__init__.py + homeassistant/components/flunearyou/repairs.py homeassistant/components/flunearyou/sensor.py homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/* diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 5e48e1561b5..75349002ec0 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -9,6 +9,7 @@ from typing import Any from pyflunearyou import Client from pyflunearyou.errors import FluNearYouError +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant @@ -26,6 +27,15 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flu Near You as config entry.""" + async_create_issue( + hass, + DOMAIN, + "integration_removal", + is_fixable=True, + severity=IssueSeverity.ERROR, + translation_key="integration_removal", + ) + websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index ee69961d1b0..fa98bf2e01e 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -3,6 +3,7 @@ "name": "Flu Near You", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flunearyou", + "dependencies": ["repairs"], "requirements": ["pyflunearyou==2.0.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/flunearyou/repairs.py b/homeassistant/components/flunearyou/repairs.py new file mode 100644 index 00000000000..f48085ba623 --- /dev/null +++ b/homeassistant/components/flunearyou/repairs.py @@ -0,0 +1,42 @@ +"""Repairs platform for the Flu Near You integration.""" +from __future__ import annotations + +import asyncio + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +class FluNearYouFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + removal_tasks = [ + self.hass.config_entries.async_remove(entry.entry_id) + for entry in self.hass.config_entries.async_entries(DOMAIN) + ] + await asyncio.gather(*removal_tasks) + return self.async_create_entry(title="Fixed issue", data={}) + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +async def async_create_fix_flow( + hass: HomeAssistant, issue_id: str +) -> FluNearYouFixFlow: + """Create flow.""" + return FluNearYouFixFlow() diff --git a/homeassistant/components/flunearyou/strings.json b/homeassistant/components/flunearyou/strings.json index 4df0326fc3b..59ec6125a34 100644 --- a/homeassistant/components/flunearyou/strings.json +++ b/homeassistant/components/flunearyou/strings.json @@ -16,5 +16,18 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + }, + "issues": { + "integration_removal": { + "title": "Flu Near You is no longer available", + "fix_flow": { + "step": { + "confirm": { + "title": "Remove Flu Near You", + "description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance." + } + } + } + } } } diff --git a/homeassistant/components/flunearyou/translations/en.json b/homeassistant/components/flunearyou/translations/en.json index 29af5b2b288..3dcbfa2a628 100644 --- a/homeassistant/components/flunearyou/translations/en.json +++ b/homeassistant/components/flunearyou/translations/en.json @@ -16,5 +16,18 @@ "title": "Configure Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "The data source that powered the Flu Near You integration is no longer available. Press SUBMIT to remove all configured instances of the integration from Home Assistant.", + "title": "Remove Flu Near You" + } + } + }, + "title": "Flu Near You is no longer available" + } } } \ No newline at end of file From b2ceb2043b00545414d09b12f264f6baa61402fe Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 4 Aug 2022 21:57:53 +0300 Subject: [PATCH 162/903] Fix arm away in Risco (#76188) --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index a7c07af3e18..fb4b8203aac 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.0"], + "requirements": ["pyrisco==0.5.2"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 48d2e47086a..c1badeb18de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1790,7 +1790,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.0 +pyrisco==0.5.2 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08e47a7aded..7ef2f0ff1b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1231,7 +1231,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.0 +pyrisco==0.5.2 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From 31d9425e4941b4b7b55804f8fc2aa605eab2791a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 20:58:12 +0200 Subject: [PATCH 163/903] Add entity category to Wiz number entities (#76191) --- homeassistant/components/wiz/number.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index 9fd700f8f9c..fc855e410fe 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -14,6 +14,7 @@ from homeassistant.components.number import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -56,6 +57,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( value_fn=lambda device: cast(Optional[int], device.state.get_speed()), set_value_fn=_async_set_speed, required_feature="effect", + entity_category=EntityCategory.CONFIG, ), WizNumberEntityDescription( key="dual_head_ratio", @@ -67,6 +69,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( value_fn=lambda device: cast(Optional[int], device.state.get_ratio()), set_value_fn=_async_set_ratio, required_feature="dual_head", + entity_category=EntityCategory.CONFIG, ), ) From 0ffdf9fb6e60e37e480c4c3da9f839af8a42274f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 21:03:13 +0200 Subject: [PATCH 164/903] Add device_tracker checks to pylint plugin (#76228) --- pylint/plugins/hass_enforce_type_hints.py | 73 +++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index d0d20cedd7c..527c9358971 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1135,6 +1135,79 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "device_tracker": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="BaseTrackerEntity", + matches=[ + TypeHintMatch( + function_name="battery_level", + return_type=["int", None], + ), + TypeHintMatch( + function_name="source_type", + return_type=["SourceType", "str"], + ), + ], + ), + ClassTypeHintMatch( + base_class="TrackerEntity", + matches=[ + TypeHintMatch( + function_name="force_update", + return_type="bool", + ), + TypeHintMatch( + function_name="location_accuracy", + return_type="int", + ), + TypeHintMatch( + function_name="location_name", + return_type=["str", None], + ), + TypeHintMatch( + function_name="latitude", + return_type=["float", None], + ), + TypeHintMatch( + function_name="longitude", + return_type=["float", None], + ), + TypeHintMatch( + function_name="state", + return_type=["str", None], + ), + ], + ), + ClassTypeHintMatch( + base_class="ScannerEntity", + matches=[ + TypeHintMatch( + function_name="ip_address", + return_type=["str", None], + ), + TypeHintMatch( + function_name="mac_address", + return_type=["str", None], + ), + TypeHintMatch( + function_name="hostname", + return_type=["str", None], + ), + TypeHintMatch( + function_name="state", + return_type="str", + ), + TypeHintMatch( + function_name="is_connected", + return_type="bool", + ), + ], + ), + ], "fan": [ ClassTypeHintMatch( base_class="Entity", From 0df4642b62b52fe3f0dd0a627fc4387aba729495 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 21:03:26 +0200 Subject: [PATCH 165/903] Remove YAML configuration from Simplepush (#76175) --- .../components/simplepush/config_flow.py | 13 --------- homeassistant/components/simplepush/notify.py | 27 +++++-------------- .../components/simplepush/strings.json | 6 ++--- .../simplepush/translations/en.json | 6 ++--- .../components/simplepush/test_config_flow.py | 13 --------- 5 files changed, 12 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/simplepush/config_flow.py b/homeassistant/components/simplepush/config_flow.py index cf08a341114..0f0073c5099 100644 --- a/homeassistant/components/simplepush/config_flow.py +++ b/homeassistant/components/simplepush/config_flow.py @@ -1,7 +1,6 @@ """Config flow for simplepush integration.""" from __future__ import annotations -import logging from typing import Any from simplepush import UnknownError, send, send_encrypted @@ -13,8 +12,6 @@ from homeassistant.data_entry_flow import FlowResult from .const import ATTR_ENCRYPTED, CONF_DEVICE_KEY, CONF_SALT, DEFAULT_NAME, DOMAIN -_LOGGER = logging.getLogger(__name__) - def validate_input(entry: dict[str, str]) -> dict[str, str] | None: """Validate user input.""" @@ -76,13 +73,3 @@ class SimplePushFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - async def async_step_import(self, import_config: dict[str, str]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - _LOGGER.warning( - "Configuration of the simplepush integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - return await self.async_step_user(import_config) diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 2e58748f323..8bcf166ad25 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -5,34 +5,24 @@ import logging from typing import Any from simplepush import BadRequest, UnknownError, send, send_encrypted -import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.components.notify.const import ATTR_DATA from homeassistant.components.repairs.issue_handler import async_create_issue from homeassistant.components.repairs.models import IssueSeverity -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_EVENT, CONF_PASSWORD from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ATTR_ENCRYPTED, ATTR_EVENT, CONF_DEVICE_KEY, CONF_SALT, DOMAIN +from .const import ATTR_EVENT, CONF_DEVICE_KEY, CONF_SALT, DOMAIN -# Configuring simplepush under the notify platform will be removed in 2022.9.0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DEVICE_KEY): cv.string, - vol.Optional(CONF_EVENT): cv.string, - vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): cv.string, - vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): cv.string, - } -) +# Configuring Simplepush under the notify has been removed in 2022.9.0 +PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -47,16 +37,11 @@ async def async_get_service( async_create_issue( hass, DOMAIN, - "deprecated_yaml", + "removed_yaml", breaks_in_ha_version="2022.9.0", is_fixable=False, severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) + translation_key="removed_yaml", ) return None diff --git a/homeassistant/components/simplepush/strings.json b/homeassistant/components/simplepush/strings.json index 77ed05c4b48..68ee5c7a9ed 100644 --- a/homeassistant/components/simplepush/strings.json +++ b/homeassistant/components/simplepush/strings.json @@ -19,9 +19,9 @@ } }, "issues": { - "deprecated_yaml": { - "title": "The Simplepush YAML configuration is being removed", - "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "removed_yaml": { + "title": "The Simplepush YAML configuration has been removed", + "description": "Configuring Simplepush using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index 8674616dda1..205d3549a52 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -19,9 +19,9 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Simplepush YAML configuration is being removed" + "removed_yaml": { + "description": "Configuring Simplepush using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Simplepush YAML configuration has been removed" } } } \ No newline at end of file diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py index 6c37fb7ffe6..6f0d6a73aa4 100644 --- a/tests/components/simplepush/test_config_flow.py +++ b/tests/components/simplepush/test_config_flow.py @@ -129,16 +129,3 @@ async def test_error_on_connection_failure(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} - - -async def test_flow_import(hass: HomeAssistant) -> None: - """Test an import flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG, - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "simplepush" - assert result["data"] == MOCK_CONFIG From 3d42c4ca87bd8bb23d4a65c7e7792f04c8dd0e4f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Aug 2022 13:22:10 -0600 Subject: [PATCH 166/903] Add reboot button to RainMachine (#75227) --- .coveragerc | 1 + .../components/rainmachine/__init__.py | 58 +++++------- .../components/rainmachine/button.py | 90 ++++++++++++++++++ .../components/rainmachine/manifest.json | 2 +- homeassistant/components/rainmachine/util.py | 93 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 212 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/rainmachine/button.py diff --git a/.coveragerc b/.coveragerc index 341494b1424..20c6fd2c60e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -976,6 +976,7 @@ omit = homeassistant/components/raincloud/* homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/binary_sensor.py + homeassistant/components/rainmachine/button.py homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index c30ce81dc6d..dccdaaba74c 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -30,11 +30,7 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity, UpdateFailed from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller @@ -49,20 +45,13 @@ from .const import ( LOGGER, ) from .model import RainMachineEntityDescription +from .util import RainMachineDataUpdateCoordinator DEFAULT_SSL = True CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] - -UPDATE_INTERVALS = { - DATA_PROVISION_SETTINGS: timedelta(minutes=1), - DATA_PROGRAMS: timedelta(seconds=30), - DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), - DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), - DATA_ZONES: timedelta(seconds=15), -} +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] CONF_CONDITION = "condition" CONF_DEWPOINT = "dewpoint" @@ -134,13 +123,21 @@ SERVICE_RESTRICT_WATERING_SCHEMA = SERVICE_SCHEMA.extend( } ) +COORDINATOR_UPDATE_INTERVAL_MAP = { + DATA_PROVISION_SETTINGS: timedelta(minutes=1), + DATA_PROGRAMS: timedelta(seconds=30), + DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), + DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), + DATA_ZONES: timedelta(seconds=15), +} + @dataclass class RainMachineData: """Define an object to be stored in `hass.data`.""" controller: Controller - coordinators: dict[str, DataUpdateCoordinator] + coordinators: dict[str, RainMachineDataUpdateCoordinator] @callback @@ -233,24 +230,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return data + async def async_init_coordinator( + coordinator: RainMachineDataUpdateCoordinator, + ) -> None: + """Initialize a RainMachineDataUpdateCoordinator.""" + await coordinator.async_initialize() + await coordinator.async_config_entry_first_refresh() + controller_init_tasks = [] coordinators = {} - - for api_category in ( - DATA_PROGRAMS, - DATA_PROVISION_SETTINGS, - DATA_RESTRICTIONS_CURRENT, - DATA_RESTRICTIONS_UNIVERSAL, - DATA_ZONES, - ): - coordinator = coordinators[api_category] = DataUpdateCoordinator( + for api_category, update_interval in COORDINATOR_UPDATE_INTERVAL_MAP.items(): + coordinator = coordinators[api_category] = RainMachineDataUpdateCoordinator( hass, - LOGGER, + entry=entry, name=f'{controller.name} ("{api_category}")', - update_interval=UPDATE_INTERVALS[api_category], + api_category=api_category, + update_interval=update_interval, update_method=partial(async_update, api_category), ) - controller_init_tasks.append(coordinator.async_refresh()) + controller_init_tasks.append(async_init_coordinator(coordinator)) await asyncio.gather(*controller_init_tasks) @@ -439,12 +437,6 @@ class RainMachineEntity(CoordinatorEntity): self.update_from_latest_data() self.async_write_ha_state() - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - await super().async_added_to_hass() - self.update_from_latest_data() - @callback def update_from_latest_data(self) -> None: """Update the state.""" - raise NotImplementedError diff --git a/homeassistant/components/rainmachine/button.py b/homeassistant/components/rainmachine/button.py new file mode 100644 index 00000000000..14bfb878642 --- /dev/null +++ b/homeassistant/components/rainmachine/button.py @@ -0,0 +1,90 @@ +"""Buttons for the RainMachine integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from regenmaschine.controller import Controller +from regenmaschine.errors import RainMachineError + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RainMachineData, RainMachineEntity +from .const import DATA_PROVISION_SETTINGS, DOMAIN +from .model import RainMachineEntityDescription + + +@dataclass +class RainMachineButtonDescriptionMixin: + """Define an entity description mixin for RainMachine buttons.""" + + push_action: Callable[[Controller], Awaitable] + + +@dataclass +class RainMachineButtonDescription( + ButtonEntityDescription, + RainMachineEntityDescription, + RainMachineButtonDescriptionMixin, +): + """Describe a RainMachine button description.""" + + +BUTTON_KIND_REBOOT = "reboot" + + +async def _async_reboot(controller: Controller) -> None: + """Reboot the RainMachine.""" + await controller.machine.reboot() + + +BUTTON_DESCRIPTIONS = ( + RainMachineButtonDescription( + key=BUTTON_KIND_REBOOT, + name="Reboot", + api_category=DATA_PROVISION_SETTINGS, + push_action=_async_reboot, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up RainMachine buttons based on a config entry.""" + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + RainMachineButton(entry, data, description) + for description in BUTTON_DESCRIPTIONS + ) + + +class RainMachineButton(RainMachineEntity, ButtonEntity): + """Define a RainMachine button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG + + entity_description: RainMachineButtonDescription + + async def async_press(self) -> None: + """Send out a restart command.""" + try: + await self.entity_description.push_action(self._data.controller) + except RainMachineError as err: + raise HomeAssistantError( + f'Error while pressing button "{self.entity_id}": {err}' + ) from err + + async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index b318ef7f295..4d60730ba6c 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.07.1"], + "requirements": ["regenmaschine==2022.07.3"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index 6bf15f2fb9c..dc772690ec5 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -1,9 +1,23 @@ """Define RainMachine utilities.""" from __future__ import annotations +from collections.abc import Awaitable, Callable +from datetime import timedelta from typing import Any from homeassistant.backports.enum import StrEnum +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import LOGGER + +SIGNAL_REBOOT_COMPLETED = "rainmachine_reboot_completed_{0}" +SIGNAL_REBOOT_REQUESTED = "rainmachine_reboot_requested_{0}" class RunStates(StrEnum): @@ -29,3 +43,82 @@ def key_exists(data: dict[str, Any], search_key: str) -> bool: if isinstance(value, dict): return key_exists(value, search_key) return False + + +class RainMachineDataUpdateCoordinator(DataUpdateCoordinator[dict]): + """Define an extended DataUpdateCoordinator.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + *, + entry: ConfigEntry, + name: str, + api_category: str, + update_interval: timedelta, + update_method: Callable[..., Awaitable], + ) -> None: + """Initialize.""" + super().__init__( + hass, + LOGGER, + name=name, + update_interval=update_interval, + update_method=update_method, + ) + + self._rebooting = False + self._signal_handler_unsubs: list[Callable[..., None]] = [] + self.config_entry = entry + self.signal_reboot_completed = SIGNAL_REBOOT_COMPLETED.format( + self.config_entry.entry_id + ) + self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( + self.config_entry.entry_id + ) + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + + @callback + def async_reboot_completed() -> None: + """Respond to a reboot completed notification.""" + LOGGER.debug("%s responding to reboot complete", self.name) + self._rebooting = False + self.last_update_success = True + self.async_update_listeners() + + @callback + def async_reboot_requested() -> None: + """Respond to a reboot request.""" + LOGGER.debug("%s responding to reboot request", self.name) + self._rebooting = True + self.last_update_success = False + self.async_update_listeners() + + for signal, func in ( + (self.signal_reboot_completed, async_reboot_completed), + (self.signal_reboot_requested, async_reboot_requested), + ): + self._signal_handler_unsubs.append( + async_dispatcher_connect(self.hass, signal, func) + ) + + @callback + def async_check_reboot_complete() -> None: + """Check whether an active reboot has been completed.""" + if self._rebooting and self.last_update_success: + LOGGER.debug("%s discovered reboot complete", self.name) + async_dispatcher_send(self.hass, self.signal_reboot_completed) + + self.async_add_listener(async_check_reboot_complete) + + @callback + def async_teardown() -> None: + """Tear the coordinator down appropriately.""" + for unsub in self._signal_handler_unsubs: + unsub() + + self.config_entry.async_on_unload(async_teardown) diff --git a/requirements_all.txt b/requirements_all.txt index c1badeb18de..3abb34ff5da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2081,7 +2081,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.07.1 +regenmaschine==2022.07.3 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ef2f0ff1b0..96845b12579 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1402,7 +1402,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.07.1 +regenmaschine==2022.07.3 # homeassistant.components.renault renault-api==0.1.11 From 343508a0151378ec4958bd04fa87ca772aaf0e4e Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 4 Aug 2022 14:28:59 -0500 Subject: [PATCH 167/903] Fix Life360 recovery from server errors (#76231) --- .../components/life360/coordinator.py | 4 +- .../components/life360/device_tracker.py | 86 +++++++++++-------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index 05eecd43cdc..ed774bba8ca 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -91,9 +91,11 @@ class Life360Data: members: dict[str, Life360Member] = field(init=False, default_factory=dict) -class Life360DataUpdateCoordinator(DataUpdateCoordinator): +class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): """Life360 data update coordinator.""" + config_entry: ConfigEntry + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize data update coordinator.""" super().__init__( diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index ebb179beba2..1fa63a7659a 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -11,10 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_ADDRESS, @@ -31,6 +28,7 @@ from .const import ( LOGGER, SHOW_DRIVING, ) +from .coordinator import Life360DataUpdateCoordinator, Life360Member _LOC_ATTRS = ( "address", @@ -95,23 +93,27 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(process_data)) -class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): +class Life360DeviceTracker( + CoordinatorEntity[Life360DataUpdateCoordinator], TrackerEntity +): """Life360 Device Tracker.""" _attr_attribution = ATTRIBUTION + _attr_unique_id: str - def __init__(self, coordinator: DataUpdateCoordinator, member_id: str) -> None: + def __init__( + self, coordinator: Life360DataUpdateCoordinator, member_id: str + ) -> None: """Initialize Life360 Entity.""" super().__init__(coordinator) self._attr_unique_id = member_id - self._data = coordinator.data.members[self.unique_id] + self._data: Life360Member | None = coordinator.data.members[member_id] + self._prev_data = self._data self._attr_name = self._data.name self._attr_entity_picture = self._data.entity_picture - self._prev_data = self._data - @property def _options(self) -> Mapping[str, Any]: """Shortcut to config entry options.""" @@ -120,16 +122,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - # Get a shortcut to this member's data. Can't guarantee it's the same dict every - # update, or that there is even data for this member every update, so need to - # update shortcut each time. - self._data = self.coordinator.data.members.get(self.unique_id) - + # Get a shortcut to this Member's data. This needs to be updated each time since + # coordinator provides a new Life360Member object each time, and it's possible + # that there is no data for this Member on some updates. if self.available: - # If nothing important has changed, then skip the update altogether. - if self._data == self._prev_data: - return + self._data = self.coordinator.data.members.get(self._attr_unique_id) + else: + self._data = None + if self._data: # Check if we should effectively throw out new location data. last_seen = self._data.last_seen prev_seen = self._prev_data.last_seen @@ -168,27 +169,21 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): """Return True if state updates should be forced.""" return False - @property - def available(self) -> bool: - """Return if entity is available.""" - # Guard against member not being in last update for some reason. - return super().available and self._data is not None - @property def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" - if self.available: + if self._data: self._attr_entity_picture = self._data.entity_picture return super().entity_picture - # All of the following will only be called if self.available is True. - @property def battery_level(self) -> int | None: """Return the battery level of the device. Percentage from 0-100. """ + if not self._data: + return None return self._data.battery_level @property @@ -202,11 +197,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): Value in meters. """ + if not self._data: + return 0 return self._data.gps_accuracy @property def driving(self) -> bool: """Return if driving.""" + if not self._data: + return False if (driving_speed := self._options.get(CONF_DRIVING_SPEED)) is not None: if self._data.speed >= driving_speed: return True @@ -222,23 +221,38 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): @property def latitude(self) -> float | None: """Return latitude value of the device.""" + if not self._data: + return None return self._data.latitude @property def longitude(self) -> float | None: """Return longitude value of the device.""" + if not self._data: + return None return self._data.longitude @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" - attrs = {} - attrs[ATTR_ADDRESS] = self._data.address - attrs[ATTR_AT_LOC_SINCE] = self._data.at_loc_since - attrs[ATTR_BATTERY_CHARGING] = self._data.battery_charging - attrs[ATTR_DRIVING] = self.driving - attrs[ATTR_LAST_SEEN] = self._data.last_seen - attrs[ATTR_PLACE] = self._data.place - attrs[ATTR_SPEED] = self._data.speed - attrs[ATTR_WIFI_ON] = self._data.wifi_on - return attrs + if not self._data: + return { + ATTR_ADDRESS: None, + ATTR_AT_LOC_SINCE: None, + ATTR_BATTERY_CHARGING: None, + ATTR_DRIVING: None, + ATTR_LAST_SEEN: None, + ATTR_PLACE: None, + ATTR_SPEED: None, + ATTR_WIFI_ON: None, + } + return { + ATTR_ADDRESS: self._data.address, + ATTR_AT_LOC_SINCE: self._data.at_loc_since, + ATTR_BATTERY_CHARGING: self._data.battery_charging, + ATTR_DRIVING: self.driving, + ATTR_LAST_SEEN: self._data.last_seen, + ATTR_PLACE: self._data.place, + ATTR_SPEED: self._data.speed, + ATTR_WIFI_ON: self._data.wifi_on, + } From d76ebbbb0b0ddb6b6cef320227b5e2a77af8fce1 Mon Sep 17 00:00:00 2001 From: Jonathan Keslin Date: Thu, 4 Aug 2022 12:37:20 -0700 Subject: [PATCH 168/903] Remove @decompil3d as maintainer on volvooncall (#76153) * Remove @decompil3d as maintainer on volvooncall * Run hassfest Signed-off-by: Franck Nijhof Co-authored-by: Franck Nijhof --- CODEOWNERS | 2 +- homeassistant/components/volvooncall/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 16523cafa81..1a322b09981 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1176,7 +1176,7 @@ build.json @home-assistant/supervisor /tests/components/vlc_telnet/ @rodripf @MartinHjelmare /homeassistant/components/volumio/ @OnFreund /tests/components/volumio/ @OnFreund -/homeassistant/components/volvooncall/ @molobrakos @decompil3d +/homeassistant/components/volvooncall/ @molobrakos /homeassistant/components/vulcan/ @Antoni-Czaplicki /tests/components/vulcan/ @Antoni-Czaplicki /homeassistant/components/wake_on_lan/ @ntilley905 diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 93b7642425c..fe7e384f72a 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -3,7 +3,7 @@ "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", "requirements": ["volvooncall==0.10.0"], - "codeowners": ["@molobrakos", "@decompil3d"], + "codeowners": ["@molobrakos"], "iot_class": "cloud_polling", "loggers": ["geopy", "hbmqtt", "volvooncall"] } From 33bf94c4b2d541edf155ffa6cdae0c0459e9eb74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 21:37:57 +0200 Subject: [PATCH 169/903] Update orjson to 3.7.11 (#76171) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3ff9fd3c114..c0221647abf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.7.8 +orjson==3.7.11 paho-mqtt==1.6.1 pillow==9.2.0 pip>=21.0,<22.3 diff --git a/pyproject.toml b/pyproject.toml index 288bcc5225a..c0d96aa5ee0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", - "orjson==3.7.8", + "orjson==3.7.11", "pip>=21.0,<22.3", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index ce77253b752..fce57620224 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.4.0 cryptography==36.0.2 -orjson==3.7.8 +orjson==3.7.11 pip>=21.0,<22.3 python-slugify==4.0.1 pyyaml==6.0 From bb58ad0f54db39d8d1e9b0de0181f3586cfc71d7 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 4 Aug 2022 22:08:24 +0200 Subject: [PATCH 170/903] Add ability to specify user(s) when sending DMs using the Twitter integration (#71310) --- homeassistant/components/twitter/notify.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index fd97303a360..d64f8ec3fbf 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, + ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) @@ -63,7 +64,7 @@ class TwitterNotificationService(BaseNotificationService): username, ): """Initialize the service.""" - self.user = username + self.default_user = username self.hass = hass self.api = TwitterAPI( consumer_key, consumer_secret, access_token_key, access_token_secret @@ -72,6 +73,7 @@ class TwitterNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Tweet a message, optionally with media.""" data = kwargs.get(ATTR_DATA) + targets = kwargs.get(ATTR_TARGET) media = None if data: @@ -79,15 +81,19 @@ class TwitterNotificationService(BaseNotificationService): if not self.hass.config.is_allowed_path(media): _LOGGER.warning("'%s' is not a whitelisted directory", media) return + + if targets: + for target in targets: + callback = partial(self.send_message_callback, message, target) + self.upload_media_then_callback(callback, media) + else: + callback = partial(self.send_message_callback, message, self.default_user) + self.upload_media_then_callback(callback, media) - callback = partial(self.send_message_callback, message) - - self.upload_media_then_callback(callback, media) - - def send_message_callback(self, message, media_id=None): + def send_message_callback(self, message, user, media_id=None): """Tweet a message, optionally with media.""" - if self.user: - user_resp = self.api.request("users/lookup", {"screen_name": self.user}) + if user: + user_resp = self.api.request("users/lookup", {"screen_name": user}) user_id = user_resp.json()[0]["id"] if user_resp.status_code != HTTPStatus.OK: self.log_error_resp(user_resp) From a987cad973e41f7d9a64fb9ebd93d513ec16ed2e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 23:10:27 +0200 Subject: [PATCH 171/903] Use attributes in unifiled light (#76019) --- homeassistant/components/unifiled/light.py | 52 ++++++---------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index 8ba9fc2b6f9..f1d3ad15a02 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -63,58 +63,34 @@ class UnifiLedLight(LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def __init__(self, light, api): + def __init__(self, light: dict[str, Any], api: unifiled) -> None: """Init Unifi LED Light.""" self._api = api self._light = light - self._name = light["name"] - self._unique_id = light["id"] - self._state = light["status"]["output"] - self._available = light["isOnline"] - self._brightness = self._api.convertfrom100to255(light["status"]["led"]) - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def available(self): - """Return the available state of this light.""" - return self._available - - @property - def brightness(self): - """Return the brightness name of this light.""" - return self._brightness - - @property - def unique_id(self): - """Return the unique id of this light.""" - return self._unique_id - - @property - def is_on(self): - """Return true if light is on.""" - return self._state + self._attr_name = light["name"] + self._light_id = light["id"] + self._attr_unique_id = light["id"] + self._attr_is_on = light["status"]["output"] + self._attr_available = light["isOnline"] + self._attr_brightness = self._api.convertfrom100to255(light["status"]["led"]) def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" self._api.setdevicebrightness( - self._unique_id, + self._light_id, str(self._api.convertfrom255to100(kwargs.get(ATTR_BRIGHTNESS, 255))), ) - self._api.setdeviceoutput(self._unique_id, 1) + self._api.setdeviceoutput(self._light_id, 1) def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - self._api.setdeviceoutput(self._unique_id, 0) + self._api.setdeviceoutput(self._light_id, 0) def update(self) -> None: """Update the light states.""" - self._state = self._api.getlightstate(self._unique_id) - self._brightness = self._api.convertfrom100to255( - self._api.getlightbrightness(self._unique_id) + self._attr_is_on = self._api.getlightstate(self._light_id) + self._attr_brightness = self._api.convertfrom100to255( + self._api.getlightbrightness(self._light_id) ) - self._available = self._api.getlightavailable(self._unique_id) + self._attr_available = self._api.getlightavailable(self._light_id) From fa9d0b9ff746e86fd334aa2ef2ab7bbc03673210 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 23:11:37 +0200 Subject: [PATCH 172/903] Use attributes in tikteck light (#76022) --- homeassistant/components/tikteck/light.py | 67 ++++++----------------- 1 file changed, 18 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 28daf4baac1..7022138d147 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -55,82 +55,51 @@ def setup_platform( class TikteckLight(LightEntity): """Representation of a Tikteck light.""" + _attr_assumed_state = True _attr_color_mode = ColorMode.HS + _attr_should_poll = False _attr_supported_color_modes = {ColorMode.HS} + hs_color: tuple[float, float] + brightness: int def __init__(self, device): """Initialize the light.""" - self._name = device["name"] - self._address = device["address"] - self._password = device["password"] - self._brightness = 255 - self._hs = [0, 0] - self._state = False + address = device["address"] + self._attr_unique_id = address + self._attr_name = device["name"] + self._attr_brightness = 255 + self._attr_hs_color = [0, 0] + self._attr_is_on = False self.is_valid = True - self._bulb = tikteck.tikteck(self._address, "Smart Light", self._password) + self._bulb = tikteck.tikteck(address, "Smart Light", device["password"]) if self._bulb.connect() is False: self.is_valid = False - _LOGGER.error("Failed to connect to bulb %s, %s", self._address, self._name) + _LOGGER.error("Failed to connect to bulb %s, %s", address, self.name) - @property - def unique_id(self): - """Return the ID of this light.""" - return self._address - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def hs_color(self): - """Return the color property.""" - return self._hs - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def assumed_state(self): - """Return the assumed state.""" - return True - - def set_state(self, red, green, blue, brightness): + def set_state(self, red: int, green: int, blue: int, brightness: int) -> bool: """Set the bulb state.""" return self._bulb.set_state(red, green, blue, brightness) def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" - self._state = True + self._attr_is_on = True hs_color = kwargs.get(ATTR_HS_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) if hs_color is not None: - self._hs = hs_color + self._attr_hs_color = hs_color if brightness is not None: - self._brightness = brightness + self._attr_brightness = brightness - rgb = color_util.color_hs_to_RGB(*self._hs) + rgb = color_util.color_hs_to_RGB(self.hs_color[0], self.hs_color[1]) self.set_state(rgb[0], rgb[1], rgb[2], self.brightness) self.schedule_update_ha_state() def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" - self._state = False + self._attr_is_on = False self.set_state(0, 0, 0, 0) self.schedule_update_ha_state() From ca4b7cca1aa08ba4c145570f2e60fe38d31c5348 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 11:45:35 -1000 Subject: [PATCH 173/903] Run black on twitter to fix CI (#76254) --- homeassistant/components/twitter/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index d64f8ec3fbf..d89755969a1 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -81,7 +81,7 @@ class TwitterNotificationService(BaseNotificationService): if not self.hass.config.is_allowed_path(media): _LOGGER.warning("'%s' is not a whitelisted directory", media) return - + if targets: for target in targets: callback = partial(self.send_message_callback, message, target) From ce871835b23413524b87a34dbf3917836b88f622 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Aug 2022 01:04:10 +0200 Subject: [PATCH 174/903] Update pyupgrade to v2.37.3 (#76257) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e259c1d063..4867f712b8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.37.2 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index c7f5d559c38..a6cb45b22b2 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.37.2 +pyupgrade==2.37.3 yamllint==1.27.1 From cb46441b7493260a7e0929854dc361d5400c79d2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 5 Aug 2022 00:28:51 +0000 Subject: [PATCH 175/903] [ci skip] Translation update --- .../advantage_air/translations/sv.json | 20 +++++++ .../components/airly/translations/sv.json | 5 ++ .../components/airtouch4/translations/sv.json | 19 ++++++ .../airvisual/translations/sensor.sv.json | 15 ++++- .../components/airvisual/translations/sv.json | 25 ++++++-- .../amberelectric/translations/sv.json | 12 ++++ .../components/anthemav/translations/he.json | 18 ++++++ .../components/apple_tv/translations/sv.json | 59 +++++++++++++++++++ .../components/arcam_fmj/translations/sv.json | 3 + .../components/atag/translations/sv.json | 3 + .../components/august/translations/sv.json | 9 ++- .../aurora_abb_powerone/translations/sv.json | 9 ++- .../components/awair/translations/he.json | 6 ++ .../binary_sensor/translations/sv.json | 28 ++++++++- .../components/bluetooth/translations/he.json | 32 ++++++++++ .../components/broadlink/translations/sv.json | 21 ++++++- .../components/bsblan/translations/sv.json | 5 +- .../components/cloud/translations/sv.json | 16 +++++ .../components/co2signal/translations/sv.json | 5 ++ .../components/coinbase/translations/sv.json | 2 + .../components/daikin/translations/sv.json | 4 +- .../components/deconz/translations/sv.json | 4 +- .../demo/translations/select.sv.json | 9 +++ .../components/denonavr/translations/sv.json | 10 +++- .../devolo_home_control/translations/sv.json | 2 + .../components/discord/translations/sv.json | 12 ++++ .../components/dsmr/translations/sv.json | 37 +++++++++++- .../components/econet/translations/sv.json | 22 +++++++ .../eight_sleep/translations/he.json | 15 +++++ .../components/esphome/translations/sv.json | 14 +++++ .../evil_genius_labs/translations/sv.json | 15 +++++ .../components/ezviz/translations/sv.json | 9 +++ .../fjaraskupan/translations/sv.json | 13 ++++ .../flunearyou/translations/en.json | 2 +- .../flunearyou/translations/fr.json | 5 ++ .../flunearyou/translations/id.json | 13 ++++ .../flunearyou/translations/pt-BR.json | 13 ++++ .../components/flux_led/translations/sv.json | 36 +++++++++++ .../components/freebox/translations/sl.json | 2 +- .../components/freebox/translations/sv.json | 1 + .../freedompro/translations/sv.json | 1 + .../components/fritz/translations/sv.json | 24 ++++++-- .../components/fritzbox/translations/sv.json | 7 ++- .../components/fronius/translations/sv.json | 18 ++++++ .../garages_amsterdam/translations/sv.json | 18 ++++++ .../components/generic/translations/he.json | 10 ++++ .../components/goalzero/translations/sv.json | 26 ++++++++ .../components/google/translations/he.json | 4 +- .../components/govee_ble/translations/he.json | 21 +++++++ .../homeassistant/translations/sv.json | 12 +++- .../homeassistant_alerts/translations/he.json | 8 +++ .../components/homekit/translations/sv.json | 4 ++ .../homekit_controller/translations/he.json | 2 +- .../translations/sensor.ca.json | 19 ++++++ .../translations/sensor.fr.json | 7 +++ .../translations/sensor.id.json | 11 ++++ .../translations/sensor.no.json | 12 ++++ .../translations/sensor.pt-BR.json | 21 +++++++ .../translations/sensor.ru.json | 7 +++ .../translations/sensor.zh-Hant.json | 12 ++++ .../homekit_controller/translations/sv.json | 1 + .../components/honeywell/translations/sv.json | 7 ++- .../components/hue/translations/sv.json | 14 ++++- .../translations/sv.json | 1 + .../hvv_departures/translations/sv.json | 23 +++++++- .../components/hyperion/translations/sv.json | 1 + .../components/iaqualink/translations/sl.json | 2 +- .../components/icloud/translations/sv.json | 10 +++- .../components/inkbird/translations/he.json | 21 +++++++ .../components/insteon/translations/sv.json | 24 +++++++- .../islamic_prayer_times/translations/sv.json | 18 +++++- .../components/isy994/translations/sv.json | 11 +++- .../components/izone/translations/sl.json | 2 +- .../components/jellyfin/translations/sv.json | 10 ++++ .../keenetic_ndms2/translations/sv.json | 2 + .../components/kmtronic/translations/sv.json | 9 +++ .../components/knx/translations/sv.json | 29 ++++++++- .../components/kodi/translations/sv.json | 30 +++++++++- .../components/konnected/translations/he.json | 2 +- .../components/konnected/translations/sv.json | 3 +- .../components/kraken/translations/sv.json | 22 +++++++ .../lacrosse_view/translations/he.json | 19 ++++++ .../lg_soundbar/translations/he.json | 17 ++++++ .../components/life360/translations/he.json | 9 +++ .../components/life360/translations/sv.json | 1 + .../components/lifx/translations/he.json | 13 ++++ .../components/litejet/translations/sv.json | 7 +++ .../litterrobot/translations/sv.json | 9 +++ .../components/local_ip/translations/sl.json | 2 +- .../logi_circle/translations/sv.json | 8 ++- .../components/lookin/translations/sv.json | 31 ++++++++++ .../components/lovelace/translations/sv.json | 2 + .../lutron_caseta/translations/sv.json | 23 ++++++++ .../components/mazda/translations/sv.json | 10 ++++ .../media_player/translations/sv.json | 3 + .../components/met/translations/sl.json | 2 +- .../components/metoffice/translations/sv.json | 15 ++++- .../components/mikrotik/translations/sv.json | 1 + .../components/mill/translations/sv.json | 3 + .../components/moat/translations/he.json | 21 +++++++ .../mobile_app/translations/sv.json | 5 ++ .../modem_callerid/translations/sv.json | 1 + .../components/monoprice/translations/sl.json | 2 +- .../motion_blinds/translations/he.json | 2 +- .../motion_blinds/translations/sv.json | 9 +++ .../components/motioneye/translations/sv.json | 16 ++++- .../components/nam/translations/sv.json | 2 + .../components/nest/translations/he.json | 1 + .../components/nest/translations/sv.json | 8 ++- .../components/netatmo/translations/sv.json | 5 ++ .../components/nexia/translations/sv.json | 1 + .../components/nextdns/translations/he.json | 21 +++++++ .../nfandroidtv/translations/sv.json | 3 +- .../nightscout/translations/sv.json | 6 +- .../components/nina/translations/he.json | 6 ++ .../components/nuheat/translations/sl.json | 2 +- .../components/octoprint/translations/sv.json | 6 ++ .../components/omnilogic/translations/sv.json | 7 ++- .../opentherm_gw/translations/he.json | 3 +- .../openweathermap/translations/sv.json | 3 +- .../components/owntracks/translations/he.json | 9 +++ .../p1_monitor/translations/sv.json | 16 +++++ .../panasonic_viera/translations/sv.json | 8 ++- .../philips_js/translations/sv.json | 1 + .../components/pi_hole/translations/sv.json | 1 + .../components/picnic/translations/sv.json | 5 +- .../components/plaato/translations/sl.json | 2 +- .../components/plaato/translations/sv.json | 31 ++++++++++ .../components/plex/translations/sv.json | 7 ++- .../components/plugwise/translations/he.json | 4 ++ .../components/powerwall/translations/sv.json | 1 + .../progettihwsw/translations/sv.json | 23 +++++++- .../components/ps4/translations/sv.json | 2 + .../pvpc_hourly_pricing/translations/sv.json | 13 ++++ .../components/qnap_qsw/translations/he.json | 6 ++ .../components/rachio/translations/sl.json | 2 +- .../radiotherm/translations/he.json | 13 +++- .../rainforest_eagle/translations/sv.json | 12 +++- .../components/rdw/translations/sv.json | 15 +++++ .../components/rfxtrx/translations/sv.json | 6 +- .../components/rhasspy/translations/he.json | 7 +++ .../components/risco/translations/sv.json | 14 +++++ .../components/roku/translations/sl.json | 2 +- .../components/roomba/translations/sv.json | 2 + .../ruckus_unleashed/translations/sv.json | 9 +++ .../components/samsungtv/translations/sv.json | 7 +++ .../components/scrape/translations/he.json | 23 ++++++++ .../screenlogic/translations/sv.json | 17 +++++- .../components/select/translations/sv.json | 9 ++- .../components/sensor/translations/he.json | 27 ++++++++- .../components/sensor/translations/sv.json | 6 ++ .../sensorpush/translations/he.json | 21 +++++++ .../components/shelly/translations/sv.json | 18 ++++++ .../simplepush/translations/en.json | 4 ++ .../simplepush/translations/fr.json | 3 + .../simplepush/translations/he.json | 17 ++++++ .../simplepush/translations/id.json | 4 ++ .../simplepush/translations/pt-BR.json | 4 ++ .../simplisafe/translations/en.json | 21 ++++++- .../simplisafe/translations/he.json | 4 +- .../simplisafe/translations/id.json | 1 + .../simplisafe/translations/pt-BR.json | 3 +- .../simplisafe/translations/sv.json | 3 +- .../components/skybell/translations/he.json | 11 +++- .../smartthings/translations/sv.json | 1 + .../components/smarttub/translations/sv.json | 4 ++ .../components/solarlog/translations/sl.json | 2 +- .../somfy_mylink/translations/sv.json | 12 ++++ .../components/sonarr/translations/sv.json | 19 +++++- .../soundtouch/translations/he.json | 17 ++++++ .../components/sql/translations/he.json | 12 ++++ .../components/subaru/translations/sv.json | 20 ++++++- .../components/switchbot/translations/he.json | 2 +- .../components/switchbot/translations/sv.json | 24 +++++++- .../switcher_kis/translations/sv.json | 13 ++++ .../components/syncthing/translations/sv.json | 4 ++ .../synology_dsm/translations/sl.json | 2 +- .../synology_dsm/translations/sv.json | 3 +- .../tankerkoenig/translations/he.json | 8 ++- .../tellduslive/translations/sv.json | 1 + .../totalconnect/translations/sv.json | 15 ++++- .../transmission/translations/he.json | 9 ++- .../components/tuya/translations/he.json | 2 + .../tuya/translations/select.sv.json | 13 +++- .../tuya/translations/sensor.sv.json | 15 +++++ .../components/tuya/translations/sv.json | 5 ++ .../twentemilieu/translations/sv.json | 4 ++ .../components/unifi/translations/sv.json | 4 +- .../components/upnp/translations/sv.json | 14 +++++ .../components/velbus/translations/sv.json | 7 +++ .../components/venstar/translations/sv.json | 14 ++++- .../components/verisure/translations/sv.json | 28 +++++++++ .../vlc_telnet/translations/sv.json | 8 +++ .../components/wallbox/translations/sv.json | 5 +- .../water_heater/translations/sv.json | 4 +- .../components/watttime/translations/sv.json | 24 ++++++++ .../components/wemo/translations/sv.json | 5 ++ .../components/withings/translations/he.json | 3 + .../components/withings/translations/sl.json | 2 +- .../wled/translations/select.sv.json | 4 +- .../wolflink/translations/sensor.sv.json | 29 +++++++++ .../components/xbox/translations/sv.json | 1 + .../xiaomi_ble/translations/he.json | 22 +++++++ .../xiaomi_ble/translations/id.json | 8 +++ .../xiaomi_miio/translations/sv.json | 18 +++++- .../components/yeelight/translations/sv.json | 3 + .../components/zwave_js/translations/sv.json | 25 +++++++- 207 files changed, 2087 insertions(+), 103 deletions(-) create mode 100644 homeassistant/components/advantage_air/translations/sv.json create mode 100644 homeassistant/components/airtouch4/translations/sv.json create mode 100644 homeassistant/components/amberelectric/translations/sv.json create mode 100644 homeassistant/components/anthemav/translations/he.json create mode 100644 homeassistant/components/apple_tv/translations/sv.json create mode 100644 homeassistant/components/bluetooth/translations/he.json create mode 100644 homeassistant/components/cloud/translations/sv.json create mode 100644 homeassistant/components/demo/translations/select.sv.json create mode 100644 homeassistant/components/econet/translations/sv.json create mode 100644 homeassistant/components/eight_sleep/translations/he.json create mode 100644 homeassistant/components/evil_genius_labs/translations/sv.json create mode 100644 homeassistant/components/fjaraskupan/translations/sv.json create mode 100644 homeassistant/components/flux_led/translations/sv.json create mode 100644 homeassistant/components/fronius/translations/sv.json create mode 100644 homeassistant/components/garages_amsterdam/translations/sv.json create mode 100644 homeassistant/components/goalzero/translations/sv.json create mode 100644 homeassistant/components/govee_ble/translations/he.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/he.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.ca.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.fr.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.id.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.no.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.ru.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/inkbird/translations/he.json create mode 100644 homeassistant/components/kraken/translations/sv.json create mode 100644 homeassistant/components/lacrosse_view/translations/he.json create mode 100644 homeassistant/components/lg_soundbar/translations/he.json create mode 100644 homeassistant/components/litejet/translations/sv.json create mode 100644 homeassistant/components/lookin/translations/sv.json create mode 100644 homeassistant/components/moat/translations/he.json create mode 100644 homeassistant/components/nextdns/translations/he.json create mode 100644 homeassistant/components/p1_monitor/translations/sv.json create mode 100644 homeassistant/components/rdw/translations/sv.json create mode 100644 homeassistant/components/rhasspy/translations/he.json create mode 100644 homeassistant/components/sensorpush/translations/he.json create mode 100644 homeassistant/components/simplepush/translations/he.json create mode 100644 homeassistant/components/soundtouch/translations/he.json create mode 100644 homeassistant/components/switcher_kis/translations/sv.json create mode 100644 homeassistant/components/tuya/translations/sensor.sv.json create mode 100644 homeassistant/components/xiaomi_ble/translations/he.json diff --git a/homeassistant/components/advantage_air/translations/sv.json b/homeassistant/components/advantage_air/translations/sv.json new file mode 100644 index 00000000000..99ecd87448c --- /dev/null +++ b/homeassistant/components/advantage_air/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adress", + "port": "Port" + }, + "description": "Anslut till API:et f\u00f6r din Advantage Air v\u00e4ggmonterade surfplatta.", + "title": "Anslut" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/sv.json b/homeassistant/components/airly/translations/sv.json index ac976224924..05d56e9d88a 100644 --- a/homeassistant/components/airly/translations/sv.json +++ b/homeassistant/components/airly/translations/sv.json @@ -18,5 +18,10 @@ "description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register" } } + }, + "system_health": { + "info": { + "requests_remaining": "\u00c5terst\u00e5ende till\u00e5tna f\u00f6rfr\u00e5gningar" + } } } \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/sv.json b/homeassistant/components/airtouch4/translations/sv.json new file mode 100644 index 00000000000..34b6a755ace --- /dev/null +++ b/homeassistant/components/airtouch4/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "no_units": "Det gick inte att hitta n\u00e5gra AirTouch 4-grupper." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "title": "St\u00e4ll in dina AirTouch 4-anslutningsdetaljer." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.sv.json b/homeassistant/components/airvisual/translations/sensor.sv.json index f1fa0bbdcd8..85a1def80fa 100644 --- a/homeassistant/components/airvisual/translations/sensor.sv.json +++ b/homeassistant/components/airvisual/translations/sensor.sv.json @@ -1,7 +1,20 @@ { "state": { "airvisual__pollutant_label": { - "co": "Kolmonoxid" + "co": "Kolmonoxid", + "n2": "Kv\u00e4vedioxid", + "o3": "Ozon", + "p1": "PM10", + "p2": "PM2,5", + "s2": "Svaveldioxid" + }, + "airvisual__pollutant_level": { + "good": "Bra", + "hazardous": "Farlig", + "moderate": "M\u00e5ttlig", + "unhealthy": "Oh\u00e4lsosam", + "unhealthy_sensitive": "Oh\u00e4lsosamt f\u00f6r k\u00e4nsliga grupper", + "very_unhealthy": "Mycket oh\u00e4lsosamt" } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index d3559f89aa0..9e32b698eaf 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -7,19 +7,36 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade.", - "invalid_api_key": "Ogiltig API-nyckel" + "invalid_api_key": "Ogiltig API-nyckel", + "location_not_found": "Platsen hittades inte" }, "step": { + "geography_by_coords": { + "data": { + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Anv\u00e4nd AirVisuals moln-API f\u00f6r att \u00f6vervaka en latitud/longitud.", + "title": "Konfigurera en geografi" + }, "geography_by_name": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "city": "Stad", + "country": "Land", + "state": "stat" + }, + "description": "Anv\u00e4nd AirVisuals moln-API f\u00f6r att \u00f6vervaka en stad/stat/land.", + "title": "Konfigurera en geografi" }, "node_pro": { "data": { "ip_address": "Enhets IP-adress / v\u00e4rdnamn", "password": "Enhetsl\u00f6senord" - } + }, + "description": "\u00d6vervaka en personlig AirVisual-enhet. L\u00f6senordet kan h\u00e4mtas fr\u00e5n enhetens anv\u00e4ndargr\u00e4nssnitt.", + "title": "Konfigurera en AirVisual Node/Pro" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/amberelectric/translations/sv.json b/homeassistant/components/amberelectric/translations/sv.json new file mode 100644 index 00000000000..7458627ef0a --- /dev/null +++ b/homeassistant/components/amberelectric/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "site_id": "Plats-ID" + }, + "description": "G\u00e5 till {api_url} f\u00f6r att skapa en API-nyckel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/he.json b/homeassistant/components/anthemav/translations/he.json new file mode 100644 index 00000000000..c3a67844fdd --- /dev/null +++ b/homeassistant/components/anthemav/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/sv.json b/homeassistant/components/apple_tv/translations/sv.json new file mode 100644 index 00000000000..28b6e2ed67b --- /dev/null +++ b/homeassistant/components/apple_tv/translations/sv.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "backoff": "Enheten accepterar inte parningsf\u00f6rfr\u00e5gningar f\u00f6r n\u00e4rvarande (du kan ha angett en ogiltig PIN-kod f\u00f6r m\u00e5nga g\u00e5nger), f\u00f6rs\u00f6k igen senare.", + "device_did_not_pair": "Inget f\u00f6rs\u00f6k att avsluta parningsprocessen gjordes fr\u00e5n enheten.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "invalid_auth": "Ogiltig autentisering", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({type})", + "step": { + "confirm": { + "description": "Du h\u00e5ller p\u00e5 att l\u00e4gga till ` {name} ` av typen ` {type} ` till Home Assistant. \n\n **F\u00f6r att slutf\u00f6ra processen kan du beh\u00f6va ange flera PIN-koder.** \n\n Observera att du *inte* kommer att kunna st\u00e4nga av din Apple TV med denna integration. Endast mediaspelaren i Home Assistant kommer att st\u00e4ngas av!", + "title": "Bekr\u00e4fta att du l\u00e4gger till Apple TV" + }, + "pair_no_pin": { + "description": "Parkoppling kr\u00e4vs f\u00f6r tj\u00e4nsten {protocol}. Ange PIN-kod {pin} p\u00e5 din enhet f\u00f6r att forts\u00e4tta.", + "title": "Parkoppling" + }, + "pair_with_pin": { + "data": { + "pin": "Pin-kod" + }, + "description": "Parning kr\u00e4vs f\u00f6r protokollet ` {protocol} `. V\u00e4nligen ange PIN-koden som visas p\u00e5 sk\u00e4rmen. Inledande nollor ska utel\u00e4mnas, dvs ange 123 om den visade koden \u00e4r 0123.", + "title": "Parkoppling" + }, + "reconfigure": { + "description": "Konfigurera om enheten f\u00f6r att \u00e5terst\u00e4lla dess funktionalitet.", + "title": "Omkonfigurering av enheten" + }, + "service_problem": { + "description": "Ett problem uppstod vid koppling av protokoll ` {protocol} `. Det kommer att ignoreras.", + "title": "Det gick inte att l\u00e4gga till tj\u00e4nsten" + }, + "user": { + "data": { + "device_input": "Enhet" + }, + "description": "B\u00f6rja med att ange enhetsnamnet (t.ex. k\u00f6k eller sovrum) eller IP-adressen f\u00f6r den Apple TV du vill l\u00e4gga till. \n\n Om du inte kan se din enhet eller har n\u00e5gra problem, f\u00f6rs\u00f6k att ange enhetens IP-adress.", + "title": "Konfigurera en ny Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Sl\u00e5 inte p\u00e5 enheten n\u00e4r du startar Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/sv.json b/homeassistant/components/arcam_fmj/translations/sv.json index 42d58bfc929..f29aa5579cc 100644 --- a/homeassistant/components/arcam_fmj/translations/sv.json +++ b/homeassistant/components/arcam_fmj/translations/sv.json @@ -7,6 +7,9 @@ }, "flow_title": "{host}", "step": { + "confirm": { + "description": "Vill du l\u00e4gga till Arcam FMJ p\u00e5 ` {host} ` till Home Assistant?" + }, "user": { "data": { "host": "V\u00e4rd", diff --git a/homeassistant/components/atag/translations/sv.json b/homeassistant/components/atag/translations/sv.json index 480da89cb4a..ae7adf0a365 100644 --- a/homeassistant/components/atag/translations/sv.json +++ b/homeassistant/components/atag/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta.", "unauthorized": "Parning nekad, kontrollera enheten f\u00f6r autentiseringsbeg\u00e4ran" diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index b4d4e8835fa..f3b6cf8d552 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -12,13 +12,18 @@ "reauth_validate": { "data": { "password": "L\u00f6senord" - } + }, + "description": "Ange l\u00f6senordet f\u00f6r {username} .", + "title": "Autentisera ett augustikonto igen" }, "user_validate": { "data": { + "login_method": "Inloggningsmetod", "password": "L\u00f6senord", "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": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/sv.json b/homeassistant/components/aurora_abb_powerone/translations/sv.json index 361fc8bbbb7..469047bc3ba 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/sv.json +++ b/homeassistant/components/aurora_abb_powerone/translations/sv.json @@ -1,16 +1,21 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "no_serial_ports": "Inga com portar funna. M\u00e5ste ha en RS485 enhet f\u00f6r att kommunicera" }, "error": { - "cannot_open_serial_port": "Kan inte \u00f6ppna serieporten, kontrollera och f\u00f6rs\u00f6k igen." + "cannot_connect": "Det g\u00e5r inte att ansluta, kontrollera seriell port, adress, elektrisk anslutning och att v\u00e4xelriktaren \u00e4r p\u00e5 (i dagsljus)", + "cannot_open_serial_port": "Kan inte \u00f6ppna serieporten, kontrollera och f\u00f6rs\u00f6k igen.", + "invalid_serial_port": "Serieporten \u00e4r inte en giltig enhet eller kunde inte \u00f6ppnas" }, "step": { "user": { "data": { + "address": "V\u00e4xelriktarens adress", "port": "RS485 eller USB-RS485 adapter port" - } + }, + "description": "V\u00e4xelriktaren m\u00e5ste anslutas via en RS485-adapter, v\u00e4lj seriell port och v\u00e4xelriktarens adress som konfigurerats p\u00e5 LCD-panelen" } } } diff --git a/homeassistant/components/awair/translations/he.json b/homeassistant/components/awair/translations/he.json index 55e8b21a52b..2494d0bbd28 100644 --- a/homeassistant/components/awair/translations/he.json +++ b/homeassistant/components/awair/translations/he.json @@ -16,6 +16,12 @@ "email": "\u05d3\u05d5\u05d0\"\u05dc" } }, + "reauth_confirm": { + "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", + "email": "\u05d3\u05d5\u05d0\"\u05dc" + } + }, "user": { "data": { "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index 8b05d4b024e..eeaac07d691 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} uppt\u00e4cker inte problem", "is_no_smoke": "{entity_name} detekterar inte r\u00f6k", "is_no_sound": "{entity_name} uppt\u00e4cker inte ljud", + "is_no_update": "{entity_name} \u00e4r uppdaterad", "is_no_vibration": "{entity_name} uppt\u00e4cker inte vibrationer", "is_not_bat_low": "{entity_name} batteri \u00e4r normalt", "is_not_cold": "{entity_name} \u00e4r inte kall", @@ -43,6 +44,7 @@ "is_smoke": "{entity_name} detekterar r\u00f6k", "is_sound": "{entity_name} uppt\u00e4cker ljud", "is_unsafe": "{entity_name} \u00e4r os\u00e4ker", + "is_update": "{entity_name} har en uppdatering tillg\u00e4nglig", "is_vibration": "{entity_name} uppt\u00e4cker vibrationer" }, "trigger_type": { @@ -62,6 +64,7 @@ "no_problem": "{entity_name} slutade uppt\u00e4cka problem", "no_smoke": "{entity_name} slutade detektera r\u00f6k", "no_sound": "{entity_name} slutade uppt\u00e4cka ljud", + "no_update": "{entity_name} blev uppdaterad", "no_vibration": "{entity_name} slutade uppt\u00e4cka vibrationer", "not_bat_low": "{entity_name} batteri normalt", "not_cold": "{entity_name} blev inte kall", @@ -87,10 +90,12 @@ "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5", "unsafe": "{entity_name} blev os\u00e4ker", + "update": "{entity_name} har en uppdatering tillg\u00e4nglig", "vibration": "{entity_name} b\u00f6rjade detektera vibrationer" } }, "device_class": { + "cold": "Kyla", "heat": "v\u00e4rme", "motion": "r\u00f6relse", "power": "effekt" @@ -105,7 +110,8 @@ "on": "L\u00e5g" }, "battery_charging": { - "off": "Laddar inte" + "off": "Laddar inte", + "on": "Laddar" }, "cold": { "off": "Normal", @@ -131,6 +137,10 @@ "off": "Normal", "on": "Varmt" }, + "light": { + "off": "Inget ljus", + "on": "Ljus uppt\u00e4ckt" + }, "lock": { "off": "L\u00e5st", "on": "Ol\u00e5st" @@ -143,6 +153,10 @@ "off": "Klart", "on": "Detekterad" }, + "moving": { + "off": "R\u00f6r sig inte", + "on": "R\u00f6r p\u00e5 sig" + }, "occupancy": { "off": "Tomt", "on": "Detekterad" @@ -151,6 +165,10 @@ "off": "St\u00e4ngd", "on": "\u00d6ppen" }, + "plug": { + "off": "Urkopplad", + "on": "Inkopplad" + }, "presence": { "off": "Borta", "on": "Hemma" @@ -159,6 +177,10 @@ "off": "Ok", "on": "Problem" }, + "running": { + "off": "K\u00f6r inte", + "on": "K\u00f6rs" + }, "safety": { "off": "S\u00e4ker", "on": "Os\u00e4ker" @@ -171,6 +193,10 @@ "off": "Klart", "on": "Detekterad" }, + "update": { + "off": "Uppdaterad", + "on": "Uppdatering tillg\u00e4nglig" + }, "vibration": { "off": "Rensa", "on": "Detekterad" diff --git a/homeassistant/components/bluetooth/translations/he.json b/homeassistant/components/bluetooth/translations/he.json new file mode 100644 index 00000000000..b5740956a9d --- /dev/null +++ b/homeassistant/components/bluetooth/translations/he.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "no_adapters": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05ea\u05d0\u05de\u05d9 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "enable_bluetooth": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u05de\u05ea\u05d0\u05dd \u05d4\u05e9\u05df \u05d4\u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/sv.json b/homeassistant/components/broadlink/translations/sv.json index bc621dfaa6a..5f48a0f975f 100644 --- a/homeassistant/components/broadlink/translations/sv.json +++ b/homeassistant/components/broadlink/translations/sv.json @@ -21,7 +21,26 @@ "finish": { "data": { "name": "Namn" - } + }, + "title": "V\u00e4lj ett namn f\u00f6r enheten" + }, + "reset": { + "description": "{name} ( {model} p\u00e5 {host} ) \u00e4r l\u00e5st. Du m\u00e5ste l\u00e5sa upp enheten f\u00f6r att autentisera och slutf\u00f6ra konfigurationen. Instruktioner:\n 1. \u00d6ppna Broadlink-appen.\n 2. Klicka p\u00e5 enheten.\n 3. Klicka p\u00e5 `...` uppe till h\u00f6ger.\n 4. Bl\u00e4ddra till botten av sidan.\n 5. Inaktivera l\u00e5set.", + "title": "L\u00e5s upp enheten" + }, + "unlock": { + "data": { + "unlock": "Ja, g\u00f6r det." + }, + "description": "{name} ( {model} p\u00e5 {host} ) \u00e4r l\u00e5st. Detta kan leda till autentiseringsproblem i Home Assistant. Vill du l\u00e5sa upp den?", + "title": "L\u00e5s upp enheten (valfritt)" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "timeout": "Timeout" + }, + "title": "Anslut till enheten" } } } diff --git a/homeassistant/components/bsblan/translations/sv.json b/homeassistant/components/bsblan/translations/sv.json index f96cbe0051f..96ec59f86dc 100644 --- a/homeassistant/components/bsblan/translations/sv.json +++ b/homeassistant/components/bsblan/translations/sv.json @@ -13,9 +13,12 @@ "data": { "host": "V\u00e4rd", "passkey": "Nyckelstr\u00e4ng", + "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "St\u00e4ll in din BSB-Lan-enhet f\u00f6r att integreras med Home Assistant.", + "title": "Anslut till BSB-Lan-enheten" } } } diff --git a/homeassistant/components/cloud/translations/sv.json b/homeassistant/components/cloud/translations/sv.json new file mode 100644 index 00000000000..528e996272f --- /dev/null +++ b/homeassistant/components/cloud/translations/sv.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa Aktiverad", + "can_reach_cert_server": "N\u00e5 certifikatserver", + "can_reach_cloud": "N\u00e5 Home Assistant Cloud", + "can_reach_cloud_auth": "N\u00e5 autentiseringsserver", + "google_enabled": "Google Aktiverad", + "logged_in": "Inloggad", + "relayer_connected": "Vidarebefodrare Ansluten", + "remote_connected": "Fj\u00e4rransluten", + "remote_enabled": "Fj\u00e4rr\u00e5tkomst Aktiverad", + "subscription_expiration": "Prenumerationens utg\u00e5ng" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/sv.json b/homeassistant/components/co2signal/translations/sv.json index 0abdae46923..1544555f8f3 100644 --- a/homeassistant/components/co2signal/translations/sv.json +++ b/homeassistant/components/co2signal/translations/sv.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "api_ratelimit": "API-hastighetsgr\u00e4nsen har \u00f6verskridits", + "unknown": "Ov\u00e4ntat fel" + }, "error": { "api_ratelimit": "API-hastighetsgr\u00e4nsen har \u00f6verskridits", "invalid_auth": "Ogiltig autentisering", diff --git a/homeassistant/components/coinbase/translations/sv.json b/homeassistant/components/coinbase/translations/sv.json index 7d6a4a507aa..c63ebf1c09f 100644 --- a/homeassistant/components/coinbase/translations/sv.json +++ b/homeassistant/components/coinbase/translations/sv.json @@ -11,6 +11,7 @@ "api_key": "API-nyckel", "api_token": "API-hemlighet" }, + "description": "Ange uppgifterna om din API-nyckel som tillhandah\u00e5lls av Coinbase.", "title": "Coinbase API-nyckeldetaljer" } } @@ -25,6 +26,7 @@ "init": { "data": { "account_balance_currencies": "Balans i pl\u00e5nboken att rapportera.", + "exchange_base": "Basvaluta f\u00f6r v\u00e4xelkurssensorer.", "exchange_rate_currencies": "Valutakurser att rapportera.", "exchnage_rate_precision": "Antal decimaler f\u00f6r v\u00e4xelkurser." }, diff --git a/homeassistant/components/daikin/translations/sv.json b/homeassistant/components/daikin/translations/sv.json index 1ea73051e9b..f12d1d4d71f 100644 --- a/homeassistant/components/daikin/translations/sv.json +++ b/homeassistant/components/daikin/translations/sv.json @@ -5,8 +5,10 @@ "cannot_connect": "Det gick inte att ansluta." }, "error": { + "api_password": "Ogiltig autentisering , anv\u00e4nd antingen API-nyckel eller l\u00f6senord.", "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index 774144c5ba3..6097d4ef3b9 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -4,6 +4,7 @@ "already_configured": "Bryggan \u00e4r redan konfigurerad", "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r bryggan p\u00e5g\u00e5r redan.", "no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes", + "no_hardware_available": "Ingen radioh\u00e5rdvara ansluten till deCONZ", "updated_instance": "Uppdaterad deCONZ-instans med ny v\u00e4rdadress" }, "error": { @@ -97,7 +98,8 @@ "deconz_devices": { "data": { "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer", - "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper" + "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper", + "allow_new_devices": "Till\u00e5t automatiskt till\u00e4gg av nya enheter" }, "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper", "title": "deCONZ-inst\u00e4llningar" diff --git a/homeassistant/components/demo/translations/select.sv.json b/homeassistant/components/demo/translations/select.sv.json new file mode 100644 index 00000000000..68d8d995563 --- /dev/null +++ b/homeassistant/components/demo/translations/select.sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Ljushastighet", + "ludicrous_speed": "Ludicrous hastighet", + "ridiculous_speed": "L\u00f6jlig hastighet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/sv.json b/homeassistant/components/denonavr/translations/sv.json index f27ff32b05c..db692b7f835 100644 --- a/homeassistant/components/denonavr/translations/sv.json +++ b/homeassistant/components/denonavr/translations/sv.json @@ -1,8 +1,16 @@ { "config": { "abort": { - "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen, att koppla bort n\u00e4t- och ethernetkablar och \u00e5teransluta dem kan hj\u00e4lpa" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen, att koppla bort n\u00e4t- och ethernetkablar och \u00e5teransluta dem kan hj\u00e4lpa", + "not_denonavr_manufacturer": "Inte en Denon AVR-n\u00e4tverksreceiver, uppt\u00e4ckt tillverkare matchade inte", + "not_denonavr_missing": "Inte en Denon AVR-n\u00e4tverksreceiver, uppt\u00e4cktsinformationen \u00e4r inte fullst\u00e4ndig" }, + "error": { + "discovery_error": "Det gick inte att hitta en Denon AVR-n\u00e4tverksreceiver" + }, + "flow_title": "{name}", "step": { "confirm": { "description": "Bekr\u00e4fta att du l\u00e4gger till receivern" diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json index 6727e5c5107..3081604f882 100644 --- a/homeassistant/components/devolo_home_control/translations/sv.json +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -13,6 +13,8 @@ }, "zeroconf_confirm": { "data": { + "mydevolo_url": "mydevolo URL", + "password": "L\u00f6senord", "username": "E-postadress / devolo-id" } } diff --git a/homeassistant/components/discord/translations/sv.json b/homeassistant/components/discord/translations/sv.json index 1b52e7816d2..38370827354 100644 --- a/homeassistant/components/discord/translations/sv.json +++ b/homeassistant/components/discord/translations/sv.json @@ -8,6 +8,18 @@ "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token" + } + }, + "user": { + "data": { + "api_token": "API Token" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/sv.json b/homeassistant/components/dsmr/translations/sv.json index 38b86e964b6..7ad8f5f0b09 100644 --- a/homeassistant/components/dsmr/translations/sv.json +++ b/homeassistant/components/dsmr/translations/sv.json @@ -1,7 +1,42 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_communicate": "Misslyckades med att kommunicera", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_communicate": "Misslyckades med att kommunicera", + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "V\u00e4lj DSMR-version", + "host": "V\u00e4rd", + "port": "Port" + }, + "title": "V\u00e4lj anslutningsadress" + }, + "setup_serial": { + "data": { + "dsmr_version": "V\u00e4lj DSMR-version", + "port": "V\u00e4lj enhet" + }, + "title": "Enhet" + }, + "setup_serial_manual_path": { + "data": { + "port": "USB-enhetens s\u00f6kv\u00e4g" + }, + "title": "S\u00f6kv\u00e4g" + }, + "user": { + "data": { + "type": "Anslutningstyp" + } + } } }, "options": { diff --git a/homeassistant/components/econet/translations/sv.json b/homeassistant/components/econet/translations/sv.json new file mode 100644 index 00000000000..5fbfb53944b --- /dev/null +++ b/homeassistant/components/econet/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "title": "Konfigurera Rheem EcoNet-konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/he.json b/homeassistant/components/eight_sleep/translations/he.json new file mode 100644 index 00000000000..e428d0009ae --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "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/esphome/translations/sv.json b/homeassistant/components/esphome/translations/sv.json index 88a9ca92377..1d4eb2a6ff3 100644 --- a/homeassistant/components/esphome/translations/sv.json +++ b/homeassistant/components/esphome/translations/sv.json @@ -7,6 +7,8 @@ }, "error": { "connection_error": "Kan inte ansluta till ESP. Se till att din YAML-fil inneh\u00e5ller en 'api:' line.", + "invalid_auth": "Ogiltig autentisering", + "invalid_psk": "Transportkrypteringsnyckeln \u00e4r ogiltig. Se till att den matchar det du har i din konfiguration", "resolve_error": "Det g\u00e5r inte att hitta IP-adressen f\u00f6r ESP med DNS-namnet. Om det h\u00e4r felet kvarst\u00e5r anger du en statisk IP-adress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", @@ -21,6 +23,18 @@ "description": "Vill du l\u00e4gga till ESPHome noden ` {name} ` till Home Assistant?", "title": "Uppt\u00e4ckt ESPHome-nod" }, + "encryption_key": { + "data": { + "noise_psk": "Krypteringsnyckel" + }, + "description": "Ange krypteringsnyckeln som du st\u00e4llt in i din konfiguration f\u00f6r {name} ." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Krypteringsnyckel" + }, + "description": "ESPHome-enheten {name} aktiverade transportkryptering eller \u00e4ndrade krypteringsnyckeln. V\u00e4nligen ange den uppdaterade nyckeln." + }, "user": { "data": { "host": "V\u00e4rddatorn", diff --git a/homeassistant/components/evil_genius_labs/translations/sv.json b/homeassistant/components/evil_genius_labs/translations/sv.json new file mode 100644 index 00000000000..d51f98c6100 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/sv.json b/homeassistant/components/ezviz/translations/sv.json index 4c047d75573..971e996a103 100644 --- a/homeassistant/components/ezviz/translations/sv.json +++ b/homeassistant/components/ezviz/translations/sv.json @@ -26,5 +26,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout f\u00f6r beg\u00e4ran (sekunder)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/sv.json b/homeassistant/components/fjaraskupan/translations/sv.json new file mode 100644 index 00000000000..baa8ed1ba40 --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du s\u00e4tta upp Fj\u00e4r\u00e5skupan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/en.json b/homeassistant/components/flunearyou/translations/en.json index 3dcbfa2a628..7e76b54b18a 100644 --- a/homeassistant/components/flunearyou/translations/en.json +++ b/homeassistant/components/flunearyou/translations/en.json @@ -22,7 +22,7 @@ "fix_flow": { "step": { "confirm": { - "description": "The data source that powered the Flu Near You integration is no longer available. Press SUBMIT to remove all configured instances of the integration from Home Assistant.", + "description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance.", "title": "Remove Flu Near You" } } diff --git a/homeassistant/components/flunearyou/translations/fr.json b/homeassistant/components/flunearyou/translations/fr.json index a9d8064d865..ebc2ccc385a 100644 --- a/homeassistant/components/flunearyou/translations/fr.json +++ b/homeassistant/components/flunearyou/translations/fr.json @@ -16,5 +16,10 @@ "title": "Configurer Flu Near You" } } + }, + "issues": { + "integration_removal": { + "title": "Flu Near You n'est plus disponible" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/id.json b/homeassistant/components/flunearyou/translations/id.json index 86afc7bb5fd..72fcfefc78d 100644 --- a/homeassistant/components/flunearyou/translations/id.json +++ b/homeassistant/components/flunearyou/translations/id.json @@ -16,5 +16,18 @@ "title": "Konfigurasikan Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Sumber data eksternal yang mendukung integrasi Flu Near You tidak lagi tersedia, sehingga integrasi tidak lagi berfungsi. \n\nTekan KIRIM untuk menghapus integrasi Flu Near You dari instans Home Assistant Anda.", + "title": "Hapus Integrasi Flu Near You" + } + } + }, + "title": "Integrasi Flu Year You tidak lagi tersedia" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pt-BR.json b/homeassistant/components/flunearyou/translations/pt-BR.json index dc63fa1baf8..eeca693be89 100644 --- a/homeassistant/components/flunearyou/translations/pt-BR.json +++ b/homeassistant/components/flunearyou/translations/pt-BR.json @@ -16,5 +16,18 @@ "title": "Configurar Flue Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "A fonte de dados externa que alimenta a integra\u00e7\u00e3o do Flu Near You n\u00e3o est\u00e1 mais dispon\u00edvel; assim, a integra\u00e7\u00e3o n\u00e3o funciona mais. \n\n Pressione ENVIAR para remover Flu Near You da sua inst\u00e2ncia do Home Assistant.", + "title": "Remova Flu Near You" + } + } + }, + "title": "Flu Near You n\u00e3o est\u00e1 mais dispon\u00edvel" + } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/sv.json b/homeassistant/components/flux_led/translations/sv.json new file mode 100644 index 00000000000..ec732eb1897 --- /dev/null +++ b/homeassistant/components/flux_led/translations/sv.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {modell} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Anpassad effekt: Lista med 1 till 16 [R,G,B] f\u00e4rger. Exempel: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Anpassad effekt: Hastighet i procent f\u00f6r effekten som byter f\u00e4rg.", + "custom_effect_transition": "Anpassad effekt: Typ av \u00f6verg\u00e5ng mellan f\u00e4rgerna.", + "mode": "Det valda l\u00e4get f\u00f6r ljusstyrka." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/sl.json b/homeassistant/components/freebox/translations/sl.json index 4dc79b32256..0a52dd187b3 100644 --- a/homeassistant/components/freebox/translations/sl.json +++ b/homeassistant/components/freebox/translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Gostitelj je \u017ee konfiguriran" + "already_configured": "Naprava je \u017ee konfigurirana" }, "error": { "cannot_connect": "Povezava ni uspela, poskusite znova", diff --git a/homeassistant/components/freebox/translations/sv.json b/homeassistant/components/freebox/translations/sv.json index ed8ebe67ae9..dbc060b4d27 100644 --- a/homeassistant/components/freebox/translations/sv.json +++ b/homeassistant/components/freebox/translations/sv.json @@ -10,6 +10,7 @@ }, "step": { "link": { + "description": "Klicka p\u00e5 \"Skicka\" och tryck sedan p\u00e5 h\u00f6gerpilen p\u00e5 routern f\u00f6r att registrera Freebox med Home Assistant. \n\n ![Plats f\u00f6r knappen p\u00e5 routern](/static/images/config_freebox.png)", "title": "L\u00e4nka Freebox-router" }, "user": { diff --git a/homeassistant/components/freedompro/translations/sv.json b/homeassistant/components/freedompro/translations/sv.json index 83f1c6b3dac..e4b85f92bba 100644 --- a/homeassistant/components/freedompro/translations/sv.json +++ b/homeassistant/components/freedompro/translations/sv.json @@ -12,6 +12,7 @@ "data": { "api_key": "API-nyckel" }, + "description": "Ange API-nyckeln fr\u00e5n https://home.freedompro.eu", "title": "Freedompro API-nyckel" } } diff --git a/homeassistant/components/fritz/translations/sv.json b/homeassistant/components/fritz/translations/sv.json index 5afcb156965..f29f7f0ba78 100644 --- a/homeassistant/components/fritz/translations/sv.json +++ b/homeassistant/components/fritz/translations/sv.json @@ -1,28 +1,44 @@ { "config": { "abort": { - "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte.", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "{name}", "step": { "confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Uppt\u00e4ckte FRITZ!Box: {name} \n\n St\u00e4ll in FRITZ!Box-verktyg f\u00f6r att styra ditt {name}", + "title": "St\u00e4ll in FRITZ!Box Tools" }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" }, + "description": "Uppdatera FRITZ! Box Tools autentiseringsuppgifter f\u00f6r: {host}.\n\nFRITZ! Box Tools kan inte logga in p\u00e5 din FRITZ!Box.", "title": "Uppdaterar FRITZ!Box Tools - referenser" }, "user": { "data": { "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Konfigurera FRITZ!Box Tools f\u00f6r att styra din FRITZ!Box.\nMinimikrav: anv\u00e4ndarnamn och l\u00f6senord.", + "title": "St\u00e4ll in FRITZ!Box Tools" } } }, diff --git a/homeassistant/components/fritzbox/translations/sv.json b/homeassistant/components/fritzbox/translations/sv.json index a028ec5f217..296f7244c1e 100644 --- a/homeassistant/components/fritzbox/translations/sv.json +++ b/homeassistant/components/fritzbox/translations/sv.json @@ -1,13 +1,17 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte.", "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_supported": "Ansluten till AVM FRITZ!Box men den kan inte styra smarta hemenheter.", "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "{name}", "step": { "confirm": { "data": { @@ -28,7 +32,8 @@ "host": "V\u00e4rd eller IP-adress", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange din AVM FRITZ!Box-information." } } } diff --git a/homeassistant/components/fronius/translations/sv.json b/homeassistant/components/fronius/translations/sv.json new file mode 100644 index 00000000000..f341a6314ee --- /dev/null +++ b/homeassistant/components/fronius/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/sv.json b/homeassistant/components/garages_amsterdam/translations/sv.json new file mode 100644 index 00000000000..e9209b99a82 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "garage_name": "Garagenamn" + }, + "title": "V\u00e4lj ett garage att \u00f6vervaka" + } + } + }, + "title": "Garage Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/he.json b/homeassistant/components/generic/translations/he.json index 3d2f8cdca4e..2a3458cd3e7 100644 --- a/homeassistant/components/generic/translations/he.json +++ b/homeassistant/components/generic/translations/he.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u05de\u05e6\u05dc\u05de\u05d4 \u05e2\u05dd \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05d5 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05de\u05ea.", "invalid_still_image": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05dc\u05d0 \u05d4\u05d7\u05d6\u05d9\u05e8\u05d4 \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d7\u05d5\u05e7\u05d9\u05ea", + "malformed_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05d2\u05d5\u05d9\u05d4", "no_still_image_or_stream_url": "\u05d9\u05e9 \u05dc\u05e6\u05d9\u05d9\u05df \u05dc\u05e4\u05d7\u05d5\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05d4\u05d6\u05e8\u05de\u05d4", + "relative_url": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d0\u05ea\u05e8\u05d9\u05dd \u05d9\u05d7\u05e1\u05d9\u05d5\u05ea \u05d0\u05d9\u05e0\u05df \u05de\u05d5\u05ea\u05e8\u05d5\u05ea", "stream_file_not_found": "\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4 (\u05d4\u05d0\u05dd ffmpeg \u05de\u05d5\u05ea\u05e7\u05df?)", "stream_http_not_found": "HTTP 404 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_io_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", @@ -15,6 +17,7 @@ "stream_no_video": "\u05d0\u05d9\u05df \u05d5\u05d9\u05d3\u05d9\u05d0\u05d5 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_not_permitted": "\u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05ea\u05e8\u05ea \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", "stream_unauthorised": "\u05d4\u05d4\u05e8\u05e9\u05d0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "template_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e2\u05d9\u05d1\u05d5\u05d3 \u05d4\u05ea\u05d1\u05e0\u05d9\u05ea. \u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "timeout": "\u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05d1\u05e2\u05ea \u05d8\u05e2\u05d9\u05e0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "unable_still_load": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05ea\u05de\u05d5\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05de\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, \u05db\u05e9\u05dc \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05d1\u05de\u05d7\u05e9\u05d1 \u05de\u05d0\u05e8\u05d7, \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d0\u05d5 \u05d0\u05d9\u05de\u05d5\u05ea). \u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" @@ -49,7 +52,9 @@ "error": { "already_exists": "\u05de\u05e6\u05dc\u05de\u05d4 \u05e2\u05dd \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05d5 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05de\u05ea.", "invalid_still_image": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05dc\u05d0 \u05d4\u05d7\u05d6\u05d9\u05e8\u05d4 \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d7\u05d5\u05e7\u05d9\u05ea", + "malformed_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05d2\u05d5\u05d9\u05d4", "no_still_image_or_stream_url": "\u05d9\u05e9 \u05dc\u05e6\u05d9\u05d9\u05df \u05dc\u05e4\u05d7\u05d5\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05d4\u05d6\u05e8\u05de\u05d4", + "relative_url": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d0\u05ea\u05e8\u05d9\u05dd \u05d9\u05d7\u05e1\u05d9\u05d5\u05ea \u05d0\u05d9\u05e0\u05df \u05de\u05d5\u05ea\u05e8\u05d5\u05ea", "stream_file_not_found": "\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4 (\u05d4\u05d0\u05dd ffmpeg \u05de\u05d5\u05ea\u05e7\u05df?)", "stream_http_not_found": "HTTP 404 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_io_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", @@ -57,6 +62,7 @@ "stream_no_video": "\u05d0\u05d9\u05df \u05d5\u05d9\u05d3\u05d9\u05d0\u05d5 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_not_permitted": "\u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05ea\u05e8\u05ea \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", "stream_unauthorised": "\u05d4\u05d4\u05e8\u05e9\u05d0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "template_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e2\u05d9\u05d1\u05d5\u05d3 \u05d4\u05ea\u05d1\u05e0\u05d9\u05ea. \u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "timeout": "\u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05d1\u05e2\u05ea \u05d8\u05e2\u05d9\u05e0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "unable_still_load": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05ea\u05de\u05d5\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05de\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, \u05db\u05e9\u05dc \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05d1\u05de\u05d7\u05e9\u05d1 \u05de\u05d0\u05e8\u05d7, \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d0\u05d5 \u05d0\u05d9\u05de\u05d5\u05ea). \u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" @@ -77,8 +83,12 @@ "rtsp_transport": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 RTSP", "still_image_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, https://...)", "stream_source": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e7\u05d5\u05e8 \u05d6\u05e8\u05dd (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, rtsp://...)", + "use_wallclock_as_timestamps": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05e9\u05e2\u05d5\u05df \u05e7\u05d9\u05e8 \u05db\u05d7\u05d5\u05ea\u05de\u05d5\u05ea \u05d6\u05de\u05df", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05d6\u05d5 \u05e2\u05e9\u05d5\u05d9\u05d4 \u05dc\u05ea\u05e7\u05df \u05d1\u05e2\u05d9\u05d5\u05ea \u05e4\u05d9\u05dc\u05d5\u05d7 \u05d0\u05d5 \u05e7\u05e8\u05d9\u05e1\u05d4 \u05d4\u05e0\u05d5\u05d1\u05e2\u05d5\u05ea \u05de\u05d9\u05d9\u05e9\u05d5\u05de\u05d9 \u05d7\u05d5\u05ea\u05de\u05ea \u05d6\u05de\u05df \u05d1\u05d0\u05d2\u05d9\u05dd \u05d1\u05de\u05e6\u05dc\u05de\u05d5\u05ea \u05de\u05e1\u05d5\u05d9\u05de\u05d5\u05ea" } } } diff --git a/homeassistant/components/goalzero/translations/sv.json b/homeassistant/components/goalzero/translations/sv.json new file mode 100644 index 00000000000..bc0c0d03eca --- /dev/null +++ b/homeassistant/components/goalzero/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "confirm_discovery": { + "description": "DHCP-reservation p\u00e5 din router rekommenderas. Om den inte \u00e4r konfigurerad kan enheten bli otillg\u00e4nglig tills Home Assistant uppt\u00e4cker den nya IP-adressen. Se din routers anv\u00e4ndarmanual." + }, + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + }, + "description": "Se dokumentationen f\u00f6r att s\u00e4kerst\u00e4lla att alla krav \u00e4r uppfyllda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/he.json b/homeassistant/components/google/translations/he.json index 191450ed8eb..691ae9547c0 100644 --- a/homeassistant/components/google/translations/he.json +++ b/homeassistant/components/google/translations/he.json @@ -3,10 +3,12 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", "oauth_error": "\u05d4\u05ea\u05e7\u05d1\u05dc\u05d5 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd.", - "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" }, "create_entry": { "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" diff --git a/homeassistant/components/govee_ble/translations/he.json b/homeassistant/components/govee_ble/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/govee_ble/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/sv.json b/homeassistant/components/homeassistant/translations/sv.json index b0f67a5754e..9e6855c5e87 100644 --- a/homeassistant/components/homeassistant/translations/sv.json +++ b/homeassistant/components/homeassistant/translations/sv.json @@ -1,9 +1,19 @@ { "system_health": { "info": { + "arch": "CPU-arkitektur", "config_dir": "Konfigurationskatalog", + "dev": "Utveckling", + "docker": "Docker", "hassio": "Supervisor", - "user": "Anv\u00e4ndare" + "installation_type": "Installationstyp", + "os_name": "Operativsystemfamilj", + "os_version": "Operativsystemversion", + "python_version": "Python-version", + "timezone": "Tidszon", + "user": "Anv\u00e4ndare", + "version": "Version", + "virtualenv": "Virtuell milj\u00f6" } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/he.json b/homeassistant/components/homeassistant_alerts/translations/he.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/he.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index 250bb634736..caddbeaf7d2 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "port_name_in_use": "Ett tillbeh\u00f6r eller brygga med samma namn eller port \u00e4r redan konfigurerat." + }, "step": { "pairing": { + "description": "F\u00f6r att slutf\u00f6ra ihopparningen f\u00f6lj instruktionerna i \"Meddelanden\" under \"HomeKit-parning\".", "title": "Para HomeKit" }, "user": { diff --git a/homeassistant/components/homekit_controller/translations/he.json b/homeassistant/components/homekit_controller/translations/he.json index 9593bbd90e4..c5ec291569d 100644 --- a/homeassistant/components/homekit_controller/translations/he.json +++ b/homeassistant/components/homekit_controller/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "user": { "data": { diff --git a/homeassistant/components/homekit_controller/translations/sensor.ca.json b/homeassistant/components/homekit_controller/translations/sensor.ca.json new file mode 100644 index 00000000000..d8abac44cac --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.ca.json @@ -0,0 +1,19 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "full": "Dispositiu final complet", + "minimal": "Dispositiu final redu\u00eft", + "none": "Cap", + "router_eligible": "Dispositiu final apte per ser encaminador (router)", + "sleepy": "Dispositiu final dorment" + }, + "homekit_controller__thread_status": { + "child": "Fill", + "detached": "Desconnectat", + "disabled": "Desactivat", + "joining": "Unint-se", + "leader": "L\u00edder", + "router": "Encaminador (router)" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.fr.json b/homeassistant/components/homekit_controller/translations/sensor.fr.json new file mode 100644 index 00000000000..5855bbc94ec --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.fr.json @@ -0,0 +1,7 @@ +{ + "state": { + "homekit_controller__thread_status": { + "child": "Enfant" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.id.json b/homeassistant/components/homekit_controller/translations/sensor.id.json new file mode 100644 index 00000000000..5697598ef54 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.id.json @@ -0,0 +1,11 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "none": "Tidak Ada" + }, + "homekit_controller__thread_status": { + "disabled": "Dinonaktifkan", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.no.json b/homeassistant/components/homekit_controller/translations/sensor.no.json new file mode 100644 index 00000000000..f0323c0326a --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.no.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Kan med kantruter", + "full": "Full End-enhet", + "minimal": "Minimal sluttenhet", + "none": "Ingen", + "router_eligible": "Ruterkvalifisert sluttenhet", + "sleepy": "S\u00f8vnig sluttenhet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.pt-BR.json b/homeassistant/components/homekit_controller/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..f4a5edf80ee --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.pt-BR.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Capacidade de roteador de borda", + "full": "Dispositivo final completo", + "minimal": "Dispositivo final m\u00ednimo", + "none": "Nenhum", + "router_eligible": "Dispositivo final qualificado para roteador", + "sleepy": "Dispositivo final sonolento" + }, + "homekit_controller__thread_status": { + "border_router": "Roteador de borda", + "child": "Filho", + "detached": "Separado", + "disabled": "Desabilitado", + "joining": "Juntar", + "leader": "L\u00edder", + "router": "Roteador" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.ru.json b/homeassistant/components/homekit_controller/translations/sensor.ru.json new file mode 100644 index 00000000000..c6e67b645e3 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.ru.json @@ -0,0 +1,7 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "sleepy": "\u0421\u043f\u044f\u0449\u0435\u0435 \u043a\u043e\u043d\u0435\u0447\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json b/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..123469d79cf --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "\u7db2\u8def\u6838\u5fc3\u80fd\u529b", + "full": "\u6240\u6709\u7d42\u7aef\u88dd\u7f6e", + "minimal": "\u6700\u4f4e\u7d42\u7aef\u88dd\u7f6e", + "none": "\u7121", + "router_eligible": "\u4e2d\u9593\u5c64\u7d42\u7aef\u88dd\u7f6e", + "sleepy": "\u5f85\u547d\u7d42\u7aef\u88dd\u7f6e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sv.json b/homeassistant/components/homekit_controller/translations/sv.json index e5a05b62b84..1766689ca69 100644 --- a/homeassistant/components/homekit_controller/translations/sv.json +++ b/homeassistant/components/homekit_controller/translations/sv.json @@ -24,6 +24,7 @@ "title": "Enheten paras redan med en annan styrenhet" }, "max_tries_error": { + "description": "Enheten har f\u00e5tt mer \u00e4n 100 misslyckade autentiseringsf\u00f6rs\u00f6k. Testa att starta om enheten och forts\u00e4tt sedan att \u00e5teruppta ihopkopplingen.", "title": "Maximalt antal autentiseringsf\u00f6rs\u00f6k har \u00f6verskridits" }, "pair": { diff --git a/homeassistant/components/honeywell/translations/sv.json b/homeassistant/components/honeywell/translations/sv.json index 23c825f256f..287e24372ba 100644 --- a/homeassistant/components/honeywell/translations/sv.json +++ b/homeassistant/components/honeywell/translations/sv.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange de autentiseringsuppgifter som anv\u00e4nds f\u00f6r att logga in p\u00e5 mytotalconnectcomfort.com." } } } diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index 1cf157f02e5..f09232f92ab 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -30,6 +30,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "F\u00f6rsta knappen", + "2": "Andra knappen", + "3": "Tredje knappen", + "4": "Fj\u00e4rde knappen", "button_1": "F\u00f6rsta knappen", "button_2": "Andra knappen", "button_3": "Tredje knappen", @@ -42,18 +46,24 @@ "turn_on": "Starta" }, "trigger_type": { + "double_short_release": "B\u00e5da \"{subtyp}\" sl\u00e4pptes", + "initial_press": "Knappen \" {subtype} \" trycktes f\u00f6rst", + "long_release": "Knappen \" {subtype} \" sl\u00e4pps efter l\u00e5ng tryckning", "remote_button_long_release": "\"{subtype}\" knappen sl\u00e4pptes efter ett l\u00e5ngt tryck", "remote_button_short_press": "\"{subtype}\" knappen nedtryckt", "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", "remote_double_button_long_press": "B\u00e5da \"{subtype}\" sl\u00e4pptes efter en l\u00e5ngtryckning", - "remote_double_button_short_press": "B\u00e5da \"{subtyp}\" sl\u00e4pptes" + "remote_double_button_short_press": "B\u00e5da \"{subtyp}\" sl\u00e4pptes", + "repeat": "Knappen \" {subtype} \" h\u00f6lls nedtryckt", + "short_release": "Knappen \" {subtype} \" sl\u00e4pps efter kort tryckning" } }, "options": { "step": { "init": { "data": { - "allow_hue_groups": "Till\u00e5t Hue-grupper" + "allow_hue_groups": "Till\u00e5t Hue-grupper", + "allow_hue_scenes": "Till\u00e5t Hue-scener" } } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/sv.json b/homeassistant/components/hunterdouglas_powerview/translations/sv.json index e572ec2c4a7..3ec2c552711 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/sv.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/sv.json @@ -7,6 +7,7 @@ "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Vill du konfigurera {name} ({host})?", diff --git a/homeassistant/components/hvv_departures/translations/sv.json b/homeassistant/components/hvv_departures/translations/sv.json index c1621e16f2c..0aa84f03b95 100644 --- a/homeassistant/components/hvv_departures/translations/sv.json +++ b/homeassistant/components/hvv_departures/translations/sv.json @@ -9,10 +9,25 @@ "no_results": "Inga resultat. F\u00f6rs\u00f6k med en annan station/adress" }, "step": { + "station": { + "data": { + "station": "Station/adress" + }, + "title": "Ange station/adress" + }, + "station_select": { + "data": { + "station": "Station/adress" + }, + "title": "V\u00e4lj station/adress" + }, "user": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till HVV API" } } }, @@ -20,8 +35,12 @@ "step": { "init": { "data": { + "filter": "V\u00e4lj linjer", + "offset": "Offset (minuter)", "real_time": "Anv\u00e4nd realtidsdata" - } + }, + "description": "\u00c4ndra alternativ f\u00f6r denna avg\u00e5ngssensor", + "title": "Alternativ" } } } diff --git a/homeassistant/components/hyperion/translations/sv.json b/homeassistant/components/hyperion/translations/sv.json index 56cee0c4ad2..9de6a5673c0 100644 --- a/homeassistant/components/hyperion/translations/sv.json +++ b/homeassistant/components/hyperion/translations/sv.json @@ -23,6 +23,7 @@ "description": "Konfigurera auktorisering till din Hyperion Ambilight-server" }, "confirm": { + "description": "Vill du l\u00e4gga till denna Hyperion Ambilight till Home Assistant? \n\n **V\u00e4rd:** {host}\n **Port:** {port}\n **ID**: {id}", "title": "Bekr\u00e4fta till\u00e4gg av Hyperion Ambilight-tj\u00e4nst" }, "create_token": { diff --git a/homeassistant/components/iaqualink/translations/sl.json b/homeassistant/components/iaqualink/translations/sl.json index 3b92f653e7f..eb3e1a4dfdd 100644 --- a/homeassistant/components/iaqualink/translations/sl.json +++ b/homeassistant/components/iaqualink/translations/sl.json @@ -4,7 +4,7 @@ "user": { "data": { "password": "Geslo", - "username": "Uporabni\u0161ko ime / e-po\u0161tni naslov" + "username": "Uporabni\u0161ko ime" }, "description": "Prosimo, vnesite uporabni\u0161ko ime in geslo za iAqualink ra\u010dun.", "title": "Pove\u017eite se z iAqualink" diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index 5aa6e9574f8..b0a052ecef4 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -2,13 +2,21 @@ "config": { "abort": { "already_configured": "Kontot har redan konfigurerats", - "no_device": "Ingen av dina enheter har \"Hitta min iPhone\" aktiverat" + "no_device": "Ingen av dina enheter har \"Hitta min iPhone\" aktiverat", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "send_verification_code": "Det gick inte att skicka verifieringskod", "validate_verification_code": "Det gick inte att verifiera verifieringskoden, v\u00e4lj en betrodd enhet och starta verifieringen igen" }, "step": { + "reauth": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ditt tidigare angivna l\u00f6senord f\u00f6r {username} fungerar inte l\u00e4ngre. Uppdatera ditt l\u00f6senord f\u00f6r att forts\u00e4tta anv\u00e4nda denna integration.", + "title": "\u00c5terautenticera integration" + }, "trusted_device": { "data": { "trusted_device": "Betrodd enhet" diff --git a/homeassistant/components/inkbird/translations/he.json b/homeassistant/components/inkbird/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/inkbird/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json index 218b8d6d748..7b1692e7ec9 100644 --- a/homeassistant/components/insteon/translations/sv.json +++ b/homeassistant/components/insteon/translations/sv.json @@ -1,17 +1,37 @@ { "config": { "abort": { - "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet" + "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "flow_title": "{name}", "step": { "confirm_usb": { "description": "Vill du konfigurera {name}?" }, + "hubv1": { + "data": { + "host": "IP-adress", + "port": "Port" + }, + "description": "Konfigurera Insteon Hub version 1 (f\u00f6re 2014).", + "title": "Insteon Hub version 1" + }, "hubv2": { "data": { + "host": "IP-adress", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Konfigurera Insteon Hub version 2.", + "title": "Insteon Hub version 2" + }, + "user": { + "data": { + "modem_type": "Modemtyp." + }, + "description": "V\u00e4lj Insteon-modemtyp." } } }, diff --git a/homeassistant/components/islamic_prayer_times/translations/sv.json b/homeassistant/components/islamic_prayer_times/translations/sv.json index f865c5a2c6a..8d9c224e8f2 100644 --- a/homeassistant/components/islamic_prayer_times/translations/sv.json +++ b/homeassistant/components/islamic_prayer_times/translations/sv.json @@ -2,6 +2,22 @@ "config": { "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du s\u00e4tta upp islamiska b\u00f6netider?", + "title": "St\u00e4ll in islamiska b\u00f6netider" + } } - } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Ber\u00e4kningsmetod f\u00f6r b\u00f6n" + } + } + } + }, + "title": "Islamiska b\u00f6netider" } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/sv.json b/homeassistant/components/isy994/translations/sv.json index 334c3e0fb0d..a3b9ccb3d2f 100644 --- a/homeassistant/components/isy994/translations/sv.json +++ b/homeassistant/components/isy994/translations/sv.json @@ -38,11 +38,20 @@ "data": { "ignore_string": "Ignorera str\u00e4ng", "restore_light_state": "\u00c5terst\u00e4ll ljusstyrkan", - "sensor_string": "Nodsensorstr\u00e4ng" + "sensor_string": "Nodsensorstr\u00e4ng", + "variable_sensor_string": "Variabel sensorstr\u00e4ng" }, "description": "St\u00e4ll in alternativen f\u00f6r ISY-integration:\n \u2022 Nodsensorstr\u00e4ng: Alla enheter eller mappar som inneh\u00e5ller 'Nodsensorstr\u00e4ng' i namnet kommer att behandlas som en sensor eller bin\u00e4r sensor.\n \u2022 Ignorera str\u00e4ng: Alla enheter med 'Ignorera str\u00e4ng' i namnet kommer att ignoreras.\n \u2022 Variabel sensorstr\u00e4ng: Varje variabel som inneh\u00e5ller 'Variabel sensorstr\u00e4ng' kommer att l\u00e4ggas till som en sensor.\n \u2022 \u00c5terst\u00e4ll ljusstyrka: Om den \u00e4r aktiverad kommer den tidigare ljusstyrkan att \u00e5terst\u00e4llas n\u00e4r du sl\u00e5r p\u00e5 en lampa ist\u00e4llet f\u00f6r enhetens inbyggda On-Level.", "title": "ISY alternativ" } } + }, + "system_health": { + "info": { + "device_connected": "ISY ansluten", + "host_reachable": "V\u00e4rden kan n\u00e5s", + "last_heartbeat": "Tid f\u00f6r senaste hj\u00e4rtslag", + "websocket_status": "Status f\u00f6r h\u00e4ndelseuttag" + } } } \ No newline at end of file diff --git a/homeassistant/components/izone/translations/sl.json b/homeassistant/components/izone/translations/sl.json index 6ce860a74af..5e42482248e 100644 --- a/homeassistant/components/izone/translations/sl.json +++ b/homeassistant/components/izone/translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "V omre\u017eju ni najdenih naprav iZone.", + "no_devices_found": "V omre\u017eju ni mogo\u010de najti nobene naprave", "single_instance_allowed": "Potrebna je samo ena konfiguracija iZone." }, "step": { diff --git a/homeassistant/components/jellyfin/translations/sv.json b/homeassistant/components/jellyfin/translations/sv.json index 23c825f256f..c48e92fa5f9 100644 --- a/homeassistant/components/jellyfin/translations/sv.json +++ b/homeassistant/components/jellyfin/translations/sv.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/keenetic_ndms2/translations/sv.json b/homeassistant/components/keenetic_ndms2/translations/sv.json index df37b421209..1e263250636 100644 --- a/homeassistant/components/keenetic_ndms2/translations/sv.json +++ b/homeassistant/components/keenetic_ndms2/translations/sv.json @@ -22,9 +22,11 @@ "step": { "user": { "data": { + "consider_home": "Intervall f\u00f6r att betraktas som hemma", "include_arp": "Anv\u00e4nd ARP-data (ignoreras om hotspot-data anv\u00e4nds)", "include_associated": "Anv\u00e4nd WiFi AP-associationsdata (ignoreras om hotspotdata anv\u00e4nds)", "interfaces": "V\u00e4lj gr\u00e4nssnitt att skanna", + "scan_interval": "Skanningsintervall", "try_hotspot": "Anv\u00e4nd \"ip hotspot\"-data (mest korrekt)" } } diff --git a/homeassistant/components/kmtronic/translations/sv.json b/homeassistant/components/kmtronic/translations/sv.json index 8ec6cc04dc9..c5d6f4a0b82 100644 --- a/homeassistant/components/kmtronic/translations/sv.json +++ b/homeassistant/components/kmtronic/translations/sv.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { "host": "V\u00e4rd", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index f5986f966d0..2c2d6f7d14d 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -18,10 +18,15 @@ } }, "routing": { + "data": { + "multicast_group": "Multicast-grupp", + "multicast_port": "Multicast-port" + }, "data_description": { "individual_address": "KNX-adress som ska anv\u00e4ndas av Home Assistant, t.ex. `0.0.4`", "local_ip": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt." - } + }, + "description": "Konfigurera routningsalternativen." }, "secure_knxkeys": { "data": { @@ -53,12 +58,32 @@ "secure_knxkeys": "Anv\u00e4nd en fil `.knxkeys` som inneh\u00e5ller s\u00e4kra IP-nycklar.", "secure_manual": "Konfigurera s\u00e4kra IP nycklar manuellt" } + }, + "tunnel": { + "data": { + "gateway": "KNX-tunnelanslutning" + }, + "description": "V\u00e4lj en gateway fr\u00e5n listan." + }, + "type": { + "data": { + "connection_type": "KNX anslutningstyp" + }, + "description": "Ange vilken anslutningstyp vi ska anv\u00e4nda f\u00f6r din KNX-anslutning.\n AUTOMATISK - Integrationen tar hand om anslutningen till din KNX Bus genom att utf\u00f6ra en gateway-skanning.\n TUNNELING - Integrationen kommer att ansluta till din KNX-buss via tunnling.\n ROUTING - Integrationen kommer att ansluta till din KNX-buss via routing." } } }, "options": { "step": { "init": { + "data": { + "connection_type": "KNX anslutningstyp", + "individual_address": "Enskild standardadress", + "multicast_group": "Multicast-grupp", + "multicast_port": "Multicast-port", + "rate_limit": "Hastighetsgr\u00e4ns", + "state_updater": "Tillst\u00e5ndsuppdaterare" + }, "data_description": { "individual_address": "KNX-adress som ska anv\u00e4ndas av Home Assistant, t.ex. `0.0.4`", "local_ip": "Anv\u00e4nd `0.0.0.0.0` f\u00f6r automatisk identifiering.", @@ -70,6 +95,8 @@ }, "tunnel": { "data": { + "host": "V\u00e4rd", + "port": "Port", "tunneling_type": "KNX tunneltyp" }, "data_description": { diff --git a/homeassistant/components/kodi/translations/sv.json b/homeassistant/components/kodi/translations/sv.json index 9102efc653e..b65fb101fd2 100644 --- a/homeassistant/components/kodi/translations/sv.json +++ b/homeassistant/components/kodi/translations/sv.json @@ -1,16 +1,44 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", "step": { "credentials": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen ange ditt Kodi-anv\u00e4ndarnamn och l\u00f6senord. Dessa finns i System/Inst\u00e4llningar/N\u00e4tverk/Tj\u00e4nster." + }, + "discovery_confirm": { + "description": "Vill du l\u00e4gga till Kodi (` {name} `) till Home Assistant?", + "title": "Uppt\u00e4ckte Kodi" }, "user": { "data": { "host": "V\u00e4rd" } + }, + "ws_port": { + "data": { + "ws_port": "Port" + }, + "description": "WebSocket-porten (kallas ibland TCP-port i Kodi). F\u00f6r att ansluta \u00f6ver WebSocket m\u00e5ste du aktivera \"Till\u00e5t program ... att styra Kodi\" i System/Inst\u00e4llningar/N\u00e4tverk/Tj\u00e4nster. Om WebSocket inte \u00e4r aktiverat, ta bort porten och l\u00e4mna tom." } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} ombads att st\u00e4ngas av", + "turn_on": "{entity_name} har ombetts att sl\u00e5 p\u00e5" + } } } \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/he.json b/homeassistant/components/konnected/translations/he.json index 622c73e2e61..ab4184fa777 100644 --- a/homeassistant/components/konnected/translations/he.json +++ b/homeassistant/components/konnected/translations/he.json @@ -31,7 +31,7 @@ }, "options_digital": { "data": { - "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + "name": "\u05e9\u05dd" } }, "options_io": { diff --git a/homeassistant/components/konnected/translations/sv.json b/homeassistant/components/konnected/translations/sv.json index 3e236f05952..e8130afd424 100644 --- a/homeassistant/components/konnected/translations/sv.json +++ b/homeassistant/components/konnected/translations/sv.json @@ -87,7 +87,8 @@ "data": { "api_host": "\u00c5sidos\u00e4tt API-v\u00e4rdens URL", "blink": "Blinka p\u00e5 panel-LED n\u00e4r du skickar tillst\u00e5nds\u00e4ndring", - "discovery": "Svara p\u00e5 uppt\u00e4cktsf\u00f6rfr\u00e5gningar i ditt n\u00e4tverk" + "discovery": "Svara p\u00e5 uppt\u00e4cktsf\u00f6rfr\u00e5gningar i ditt n\u00e4tverk", + "override_api_host": "\u00c5sidos\u00e4tt standardwebbadress f\u00f6r Home Assistant API-v\u00e4rdpanel" }, "description": "V\u00e4lj \u00f6nskat beteende f\u00f6r din panel", "title": "Konfigurera \u00d6vrigt" diff --git a/homeassistant/components/kraken/translations/sv.json b/homeassistant/components/kraken/translations/sv.json new file mode 100644 index 00000000000..7476ca4b29a --- /dev/null +++ b/homeassistant/components/kraken/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsintervall", + "tracked_asset_pairs": "Sp\u00e5rade tillg\u00e5ngspar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/he.json b/homeassistant/components/lacrosse_view/translations/he.json new file mode 100644 index 00000000000..fe6357d0150 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "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/lg_soundbar/translations/he.json b/homeassistant/components/lg_soundbar/translations/he.json new file mode 100644 index 00000000000..25fe66938d7 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/he.json b/homeassistant/components/life360/translations/he.json index e6fa6cd1db9..e4998f86963 100644 --- a/homeassistant/components/life360/translations/he.json +++ b/homeassistant/components/life360/translations/he.json @@ -1,16 +1,25 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "invalid_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index ac104f24f39..77bb8815340 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -11,6 +11,7 @@ "error": { "already_configured": "Konto har redan konfigurerats", "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "invalid_username": "Ogiltigt anv\u00e4ndarnmn", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/lifx/translations/he.json b/homeassistant/components/lifx/translations/he.json index 380dbc5d7fc..0ea2e6e551b 100644 --- a/homeassistant/components/lifx/translations/he.json +++ b/homeassistant/components/lifx/translations/he.json @@ -1,8 +1,21 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{label} ({host}) {serial}", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/sv.json b/homeassistant/components/litejet/translations/sv.json new file mode 100644 index 00000000000..f865c5a2c6a --- /dev/null +++ b/homeassistant/components/litejet/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sv.json b/homeassistant/components/litterrobot/translations/sv.json index 23c825f256f..939b543adea 100644 --- a/homeassistant/components/litterrobot/translations/sv.json +++ b/homeassistant/components/litterrobot/translations/sv.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/local_ip/translations/sl.json b/homeassistant/components/local_ip/translations/sl.json index 06c5f4182d0..0294e61515c 100644 --- a/homeassistant/components/local_ip/translations/sl.json +++ b/homeassistant/components/local_ip/translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Dovoljena je samo ena konfiguracija lokalnega IP-ja." + "single_instance_allowed": "\u017de konfigurirano. Mo\u017ena je samo ena konfiguracija." }, "step": { "user": { diff --git a/homeassistant/components/logi_circle/translations/sv.json b/homeassistant/components/logi_circle/translations/sv.json index 0d4d01c062b..232b9448ce1 100644 --- a/homeassistant/components/logi_circle/translations/sv.json +++ b/homeassistant/components/logi_circle/translations/sv.json @@ -1,11 +1,15 @@ { "config": { "abort": { + "already_configured": "Konto har redan konfigurerats", "external_error": "Undantag intr\u00e4ffade fr\u00e5n ett annat fl\u00f6de.", - "external_setup": "Logi Circle har konfigurerats fr\u00e5n ett annat fl\u00f6de." + "external_setup": "Logi Circle har konfigurerats fr\u00e5n ett annat fl\u00f6de.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen." }, "error": { - "follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera innan du trycker p\u00e5 Skicka." + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera innan du trycker p\u00e5 Skicka.", + "invalid_auth": "Ogiltig autentisering" }, "step": { "auth": { diff --git a/homeassistant/components/lookin/translations/sv.json b/homeassistant/components/lookin/translations/sv.json new file mode 100644 index 00000000000..6f9d5f8cc71 --- /dev/null +++ b/homeassistant/components/lookin/translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Namn" + } + }, + "discovery_confirm": { + "description": "Vill du konfigurera {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "IP-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/sv.json b/homeassistant/components/lovelace/translations/sv.json index c0b5bf9f948..60b790b184f 100644 --- a/homeassistant/components/lovelace/translations/sv.json +++ b/homeassistant/components/lovelace/translations/sv.json @@ -2,6 +2,8 @@ "system_health": { "info": { "dashboards": "Kontrollpaneler", + "mode": "L\u00e4ge", + "resources": "Resurser", "views": "Vyer" } } diff --git a/homeassistant/components/lutron_caseta/translations/sv.json b/homeassistant/components/lutron_caseta/translations/sv.json index 1d512e32d47..3f8e2de7d0f 100644 --- a/homeassistant/components/lutron_caseta/translations/sv.json +++ b/homeassistant/components/lutron_caseta/translations/sv.json @@ -1,6 +1,29 @@ { + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "not_lutron_device": "Uppt\u00e4ckt enhet \u00e4r inte en Lutron-enhet" + }, + "flow_title": "{name} ({host})", + "step": { + "link": { + "description": "F\u00f6r att para med {name} ( {host} ), efter att ha skickat in detta formul\u00e4r, tryck p\u00e5 den svarta knappen p\u00e5 baksidan av bryggan.", + "title": "Para ihop med bryggan" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Ange enhetens IP-adress.", + "title": "Automatisk anslutning till bryggan" + } + } + }, "device_automation": { "trigger_subtype": { + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", "close_1": "St\u00e4ng 1", "close_2": "St\u00e4ng 2", "close_3": "St\u00e4ng 3", diff --git a/homeassistant/components/mazda/translations/sv.json b/homeassistant/components/mazda/translations/sv.json index 24f538688bd..c71d7b4faa4 100644 --- a/homeassistant/components/mazda/translations/sv.json +++ b/homeassistant/components/mazda/translations/sv.json @@ -1,5 +1,15 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "account_locked": "Kontot \u00e4r l\u00e5st. V\u00e4nligen f\u00f6rs\u00f6k igen senare.", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/media_player/translations/sv.json b/homeassistant/components/media_player/translations/sv.json index 159ff201fba..e84edf74bb0 100644 --- a/homeassistant/components/media_player/translations/sv.json +++ b/homeassistant/components/media_player/translations/sv.json @@ -10,6 +10,9 @@ }, "trigger_type": { "buffering": "{entity_name} b\u00f6rjar buffra", + "idle": "{entity_name} blir inaktiv", + "paused": "{entity_name} \u00e4r pausad", + "playing": "{entity_name} b\u00f6rjar spela", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/met/translations/sl.json b/homeassistant/components/met/translations/sl.json index 09e48c1238e..ac64e2d8e53 100644 --- a/homeassistant/components/met/translations/sl.json +++ b/homeassistant/components/met/translations/sl.json @@ -6,7 +6,7 @@ "elevation": "Nadmorska vi\u0161ina", "latitude": "Zemljepisna \u0161irina", "longitude": "Zemljepisna dol\u017eina", - "name": "Ime" + "name": "Naziv" }, "description": "Meteorolo\u0161ki institut", "title": "Lokacija" diff --git a/homeassistant/components/metoffice/translations/sv.json b/homeassistant/components/metoffice/translations/sv.json index f4a63bb449d..b16a23db7f2 100644 --- a/homeassistant/components/metoffice/translations/sv.json +++ b/homeassistant/components/metoffice/translations/sv.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Latitud och longitud kommer att anv\u00e4ndas f\u00f6r att hitta n\u00e4rmaste v\u00e4derstation.", + "title": "Anslut till UK Met Office" } } } diff --git a/homeassistant/components/mikrotik/translations/sv.json b/homeassistant/components/mikrotik/translations/sv.json index 39e645a4c03..bc93490db14 100644 --- a/homeassistant/components/mikrotik/translations/sv.json +++ b/homeassistant/components/mikrotik/translations/sv.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Anslutningen misslyckades", + "invalid_auth": "Ogiltig autentisering", "name_exists": "Namnet finns" }, "step": { diff --git a/homeassistant/components/mill/translations/sv.json b/homeassistant/components/mill/translations/sv.json index 5c7362eeb7b..a7c8f4e0ea3 100644 --- a/homeassistant/components/mill/translations/sv.json +++ b/homeassistant/components/mill/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "error": { "cannot_connect": "Det gick inte att ansluta." }, diff --git a/homeassistant/components/moat/translations/he.json b/homeassistant/components/moat/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/moat/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/sv.json b/homeassistant/components/mobile_app/translations/sv.json index 4fd209e10cf..8757116010a 100644 --- a/homeassistant/components/mobile_app/translations/sv.json +++ b/homeassistant/components/mobile_app/translations/sv.json @@ -9,5 +9,10 @@ } } }, + "device_automation": { + "action_type": { + "notify": "Skicka ett meddelande" + } + }, "title": "Mobilapp" } \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/sv.json b/homeassistant/components/modem_callerid/translations/sv.json index 584cc33c277..531a1029e21 100644 --- a/homeassistant/components/modem_callerid/translations/sv.json +++ b/homeassistant/components/modem_callerid/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "no_devices_found": "Inga \u00e5terst\u00e5ende enheter hittades" }, "error": { diff --git a/homeassistant/components/monoprice/translations/sl.json b/homeassistant/components/monoprice/translations/sl.json index 02713888762..3f3b3cbb325 100644 --- a/homeassistant/components/monoprice/translations/sl.json +++ b/homeassistant/components/monoprice/translations/sl.json @@ -4,7 +4,7 @@ "already_configured": "Naprava je \u017ee konfigurirana" }, "error": { - "cannot_connect": "Povezava ni uspela, poskusite znova", + "cannot_connect": "Povezava ni uspela", "unknown": "Nepri\u010dakovana napaka" }, "step": { diff --git a/homeassistant/components/motion_blinds/translations/he.json b/homeassistant/components/motion_blinds/translations/he.json index 3cf199985e5..e41857361b9 100644 --- a/homeassistant/components/motion_blinds/translations/he.json +++ b/homeassistant/components/motion_blinds/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e2\u05d5\u05d3\u05db\u05e0\u05d5", + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "connection_error": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, diff --git a/homeassistant/components/motion_blinds/translations/sv.json b/homeassistant/components/motion_blinds/translations/sv.json index 5cee7f232c8..1deca42bc26 100644 --- a/homeassistant/components/motion_blinds/translations/sv.json +++ b/homeassistant/components/motion_blinds/translations/sv.json @@ -20,5 +20,14 @@ "description": "K\u00f6r installationen igen om du vill ansluta ytterligare Motion Gateways" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "V\u00e4nta p\u00e5 multicast push vid uppdatering" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/sv.json b/homeassistant/components/motioneye/translations/sv.json index 75bf4f2e2f9..38517e7571e 100644 --- a/homeassistant/components/motioneye/translations/sv.json +++ b/homeassistant/components/motioneye/translations/sv.json @@ -1,9 +1,23 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_url": "Ogiltig URL", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "admin_username": "Admin Anv\u00e4ndarnamn" + "admin_password": "Admin L\u00f6senord", + "admin_username": "Admin Anv\u00e4ndarnamn", + "surveillance_password": "\u00d6vervakning L\u00f6senord", + "surveillance_username": "\u00d6vervakning [%key:common::config_flow::data::anv\u00e4ndarnamn%]", + "url": "URL" } } } diff --git a/homeassistant/components/nam/translations/sv.json b/homeassistant/components/nam/translations/sv.json index ffa62b8fae2..55d985814df 100644 --- a/homeassistant/components/nam/translations/sv.json +++ b/homeassistant/components/nam/translations/sv.json @@ -2,10 +2,12 @@ "config": { "abort": { "device_unsupported": "Enheten st\u00f6ds ej", + "reauth_successful": "\u00c5terautentisering lyckades", "reauth_unsuccessful": "\u00c5terautentiseringen misslyckades. Ta bort integrationen och konfigurera den igen." }, "error": { "cannot_connect": "Det gick inte att ansluta ", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index 743b80f69a1..7c41202e85e 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 5cc9d3d68c0..1e2082c4a32 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -6,14 +6,20 @@ "abort": { "already_configured": "Konto har redan konfigurerats", "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", - "reauth_successful": "\u00c5terautentisering lyckades" + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "reauth_successful": "\u00c5terautentisering lyckades", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "error": { "internal_error": "Internt fel vid validering av kod", + "invalid_pin": "Ogiltig Pin-kod", "timeout": "Timeout vid valididering av kod", "unknown": "Ok\u00e4nt fel vid validering av kod" }, "step": { + "auth": { + "title": "L\u00e4nka Google-konto" + }, "auth_upgrade": { "description": "App Auth har fasats ut av Google f\u00f6r att f\u00f6rb\u00e4ttra s\u00e4kerheten, och du m\u00e5ste vidta \u00e5tg\u00e4rder genom att skapa nya applikationsuppgifter. \n\n \u00d6ppna [dokumentationen]( {more_info_url} ) f\u00f6r att f\u00f6lja med eftersom n\u00e4sta steg guidar dig genom stegen du beh\u00f6ver ta f\u00f6r att \u00e5terst\u00e4lla \u00e5tkomsten till dina Nest-enheter.", "title": "Nest: Utfasning av appautentisering" diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index 2fe3a86b0c8..cd91ea65c81 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "reauth_successful": "\u00c5terautentisering lyckades", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "create_entry": { @@ -12,6 +13,10 @@ "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "description": "Netatmo-integrationen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" } } }, diff --git a/homeassistant/components/nexia/translations/sv.json b/homeassistant/components/nexia/translations/sv.json index b00dc6e93b2..554d73169c6 100644 --- a/homeassistant/components/nexia/translations/sv.json +++ b/homeassistant/components/nexia/translations/sv.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Varum\u00e4rke", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/nextdns/translations/he.json b/homeassistant/components/nextdns/translations/he.json new file mode 100644 index 00000000000..f4563189497 --- /dev/null +++ b/homeassistant/components/nextdns/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "profiles": { + "data": { + "profile": "\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc" + } + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/sv.json b/homeassistant/components/nfandroidtv/translations/sv.json index c832c7a8880..f545dbda351 100644 --- a/homeassistant/components/nfandroidtv/translations/sv.json +++ b/homeassistant/components/nfandroidtv/translations/sv.json @@ -12,7 +12,8 @@ "data": { "host": "V\u00e4rd", "name": "Namn" - } + }, + "description": "Se dokumentationen f\u00f6r att s\u00e4kerst\u00e4lla att alla krav \u00e4r uppfyllda." } } } diff --git a/homeassistant/components/nightscout/translations/sv.json b/homeassistant/components/nightscout/translations/sv.json index d51243a77f1..ec8da0ee70f 100644 --- a/homeassistant/components/nightscout/translations/sv.json +++ b/homeassistant/components/nightscout/translations/sv.json @@ -5,13 +5,17 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "api_key": "API-nyckel", "url": "URL" - } + }, + "description": "- URL: adressen till din nightscout-instans. Det vill s\u00e4ga: https://myhomeassistant.duckdns.org:5423\n- API-nyckel (valfritt): Anv\u00e4nd endast om din instans \u00e4r skyddad (auth_default_roles != readable).", + "title": "Ange din Nightscout-serverinformation." } } } diff --git a/homeassistant/components/nina/translations/he.json b/homeassistant/components/nina/translations/he.json index 22681442909..d1c0f783903 100644 --- a/homeassistant/components/nina/translations/he.json +++ b/homeassistant/components/nina/translations/he.json @@ -7,5 +7,11 @@ "cannot_connect": "\u05d4\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" } + }, + "options": { + "error": { + "cannot_connect": "\u05d4\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" + } } } \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/sl.json b/homeassistant/components/nuheat/translations/sl.json index e64f7d1d381..ac3d3716357 100644 --- a/homeassistant/components/nuheat/translations/sl.json +++ b/homeassistant/components/nuheat/translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Termostat je \u017ee konfiguriran" + "already_configured": "Naprava je \u017ee konfigurirana" }, "error": { "cannot_connect": "Povezava ni uspela, poskusite znova", diff --git a/homeassistant/components/octoprint/translations/sv.json b/homeassistant/components/octoprint/translations/sv.json index 08b58e15cc6..af10a29c982 100644 --- a/homeassistant/components/octoprint/translations/sv.json +++ b/homeassistant/components/octoprint/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "auth_failed": "Det gick inte att h\u00e4mta applikationens API-nyckel", + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/omnilogic/translations/sv.json b/homeassistant/components/omnilogic/translations/sv.json index 70e9ad8a483..407893a7018 100644 --- a/homeassistant/components/omnilogic/translations/sv.json +++ b/homeassistant/components/omnilogic/translations/sv.json @@ -1,8 +1,12 @@ { "config": { + "error": { + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } @@ -12,7 +16,8 @@ "step": { "init": { "data": { - "ph_offset": "pH-f\u00f6rskjutning (positiv eller negativ)" + "ph_offset": "pH-f\u00f6rskjutning (positiv eller negativ)", + "polling_interval": "Pollingintervall (i sekunder)" } } } diff --git a/homeassistant/components/opentherm_gw/translations/he.json b/homeassistant/components/opentherm_gw/translations/he.json index eddeffa2ed0..f1089662414 100644 --- a/homeassistant/components/opentherm_gw/translations/he.json +++ b/homeassistant/components/opentherm_gw/translations/he.json @@ -3,7 +3,8 @@ "error": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "id_exists": "\u05de\u05d6\u05d4\u05d4 \u05d4\u05e9\u05e2\u05e8 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd" + "id_exists": "\u05de\u05d6\u05d4\u05d4 \u05d4\u05e9\u05e2\u05e8 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" }, "step": { "init": { diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json index 64212920aa7..a0e156ae448 100644 --- a/homeassistant/components/openweathermap/translations/sv.json +++ b/homeassistant/components/openweathermap/translations/sv.json @@ -4,7 +4,8 @@ "already_configured": "OpenWeatherMap-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel" }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/he.json b/homeassistant/components/owntracks/translations/he.json index 10bd4cb9a41..82dddbc7034 100644 --- a/homeassistant/components/owntracks/translations/he.json +++ b/homeassistant/components/owntracks/translations/he.json @@ -3,6 +3,15 @@ "abort": { "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "create_entry": { + "default": "\n\u05d1\u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({android_url}), \u05dc\u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05e2\u05d3\u05e4\u05d5\u05ea -> \u05d7\u05d9\u05d1\u05d5\u05e8. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP \u05e4\u05e8\u05d8\u05d9\n - \u05de\u05d0\u05e8\u05d7: {webhook_url}\n - \u05d6\u05d9\u05d4\u05d5\u05d9:\n - \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n - \u05de\u05d6\u05d4\u05d4 \u05d4\u05ea\u05e7\u05df: `'<\u05e9\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05e9\u05dc\u05da>'`\n\n\u05d1\u05de\u05d5\u05e6\u05e8\u05d9 \u05d0\u05e4\u05dc, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({ios_url}), \u05dc\u05d4\u05e7\u05d9\u05e9 \u05e2\u05dc \u05d4\u05e1\u05de\u05dc (i) \u05d1\u05e4\u05d9\u05e0\u05d4 \u05d4\u05d9\u05de\u05e0\u05d9\u05ea \u05d4\u05e2\u05dc\u05d9\u05d5\u05e0\u05d4 -> \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP\n - \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8: {webhook_url}\n - \u05d4\u05e4\u05e2\u05dc\u05ea \u05d0\u05d9\u05de\u05d5\u05ea\n - \u05de\u05d6\u05d4\u05d4 \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n\n{secret}\n\n\u05e0\u05d9\u05ea\u05df \u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea [\u05d4\u05ea\u05d9\u05e2\u05d5\u05d3]({docs_url}) \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05d5\u05d5\u05d3\u05d0\u05d5\u05ea \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea OwnTracks?", + "title": "\u05d4\u05d2\u05d3\u05e8\u05ea OwnTracks" + } } } } \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/sv.json b/homeassistant/components/p1_monitor/translations/sv.json new file mode 100644 index 00000000000..2dcba36c76a --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + }, + "description": "Konfigurera P1 Monitor f\u00f6r att integrera med Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/sv.json b/homeassistant/components/panasonic_viera/translations/sv.json index 326c8c57dc5..a35794646b7 100644 --- a/homeassistant/components/panasonic_viera/translations/sv.json +++ b/homeassistant/components/panasonic_viera/translations/sv.json @@ -11,13 +11,17 @@ "pairing": { "data": { "pin": "Pin-kod" - } + }, + "description": "Ange Pin-kod som visas p\u00e5 din TV", + "title": "Ihopkoppling" }, "user": { "data": { "host": "IP-adress", "name": "Namn" - } + }, + "description": "Ange din Panasonic Viera TV:s IP-adress", + "title": "St\u00e4ll in din TV" } } } diff --git a/homeassistant/components/philips_js/translations/sv.json b/homeassistant/components/philips_js/translations/sv.json index 47b456c1ff7..290b8a9d82b 100644 --- a/homeassistant/components/philips_js/translations/sv.json +++ b/homeassistant/components/philips_js/translations/sv.json @@ -13,6 +13,7 @@ "data": { "pin": "PIN-kod" }, + "description": "Ange PIN-koden som visas p\u00e5 din TV", "title": "Para ihop" }, "user": { diff --git a/homeassistant/components/pi_hole/translations/sv.json b/homeassistant/components/pi_hole/translations/sv.json index a1a0a54f9af..589fe66fa9b 100644 --- a/homeassistant/components/pi_hole/translations/sv.json +++ b/homeassistant/components/pi_hole/translations/sv.json @@ -20,6 +20,7 @@ "name": "Namn", "port": "Port", "ssl": "Anv\u00e4nd ett SSL certifikat", + "statistics_only": "Endast statistik", "verify_ssl": "Verifiera SSL-certifikat" } } diff --git a/homeassistant/components/picnic/translations/sv.json b/homeassistant/components/picnic/translations/sv.json index 60959ec71fb..bff94714036 100644 --- a/homeassistant/components/picnic/translations/sv.json +++ b/homeassistant/components/picnic/translations/sv.json @@ -5,11 +5,14 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "country_code": "Landskod", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/plaato/translations/sl.json b/homeassistant/components/plaato/translations/sl.json index af6dd165225..2a8f1836def 100644 --- a/homeassistant/components/plaato/translations/sl.json +++ b/homeassistant/components/plaato/translations/sl.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Plaato Webhook?", + "description": "Ali \u017eelite za\u010deti z nastavitvijo?", "title": "Nastavite Plaato Webhook" } } diff --git a/homeassistant/components/plaato/translations/sv.json b/homeassistant/components/plaato/translations/sv.json index 6368ff7222e..fe6b9055d06 100644 --- a/homeassistant/components/plaato/translations/sv.json +++ b/homeassistant/components/plaato/translations/sv.json @@ -14,9 +14,40 @@ "no_auth_token": "Du m\u00e5ste l\u00e4gga till en autentiseringstoken" }, "step": { + "api_method": { + "data": { + "token": "Klistra in Auth Token h\u00e4r", + "use_webhook": "Anv\u00e4nd webhook" + }, + "description": "F\u00f6r att kunna fr\u00e5ga API:et kr\u00e4vs en `auth_token` som kan erh\u00e5llas genom att f\u00f6lja [dessa](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instruktioner \n\n Vald enhet: ** {device_type} ** \n\n Om du hellre anv\u00e4nder den inbyggda webhook-metoden (endast Airlock) markera rutan nedan och l\u00e4mna Auth Token tom", + "title": "V\u00e4lj API-metod" + }, "user": { + "data": { + "device_name": "Namnge din enhet", + "device_type": "Typ av Plaato-enhet" + }, "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Plaato Webhook?", "title": "Konfigurera Plaato Webhook" + }, + "webhook": { + "description": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Plaato Airlock.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) f\u00f6r mer information.", + "title": "Webhook att anv\u00e4nda" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Uppdateringsintervall (minuter)" + }, + "description": "St\u00e4ll in uppdateringsintervallet (minuter)", + "title": "Alternativ f\u00f6r Plaato" + }, + "webhook": { + "description": "Webhook info: \n\n - URL: ` {webhook_url} `\n - Metod: POST \n\n", + "title": "Alternativ f\u00f6r Plaato Airlock" } } } diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index bd5b5570ac0..c79fdd881fc 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -4,6 +4,7 @@ "all_configured": "Alla l\u00e4nkade servrar har redan konfigurerats", "already_configured": "Denna Plex-server \u00e4r redan konfigurerad", "already_in_progress": "Plex konfigureras", + "reauth_successful": "\u00c5terautentisering lyckades", "token_request_timeout": "Timeout att erh\u00e5lla token", "unknown": "Misslyckades av ok\u00e4nd anledning" }, @@ -22,7 +23,8 @@ "ssl": "Anv\u00e4nd ett SSL certifikat", "token": "Token (valfritt)", "verify_ssl": "Verifiera SSL-certifikat" - } + }, + "title": "Manuell Plex-konfiguration" }, "select_server": { "data": { @@ -31,6 +33,9 @@ "description": "V\u00e4lj flera servrar tillg\u00e4ngliga, v\u00e4lj en:", "title": "V\u00e4lj Plex-server" }, + "user": { + "description": "Forts\u00e4tt till [plex.tv](https://plex.tv) f\u00f6r att l\u00e4nka en Plex-server." + }, "user_advanced": { "data": { "setup_method": "Inst\u00e4llningsmetod" diff --git a/homeassistant/components/plugwise/translations/he.json b/homeassistant/components/plugwise/translations/he.json index db3eeef2d53..f8a4c722a8f 100644 --- a/homeassistant/components/plugwise/translations/he.json +++ b/homeassistant/components/plugwise/translations/he.json @@ -11,6 +11,10 @@ "flow_title": "{name}", "step": { "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "port": "\u05e4\u05ea\u05d7\u05d4" + }, "description": "\u05de\u05d5\u05e6\u05e8:" }, "user_gateway": { diff --git a/homeassistant/components/powerwall/translations/sv.json b/homeassistant/components/powerwall/translations/sv.json index fd0d0eb86c8..bc4dcc606ff 100644 --- a/homeassistant/components/powerwall/translations/sv.json +++ b/homeassistant/components/powerwall/translations/sv.json @@ -10,6 +10,7 @@ "unknown": "Ov\u00e4ntat fel", "wrong_version": "Powerwall anv\u00e4nder en programvaruversion som inte st\u00f6ds. T\u00e4nk p\u00e5 att uppgradera eller rapportera det h\u00e4r problemet s\u00e5 att det kan l\u00f6sas." }, + "flow_title": "{name} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/progettihwsw/translations/sv.json b/homeassistant/components/progettihwsw/translations/sv.json index c7c31766d08..2c4573174a5 100644 --- a/homeassistant/components/progettihwsw/translations/sv.json +++ b/homeassistant/components/progettihwsw/translations/sv.json @@ -14,8 +14,27 @@ "relay_10": "Rel\u00e4 10", "relay_11": "Rel\u00e4 11", "relay_12": "Rel\u00e4 12", - "relay_13": "Rel\u00e4 13" - } + "relay_13": "Rel\u00e4 13", + "relay_14": "Rel\u00e4 14", + "relay_15": "Rel\u00e4 15", + "relay_16": "Rel\u00e4 16", + "relay_2": "Rel\u00e4 2", + "relay_3": "Rel\u00e4 3", + "relay_4": "Rel\u00e4 4", + "relay_5": "Rel\u00e4 5", + "relay_6": "Rel\u00e4 6", + "relay_7": "Rel\u00e4 7", + "relay_8": "Rel\u00e4 8", + "relay_9": "Rel\u00e4 9" + }, + "title": "St\u00e4ll in rel\u00e4er" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "title": "St\u00e4ll in kort" } } } diff --git a/homeassistant/components/ps4/translations/sv.json b/homeassistant/components/ps4/translations/sv.json index 7435865c5d0..26af627749d 100644 --- a/homeassistant/components/ps4/translations/sv.json +++ b/homeassistant/components/ps4/translations/sv.json @@ -1,12 +1,14 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "credential_error": "Fel n\u00e4r f\u00f6rs\u00f6ker h\u00e4mta autentiseringsuppgifter.", "no_devices_found": "Inga PlayStation 4 enheter hittades p\u00e5 n\u00e4tverket.", "port_987_bind_error": "Kunde inte binda till port 987.", "port_997_bind_error": "Kunde inte binda till port 997." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "credential_timeout": "Autentiseringstj\u00e4nsten orsakade timeout. Tryck p\u00e5 Skicka f\u00f6r att starta om.", "login_failed": "Misslyckades med att para till PlayStation 4. Verifiera PIN-koden \u00e4r korrekt.", "no_ipaddress": "Ange IP-adressen f\u00f6r PlayStation 4 du vill konfigurera." diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json index e45b374e5da..fe05a9cbfd5 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json @@ -7,6 +7,19 @@ "user": { "data": { "name": "Sensornamn", + "power": "Kontrakterad effekt (kW)", + "power_p3": "Kontrakterad effekt f\u00f6r dalperiod P3 (kW)", + "tariff": "Till\u00e4mplig taxa per geografisk zon" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Kontrakterad effekt (kW)", + "power_p3": "Kontrakterad effekt f\u00f6r dalperiod P3 (kW)", "tariff": "Till\u00e4mplig taxa per geografisk zon" } } diff --git a/homeassistant/components/qnap_qsw/translations/he.json b/homeassistant/components/qnap_qsw/translations/he.json index fbe984e0b32..5d4ffba2b2b 100644 --- a/homeassistant/components/qnap_qsw/translations/he.json +++ b/homeassistant/components/qnap_qsw/translations/he.json @@ -8,6 +8,12 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "discovered_connection": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/rachio/translations/sl.json b/homeassistant/components/rachio/translations/sl.json index a7febddee05..20f523d8d5a 100644 --- a/homeassistant/components/rachio/translations/sl.json +++ b/homeassistant/components/rachio/translations/sl.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_key": "Klju\u010d API za ra\u010dun Rachio." + "api_key": "API Klju\u010d" }, "description": "Potrebovali boste API klju\u010d iz https://app.rach.io/. Izberite ' nastavitve ra\u010duna in kliknite 'get API KEY'.", "title": "Pove\u017eite se z napravo Rachio" diff --git a/homeassistant/components/radiotherm/translations/he.json b/homeassistant/components/radiotherm/translations/he.json index 77232a68dd2..bdf76e7c217 100644 --- a/homeassistant/components/radiotherm/translations/he.json +++ b/homeassistant/components/radiotherm/translations/he.json @@ -3,6 +3,17 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, - "flow_title": "{name} {model} ({host})" + "error": { + "cannot_connect": "\u05d4\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" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/sv.json b/homeassistant/components/rainforest_eagle/translations/sv.json index eba844f6c03..80a0ca5600a 100644 --- a/homeassistant/components/rainforest_eagle/translations/sv.json +++ b/homeassistant/components/rainforest_eagle/translations/sv.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "host": "V\u00e4rd" + "cloud_id": "Moln-ID", + "host": "V\u00e4rd", + "install_code": "Installationskod" } } } diff --git a/homeassistant/components/rdw/translations/sv.json b/homeassistant/components/rdw/translations/sv.json new file mode 100644 index 00000000000..cdb6aeb44be --- /dev/null +++ b/homeassistant/components/rdw/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown_license_plate": "Ok\u00e4nd registreringsskylt" + }, + "step": { + "user": { + "data": { + "license_plate": "Registreringsskylt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json index 304513880e6..be9e6e944a7 100644 --- a/homeassistant/components/rfxtrx/translations/sv.json +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + "already_configured": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." }, "step": { "setup_network": { diff --git a/homeassistant/components/rhasspy/translations/he.json b/homeassistant/components/rhasspy/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json index 1f7f95730d1..662583561d0 100644 --- a/homeassistant/components/risco/translations/sv.json +++ b/homeassistant/components/risco/translations/sv.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", + "pin": "Pin-kod", "username": "Anv\u00e4ndarnamn" } } @@ -10,8 +20,12 @@ }, "options": { "step": { + "ha_to_risco": { + "title": "Mappa Home Assistant tillst\u00e5nd till Risco tillst\u00e5nd" + }, "risco_to_ha": { "data": { + "A": "Grupp A", "B": "Grupp B", "C": "Grupp C", "D": "Grupp D", diff --git a/homeassistant/components/roku/translations/sl.json b/homeassistant/components/roku/translations/sl.json index bb39edf9753..6960298d192 100644 --- a/homeassistant/components/roku/translations/sl.json +++ b/homeassistant/components/roku/translations/sl.json @@ -5,7 +5,7 @@ "unknown": "Nepri\u010dakovana napaka" }, "error": { - "cannot_connect": "Povezava ni uspela, poskusite znova" + "cannot_connect": "Povezava ni uspela" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roomba/translations/sv.json b/homeassistant/components/roomba/translations/sv.json index 1c731cd2af9..349721d149a 100644 --- a/homeassistant/components/roomba/translations/sv.json +++ b/homeassistant/components/roomba/translations/sv.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Tryck och h\u00e5ll hemknappen p\u00e5 {name} tills enheten genererar ett ljud (cirka tv\u00e5 sekunder), skicka sedan inom 30 sekunder.", @@ -18,6 +19,7 @@ "data": { "password": "L\u00f6senord" }, + "description": "L\u00f6senordet kunde inte h\u00e4mtas fr\u00e5n enheten automatiskt. F\u00f6lj stegen som beskrivs i dokumentationen p\u00e5: {auth_help_url}", "title": "Ange l\u00f6senord" }, "manual": { diff --git a/homeassistant/components/ruckus_unleashed/translations/sv.json b/homeassistant/components/ruckus_unleashed/translations/sv.json index a265d988aaa..f85a02855d6 100644 --- a/homeassistant/components/ruckus_unleashed/translations/sv.json +++ b/homeassistant/components/ruckus_unleashed/translations/sv.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { "host": "V\u00e4rd", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index feff1c2fc86..67c8373d0e3 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -6,8 +6,12 @@ "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", "id_missing": "Denna Samsung-enhet har inget serienummer.", "not_supported": "Denna Samsung enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte.", + "reauth_successful": "\u00c5terautentisering lyckades", "unknown": "Ov\u00e4ntat fel" }, + "error": { + "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant." + }, "flow_title": "{device}", "step": { "confirm": { @@ -16,6 +20,9 @@ "pairing": { "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." }, + "reauth_confirm": { + "description": "N\u00e4r du har skickat, acceptera popup-f\u00f6nstret p\u00e5 {enheten} som beg\u00e4r auktorisering inom 30 sekunder eller ange PIN-koden." + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress", diff --git a/homeassistant/components/scrape/translations/he.json b/homeassistant/components/scrape/translations/he.json index 463ce9035f4..6dd1e6845de 100644 --- a/homeassistant/components/scrape/translations/he.json +++ b/homeassistant/components/scrape/translations/he.json @@ -1,10 +1,33 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "step": { "user": { "data": { "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + }, + "data_description": { + "state_class": "\u05d4-state_class \u05e9\u05dc \u05d4\u05d7\u05d9\u05d9\u05e9\u05df" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + }, + "data_description": { + "state_class": "\u05d4-state_class \u05e9\u05dc \u05d4\u05d7\u05d9\u05d9\u05e9\u05df" } } } diff --git a/homeassistant/components/screenlogic/translations/sv.json b/homeassistant/components/screenlogic/translations/sv.json index 54ae7f2b212..251a00731d3 100644 --- a/homeassistant/components/screenlogic/translations/sv.json +++ b/homeassistant/components/screenlogic/translations/sv.json @@ -6,17 +6,32 @@ "error": { "cannot_connect": "Kunde inte ansluta" }, + "flow_title": "{name}", "step": { "gateway_entry": { "data": { + "ip_address": "IP-adress", "port": "Port" - } + }, + "description": "Ange din ScreenLogic Gateway-information.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "F\u00f6ljande ScreenLogic-gateways uppt\u00e4cktes. V\u00e4lj en att konfigurera, eller v\u00e4lj att manuellt konfigurera en ScreenLogic-gateway.", + "title": "ScreenLogic" } } }, "options": { "step": { "init": { + "data": { + "scan_interval": "Sekunder mellan skanningar" + }, + "description": "Ange inst\u00e4llningar f\u00f6r {gateway_name}", "title": "ScreenLogic" } } diff --git a/homeassistant/components/select/translations/sv.json b/homeassistant/components/select/translations/sv.json index d388cb6c622..6c5c5126002 100644 --- a/homeassistant/components/select/translations/sv.json +++ b/homeassistant/components/select/translations/sv.json @@ -1,7 +1,14 @@ { "device_automation": { + "action_type": { + "select_option": "\u00c4ndra alternativet {entity_name}" + }, "condition_type": { "selected_option": "Nuvarande {entity_name} markerad option" + }, + "trigger_type": { + "current_option_changed": "Alternativet {entity_name} har \u00e4ndrats" } - } + }, + "title": "V\u00e4lj" } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index 7f2dd33a023..fc0ba9b48c4 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -3,12 +3,35 @@ "condition_type": { "is_apparent_power": "\u05d4\u05e2\u05d5\u05e6\u05de\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} \u05de\u05e1\u05ea\u05de\u05e0\u05ea", "is_battery_level": "\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea \u05e9\u05dc {entity_name}", - "is_reactive_power": "\u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}" + "is_current": "\u05db\u05e2\u05ea {entity_name}", + "is_energy": "\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", + "is_gas": "\u05db\u05e2\u05ea {entity_name} \u05d2\u05d6", + "is_illuminance": "\u05e2\u05d5\u05e6\u05de\u05ea \u05d4\u05d0\u05e8\u05d4 {entity_name} \u05e0\u05d5\u05db\u05d7\u05d9\u05ea", + "is_pm1": "\u05e8\u05de\u05ea \u05d4\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} PM1", + "is_reactive_power": "\u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", + "is_temperature": "\u05db\u05e2\u05ea {entity_name} \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4" }, "trigger_type": { "apparent_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05dc\u05db\u05d0\u05d5\u05e8\u05d4", "battery_level": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4", - "reactive_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9" + "current": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05e0\u05d5\u05db\u05d7\u05d9\u05d9\u05dd", + "energy": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4", + "frequency": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05ea\u05d3\u05e8\u05d9\u05dd", + "gas": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d2\u05d6", + "humidity": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05dc\u05d7\u05d5\u05ea", + "illuminance": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e2\u05d5\u05e6\u05de\u05ea \u05d4\u05d0\u05e8\u05d4", + "nitrogen_dioxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d7\u05e0\u05e7\u05df \u05d4\u05d3\u05d5-\u05d7\u05de\u05e6\u05e0\u05d9", + "ozone": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d0\u05d5\u05d6\u05d5\u05df", + "pm1": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 PM1", + "pm10": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 PM10", + "pm25": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 PM2.5", + "power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7", + "power_factor": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05d2\u05d5\u05e8\u05dd \u05d4\u05d4\u05e1\u05e4\u05e7", + "pressure": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05dc\u05d7\u05e5", + "reactive_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9", + "temperature": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4", + "value": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e2\u05e8\u05da", + "voltage": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05ea\u05d7" } }, "state": { diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 0fae4fe1047..24b137a420b 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -33,15 +33,21 @@ "carbon_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koldioxidkoncentrationen", "carbon_monoxide": "{entity_name} f\u00f6r\u00e4ndringar av kolmonoxidkoncentrationen", "energy": "Energif\u00f6r\u00e4ndringar", + "frequency": "{entity_name} frekvens\u00e4ndringar", "gas": "{entity_name} gasf\u00f6r\u00e4ndringar", "humidity": "{entity_name} fuktighet \u00e4ndras", "illuminance": "{entity_name} belysning \u00e4ndras", "nitrogen_dioxide": "{entity_name} kv\u00e4vedioxidkoncentrationen f\u00f6r\u00e4ndras.", + "ozone": "{entity_name} ozonkoncentrationen f\u00f6r\u00e4ndras", + "pm1": "{entity_name} PM1-koncentrationsf\u00f6r\u00e4ndringar", + "pm10": "{entity_name} PM10-koncentrations\u00e4ndringar", + "pm25": "{entity_name} PM2.5-koncentrationsf\u00f6r\u00e4ndringar", "power": "{entity_name} effektf\u00f6r\u00e4ndringar", "power_factor": "effektfaktorf\u00f6r\u00e4ndringar", "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", "reactive_power": "{entity_name} reaktiv effekt\u00e4ndring", "signal_strength": "{entity_name} signalstyrka \u00e4ndras", + "sulphur_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koncentrationen av svaveldioxid", "temperature": "{entity_name} temperaturf\u00f6r\u00e4ndringar", "value": "{entity_name} v\u00e4rde \u00e4ndras", "volatile_organic_compounds": "{entity_name} koncentrations\u00e4ndringar av flyktiga organiska \u00e4mnen", diff --git a/homeassistant/components/sensorpush/translations/he.json b/homeassistant/components/sensorpush/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/sensorpush/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index 458f4be3b24..d4f3f6f400e 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "unsupported_firmware": "Enheten anv\u00e4nder en firmwareversion som inte st\u00f6ds." + }, "error": { "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support", "invalid_auth": "Ogiltig autentisering" @@ -10,7 +13,22 @@ "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } + }, + "user": { + "description": "F\u00f6re installationen m\u00e5ste batteridrivna enheter v\u00e4ckas, du kan nu v\u00e4cka enheten med en knapp p\u00e5 den." } } + }, + "device_automation": { + "trigger_subtype": { + "button4": "Fj\u00e4rde knappen" + }, + "trigger_type": { + "btn_down": "\"{subtype}\" knappen nedtryckt", + "btn_up": "\"{subtype}\" knappen uppsl\u00e4ppt", + "double_push": "{subtype} dubbeltryck", + "long_push": "{subtype} l\u00e5ngtryck", + "single_push": "{subtyp} enkeltryck" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index 205d3549a52..cb294813b88 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -19,6 +19,10 @@ } }, "issues": { + "deprecated_yaml": { + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Simplepush YAML configuration is being removed" + }, "removed_yaml": { "description": "Configuring Simplepush using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Simplepush YAML configuration has been removed" diff --git a/homeassistant/components/simplepush/translations/fr.json b/homeassistant/components/simplepush/translations/fr.json index 49356553426..e92ef0263d0 100644 --- a/homeassistant/components/simplepush/translations/fr.json +++ b/homeassistant/components/simplepush/translations/fr.json @@ -21,6 +21,9 @@ "issues": { "deprecated_yaml": { "title": "La configuration YAML pour Simplepush est en cours de suppression" + }, + "removed_yaml": { + "title": "La configuration YAML pour Simplepush a \u00e9t\u00e9 supprim\u00e9e" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/he.json b/homeassistant/components/simplepush/translations/he.json new file mode 100644 index 00000000000..880c7074257 --- /dev/null +++ b/homeassistant/components/simplepush/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/id.json b/homeassistant/components/simplepush/translations/id.json index 715cb3c893b..de54996d014 100644 --- a/homeassistant/components/simplepush/translations/id.json +++ b/homeassistant/components/simplepush/translations/id.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Proses konfigurasi Simplepush lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", "title": "Konfigurasi YAML Simplepush dalam proses penghapusan" + }, + "removed_yaml": { + "description": "Proses konfigurasi Simplepush lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Simplepush telah dihapus" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pt-BR.json b/homeassistant/components/simplepush/translations/pt-BR.json index f0a330ff1d3..90d21e1c44c 100644 --- a/homeassistant/components/simplepush/translations/pt-BR.json +++ b/homeassistant/components/simplepush/translations/pt-BR.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "A configura\u00e7\u00e3o do Simplepush usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Simplepush YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A configura\u00e7\u00e3o Simplepush YAML est\u00e1 sendo removida" + }, + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Simplepush usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do Simplepush do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Simplepush YAML foi removida" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 245bb18351e..baa167ca951 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", + "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, @@ -11,10 +12,28 @@ "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "Unexpected error" }, + "progress": { + "email_2fa": "Check your email for a verification link from Simplisafe." + }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please re-enter the password for {username}.", + "title": "Reauthenticate Integration" + }, + "sms_2fa": { + "data": { + "code": "Code" + }, + "description": "Input the two-factor authentication code sent to you via SMS." + }, "user": { "data": { - "auth_code": "Authorization Code" + "auth_code": "Authorization Code", + "password": "Password", + "username": "Username" }, "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL." } diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json index 85a9e002b77..70ab1cff6ac 100644 --- a/homeassistant/components/simplisafe/translations/he.json +++ b/homeassistant/components/simplisafe/translations/he.json @@ -12,7 +12,7 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05ea\u05d5\u05e7\u05e3 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05e4\u05d2 \u05d0\u05d5 \u05d1\u05d5\u05d8\u05dc. \u05d9\u05e9 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da.", + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username}.", "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" }, "sms_2fa": { @@ -23,7 +23,7 @@ "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05d3\u05d5\u05d0\"\u05dc" + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } } diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index e9919dc734b..4d5ddf18ed6 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Akun sudah terdaftar", "invalid_auth": "Autentikasi tidak valid", + "invalid_auth_code_length": "Panjang kode otorisasi SimpliSafe adalah 45 karakter", "unknown": "Kesalahan yang tidak diharapkan" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index 74b30d2a9ef..87e3c6f0303 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Conta j\u00e1 cadastrada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_auth_code_length": "Os c\u00f3digos de autoriza\u00e7\u00e3o SimpliSafe t\u00eam 45 caracteres", "unknown": "Erro inesperado" }, "progress": { @@ -34,7 +35,7 @@ "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "O SimpliSafe autentica os usu\u00e1rios por meio de seu aplicativo da web. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de come\u00e7ar. \n\n Quando estiver pronto, clique [aqui]( {url} ) para abrir o aplicativo Web SimpliSafe e insira suas credenciais. Quando o processo estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o da URL do aplicativo Web SimpliSafe." + "description": "O SimpliSafe autentica os usu\u00e1rios por meio de seu aplicativo da web. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de come\u00e7ar. \n\n Quando estiver pronto, clique [aqui]( {url} ) para abrir o aplicativo da web SimpliSafe e insira suas credenciais. Se voc\u00ea j\u00e1 fez login no SimpliSafe em seu navegador, voc\u00ea pode querer abrir uma nova guia e copiar/colar o URL acima nessa guia. \n\n Quando o processo estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o da URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index e7e8ec28715..2e24eb856bd 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -32,7 +32,8 @@ "auth_code": "Auktoriseringskod", "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "description": "SimpliSafe autentiserar anv\u00e4ndare via sin webbapp. P\u00e5 grund av tekniska begr\u00e4nsningar finns det ett manuellt steg i slutet av denna process; se till att du l\u00e4ser [dokumentationen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) innan du b\u00f6rjar. \n\n N\u00e4r du \u00e4r redo klickar du [h\u00e4r]( {url} ) f\u00f6r att \u00f6ppna webbappen SimpliSafe och ange dina referenser. N\u00e4r processen \u00e4r klar, \u00e5terv\u00e4nd hit och mata in auktoriseringskoden fr\u00e5n SimpliSafe-webbappens URL." } } }, diff --git a/homeassistant/components/skybell/translations/he.json b/homeassistant/components/skybell/translations/he.json index 28e8ddd34c9..21b9822e248 100644 --- a/homeassistant/components/skybell/translations/he.json +++ b/homeassistant/components/skybell/translations/he.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "user": { "data": { - "email": "\u05d3\u05d5\u05d0\"\u05dc" + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } } } diff --git a/homeassistant/components/smartthings/translations/sv.json b/homeassistant/components/smartthings/translations/sv.json index 21591e7c256..309ef659d56 100644 --- a/homeassistant/components/smartthings/translations/sv.json +++ b/homeassistant/components/smartthings/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "invalid_webhook_url": "Home Assistant \u00e4r inte korrekt konfigurerad f\u00f6r att ta emot uppdateringar fr\u00e5n SmartThings. Webhook-adressen \u00e4r ogiltig:\n > {webhook_url} \n\n Uppdatera din konfiguration enligt [instruktionerna]( {component_url} ), starta om Home Assistant och f\u00f6rs\u00f6k igen.", "no_available_locations": "Det finns inga tillg\u00e4ngliga SmartThings-platser att st\u00e4lla in i Home Assistant." }, "error": { diff --git a/homeassistant/components/smarttub/translations/sv.json b/homeassistant/components/smarttub/translations/sv.json index bfa44298487..9ebde76e717 100644 --- a/homeassistant/components/smarttub/translations/sv.json +++ b/homeassistant/components/smarttub/translations/sv.json @@ -8,6 +8,10 @@ "invalid_auth": "Ogiltig autentisering" }, "step": { + "reauth_confirm": { + "description": "SmartTub-integrationen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "email": "E-post", diff --git a/homeassistant/components/solarlog/translations/sl.json b/homeassistant/components/solarlog/translations/sl.json index 8f2682b3fb1..2e90b93f1ca 100644 --- a/homeassistant/components/solarlog/translations/sl.json +++ b/homeassistant/components/solarlog/translations/sl.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ime gostitelja ali ip naslov va\u0161e naprave Solar-Log", + "host": "Gostitelj", "name": "Predpona, ki jo \u017eelite uporabiti za senzorje Solar-log" }, "title": "Dolo\u010dite povezavo Solar-Log" diff --git a/homeassistant/components/somfy_mylink/translations/sv.json b/homeassistant/components/somfy_mylink/translations/sv.json index 9ab3fa7ec0c..6cacc18d1a9 100644 --- a/homeassistant/components/somfy_mylink/translations/sv.json +++ b/homeassistant/components/somfy_mylink/translations/sv.json @@ -3,6 +3,11 @@ "flow_title": "{mac} ({ip})", "step": { "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port", + "system_id": "System-ID" + }, "description": "System-ID kan erh\u00e5llas i MyLink-appen under Integration genom att v\u00e4lja vilken tj\u00e4nst som helst som inte kommer fr\u00e5n molnet." } } @@ -13,7 +18,14 @@ }, "step": { "init": { + "data": { + "target_id": "Konfigurera alternativ f\u00f6r ett skydd." + }, "title": "Konfigurera MyLink-alternativ" + }, + "target_config": { + "description": "Konfigurera alternativ f\u00f6r ` {target_name} `", + "title": "Konfigurera MyLink Cover" } } } diff --git a/homeassistant/components/sonarr/translations/sv.json b/homeassistant/components/sonarr/translations/sv.json index 9128ea57a38..c17fa818ed8 100644 --- a/homeassistant/components/sonarr/translations/sv.json +++ b/homeassistant/components/sonarr/translations/sv.json @@ -2,16 +2,33 @@ "config": { "abort": { "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades", "unknown": "Ov\u00e4ntat fel" }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "{name}", "step": { + "reauth_confirm": { + "description": "Sonarr-integrationen m\u00e5ste autentiseras manuellt med Sonarr API:n som finns p\u00e5: {url}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { - "api_key": "API nyckel" + "api_key": "API nyckel", + "verify_ssl": "Verifiera SSL-certifikat" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Antal kommande dagar att visa", + "wanted_max_items": "Max antal \u00f6nskade objekt att visa" } } } diff --git a/homeassistant/components/soundtouch/translations/he.json b/homeassistant/components/soundtouch/translations/he.json new file mode 100644 index 00000000000..25fe66938d7 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/he.json b/homeassistant/components/sql/translations/he.json index 9b9bda1ed3f..0e44d72da00 100644 --- a/homeassistant/components/sql/translations/he.json +++ b/homeassistant/components/sql/translations/he.json @@ -3,19 +3,31 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "db_url_invalid": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e1\u05d3 \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea" + }, "step": { "user": { "data": { "name": "\u05e9\u05dd" + }, + "data_description": { + "name": "\u05e9\u05dd \u05e9\u05d9\u05e9\u05de\u05e9 \u05dc\u05e2\u05e8\u05da Config \u05d5\u05d2\u05dd \u05dc\u05d7\u05d9\u05d9\u05e9\u05df" } } } }, "options": { + "error": { + "db_url_invalid": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e1\u05d3 \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea" + }, "step": { "init": { "data": { "name": "\u05e9\u05dd" + }, + "data_description": { + "name": "\u05e9\u05dd \u05e9\u05d9\u05e9\u05de\u05e9 \u05dc\u05e2\u05e8\u05da Config \u05d5\u05d2\u05dd \u05dc\u05d7\u05d9\u05d9\u05e9\u05df" } } } diff --git a/homeassistant/components/subaru/translations/sv.json b/homeassistant/components/subaru/translations/sv.json index 38d552ff4aa..e89f29fc04a 100644 --- a/homeassistant/components/subaru/translations/sv.json +++ b/homeassistant/components/subaru/translations/sv.json @@ -17,7 +17,8 @@ "data": { "pin": "PIN-kod" }, - "description": "Ange din MySubaru PIN-kod\n OBS: Alla fordon p\u00e5 kontot m\u00e5ste ha samma PIN-kod" + "description": "Ange din MySubaru PIN-kod\n OBS: Alla fordon p\u00e5 kontot m\u00e5ste ha samma PIN-kod", + "title": "Subaru Starlink-konfiguration" }, "two_factor": { "description": "Tv\u00e5faktorautentisering kr\u00e4vs", @@ -31,8 +32,23 @@ }, "user": { "data": { + "country": "V\u00e4lj land", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen ange dina MySubaru-uppgifter\n OBS: Initial installation kan ta upp till 30 sekunder", + "title": "Subaru Starlink-konfiguration" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Aktivera fordonsavl\u00e4sning" + }, + "description": "N\u00e4r den \u00e4r aktiverad skickar fordonsavl\u00e4sning ett fj\u00e4rrkommando till ditt fordon varannan timme f\u00f6r att f\u00e5 nya sensordata. Utan fordonsavl\u00e4sning tas nya sensordata endast emot n\u00e4r fordonet automatiskt skickar data (normalt efter motoravst\u00e4ngning).", + "title": "Subaru Starlink-alternativ" } } } diff --git a/homeassistant/components/switchbot/translations/he.json b/homeassistant/components/switchbot/translations/he.json index 836cd8b06b4..7f7974024b1 100644 --- a/homeassistant/components/switchbot/translations/he.json +++ b/homeassistant/components/switchbot/translations/he.json @@ -5,7 +5,7 @@ "cannot_connect": "\u05d4\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" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json index 6b1608c9da6..f2c86841487 100644 --- a/homeassistant/components/switchbot/translations/sv.json +++ b/homeassistant/components/switchbot/translations/sv.json @@ -1,9 +1,31 @@ { "config": { + "abort": { + "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "switchbot_unsupported_type": "Switchbot-typ som inte st\u00f6ds.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({address})", "step": { "user": { "data": { - "address": "Enhetsadress" + "address": "Enhetsadress", + "mac": "Enhetens MAC-adress", + "name": "Namn", + "password": "L\u00f6senord" + }, + "title": "Konfigurera Switchbot-enhet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Antal ompr\u00f6vningar", + "retry_timeout": "Timeout mellan \u00e5terf\u00f6rs\u00f6k", + "scan_timeout": "Hur l\u00e4nge ska man s\u00f6ka efter annonsdata", + "update_time": "Tid mellan uppdateringar (sekunder)" } } } diff --git a/homeassistant/components/switcher_kis/translations/sv.json b/homeassistant/components/switcher_kis/translations/sv.json new file mode 100644 index 00000000000..18a80850e45 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/sv.json b/homeassistant/components/syncthing/translations/sv.json index 9be5ea00256..a77f97f91e7 100644 --- a/homeassistant/components/syncthing/translations/sv.json +++ b/homeassistant/components/syncthing/translations/sv.json @@ -1,8 +1,12 @@ { "config": { + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { + "title": "St\u00e4ll in Syncthing-integration", "token": "Token", "url": "URL", "verify_ssl": "Verifiera SSL-certifikat" diff --git a/homeassistant/components/synology_dsm/translations/sl.json b/homeassistant/components/synology_dsm/translations/sl.json index 91d32273e62..d458c167bfa 100644 --- a/homeassistant/components/synology_dsm/translations/sl.json +++ b/homeassistant/components/synology_dsm/translations/sl.json @@ -6,7 +6,7 @@ "error": { "missing_data": "Manjkajo\u010di podatki: poskusite pozneje ali v drugi konfiguraciji", "otp_failed": "Dvostopenjska avtentikacija ni uspela. Poskusite z novim geslom", - "unknown": "Neznana napaka: za ve\u010d podrobnosti preverite dnevnike" + "unknown": "Nepri\u010dakovana napaka" }, "flow_title": "Synology DSM {name} ({host})", "step": { diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index acfc243382d..95a8af99f63 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -9,7 +9,8 @@ "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", "missing_data": "Saknade data: f\u00f6rs\u00f6k igen senare eller en annan konfiguration", - "otp_failed": "Tv\u00e5stegsautentisering misslyckades, f\u00f6rs\u00f6k igen med ett nytt l\u00f6senord" + "otp_failed": "Tv\u00e5stegsautentisering misslyckades, f\u00f6rs\u00f6k igen med ett nytt l\u00f6senord", + "unknown": "Ov\u00e4ntat fel" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/tankerkoenig/translations/he.json b/homeassistant/components/tankerkoenig/translations/he.json index 9ccb9aecfe6..3f5da8cd8d5 100644 --- a/homeassistant/components/tankerkoenig/translations/he.json +++ b/homeassistant/components/tankerkoenig/translations/he.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/tellduslive/translations/sv.json b/homeassistant/components/tellduslive/translations/sv.json index 98fa56f928f..25d33f3afdf 100644 --- a/homeassistant/components/tellduslive/translations/sv.json +++ b/homeassistant/components/tellduslive/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", "unknown": "Ok\u00e4nt fel intr\u00e4ffade" }, diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json index 064a6b2f9d3..fb9e0feb4e0 100644 --- a/homeassistant/components/totalconnect/translations/sv.json +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -1,12 +1,23 @@ { "config": { "abort": { - "already_configured": "Kontot har redan konfigurerats" + "already_configured": "Kontot har redan konfigurerats", + "no_locations": "Inga platser \u00e4r tillg\u00e4ngliga f\u00f6r den h\u00e4r anv\u00e4ndaren, kontrollera TotalConnect-inst\u00e4llningarna", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "usercode": "Anv\u00e4ndarkoden \u00e4r inte giltig f\u00f6r denna anv\u00e4ndare p\u00e5 denna plats" }, "step": { + "locations": { + "description": "Ange anv\u00e4ndarkoden f\u00f6r denna anv\u00e4ndare p\u00e5 plats {location_id}", + "title": "Anv\u00e4ndarkoder f\u00f6r plats" + }, + "reauth_confirm": { + "description": "Total Connect m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/transmission/translations/he.json b/homeassistant/components/transmission/translations/he.json index 6f8286290d4..73c5f6c7384 100644 --- a/homeassistant/components/transmission/translations/he.json +++ b/homeassistant/components/transmission/translations/he.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 737ca754202..713f8eafc02 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -7,6 +7,8 @@ "step": { "user": { "data": { + "access_id": "\u05de\u05d6\u05d4\u05d4 \u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc Tuya IoT", + "access_secret": "\u05e1\u05d5\u05d3 \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc Tuya IoT", "country_code": "\u05de\u05d3\u05d9\u05e0\u05d4", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05d7\u05e9\u05d1\u05d5\u05df" diff --git a/homeassistant/components/tuya/translations/select.sv.json b/homeassistant/components/tuya/translations/select.sv.json index 05092fe808c..c34c40ebacd 100644 --- a/homeassistant/components/tuya/translations/select.sv.json +++ b/homeassistant/components/tuya/translations/select.sv.json @@ -3,11 +3,22 @@ "tuya__humidifier_spray_mode": { "humidity": "Luftfuktighet" }, + "tuya__led_type": { + "halogen": "Halogen", + "incandescent": "Gl\u00f6dlampa", + "led": "LED" + }, "tuya__light_mode": { - "none": "Av" + "none": "Av", + "pos": "Ange omkopplarens plats", + "relay": "Indikera p\u00e5/av-l\u00e4ge" }, "tuya__relay_status": { + "last": "Kom ih\u00e5g senaste tillst\u00e5ndet", + "memory": "Kom ih\u00e5g senaste tillst\u00e5ndet", + "off": "Av", "on": "P\u00e5", + "power_off": "Av", "power_on": "P\u00e5" }, "tuya__vacuum_mode": { diff --git a/homeassistant/components/tuya/translations/sensor.sv.json b/homeassistant/components/tuya/translations/sensor.sv.json new file mode 100644 index 00000000000..a3dd5ff28df --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.sv.json @@ -0,0 +1,15 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Koktemperatur", + "cooling": "Kyler", + "heating": "V\u00e4rmer", + "heating_temp": "Uppv\u00e4rmningstemperatur", + "reserve_1": "Reserv 1", + "reserve_2": "Reserv 2", + "reserve_3": "Reserv 3", + "standby": "Standby", + "warm": "Bevarande av v\u00e4rme" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sv.json b/homeassistant/components/tuya/translations/sv.json index 344aea60f6f..1add4cb2e2d 100644 --- a/homeassistant/components/tuya/translations/sv.json +++ b/homeassistant/components/tuya/translations/sv.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "login_error": "Inloggningsfel ( {code} ): {msg}" + }, "step": { "user": { "data": { + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", "country_code": "Landskod f\u00f6r ditt konto (t.ex. 1 f\u00f6r USA eller 86 f\u00f6r Kina)", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/twentemilieu/translations/sv.json b/homeassistant/components/twentemilieu/translations/sv.json index 4e8bb592d05..b5f18b85ca3 100644 --- a/homeassistant/components/twentemilieu/translations/sv.json +++ b/homeassistant/components/twentemilieu/translations/sv.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_address": "Adress hittades inte i serviceomr\u00e5det Twente Milieu." }, "step": { diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index 3c7e9dcc110..cdc66ea5126 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -2,13 +2,15 @@ "config": { "abort": { "already_configured": "Controller-platsen \u00e4r redan konfigurerad", - "configuration_updated": "Konfigurationen uppdaterad" + "configuration_updated": "Konfigurationen uppdaterad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter", "service_unavailable": "Ingen tj\u00e4nst tillg\u00e4nglig", "unknown_client_mac": "Ingen klient tillg\u00e4nglig p\u00e5 den MAC-adressen" }, + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/translations/sv.json b/homeassistant/components/upnp/translations/sv.json index a067f819bba..1b1e98e987e 100644 --- a/homeassistant/components/upnp/translations/sv.json +++ b/homeassistant/components/upnp/translations/sv.json @@ -13,6 +13,20 @@ "step": { "ssdp_confirm": { "description": "Vill du konfigurera denna UPnP/IGD enhet?" + }, + "user": { + "data": { + "unique_id": "Enhet" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsintervall (sekunder, minst 30)" + } } } } diff --git a/homeassistant/components/velbus/translations/sv.json b/homeassistant/components/velbus/translations/sv.json index 6850c445703..eaff18c4195 100644 --- a/homeassistant/components/velbus/translations/sv.json +++ b/homeassistant/components/velbus/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/venstar/translations/sv.json b/homeassistant/components/venstar/translations/sv.json index 23c825f256f..61e6843a66f 100644 --- a/homeassistant/components/venstar/translations/sv.json +++ b/homeassistant/components/venstar/translations/sv.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "pin": "Pin-kod", + "ssl": "Anv\u00e4nd ett SSL certifikat", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till Venstar-termostaten" } } } diff --git a/homeassistant/components/verisure/translations/sv.json b/homeassistant/components/verisure/translations/sv.json index 1113a387339..f47e55d4df3 100644 --- a/homeassistant/components/verisure/translations/sv.json +++ b/homeassistant/components/verisure/translations/sv.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, "error": { + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel", "unknown_mfa": "Ett ok\u00e4nt fel har intr\u00e4ffat under konfiguration av MFA" }, "step": { + "installation": { + "data": { + "giid": "Installation" + }, + "description": "Home Assistant hittade flera Verisure-installationer i ditt Mina sidor-konto. V\u00e4lj installationen som du vill l\u00e4gga till i Home Assistant." + }, "mfa": { "data": { "code": "Verifieringskod", @@ -13,6 +24,8 @@ }, "reauth_confirm": { "data": { + "description": "Autentisera p\u00e5 nytt med ditt Verisure mina sidor-konto.", + "email": "E-post", "password": "L\u00f6senord" } }, @@ -24,9 +37,24 @@ }, "user": { "data": { + "description": "Logga in med ditt Verisure My Pages-konto.", + "email": "E-post", "password": "L\u00f6senord" } } } + }, + "options": { + "error": { + "code_format_mismatch": "Standard-PIN-koden matchar inte det antal siffror som kr\u00e4vs" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Antal siffror i PIN-kod f\u00f6r l\u00e5s", + "lock_default_code": "Standard PIN-kod f\u00f6r l\u00e5s, anv\u00e4nds om ingen anges" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/sv.json b/homeassistant/components/vlc_telnet/translations/sv.json index eba844f6c03..f8a77ff3086 100644 --- a/homeassistant/components/vlc_telnet/translations/sv.json +++ b/homeassistant/components/vlc_telnet/translations/sv.json @@ -1,6 +1,14 @@ { "config": { + "abort": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "hassio_confirm": { + "description": "Vill du ansluta till till\u00e4gget {addon} ?" + }, "user": { "data": { "host": "V\u00e4rd" diff --git a/homeassistant/components/wallbox/translations/sv.json b/homeassistant/components/wallbox/translations/sv.json index 79a05e057b8..65f079ab79f 100644 --- a/homeassistant/components/wallbox/translations/sv.json +++ b/homeassistant/components/wallbox/translations/sv.json @@ -1,16 +1,19 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "reauth_invalid": "\u00c5terautentisering misslyckades; Serienumret st\u00e4mmer inte \u00f6verens med originalet", "unknown": "Ov\u00e4ntat fel" }, "step": { "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } }, diff --git a/homeassistant/components/water_heater/translations/sv.json b/homeassistant/components/water_heater/translations/sv.json index 9b45fec830e..99980e832c0 100644 --- a/homeassistant/components/water_heater/translations/sv.json +++ b/homeassistant/components/water_heater/translations/sv.json @@ -11,7 +11,9 @@ "electric": "Elektrisk", "gas": "Gas", "heat_pump": "V\u00e4rmepump", - "off": "Av" + "high_demand": "H\u00f6g efterfr\u00e5gan", + "off": "Av", + "performance": "Prestanda" } } } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/sv.json b/homeassistant/components/watttime/translations/sv.json index dadc8b53d2d..f8a3d0b7fcf 100644 --- a/homeassistant/components/watttime/translations/sv.json +++ b/homeassistant/components/watttime/translations/sv.json @@ -1,6 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel", + "unknown_coordinates": "Inga data f\u00f6r latitud/longitud" + }, "step": { + "coordinates": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + }, "location": { "data": { "location_type": "Plats" @@ -15,5 +29,15 @@ "description": "Ange ditt anv\u00e4ndarnamn och l\u00f6senord:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Visa \u00f6vervakad plats p\u00e5 kartan" + }, + "title": "Konfigurera WattTime" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/sv.json b/homeassistant/components/wemo/translations/sv.json index be479aa1333..c489579828f 100644 --- a/homeassistant/components/wemo/translations/sv.json +++ b/homeassistant/components/wemo/translations/sv.json @@ -9,5 +9,10 @@ "description": "Vill du konfigurera Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo-knappen trycktes in i 2 sekunder" + } } } \ No newline at end of file diff --git a/homeassistant/components/withings/translations/he.json b/homeassistant/components/withings/translations/he.json index 6bcd8bf9a9d..5624b795692 100644 --- a/homeassistant/components/withings/translations/he.json +++ b/homeassistant/components/withings/translations/he.json @@ -15,6 +15,9 @@ }, "reauth": { "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" } } } diff --git a/homeassistant/components/withings/translations/sl.json b/homeassistant/components/withings/translations/sl.json index 0b1ef34bf7c..301bceff9db 100644 --- a/homeassistant/components/withings/translations/sl.json +++ b/homeassistant/components/withings/translations/sl.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", - "missing_configuration": "Integracija Withings ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." + "missing_configuration": "Komponenta Netatmo ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." }, "create_entry": { "default": "Uspe\u0161no overjen z Withings." diff --git a/homeassistant/components/wled/translations/select.sv.json b/homeassistant/components/wled/translations/select.sv.json index 1c3bb4c1ff4..7743fe99020 100644 --- a/homeassistant/components/wled/translations/select.sv.json +++ b/homeassistant/components/wled/translations/select.sv.json @@ -1,7 +1,9 @@ { "state": { "wled__live_override": { - "1": "P\u00e5" + "0": "Av", + "1": "P\u00e5", + "2": "Tills enheten startar om" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.sv.json b/homeassistant/components/wolflink/translations/sensor.sv.json index 797a7d6a230..faf7a2b9673 100644 --- a/homeassistant/components/wolflink/translations/sensor.sv.json +++ b/homeassistant/components/wolflink/translations/sensor.sv.json @@ -11,6 +11,15 @@ "at_frostschutz": "OT frostskydd", "aus": "Inaktiverad", "auto": "Automatiskt", + "auto_on_cool": "AutoOnCool", + "automatik_aus": "Automatisk avst\u00e4ngning", + "automatik_ein": "Automatiskt p\u00e5", + "bereit_keine_ladung": "Redo, laddas inte", + "betrieb_ohne_brenner": "Arbetar utan br\u00e4nnare", + "cooling": "Kyler", + "deaktiviert": "Inaktiv", + "eco": "Eco", + "ein": "Aktiverad", "estrichtrocknung": "Avj\u00e4mningstorkning", "externe_deaktivierung": "Extern avaktivering", "fernschalter_ein": "Fj\u00e4rrkontroll aktiverad", @@ -41,8 +50,28 @@ "perm_cooling": "PermCooling", "permanent": "Permanent", "permanentbetrieb": "Permanent l\u00e4ge", + "reduzierter_betrieb": "Begr\u00e4nsat l\u00e4ge", + "rt_abschaltung": "RT avst\u00e4ngning", + "rt_frostschutz": "RT frostskydd", + "ruhekontakt": "Kontakt f\u00f6r vila", + "schornsteinfeger": "Utsl\u00e4ppstest", + "smart_grid": "Smarta eln\u00e4t", + "smart_home": "Smart hem", + "softstart": "Mjukstart", + "solarbetrieb": "Solcellsl\u00e4ge", + "sparbetrieb": "Ekonomil\u00e4ge", "sparen": "Ekonomi", + "spreizung_hoch": "dT f\u00f6r bred", + "spreizung_kf": "Spridning KF", + "stabilisierung": "Stabilisering", + "standby": "Standby", + "start": "Starta", + "storung": "Fel", + "taktsperre": "Anti-cykel", + "telefonfernschalter": "Telefonfj\u00e4rrbrytare", "test": "Test", + "tpw": "TPW", + "urlaubsmodus": "Semesterl\u00e4ge", "vorspulen": "Ing\u00e5ngssk\u00f6ljning", "warmwasser": "Varmvatten", "warmwasser_schnellstart": "Snabbstart f\u00f6r varmvatten", diff --git a/homeassistant/components/xbox/translations/sv.json b/homeassistant/components/xbox/translations/sv.json index cb98fb350b7..e9c8dbb2610 100644 --- a/homeassistant/components/xbox/translations/sv.json +++ b/homeassistant/components/xbox/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, diff --git a/homeassistant/components/xiaomi_ble/translations/he.json b/homeassistant/components/xiaomi_ble/translations/he.json new file mode 100644 index 00000000000..b90a366130a --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json index 5d01dcab709..e6e29966bc7 100644 --- a/homeassistant/components/xiaomi_ble/translations/id.json +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -9,11 +9,19 @@ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "reauth_successful": "Autentikasi ulang berhasil" }, + "error": { + "decryption_failed": "Bindkey yang disediakan tidak berfungsi, data sensor tidak dapat didekripsi. Silakan periksa dan coba lagi.", + "expected_24_characters": "Diharapkan bindkey berupa 24 karakter heksadesimal.", + "expected_32_characters": "Diharapkan bindkey berupa 32 karakter heksadesimal." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Ingin menyiapkan {name}?" }, + "confirm_slow": { + "description": "Belum ada siaran dari perangkat ini dalam menit terakhir jadi kami tidak yakin apakah perangkat ini menggunakan enkripsi atau tidak. Ini mungkin terjadi karena perangkat menggunakan interval siaran yang lambat. Konfirmasikan sekarang untuk menambahkan perangkat ini, dan ketika siaran diterima nanti, Anda akan diminta untuk memasukkan kunci bind jika diperlukan." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json index a52c1ecbf98..37452a61e79 100644 --- a/homeassistant/components/xiaomi_miio/translations/sv.json +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -32,8 +32,10 @@ }, "manual": { "data": { - "host": "IP-adress" - } + "host": "IP-adress", + "token": "API Token" + }, + "description": "Du beh\u00f6ver de 32 tecknen API Token , se https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00f6r instruktioner. Observera att denna API Token skiljer sig fr\u00e5n nyckeln som anv\u00e4nds av Xiaomi Aqara-integrationen." }, "reauth_confirm": { "description": "Xiaomi Miio-integrationen m\u00e5ste autentisera ditt konto igen f\u00f6r att uppdatera tokens eller l\u00e4gga till saknade molnuppgifter.", @@ -46,5 +48,17 @@ "description": "V\u00e4lj Xiaomi Miio-enheten f\u00f6r att st\u00e4lla in." } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Molnuppgifter \u00e4r ofullst\u00e4ndiga, v\u00e4nligen fyll i anv\u00e4ndarnamn, l\u00f6senord och land" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Anv\u00e4nd moln f\u00f6r att f\u00e5 anslutna underenheter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/sv.json b/homeassistant/components/yeelight/translations/sv.json index 8575090ff7c..7a8780c0bf1 100644 --- a/homeassistant/components/yeelight/translations/sv.json +++ b/homeassistant/components/yeelight/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta." }, diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index 8f8777f92b0..65ddbb3dce8 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -12,6 +12,14 @@ }, "flow_title": "{name}", "step": { + "configure_addon": { + "data": { + "s0_legacy_key": "S0-nyckel (\u00e4ldre)", + "s2_access_control_key": "S2-\u00e5tkomstkontrollnyckel", + "s2_authenticated_key": "S2 Autentiserad nyckel", + "s2_unauthenticated_key": "S2 oautentiserad nyckel" + } + }, "usb_confirm": { "description": "Vill du konfigurera {name} med Z-Wave JS till\u00e4gget?" }, @@ -22,6 +30,10 @@ }, "device_automation": { "action_type": { + "clear_lock_usercode": "Rensa anv\u00e4ndarkoden p\u00e5 {entity_name}", + "ping": "Pinga enhet", + "refresh_value": "Uppdatera v\u00e4rdet/v\u00e4rdena f\u00f6r {entity_name}", + "reset_meter": "\u00c5terst\u00e4ll m\u00e4tare p\u00e5 {subtype}", "set_config_parameter": "Ange v\u00e4rde f\u00f6r konfigurationsparametern {subtype}", "set_lock_usercode": "Ange en anv\u00e4ndarkod p\u00e5 {entity_name}", "set_value": "St\u00e4ll in v\u00e4rdet f\u00f6r ett Z-Wave-v\u00e4rde" @@ -32,7 +44,14 @@ "value": "Nuvarande v\u00e4rde f\u00f6r ett Z-Wave v\u00e4rde" }, "trigger_type": { - "zwave_js.value_updated.config_parameter": "V\u00e4rde\u00e4ndring p\u00e5 konfigurationsparameter {subtype}" + "event.notification.entry_control": "Skickat ett meddelande om kontroll av intr\u00e4de", + "event.notification.notification": "Skickade ett meddelande", + "event.value_notification.basic": "Grundl\u00e4ggande CC-h\u00e4ndelse p\u00e5 {subtype}", + "event.value_notification.central_scene": "Central scen\u00e5tg\u00e4rd p\u00e5 {subtype}", + "event.value_notification.scene_activation": "Scenaktivering p\u00e5 {subtype}", + "state.node_status": "Nodstatus \u00e4ndrad", + "zwave_js.value_updated.config_parameter": "V\u00e4rde\u00e4ndring p\u00e5 konfigurationsparameter {subtype}", + "zwave_js.value_updated.value": "V\u00e4rdef\u00f6r\u00e4ndring p\u00e5 ett Z-Wave JS-v\u00e4rde" } }, "options": { @@ -56,6 +75,10 @@ "data": { "emulate_hardware": "Emulera h\u00e5rdvara", "log_level": "Loggniv\u00e5", + "s0_legacy_key": "S0-nyckel (\u00e4ldre)", + "s2_access_control_key": "S2-\u00e5tkomstkontrollnyckel", + "s2_authenticated_key": "S2 Autentiserad nyckel", + "s2_unauthenticated_key": "S2 oautentiserad nyckel", "usb_path": "USB-enhetens s\u00f6kv\u00e4g" } }, From 3b29cbcd61fdf9007b426bad0d4fd73590144cff Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Aug 2022 10:11:20 +0200 Subject: [PATCH 176/903] Support creating persistent repairs issues (#76211) --- .../components/repairs/issue_handler.py | 4 + .../components/repairs/issue_registry.py | 115 ++++++++++++----- .../components/repairs/websocket_api.py | 2 +- tests/components/repairs/test_init.py | 8 ++ .../components/repairs/test_issue_registry.py | 117 ++++++++++++++++-- .../components/repairs/test_websocket_api.py | 5 +- 6 files changed, 209 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index c139026ec48..a08fff29598 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -89,6 +89,7 @@ def async_create_issue( issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, + is_persistent: bool = False, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -110,6 +111,7 @@ def async_create_issue( issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + is_persistent=is_persistent, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -124,6 +126,7 @@ def create_issue( *, breaks_in_ha_version: str | None = None, is_fixable: bool, + is_persistent: bool = False, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -139,6 +142,7 @@ def create_issue( issue_id, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + is_persistent=is_persistent, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index bd201f1007c..c7502ecf397 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -3,7 +3,7 @@ from __future__ import annotations import dataclasses from datetime import datetime -from typing import Optional, cast +from typing import Any, cast from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant, callback @@ -15,7 +15,8 @@ from .models import IssueSeverity DATA_REGISTRY = "issue_registry" EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED = "repairs_issue_registry_updated" STORAGE_KEY = "repairs.issue_registry" -STORAGE_VERSION = 1 +STORAGE_VERSION_MAJOR = 1 +STORAGE_VERSION_MINOR = 2 SAVE_DELAY = 10 @@ -29,14 +30,53 @@ class IssueEntry: dismissed_version: str | None domain: str is_fixable: bool | None - issue_id: str + is_persistent: bool # Used if an integration creates issues for other integrations (ie alerts) issue_domain: str | None + issue_id: str learn_more_url: str | None severity: IssueSeverity | None translation_key: str | None translation_placeholders: dict[str, str] | None + def to_json(self) -> dict[str, Any]: + """Return a JSON serializable representation for storage.""" + result = { + "created": self.created.isoformat(), + "dismissed_version": self.dismissed_version, + "domain": self.domain, + "is_persistent": False, + "issue_id": self.issue_id, + } + if not self.is_persistent: + return result + return { + **result, + "breaks_in_ha_version": self.breaks_in_ha_version, + "is_fixable": self.is_fixable, + "is_persistent": True, + "issue_domain": self.issue_domain, + "issue_id": self.issue_id, + "learn_more_url": self.learn_more_url, + "severity": self.severity, + "translation_key": self.translation_key, + "translation_placeholders": self.translation_placeholders, + } + + +class IssueRegistryStore(Store[dict[str, list[dict[str, Any]]]]): + """Store entity registry data.""" + + async def _async_migrate_func( + self, old_major_version: int, old_minor_version: int, old_data: dict[str, Any] + ) -> dict[str, Any]: + """Migrate to the new version.""" + if old_major_version == 1 and old_minor_version < 2: + # Version 1.2 adds is_persistent + for issue in old_data["issues"]: + issue["is_persistent"] = False + return old_data + class IssueRegistry: """Class to hold a registry of issues.""" @@ -45,8 +85,12 @@ class IssueRegistry: """Initialize the issue registry.""" self.hass = hass self.issues: dict[tuple[str, str], IssueEntry] = {} - self._store = Store[dict[str, list[dict[str, Optional[str]]]]]( - hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + self._store = IssueRegistryStore( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, + minor_version=STORAGE_VERSION_MINOR, ) @callback @@ -63,6 +107,7 @@ class IssueRegistry: issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, + is_persistent: bool, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -78,6 +123,7 @@ class IssueRegistry: dismissed_version=None, domain=domain, is_fixable=is_fixable, + is_persistent=is_persistent, issue_domain=issue_domain, issue_id=issue_id, learn_more_url=learn_more_url, @@ -97,6 +143,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + is_persistent=is_persistent, issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, @@ -151,21 +198,39 @@ class IssueRegistry: if isinstance(data, dict): for issue in data["issues"]: - assert issue["created"] and issue["domain"] and issue["issue_id"] - issues[(issue["domain"], issue["issue_id"])] = IssueEntry( - active=False, - breaks_in_ha_version=None, - created=cast(datetime, dt_util.parse_datetime(issue["created"])), - dismissed_version=issue["dismissed_version"], - domain=issue["domain"], - is_fixable=None, - issue_id=issue["issue_id"], - issue_domain=None, - learn_more_url=None, - severity=None, - translation_key=None, - translation_placeholders=None, - ) + created = cast(datetime, dt_util.parse_datetime(issue["created"])) + if issue["is_persistent"]: + issues[(issue["domain"], issue["issue_id"])] = IssueEntry( + active=True, + breaks_in_ha_version=issue["breaks_in_ha_version"], + created=created, + dismissed_version=issue["dismissed_version"], + domain=issue["domain"], + is_fixable=issue["is_fixable"], + is_persistent=issue["is_persistent"], + issue_id=issue["issue_id"], + issue_domain=issue["issue_domain"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + else: + issues[(issue["domain"], issue["issue_id"])] = IssueEntry( + active=False, + breaks_in_ha_version=None, + created=created, + dismissed_version=issue["dismissed_version"], + domain=issue["domain"], + is_fixable=None, + is_persistent=issue["is_persistent"], + issue_id=issue["issue_id"], + issue_domain=None, + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) self.issues = issues @@ -179,15 +244,7 @@ class IssueRegistry: """Return data of issue registry to store in a file.""" data = {} - data["issues"] = [ - { - "created": entry.created.isoformat(), - "dismissed_version": entry.dismissed_version, - "domain": entry.domain, - "issue_id": entry.issue_id, - } - for entry in self.issues.values() - ] + data["issues"] = [entry.to_json() for entry in self.issues.values()] return data diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 2e9fcc5f8e4..ff0ac5ba8f9 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -64,7 +64,7 @@ def ws_list_issues( """Return a list of issues.""" def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: - result = {k: v for k, v in kv_pairs if k not in ("active")} + result = {k: v for k, v in kv_pairs if k not in ("active", "is_persistent")} result["ignored"] = result["dismissed_version"] is not None result["created"] = result["created"].isoformat() return result diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index d70f6c6e11d..1fc8367e4c3 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -68,6 +68,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -98,6 +99,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], is_fixable=issues[0]["is_fixable"], + is_persistent=False, issue_domain="my_issue_domain", learn_more_url="blablabla", severity=issues[0]["severity"], @@ -146,6 +148,7 @@ async def test_create_issue_invalid_version( issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -192,6 +195,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -283,6 +287,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], is_fixable=issues[0]["is_fixable"], + is_persistent=False, learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -351,6 +356,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -422,6 +428,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -492,6 +499,7 @@ async def test_sync_methods( "sync_issue", breaks_in_ha_version="2022.9", is_fixable=True, + is_persistent=False, learn_more_url="https://theuselessweb.com", severity=IssueSeverity.ERROR, translation_key="abc_123", diff --git a/tests/components/repairs/test_issue_registry.py b/tests/components/repairs/test_issue_registry.py index 523f75bfdc2..ff6c4b996da 100644 --- a/tests/components/repairs/test_issue_registry.py +++ b/tests/components/repairs/test_issue_registry.py @@ -21,6 +21,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "domain": "test", "issue_id": "issue_1", "is_fixable": True, + "is_persistent": False, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -31,6 +32,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "domain": "test", "issue_id": "issue_2", "is_fixable": True, + "is_persistent": False, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", @@ -41,11 +43,23 @@ async def test_load_issues(hass: HomeAssistant) -> None: "domain": "test", "issue_id": "issue_3", "is_fixable": True, + "is_persistent": False, "learn_more_url": "https://checkboxrace.com", "severity": "other", "translation_key": "even_worse", "translation_placeholders": {"def": "789"}, }, + { + "breaks_in_ha_version": "2022.6", + "domain": "test", + "issue_id": "issue_4", + "is_fixable": True, + "is_persistent": True, + "learn_more_url": "https://checkboxrace.com/blah", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"xyz": "abc"}, + }, ] events = async_capture_events( @@ -59,6 +73,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=issue["is_persistent"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -67,7 +82,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert len(events) == 3 + assert len(events) == 4 assert events[0].data == { "action": "create", "domain": "test", @@ -83,12 +98,17 @@ async def test_load_issues(hass: HomeAssistant) -> None: "domain": "test", "issue_id": "issue_3", } + assert events[3].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_4", + } async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) await hass.async_block_till_done() - assert len(events) == 4 - assert events[3].data == { + assert len(events) == 5 + assert events[4].data == { "action": "update", "domain": "test", "issue_id": "issue_1", @@ -97,17 +117,18 @@ async def test_load_issues(hass: HomeAssistant) -> None: async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) await hass.async_block_till_done() - assert len(events) == 5 - assert events[4].data == { + assert len(events) == 6 + assert events[5].data == { "action": "remove", "domain": "test", "issue_id": "issue_3", } registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] - assert len(registry.issues) == 2 + assert len(registry.issues) == 3 issue1 = registry.async_get_issue("test", "issue_1") issue2 = registry.async_get_issue("test", "issue_2") + issue4 = registry.async_get_issue("test", "issue_4") registry2 = issue_registry.IssueRegistry(hass) await flush_store(registry._store) @@ -116,17 +137,91 @@ async def test_load_issues(hass: HomeAssistant) -> None: assert list(registry.issues) == list(registry2.issues) issue1_registry2 = registry2.async_get_issue("test", "issue_1") - assert issue1_registry2.created == issue1.created - assert issue1_registry2.dismissed_version == issue1.dismissed_version + assert issue1_registry2 == issue_registry.IssueEntry( + active=False, + breaks_in_ha_version=None, + created=issue1.created, + dismissed_version=issue1.dismissed_version, + domain=issue1.domain, + is_fixable=None, + is_persistent=issue1.is_persistent, + issue_domain=None, + issue_id=issue1.issue_id, + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) issue2_registry2 = registry2.async_get_issue("test", "issue_2") - assert issue2_registry2.created == issue2.created - assert issue2_registry2.dismissed_version == issue2.dismissed_version + assert issue2_registry2 == issue_registry.IssueEntry( + active=False, + breaks_in_ha_version=None, + created=issue2.created, + dismissed_version=issue2.dismissed_version, + domain=issue2.domain, + is_fixable=None, + is_persistent=issue2.is_persistent, + issue_domain=None, + issue_id=issue2.issue_id, + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) + issue4_registry2 = registry2.async_get_issue("test", "issue_4") + assert issue4_registry2 == issue4 async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> None: """Test loading stored issues on start.""" hass_storage[issue_registry.STORAGE_KEY] = { - "version": issue_registry.STORAGE_VERSION, + "version": issue_registry.STORAGE_VERSION_MAJOR, + "minor_version": issue_registry.STORAGE_VERSION_MINOR, + "data": { + "issues": [ + { + "created": "2022-07-19T09:41:13.746514+00:00", + "dismissed_version": "2022.7.0.dev0", + "domain": "test", + "is_persistent": False, + "issue_id": "issue_1", + }, + { + "created": "2022-07-19T19:41:13.746514+00:00", + "dismissed_version": None, + "domain": "test", + "is_persistent": False, + "issue_id": "issue_2", + }, + { + "breaks_in_ha_version": "2022.6", + "created": "2022-07-19T19:41:13.746514+00:00", + "dismissed_version": None, + "domain": "test", + "issue_domain": "blubb", + "issue_id": "issue_4", + "is_fixable": True, + "is_persistent": True, + "learn_more_url": "https://checkboxrace.com/blah", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"xyz": "abc"}, + }, + ] + }, + } + + assert await async_setup_component(hass, DOMAIN, {}) + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 3 + + +async def test_migration_1_1(hass: HomeAssistant, hass_storage) -> None: + """Test migration from version 1.1.""" + hass_storage[issue_registry.STORAGE_KEY] = { + "version": 1, + "minor_version": 1, "data": { "issues": [ { diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index d778b043832..359024f9fe5 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -44,6 +44,7 @@ async def create_issues(hass, ws_client): issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -379,13 +380,14 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> # Add an inactive issue, this should not be exposed in the list hass_storage[issue_registry.STORAGE_KEY] = { - "version": issue_registry.STORAGE_VERSION, + "version": issue_registry.STORAGE_VERSION_MAJOR, "data": { "issues": [ { "created": "2022-07-19T09:41:13.746514+00:00", "dismissed_version": None, "domain": "test", + "is_persistent": False, "issue_id": "issue_3_inactive", "issue_domain": None, }, @@ -435,6 +437,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], From babb3d10a1cc113522e43a75563301676dc1accc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Aug 2022 10:59:43 +0200 Subject: [PATCH 177/903] Deprecate the Deutsche Bahn (#76286) --- .../components/deutsche_bahn/manifest.json | 1 + .../components/deutsche_bahn/sensor.py | 17 ++++++++++++++++- .../components/deutsche_bahn/strings.json | 8 ++++++++ .../deutsche_bahn/translations/en.json | 8 ++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/deutsche_bahn/strings.json create mode 100644 homeassistant/components/deutsche_bahn/translations/en.json diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index 1eeb2241db5..dd69a940de0 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -3,6 +3,7 @@ "name": "Deutsche Bahn", "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", "requirements": ["schiene==0.23"], + "dependencies": ["repairs"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["schiene"] diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index 6c784fa9a89..07c330fd402 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -2,10 +2,12 @@ from __future__ import annotations from datetime import timedelta +import logging import schiene import voluptuous as vol +from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_OFFSET from homeassistant.core import HomeAssistant @@ -33,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + def setup_platform( hass: HomeAssistant, @@ -45,7 +49,18 @@ def setup_platform( destination = config[CONF_DESTINATION] offset = config[CONF_OFFSET] only_direct = config[CONF_ONLY_DIRECT] - + create_issue( + hass, + "deutsche_bahn", + "pending_removal", + breaks_in_ha_version="2022.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The Deutsche Bahn sensor component is deprecated and will be removed in Home Assistant 2022.11" + ) add_entities([DeutscheBahnSensor(start, destination, offset, only_direct)], True) diff --git a/homeassistant/components/deutsche_bahn/strings.json b/homeassistant/components/deutsche_bahn/strings.json new file mode 100644 index 00000000000..c88668862da --- /dev/null +++ b/homeassistant/components/deutsche_bahn/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The Deutsche Bahn integration is being removed", + "description": "The Deutsche Bahn integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.11.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the Deutsche Bahn YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/deutsche_bahn/translations/en.json b/homeassistant/components/deutsche_bahn/translations/en.json new file mode 100644 index 00000000000..0ad1f14d1cc --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The Deutsche Bahn integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.11.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the Deutsche Bahn YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Deutsche Bahn integration is being removed" + } + } +} \ No newline at end of file From 34dcc74491048b64ae847d48591402c8a3b0623d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 5 Aug 2022 05:06:40 -0400 Subject: [PATCH 178/903] Bump ZHA dependencies (#76275) --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 179302af1cd..bad84054f1f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,14 +4,14 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.2", + "bellows==0.31.3", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.48.0", + "zigpy==0.49.0", "zigpy-xbee==0.15.0", - "zigpy-zigate==0.9.0", + "zigpy-zigate==0.9.1", "zigpy-znp==0.8.1" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3abb34ff5da..1b6db67f18c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.2 +bellows==0.31.3 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -2535,13 +2535,13 @@ zigpy-deconz==0.18.0 zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.9.0 +zigpy-zigate==0.9.1 # homeassistant.components.zha zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.48.0 +zigpy==0.49.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96845b12579..8e063b257ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.2 +bellows==0.31.3 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -1706,13 +1706,13 @@ zigpy-deconz==0.18.0 zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.9.0 +zigpy-zigate==0.9.1 # homeassistant.components.zha zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.48.0 +zigpy==0.49.0 # homeassistant.components.zwave_js zwave-js-server-python==0.40.0 From 44aa49dde8d9db962a50875b16a1e8fe7647cdf1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Aug 2022 11:22:55 +0200 Subject: [PATCH 179/903] Bump pydeconz to v102 (#76287) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 6384ebfcd5f..e4e412056e6 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==101"], + "requirements": ["pydeconz==102"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 1b6db67f18c..e6b42839383 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1461,7 +1461,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==101 +pydeconz==102 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e063b257ff..b128bc80b21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==101 +pydeconz==102 # homeassistant.components.dexcom pydexcom==0.2.3 From 861b694cffbeb0813dc75320924307cfea3acfde Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 5 Aug 2022 11:39:51 +0200 Subject: [PATCH 180/903] Use attributes in litejet light (#76031) --- homeassistant/components/litejet/light.py | 67 ++++++++--------------- 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index 74395117c46..a41a34016d9 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -1,7 +1,11 @@ """Support for LiteJet lights.""" +from __future__ import annotations + import logging from typing import Any +from pylitejet import LiteJet + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, @@ -27,13 +31,13 @@ async def async_setup_entry( ) -> None: """Set up entry.""" - system = hass.data[DOMAIN] + system: LiteJet = hass.data[DOMAIN] - def get_entities(system): + def get_entities(system: LiteJet) -> list[LiteJetLight]: entities = [] - for i in system.loads(): - name = system.get_load_name(i) - entities.append(LiteJetLight(config_entry, system, i, name)) + for index in system.loads(): + name = system.get_load_name(index) + entities.append(LiteJetLight(config_entry, system, index, name)) return entities async_add_entities(await hass.async_add_executor_job(get_entities, system), True) @@ -43,16 +47,22 @@ class LiteJetLight(LightEntity): """Representation of a single LiteJet light.""" _attr_color_mode = ColorMode.BRIGHTNESS + _attr_should_poll = False _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_features = LightEntityFeature.TRANSITION - def __init__(self, config_entry, lj, i, name): # pylint: disable=invalid-name + def __init__( + self, config_entry: ConfigEntry, litejet: LiteJet, index: int, name: str + ) -> None: """Initialize a LiteJet light.""" self._config_entry = config_entry - self._lj = lj - self._index = i - self._brightness = 0 - self._name = name + self._lj = litejet + self._index = index + self._attr_brightness = 0 + self._attr_is_on = False + self._attr_name = name + self._attr_unique_id = f"{config_entry.entry_id}_{index}" + self._attr_extra_state_attributes = {ATTR_NUMBER: self._index} async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" @@ -63,41 +73,11 @@ class LiteJetLight(LightEntity): """Entity being removed from hass.""" self._lj.unsubscribe(self._on_load_changed) - def _on_load_changed(self): + def _on_load_changed(self) -> None: """Handle state changes.""" - _LOGGER.debug("Updating due to notification for %s", self._name) + _LOGGER.debug("Updating due to notification for %s", self.name) self.schedule_update_ha_state(True) - @property - def name(self): - """Return the light's name.""" - return self._name - - @property - def unique_id(self): - """Return a unique identifier for this light.""" - return f"{self._config_entry.entry_id}_{self._index}" - - @property - def brightness(self): - """Return the light's brightness.""" - return self._brightness - - @property - def is_on(self): - """Return if the light is on.""" - return self._brightness != 0 - - @property - def should_poll(self): - """Return that lights do not require polling.""" - return False - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return {ATTR_NUMBER: self._index} - def turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" @@ -129,4 +109,5 @@ class LiteJetLight(LightEntity): def update(self) -> None: """Retrieve the light's brightness from the LiteJet system.""" - self._brightness = int(self._lj.get_load_level(self._index) / 99 * 255) + self._attr_brightness = int(self._lj.get_load_level(self._index) / 99 * 255) + self._attr_is_on = self.brightness != 0 From b3660901757d3a953cda3a7d70175ac5cb5d9cf8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Aug 2022 12:07:51 +0200 Subject: [PATCH 181/903] Allow creating fixable repairs issues without flows (#76224) * Allow creating fixable repairs issues without flows * Add test * Adjust test --- homeassistant/components/demo/__init__.py | 10 +++ homeassistant/components/demo/repairs.py | 9 ++- homeassistant/components/demo/strings.json | 13 +++- .../components/demo/translations/en.json | 13 +++- homeassistant/components/repairs/__init__.py | 2 + .../components/repairs/issue_handler.py | 37 +++++++--- tests/components/demo/test_init.py | 30 +++++++- .../components/repairs/test_websocket_api.py | 68 +++++++++++-------- 8 files changed, 140 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 3f0bb09cdbd..2e01e2c3c6b 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -211,6 +211,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: translation_key="unfixable_problem", ) + async_create_issue( + hass, + DOMAIN, + "bad_psu", + is_fixable=True, + learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU", + severity=IssueSeverity.CRITICAL, + translation_key="bad_psu", + ) + return True diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/demo/repairs.py index e5d31c18971..2d7c8b4cbcc 100644 --- a/homeassistant/components/demo/repairs.py +++ b/homeassistant/components/demo/repairs.py @@ -5,7 +5,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.repairs import RepairsFlow +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow class DemoFixFlow(RepairsFlow): @@ -23,11 +23,16 @@ class DemoFixFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle the confirm step of a fix flow.""" if user_input is not None: - return self.async_create_entry(title="Fixed issue", data={}) + return self.async_create_entry(title="", data={}) return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) async def async_create_fix_flow(hass, issue_id): """Create flow.""" + if issue_id == "bad_psu": + # The bad_psu issue doesn't have its own flow + return ConfirmRepairFlow() + + # Other issues have a custom flow return DemoFixFlow() diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index a3a5b11f336..e02d64f157f 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -1,13 +1,24 @@ { "title": "Demo", "issues": { + "bad_psu": { + "title": "The power supply is not stable", + "fix_flow": { + "step": { + "confirm": { + "title": "The power supply needs to be replaced", + "description": "Press SUBMIT to confirm the power supply has been replaced" + } + } + } + }, "out_of_blinker_fluid": { "title": "The blinker fluid is empty and needs to be refilled", "fix_flow": { "step": { "confirm": { "title": "Blinker fluid needs to be refilled", - "description": "Press OK when blinker fluid has been refilled" + "description": "Press SUBMIT when blinker fluid has been refilled" } } } diff --git a/homeassistant/components/demo/translations/en.json b/homeassistant/components/demo/translations/en.json index 11378fb94d4..a98f0d3c28d 100644 --- a/homeassistant/components/demo/translations/en.json +++ b/homeassistant/components/demo/translations/en.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Press SUBMIT to confirm the power supply has been replaced", + "title": "The power supply needs to be replaced" + } + } + }, + "title": "The power supply is not stable" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Press OK when blinker fluid has been refilled", + "description": "Press SUBMIT when blinker fluid has been refilled", "title": "Blinker fluid needs to be refilled" } } diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index 4471def0dcd..5014baff834 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -7,6 +7,7 @@ from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api from .const import DOMAIN from .issue_handler import ( + ConfirmRepairFlow, async_create_issue, async_delete_issue, create_issue, @@ -21,6 +22,7 @@ __all__ = [ "create_issue", "delete_issue", "DOMAIN", + "ConfirmRepairFlow", "IssueSeverity", "RepairsFlow", ] diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index a08fff29598..23f37754ffe 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -5,6 +5,7 @@ import functools as ft from typing import Any from awesomeversion import AwesomeVersion, AwesomeVersionStrategy +import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.core import HomeAssistant, callback @@ -19,6 +20,26 @@ from .issue_registry import async_get as async_get_issue_registry from .models import IssueSeverity, RepairsFlow, RepairsProtocol +class ConfirmRepairFlow(RepairsFlow): + """Handler for an issue fixing flow without any side effects.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await (self.async_step_confirm()) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + return self.async_create_entry(title="", data={}) + + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + class RepairsFlowManager(data_entry_flow.FlowManager): """Manage repairs flows.""" @@ -30,14 +51,6 @@ class RepairsFlowManager(data_entry_flow.FlowManager): data: dict[str, Any] | None = None, ) -> RepairsFlow: """Create a flow. platform is a repairs module.""" - if "platforms" not in self.hass.data[DOMAIN]: - await async_process_repairs_platforms(self.hass) - - platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"] - if handler_key not in platforms: - raise data_entry_flow.UnknownHandler - platform = platforms[handler_key] - assert data and "issue_id" in data issue_id = data["issue_id"] @@ -46,6 +59,14 @@ class RepairsFlowManager(data_entry_flow.FlowManager): if issue is None or not issue.is_fixable: raise data_entry_flow.UnknownStep + if "platforms" not in self.hass.data[DOMAIN]: + await async_process_repairs_platforms(self.hass) + + platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"] + if handler_key not in platforms: + return ConfirmRepairFlow() + platform = platforms[handler_key] + return await platform.async_create_fix_flow(self.hass, issue_id) async def async_finish_flow( diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 5b322cb776f..ba8baa0d487 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -129,6 +129,20 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "translation_key": "unfixable_problem", "translation_placeholders": None, }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": True, + "issue_domain": None, + "issue_id": "bad_psu", + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "bad_psu", + "translation_placeholders": None, + }, ] } @@ -164,7 +178,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "description_placeholders": None, "flow_id": flow_id, "handler": "demo", - "title": "Fixed issue", + "title": "", "type": "create_entry", "version": 1, } @@ -203,5 +217,19 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "translation_key": "unfixable_problem", "translation_placeholders": None, }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": True, + "issue_domain": None, + "issue_id": "bad_psu", + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "bad_psu", + "translation_placeholders": None, + }, ] } diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 359024f9fe5..a47a7a899ea 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -21,21 +21,24 @@ from homeassistant.setup import async_setup_component from tests.common import mock_platform +DEFAULT_ISSUES = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "fake_integration", + "issue_id": "issue_1", + "is_fixable": True, + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + } +] -async def create_issues(hass, ws_client): + +async def create_issues(hass, ws_client, issues=None): """Create issues.""" - issues = [ - { - "breaks_in_ha_version": "2022.9", - "domain": "fake_integration", - "issue_id": "issue_1", - "is_fixable": True, - "learn_more_url": "https://theuselessweb.com", - "severity": "error", - "translation_key": "abc_123", - "translation_placeholders": {"abc": "123"}, - }, - ] + if issues is None: + issues = DEFAULT_ISSUES for issue in issues: async_create_issue( @@ -79,23 +82,22 @@ class MockFixFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" - return await (self.async_step_confirm()) + return await (self.async_step_custom_step()) - async def async_step_confirm( + async def async_step_custom_step( self, user_input: dict[str, str] | None = None ) -> data_entry_flow.FlowResult: - """Handle the confirm step of a fix flow.""" + """Handle a custom_step step of a fix flow.""" if user_input is not None: - return self.async_create_entry(title=None, data=None) + return self.async_create_entry(title="", data={}) - return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + return self.async_show_form(step_id="custom_step", data_schema=vol.Schema({})) @pytest.fixture(autouse=True) async def mock_repairs_integration(hass): """Mock a repairs integration.""" hass.config.components.add("fake_integration") - hass.config.components.add("integration_without_diagnostics") def async_create_fix_flow(hass, issue_id): return MockFixFlow() @@ -107,7 +109,7 @@ async def mock_repairs_integration(hass): ) mock_platform( hass, - "integration_without_diagnostics.repairs", + "integration_without_repairs.repairs", Mock(spec=[]), ) @@ -237,7 +239,16 @@ async def test_fix_non_existing_issue( } -async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> None: +@pytest.mark.parametrize( + "domain, step", + ( + ("fake_integration", "custom_step"), + ("fake_integration_default_handler", "confirm"), + ), +) +async def test_fix_issue( + hass: HomeAssistant, hass_client, hass_ws_client, domain, step +) -> None: """Test we can fix an issue.""" assert await async_setup_component(hass, "http", {}) assert await async_setup_component(hass, DOMAIN, {}) @@ -245,12 +256,11 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No ws_client = await hass_ws_client(hass) client = await hass_client() - await create_issues(hass, ws_client) + issues = [{**DEFAULT_ISSUES[0], "domain": domain}] + await create_issues(hass, ws_client, issues=issues) url = "/api/repairs/issues/fix" - resp = await client.post( - url, json={"handler": "fake_integration", "issue_id": "issue_1"} - ) + resp = await client.post(url, json={"handler": domain, "issue_id": "issue_1"}) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -261,9 +271,9 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No "description_placeholders": None, "errors": None, "flow_id": ANY, - "handler": "fake_integration", + "handler": domain, "last_step": None, - "step_id": "confirm", + "step_id": step, "type": "form", } @@ -286,8 +296,8 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No "description": None, "description_placeholders": None, "flow_id": flow_id, - "handler": "fake_integration", - "title": None, + "handler": domain, + "title": "", "type": "create_entry", "version": 1, } From 9aa88384794186d46a07f14201329b58cd5f267b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Aug 2022 13:16:29 +0200 Subject: [PATCH 182/903] Allow storing arbitrary data in repairs issues (#76288) --- homeassistant/components/demo/repairs.py | 7 ++- .../components/flunearyou/repairs.py | 6 ++- .../components/repairs/issue_handler.py | 14 ++++-- .../components/repairs/issue_registry.py | 7 +++ homeassistant/components/repairs/models.py | 8 ++- .../components/repairs/websocket_api.py | 3 +- .../components/repairs/test_issue_registry.py | 4 ++ .../components/repairs/test_websocket_api.py | 50 +++++++++++++------ 8 files changed, 76 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/demo/repairs.py index 2d7c8b4cbcc..cddc937a71a 100644 --- a/homeassistant/components/demo/repairs.py +++ b/homeassistant/components/demo/repairs.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.core import HomeAssistant class DemoFixFlow(RepairsFlow): @@ -28,7 +29,11 @@ class DemoFixFlow(RepairsFlow): return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) -async def async_create_fix_flow(hass, issue_id): +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: """Create flow.""" if issue_id == "bad_psu": # The bad_psu issue doesn't have its own flow diff --git a/homeassistant/components/flunearyou/repairs.py b/homeassistant/components/flunearyou/repairs.py index f48085ba623..df81a1ae576 100644 --- a/homeassistant/components/flunearyou/repairs.py +++ b/homeassistant/components/flunearyou/repairs.py @@ -36,7 +36,9 @@ class FluNearYouFixFlow(RepairsFlow): async def async_create_fix_flow( - hass: HomeAssistant, issue_id: str -) -> FluNearYouFixFlow: + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: """Create flow.""" return FluNearYouFixFlow() diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 23f37754ffe..5695e99998b 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -64,10 +64,14 @@ class RepairsFlowManager(data_entry_flow.FlowManager): platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"] if handler_key not in platforms: - return ConfirmRepairFlow() - platform = platforms[handler_key] + flow: RepairsFlow = ConfirmRepairFlow() + else: + platform = platforms[handler_key] + flow = await platform.async_create_fix_flow(self.hass, issue_id, issue.data) - return await platform.async_create_fix_flow(self.hass, issue_id) + flow.issue_id = issue_id + flow.data = issue.data + return flow async def async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult @@ -109,6 +113,7 @@ def async_create_issue( *, issue_domain: str | None = None, breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool = False, learn_more_url: str | None = None, @@ -131,6 +136,7 @@ def async_create_issue( issue_id, issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, + data=data, is_fixable=is_fixable, is_persistent=is_persistent, learn_more_url=learn_more_url, @@ -146,6 +152,7 @@ def create_issue( issue_id: str, *, breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool = False, learn_more_url: str | None = None, @@ -162,6 +169,7 @@ def create_issue( domain, issue_id, breaks_in_ha_version=breaks_in_ha_version, + data=data, is_fixable=is_fixable, is_persistent=is_persistent, learn_more_url=learn_more_url, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index c7502ecf397..f9a15e0f165 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -27,6 +27,7 @@ class IssueEntry: active: bool breaks_in_ha_version: str | None created: datetime + data: dict[str, str | int | float | None] | None dismissed_version: str | None domain: str is_fixable: bool | None @@ -53,6 +54,7 @@ class IssueEntry: return { **result, "breaks_in_ha_version": self.breaks_in_ha_version, + "data": self.data, "is_fixable": self.is_fixable, "is_persistent": True, "issue_domain": self.issue_domain, @@ -106,6 +108,7 @@ class IssueRegistry: *, issue_domain: str | None = None, breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool, learn_more_url: str | None = None, @@ -120,6 +123,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=breaks_in_ha_version, created=dt_util.utcnow(), + data=data, dismissed_version=None, domain=domain, is_fixable=is_fixable, @@ -142,6 +146,7 @@ class IssueRegistry: issue, active=True, breaks_in_ha_version=breaks_in_ha_version, + data=data, is_fixable=is_fixable, is_persistent=is_persistent, issue_domain=issue_domain, @@ -204,6 +209,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=issue["breaks_in_ha_version"], created=created, + data=issue["data"], dismissed_version=issue["dismissed_version"], domain=issue["domain"], is_fixable=issue["is_fixable"], @@ -220,6 +226,7 @@ class IssueRegistry: active=False, breaks_in_ha_version=None, created=created, + data=None, dismissed_version=issue["dismissed_version"], domain=issue["domain"], is_fixable=None, diff --git a/homeassistant/components/repairs/models.py b/homeassistant/components/repairs/models.py index 2a6eeb15269..1022c50e1f2 100644 --- a/homeassistant/components/repairs/models.py +++ b/homeassistant/components/repairs/models.py @@ -19,11 +19,17 @@ class IssueSeverity(StrEnum): class RepairsFlow(data_entry_flow.FlowHandler): """Handle a flow for fixing an issue.""" + issue_id: str + data: dict[str, str | int | float | None] | None + class RepairsProtocol(Protocol): """Define the format of repairs platforms.""" async def async_create_fix_flow( - self, hass: HomeAssistant, issue_id: str + self, + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create a flow to fix a fixable issue.""" diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index ff0ac5ba8f9..192c9f5ac66 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -64,7 +64,8 @@ def ws_list_issues( """Return a list of issues.""" def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: - result = {k: v for k, v in kv_pairs if k not in ("active", "is_persistent")} + excluded_keys = ("active", "data", "is_persistent") + result = {k: v for k, v in kv_pairs if k not in excluded_keys} result["ignored"] = result["dismissed_version"] is not None result["created"] = result["created"].isoformat() return result diff --git a/tests/components/repairs/test_issue_registry.py b/tests/components/repairs/test_issue_registry.py index ff6c4b996da..76faafce1c7 100644 --- a/tests/components/repairs/test_issue_registry.py +++ b/tests/components/repairs/test_issue_registry.py @@ -51,6 +51,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: }, { "breaks_in_ha_version": "2022.6", + "data": {"entry_id": "123"}, "domain": "test", "issue_id": "issue_4", "is_fixable": True, @@ -141,6 +142,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: active=False, breaks_in_ha_version=None, created=issue1.created, + data=None, dismissed_version=issue1.dismissed_version, domain=issue1.domain, is_fixable=None, @@ -157,6 +159,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: active=False, breaks_in_ha_version=None, created=issue2.created, + data=None, dismissed_version=issue2.dismissed_version, domain=issue2.domain, is_fixable=None, @@ -196,6 +199,7 @@ async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> { "breaks_in_ha_version": "2022.6", "created": "2022-07-19T19:41:13.746514+00:00", + "data": {"entry_id": "123"}, "dismissed_version": None, "domain": "test", "issue_domain": "blubb", diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index a47a7a899ea..1cb83d81b06 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -37,6 +37,17 @@ DEFAULT_ISSUES = [ async def create_issues(hass, ws_client, issues=None): """Create issues.""" + + def api_issue(issue): + excluded_keys = ("data",) + return dict( + {key: issue[key] for key in issue if key not in excluded_keys}, + created=ANY, + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + if issues is None: issues = DEFAULT_ISSUES @@ -46,6 +57,7 @@ async def create_issues(hass, ws_client, issues=None): issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + data=issue.get("data"), is_fixable=issue["is_fixable"], is_persistent=False, learn_more_url=issue["learn_more_url"], @@ -58,22 +70,17 @@ async def create_issues(hass, ws_client, issues=None): msg = await ws_client.receive_json() assert msg["success"] - assert msg["result"] == { - "issues": [ - dict( - issue, - created=ANY, - dismissed_version=None, - ignored=False, - issue_domain=None, - ) - for issue in issues - ] - } + assert msg["result"] == {"issues": [api_issue(issue) for issue in issues]} return issues +EXPECTED_DATA = { + "issue_1": None, + "issue_2": {"blah": "bleh"}, +} + + class MockFixFlow(RepairsFlow): """Handler for an issue fixing flow.""" @@ -82,6 +89,9 @@ class MockFixFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" + assert self.issue_id in EXPECTED_DATA + assert self.data == EXPECTED_DATA[self.issue_id] + return await (self.async_step_custom_step()) async def async_step_custom_step( @@ -99,7 +109,10 @@ async def mock_repairs_integration(hass): """Mock a repairs integration.""" hass.config.components.add("fake_integration") - def async_create_fix_flow(hass, issue_id): + def async_create_fix_flow(hass, issue_id, data): + assert issue_id in EXPECTED_DATA + assert data == EXPECTED_DATA[issue_id] + return MockFixFlow() mock_platform( @@ -256,11 +269,18 @@ async def test_fix_issue( ws_client = await hass_ws_client(hass) client = await hass_client() - issues = [{**DEFAULT_ISSUES[0], "domain": domain}] + issues = [ + { + **DEFAULT_ISSUES[0], + "data": {"blah": "bleh"}, + "domain": domain, + "issue_id": "issue_2", + } + ] await create_issues(hass, ws_client, issues=issues) url = "/api/repairs/issues/fix" - resp = await client.post(url, json={"handler": domain, "issue_id": "issue_1"}) + resp = await client.post(url, json={"handler": domain, "issue_id": "issue_2"}) assert resp.status == HTTPStatus.OK data = await resp.json() From 741efb89d50fab37e8ed27301543f11cbc559f39 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 5 Aug 2022 13:17:46 +0200 Subject: [PATCH 183/903] Remove deprecated `send_if_off` option for MQTT climate (#76293) * Remove `send_if_off` option for mqtt climate * Use cv.remove() --- homeassistant/components/mqtt/climate.py | 38 +++----- tests/components/mqtt/test_climate.py | 116 ----------------------- 2 files changed, 11 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 30263798740..bf53544b491 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -99,7 +99,7 @@ CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" CONF_PRESET_MODES_LIST = "preset_modes" -# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +# Support CONF_SEND_IF_OFF is removed with release 2022.9 CONF_SEND_IF_OFF = "send_if_off" CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template" CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" @@ -284,8 +284,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_SEND_IF_OFF): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_ACTION_TOPIC): valid_subscribe_topic, # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together @@ -334,8 +332,8 @@ PLATFORM_SCHEMA_MODERN = vol.All( # Configuring MQTT Climate under the climate platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - cv.deprecated(CONF_SEND_IF_OFF), + # Support CONF_SEND_IF_OFF is removed with release 2022.9 + cv.removed(CONF_SEND_IF_OFF), # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), @@ -353,8 +351,8 @@ _DISCOVERY_SCHEMA_BASE = _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA DISCOVERY_SCHEMA = vol.All( _DISCOVERY_SCHEMA_BASE, - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - cv.deprecated(CONF_SEND_IF_OFF), + # Support CONF_SEND_IF_OFF is removed with release 2022.9 + cv.removed(CONF_SEND_IF_OFF), # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), @@ -437,8 +435,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._feature_preset_mode = False self._optimistic_preset_mode = None - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - self._send_if_off = True # AWAY and HOLD mode topics and templates are deprecated, # support will be removed with release 2022.9 self._hold_list = [] @@ -511,10 +507,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._command_templates = command_templates - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if CONF_SEND_IF_OFF in config: - self._send_if_off = config[CONF_SEND_IF_OFF] - # AWAY and HOLD mode topics and templates are deprecated, # support will be removed with release 2022.9 if CONF_HOLD_LIST in config: @@ -871,10 +863,8 @@ class MqttClimate(MqttEntity, ClimateEntity): # optimistic mode setattr(self, attr, temp) - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._send_if_off or self._current_operation != HVACMode.OFF: - payload = self._command_templates[cmnd_template](temp) - await self._publish(cmnd_topic, payload) + payload = self._command_templates[cmnd_template](temp) + await self._publish(cmnd_topic, payload) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" @@ -910,12 +900,8 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._send_if_off or self._current_operation != HVACMode.OFF: - payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE]( - swing_mode - ) - await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) + payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](swing_mode) + await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._current_swing_mode = swing_mode @@ -923,10 +909,8 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target temperature.""" - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._send_if_off or self._current_operation != HVACMode.OFF: - payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) - await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) + payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) + await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: self._current_fan_mode = fan_mode diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index aec83a85227..679f853a3a8 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -349,44 +349,6 @@ async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("fan_mode") == "high" -# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 -@pytest.mark.parametrize( - "send_if_off,assert_async_publish", - [ - ({}, [call("fan-mode-topic", "low", 0, False)]), - ({"send_if_off": True}, [call("fan-mode-topic", "low", 0, False)]), - ({"send_if_off": False}, []), - ], -) -async def test_set_fan_mode_send_if_off( - hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish -): - """Test setting of fan mode if the hvac is off.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config[CLIMATE_DOMAIN].update(send_if_off) - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(ENTITY_CLIMATE) is not None - - # Turn on HVAC - await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) - mqtt_mock.async_publish.reset_mock() - # Updates for fan_mode should be sent when the device is turned on - await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_called_once_with("fan-mode-topic", "high", 0, False) - - # Turn off HVAC - await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.state == "off" - - # Updates for fan_mode should be sent if SEND_IF_OFF is not set or is True - mqtt_mock.async_publish.reset_mock() - await common.async_set_fan_mode(hass, "low", ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_has_calls(assert_async_publish) - - async def test_set_swing_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting swing mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -442,44 +404,6 @@ async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("swing_mode") == "on" -# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 -@pytest.mark.parametrize( - "send_if_off,assert_async_publish", - [ - ({}, [call("swing-mode-topic", "on", 0, False)]), - ({"send_if_off": True}, [call("swing-mode-topic", "on", 0, False)]), - ({"send_if_off": False}, []), - ], -) -async def test_set_swing_mode_send_if_off( - hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish -): - """Test setting of swing mode if the hvac is off.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config[CLIMATE_DOMAIN].update(send_if_off) - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(ENTITY_CLIMATE) is not None - - # Turn on HVAC - await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) - mqtt_mock.async_publish.reset_mock() - # Updates for swing_mode should be sent when the device is turned on - await common.async_set_swing_mode(hass, "off", ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_called_once_with("swing-mode-topic", "off", 0, False) - - # Turn off HVAC - await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.state == "off" - - # Updates for swing_mode should be sent if SEND_IF_OFF is not set or is True - mqtt_mock.async_publish.reset_mock() - await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_has_calls(assert_async_publish) - - async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test setting the target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -517,46 +441,6 @@ async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): mqtt_mock.async_publish.reset_mock() -# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 -@pytest.mark.parametrize( - "send_if_off,assert_async_publish", - [ - ({}, [call("temperature-topic", "21.0", 0, False)]), - ({"send_if_off": True}, [call("temperature-topic", "21.0", 0, False)]), - ({"send_if_off": False}, []), - ], -) -async def test_set_target_temperature_send_if_off( - hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish -): - """Test setting of target temperature if the hvac is off.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config[CLIMATE_DOMAIN].update(send_if_off) - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(ENTITY_CLIMATE) is not None - - # Turn on HVAC - await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) - mqtt_mock.async_publish.reset_mock() - # Updates for target temperature should be sent when the device is turned on - await common.async_set_temperature(hass, 16.0, ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_called_once_with( - "temperature-topic", "16.0", 0, False - ) - - # Turn off HVAC - await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.state == "off" - - # Updates for target temperature sent should be if SEND_IF_OFF is not set or is True - mqtt_mock.async_publish.reset_mock() - await common.async_set_temperature(hass, 21.0, ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_has_calls(assert_async_publish) - - async def test_set_target_temperature_pessimistic( hass, mqtt_mock_entry_with_yaml_config ): From cdde4f9925665fde2ad41fba8830878eb4e3b20b Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 5 Aug 2022 14:49:34 +0200 Subject: [PATCH 184/903] Add bluetooth API to allow rediscovery of address (#76005) * Add API to allow rediscovery of domains * Switch to clearing per device * Drop unneded change --- .../components/bluetooth/__init__.py | 12 ++++++ homeassistant/components/bluetooth/match.py | 10 +++-- .../components/fjaraskupan/__init__.py | 10 +++++ tests/components/bluetooth/test_init.py | 40 +++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 0b81472f838..ed04ca401ed 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -214,6 +214,13 @@ def async_track_unavailable( return manager.async_track_unavailable(callback, address) +@hass_callback +def async_rediscover_address(hass: HomeAssistant, address: str) -> None: + """Trigger discovery of devices which have already been seen.""" + manager: BluetoothManager = hass.data[DOMAIN] + manager.async_rediscover_address(address) + + async def _async_has_bluetooth_adapter() -> bool: """Return if the device has a bluetooth adapter.""" return bool(await async_get_bluetooth_adapters()) @@ -545,3 +552,8 @@ class BluetoothManager: # change the bluetooth dongle. _LOGGER.error("Error stopping scanner: %s", ex) uninstall_multiple_bleak_catcher() + + @hass_callback + def async_rediscover_address(self, address: str) -> None: + """Trigger discovery of devices which have already been seen.""" + self._integration_matcher.async_clear_address(address) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 2cd4f62ae5e..9c942d9f411 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -10,7 +10,7 @@ from lru import LRU # pylint: disable=no-name-in-module from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional if TYPE_CHECKING: - from collections.abc import Mapping + from collections.abc import MutableMapping from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData @@ -70,7 +70,7 @@ class IntegrationMatcher: self._integration_matchers = integration_matchers # Some devices use a random address so we need to use # an LRU to avoid memory issues. - self._matched: Mapping[str, IntegrationMatchHistory] = LRU( + self._matched: MutableMapping[str, IntegrationMatchHistory] = LRU( MAX_REMEMBER_ADDRESSES ) @@ -78,6 +78,10 @@ class IntegrationMatcher: """Clear the history.""" self._matched = {} + def async_clear_address(self, address: str) -> None: + """Clear the history matches for a set of domains.""" + self._matched.pop(address, None) + def match_domains(self, device: BLEDevice, adv_data: AdvertisementData) -> set[str]: """Return the domains that are matched.""" matched_domains: set[str] = set() @@ -98,7 +102,7 @@ class IntegrationMatcher: previous_match.service_data |= bool(adv_data.service_data) previous_match.service_uuids |= bool(adv_data.service_uuids) else: - self._matched[device.address] = IntegrationMatchHistory( # type: ignore[index] + self._matched[device.address] = IntegrationMatchHistory( manufacturer_data=bool(adv_data.manufacturer_data), service_data=bool(adv_data.service_data), service_uuids=bool(adv_data.service_uuids), diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index fbd2f13d2b4..28032b3f997 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -14,11 +14,13 @@ from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfoBleak, async_address_present, + async_rediscover_address, async_register_callback, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -113,6 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device = Device(service_info.device) device_info = DeviceInfo( + connections={(dr.CONNECTION_BLUETOOTH, service_info.address)}, identifiers={(DOMAIN, service_info.address)}, manufacturer="Fjäråskupan", name="Fjäråskupan", @@ -175,4 +178,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + for device_entry in dr.async_entries_for_config_entry( + dr.async_get(hass), entry.entry_id + ): + for conn in device_entry.connections: + if conn[0] == dr.CONNECTION_BLUETOOTH: + async_rediscover_address(hass, conn[1]) + return unload_ok diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ba315b1f380..6cd22505dc4 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -16,6 +16,7 @@ from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfo, async_process_advertisements, + async_rediscover_address, async_track_unavailable, models, ) @@ -560,6 +561,45 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( assert len(mock_config_flow.mock_calls) == 0 +async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): + """Test bluetooth discovery can be re-enabled for a given domain.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "switchbot" + + async_rediscover_address(hass, "44:44:33:11:23:45") + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[1][1][0] == "switchbot" + + async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): """Test the async_discovered_device API.""" mock_bt = [] From df67a8cd4f8df91a153778009a74be1e3876ca53 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 5 Aug 2022 09:34:21 -0400 Subject: [PATCH 185/903] Fix ZHA light color temp support (#76305) --- .../components/zha/core/channels/lighting.py | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 36bb0beb17d..1754b9aff68 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -1,7 +1,7 @@ """Lighting channels module for Zigbee Home Automation.""" from __future__ import annotations -from contextlib import suppress +from functools import cached_property from zigpy.zcl.clusters import lighting @@ -46,17 +46,8 @@ class ColorChannel(ZigbeeChannel): "color_loop_active": False, } - @property - def color_capabilities(self) -> int: - """Return color capabilities of the light.""" - with suppress(KeyError): - return self.cluster["color_capabilities"] - if self.cluster.get("color_temperature") is not None: - return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP - return self.CAPABILITIES_COLOR_XY - - @property - def zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities: + @cached_property + def color_capabilities(self) -> lighting.Color.ColorCapabilities: """Return ZCL color capabilities of the light.""" color_capabilities = self.cluster.get("color_capabilities") if color_capabilities is None: @@ -117,43 +108,41 @@ class ColorChannel(ZigbeeChannel): def hs_supported(self) -> bool: """Return True if the channel supports hue and saturation.""" return ( - self.zcl_color_capabilities is not None + self.color_capabilities is not None and lighting.Color.ColorCapabilities.Hue_and_saturation - in self.zcl_color_capabilities + in self.color_capabilities ) @property def enhanced_hue_supported(self) -> bool: """Return True if the channel supports enhanced hue and saturation.""" return ( - self.zcl_color_capabilities is not None - and lighting.Color.ColorCapabilities.Enhanced_hue - in self.zcl_color_capabilities + self.color_capabilities is not None + and lighting.Color.ColorCapabilities.Enhanced_hue in self.color_capabilities ) @property def xy_supported(self) -> bool: """Return True if the channel supports xy.""" return ( - self.zcl_color_capabilities is not None + self.color_capabilities is not None and lighting.Color.ColorCapabilities.XY_attributes - in self.zcl_color_capabilities + in self.color_capabilities ) @property def color_temp_supported(self) -> bool: """Return True if the channel supports color temperature.""" return ( - self.zcl_color_capabilities is not None + self.color_capabilities is not None and lighting.Color.ColorCapabilities.Color_temperature - in self.zcl_color_capabilities - ) + in self.color_capabilities + ) or self.color_temperature is not None @property def color_loop_supported(self) -> bool: """Return True if the channel supports color loop.""" return ( - self.zcl_color_capabilities is not None - and lighting.Color.ColorCapabilities.Color_loop - in self.zcl_color_capabilities + self.color_capabilities is not None + and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities ) From a0ef3ad21b88b2c4c692682afaf64b493d96f682 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 5 Aug 2022 16:06:19 +0200 Subject: [PATCH 186/903] Use stored philips_js system data on start (#75981) Co-authored-by: Martin Hjelmare --- homeassistant/components/philips_js/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 9e574e69f90..24b3f9a91e0 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -38,15 +38,22 @@ LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Philips TV from a config entry.""" + system: SystemType | None = entry.data.get(CONF_SYSTEM) tvapi = PhilipsTV( entry.data[CONF_HOST], entry.data[CONF_API_VERSION], username=entry.data.get(CONF_USERNAME), password=entry.data.get(CONF_PASSWORD), + system=system, ) coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options) await coordinator.async_refresh() + + if (actual_system := tvapi.system) and actual_system != system: + data = {**entry.data, CONF_SYSTEM: actual_system} + hass.config_entries.async_update_entry(entry, data=data) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator From 742877f79b47b7805d90e4b17cf3e168fc3308ce Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Aug 2022 16:28:52 +0200 Subject: [PATCH 187/903] Revert "Disable Spotify Media Player entity by default (#69372)" (#76250) --- homeassistant/components/spotify/media_player.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 04f523c2d4b..ea41067e7e9 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -40,7 +40,7 @@ from .util import fetch_image_url _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=1) +SCAN_INTERVAL = timedelta(seconds=30) SUPPORT_SPOTIFY = ( MediaPlayerEntityFeature.BROWSE_MEDIA @@ -107,7 +107,6 @@ def spotify_exception_handler(func): class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" - _attr_entity_registry_enabled_default = False _attr_has_entity_name = True _attr_icon = "mdi:spotify" _attr_media_content_type = MEDIA_TYPE_MUSIC From c2f026d0a7feb801f36a04f26fe29996d73c20be Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 6 Aug 2022 01:34:27 +0200 Subject: [PATCH 188/903] Minor deCONZ clean up (#76323) * Rename secondary_temperature with internal_temperature * Prefix binary and sensor descriptions matching on all sensor devices with COMMON_ * Always create entities in the same order Its been reported previously that if the integration is removed and setup again that entity IDs can change if not sorted in the numerical order * Rename alarmsystems to alarm_systems * Use websocket enums * Don't use legacy pydeconz constants * Bump pydeconz to v103 * unsub -> unsubscribe --- .../components/deconz/alarm_control_panel.py | 10 +++++----- .../components/deconz/binary_sensor.py | 9 +++++---- .../components/deconz/diagnostics.py | 4 ++-- homeassistant/components/deconz/gateway.py | 2 +- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/number.py | 4 ++-- homeassistant/components/deconz/sensor.py | 20 ++++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/conftest.py | 6 +++--- tests/components/deconz/test_diagnostics.py | 6 +++--- tests/components/deconz/test_gateway.py | 6 +++--- 12 files changed, 38 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index e1fb0757b12..59b4b9e4f8e 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -48,7 +48,7 @@ def get_alarm_system_id_for_unique_id( gateway: DeconzGateway, unique_id: str ) -> str | None: """Retrieve alarm system ID the unique ID is registered to.""" - for alarm_system in gateway.api.alarmsystems.values(): + for alarm_system in gateway.api.alarm_systems.values(): if unique_id in alarm_system.devices: return alarm_system.resource_id return None @@ -122,27 +122,27 @@ class DeconzAlarmControlPanel(DeconzDevice[AncillaryControl], AlarmControlPanelE async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if code: - await self.gateway.api.alarmsystems.arm( + await self.gateway.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.AWAY, code ) async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if code: - await self.gateway.api.alarmsystems.arm( + await self.gateway.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.STAY, code ) async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" if code: - await self.gateway.api.alarmsystems.arm( + await self.gateway.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.NIGHT, code ) async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if code: - await self.gateway.api.alarmsystems.arm( + await self.gateway.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.DISARM, code ) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index a7dbc2eacff..08cb8753bb6 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -159,7 +159,7 @@ ENTITY_DESCRIPTIONS = { ], } -BINARY_SENSOR_DESCRIPTIONS = [ +COMMON_BINARY_SENSOR_DESCRIPTIONS = [ DeconzBinarySensorDescription( key="tampered", value_fn=lambda device: device.tampered, @@ -215,7 +215,8 @@ async def async_setup_entry( sensor = gateway.api.sensors[sensor_id] for description in ( - ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS + ENTITY_DESCRIPTIONS.get(type(sensor), []) + + COMMON_BINARY_SENSOR_DESCRIPTIONS ): if ( not hasattr(sensor, description.key) @@ -283,8 +284,8 @@ class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): if self._device.on is not None: attr[ATTR_ON] = self._device.on - if self._device.secondary_temperature is not None: - attr[ATTR_TEMPERATURE] = self._device.secondary_temperature + if self._device.internal_temperature is not None: + attr[ATTR_TEMPERATURE] = self._device.internal_temperature if isinstance(self._device, Presence): diff --git a/homeassistant/components/deconz/diagnostics.py b/homeassistant/components/deconz/diagnostics.py index 11854421512..5b7986fc4c9 100644 --- a/homeassistant/components/deconz/diagnostics.py +++ b/homeassistant/components/deconz/diagnostics.py @@ -26,7 +26,7 @@ async def async_get_config_entry_diagnostics( gateway.api.config.raw, REDACT_DECONZ_CONFIG ) diag["websocket_state"] = ( - gateway.api.websocket.state if gateway.api.websocket else "Unknown" + gateway.api.websocket.state.value if gateway.api.websocket else "Unknown" ) diag["deconz_ids"] = gateway.deconz_ids diag["entities"] = gateway.entities @@ -37,7 +37,7 @@ async def async_get_config_entry_diagnostics( } for event in gateway.events } - diag["alarm_systems"] = {k: v.raw for k, v in gateway.api.alarmsystems.items()} + diag["alarm_systems"] = {k: v.raw for k, v in gateway.api.alarm_systems.items()} diag["groups"] = {k: v.raw for k, v in gateway.api.groups.items()} diag["lights"] = {k: v.raw for k, v in gateway.api.lights.items()} diag["scenes"] = {k: v.raw for k, v in gateway.api.scenes.items()} diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index f8e4548cf91..6f29cef5190 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -169,7 +169,7 @@ class DeconzGateway: ) ) - for device_id in deconz_device_interface: + for device_id in sorted(deconz_device_interface, key=int): async_add_device(EventType.ADDED, device_id) initializing = False diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index e4e412056e6..51de538324f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==102"], + "requirements": ["pydeconz==103"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 636711d609d..b4a4ba415c0 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from pydeconz.models.event import EventType -from pydeconz.models.sensor.presence import PRESENCE_DELAY, Presence +from pydeconz.models.sensor.presence import Presence from homeassistant.components.number import ( DOMAIN, @@ -42,7 +42,7 @@ ENTITY_DESCRIPTIONS = { key="delay", value_fn=lambda device: device.delay, suffix="Delay", - update_key=PRESENCE_DELAY, + update_key="delay", native_max_value=65535, native_min_value=0, native_step=1, diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 941729ac8c2..5c1fe61c7a7 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -209,7 +209,7 @@ ENTITY_DESCRIPTIONS = { } -SENSOR_DESCRIPTIONS = [ +COMMON_SENSOR_DESCRIPTIONS = [ DeconzSensorDescription( key="battery", value_fn=lambda device: device.battery, @@ -221,8 +221,8 @@ SENSOR_DESCRIPTIONS = [ entity_category=EntityCategory.DIAGNOSTIC, ), DeconzSensorDescription( - key="secondary_temperature", - value_fn=lambda device: device.secondary_temperature, + key="internal_temperature", + value_fn=lambda device: device.internal_temperature, suffix="Temperature", update_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, @@ -253,7 +253,7 @@ async def async_setup_entry( known_entities = set(gateway.entities[DOMAIN]) for description in ( - ENTITY_DESCRIPTIONS.get(type(sensor), []) + SENSOR_DESCRIPTIONS + ENTITY_DESCRIPTIONS.get(type(sensor), []) + COMMON_SENSOR_DESCRIPTIONS ): if ( not hasattr(sensor, description.key) @@ -341,8 +341,8 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): if self._device.on is not None: attr[ATTR_ON] = self._device.on - if self._device.secondary_temperature is not None: - attr[ATTR_TEMPERATURE] = self._device.secondary_temperature + if self._device.internal_temperature is not None: + attr[ATTR_TEMPERATURE] = self._device.internal_temperature if isinstance(self._device, Consumption): attr[ATTR_POWER] = self._device.power @@ -383,14 +383,16 @@ class DeconzBatteryTracker: self.sensor = gateway.api.sensors[sensor_id] self.gateway = gateway self.async_add_entities = async_add_entities - self.unsub = self.sensor.subscribe(self.async_update_callback) + self.unsubscribe = self.sensor.subscribe(self.async_update_callback) @callback def async_update_callback(self) -> None: """Update the device's state.""" if "battery" in self.sensor.changed_keys: - self.unsub() + self.unsubscribe() known_entities = set(self.gateway.entities[DOMAIN]) - entity = DeconzSensor(self.sensor, self.gateway, SENSOR_DESCRIPTIONS[0]) + entity = DeconzSensor( + self.sensor, self.gateway, COMMON_SENSOR_DESCRIPTIONS[0] + ) if entity.unique_id not in known_entities: self.async_add_entities([entity]) diff --git a/requirements_all.txt b/requirements_all.txt index e6b42839383..b638e66be56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1461,7 +1461,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==102 +pydeconz==103 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b128bc80b21..1f4cd646e4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==102 +pydeconz==103 # homeassistant.components.dexcom pydexcom==0.2.3 diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 8b92e94416a..44411ca40cf 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations from unittest.mock import patch -from pydeconz.websocket import SIGNAL_CONNECTION_STATE, SIGNAL_DATA +from pydeconz.websocket import Signal import pytest from tests.components.light.conftest import mock_light_profiles # noqa: F401 @@ -20,10 +20,10 @@ def mock_deconz_websocket(): if data: mock.return_value.data = data - await pydeconz_gateway_session_handler(signal=SIGNAL_DATA) + await pydeconz_gateway_session_handler(signal=Signal.DATA) elif state: mock.return_value.state = state - await pydeconz_gateway_session_handler(signal=SIGNAL_CONNECTION_STATE) + await pydeconz_gateway_session_handler(signal=Signal.CONNECTION_STATE) else: raise NotImplementedError diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index d0905f5ba5f..459e0e910ab 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -1,6 +1,6 @@ """Test deCONZ diagnostics.""" -from pydeconz.websocket import STATE_RUNNING +from pydeconz.websocket import State from homeassistant.components.deconz.const import CONF_MASTER_GATEWAY from homeassistant.components.diagnostics import REDACTED @@ -17,7 +17,7 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - await mock_deconz_websocket(state=STATE_RUNNING) + await mock_deconz_websocket(state=State.RUNNING) await hass.async_block_till_done() assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { @@ -44,7 +44,7 @@ async def test_entry_diagnostics( "uuid": "1234", "websocketport": 1234, }, - "websocket_state": STATE_RUNNING, + "websocket_state": State.RUNNING.value, "deconz_ids": {}, "entities": { str(Platform.ALARM_CONTROL_PANEL): [], diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index e6ded3981c4..9471752eb8d 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -5,7 +5,7 @@ from copy import deepcopy from unittest.mock import patch import pydeconz -from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING +from pydeconz.websocket import State import pytest from homeassistant.components import ssdp @@ -223,12 +223,12 @@ async def test_connection_status_signalling( assert hass.states.get("binary_sensor.presence").state == STATE_OFF - await mock_deconz_websocket(state=STATE_RETRYING) + await mock_deconz_websocket(state=State.RETRYING) await hass.async_block_till_done() assert hass.states.get("binary_sensor.presence").state == STATE_UNAVAILABLE - await mock_deconz_websocket(state=STATE_RUNNING) + await mock_deconz_websocket(state=State.RUNNING) await hass.async_block_till_done() assert hass.states.get("binary_sensor.presence").state == STATE_OFF From 32a2999b8523dcbffea03992318c1403f7a78477 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 6 Aug 2022 00:24:46 +0000 Subject: [PATCH 189/903] [ci skip] Translation update --- .../accuweather/translations/sv.json | 3 + .../components/acmeda/translations/sv.json | 3 + .../components/adax/translations/sv.json | 26 ++++- .../components/airly/translations/sv.json | 2 + .../components/airnow/translations/sv.json | 12 +- .../components/airzone/translations/sv.json | 13 +++ .../aladdin_connect/translations/sv.json | 1 + .../alarmdecoder/translations/sv.json | 3 + .../components/almond/translations/sv.json | 3 +- .../ambee/translations/sensor.sv.json | 1 + .../components/ambee/translations/sv.json | 12 +- .../amberelectric/translations/sv.json | 8 ++ .../ambiclimate/translations/sv.json | 4 +- .../components/androidtv/translations/sv.json | 63 ++++++++++ .../components/apple_tv/translations/sv.json | 17 ++- .../aseko_pool_live/translations/sv.json | 20 ++++ .../components/august/translations/sv.json | 3 +- .../aussie_broadband/translations/sv.json | 7 ++ .../components/auth/translations/de.json | 4 +- .../components/awair/translations/sv.json | 3 +- .../azure_event_hub/translations/sv.json | 49 ++++++++ .../components/balboa/translations/sv.json | 28 +++++ .../binary_sensor/translations/sv.json | 25 +++- .../components/blebox/translations/sv.json | 1 + .../components/blink/translations/sv.json | 2 + .../components/bond/translations/sv.json | 5 +- .../components/braviatv/translations/sv.json | 3 + .../components/brunt/translations/sv.json | 20 +++- .../components/button/translations/sv.json | 11 ++ .../components/canary/translations/sv.json | 20 ++++ .../components/cast/translations/sv.json | 14 ++- .../components/climacell/translations/sv.json | 13 +++ .../components/cloud/translations/sv.json | 1 + .../cloudflare/translations/sv.json | 42 +++++++ .../components/coinbase/translations/sv.json | 5 + .../components/cpuspeed/translations/sv.json | 15 +++ .../components/deluge/translations/sv.json | 15 ++- .../components/demo/translations/ca.json | 11 ++ .../components/demo/translations/hu.json | 11 ++ .../components/demo/translations/id.json | 13 ++- .../components/demo/translations/no.json | 13 ++- .../components/demo/translations/pt-BR.json | 11 ++ .../components/demo/translations/sv.json | 11 ++ .../components/demo/translations/zh-Hant.json | 13 ++- .../components/denonavr/translations/sv.json | 3 + .../derivative/translations/sv.json | 9 +- .../deutsche_bahn/translations/ca.json | 7 ++ .../deutsche_bahn/translations/de.json | 8 ++ .../deutsche_bahn/translations/hu.json | 8 ++ .../deutsche_bahn/translations/id.json | 8 ++ .../deutsche_bahn/translations/no.json | 8 ++ .../deutsche_bahn/translations/pt-BR.json | 8 ++ .../deutsche_bahn/translations/sv.json | 8 ++ .../deutsche_bahn/translations/zh-Hant.json | 8 ++ .../devolo_home_control/translations/sv.json | 7 +- .../devolo_home_network/translations/sv.json | 25 ++++ .../components/dexcom/translations/sv.json | 19 ++- .../diagnostics/translations/sv.json | 3 + .../dialogflow/translations/sv.json | 1 + .../components/discord/translations/sv.json | 6 +- .../components/dlna_dmr/translations/sv.json | 49 +++++++- .../components/dlna_dms/translations/sv.json | 24 ++++ .../components/dnsip/translations/sv.json | 29 +++++ .../components/doorbird/translations/sv.json | 3 + .../components/dsmr/translations/sv.json | 3 +- .../components/eafm/translations/sv.json | 17 +++ .../components/ecobee/translations/sv.json | 3 + .../components/efergy/translations/sv.json | 7 +- .../components/elkm1/translations/sv.json | 21 +++- .../components/elmax/translations/sv.json | 22 +++- .../enphase_envoy/translations/sv.json | 3 +- .../environment_canada/translations/sv.json | 23 ++++ .../components/epson/translations/sv.json | 3 +- .../evil_genius_labs/translations/sv.json | 1 + .../components/ezviz/translations/sv.json | 21 +++- .../faa_delays/translations/sv.json | 8 +- .../components/fan/translations/sv.json | 1 + .../components/fibaro/translations/sv.json | 2 + .../components/filesize/translations/sv.json | 14 ++- .../fireservicerota/translations/sv.json | 18 +++ .../components/fivem/translations/sv.json | 21 ++++ .../components/flipr/translations/sv.json | 3 +- .../components/flo/translations/sv.json | 5 +- .../flunearyou/translations/de.json | 13 +++ .../flunearyou/translations/hu.json | 13 +++ .../flunearyou/translations/ja.json | 12 ++ .../flunearyou/translations/no.json | 13 +++ .../flunearyou/translations/ru.json | 13 +++ .../flunearyou/translations/sv.json | 16 +++ .../flunearyou/translations/zh-Hant.json | 13 +++ .../forecast_solar/translations/sv.json | 10 +- .../forked_daapd/translations/sv.json | 9 ++ .../components/foscam/translations/sv.json | 2 + .../components/fritz/translations/sv.json | 6 +- .../components/fronius/translations/sv.json | 11 +- .../components/generic/translations/sv.json | 23 +++- .../components/github/translations/de.json | 2 +- .../components/github/translations/sv.json | 19 +++ .../components/goodwe/translations/sv.json | 4 +- .../components/google/translations/sv.json | 24 ++++ .../components/gpslogger/translations/sv.json | 1 + .../components/group/translations/sv.json | 69 ++++++++++- .../components/heos/translations/sv.json | 3 + .../components/hive/translations/de.json | 2 +- .../components/homekit/translations/sv.json | 23 ++++ .../translations/select.sv.json | 9 ++ .../translations/sensor.de.json | 21 ++++ .../translations/sensor.hu.json | 21 ++++ .../translations/sensor.ja.json | 21 ++++ .../translations/sensor.no.json | 9 ++ .../translations/sensor.sv.json | 21 ++++ .../translations/sensor.zh-Hant.json | 13 ++- .../homekit_controller/translations/sv.json | 2 + .../homewizard/translations/sv.json | 15 +++ .../components/honeywell/translations/sv.json | 11 ++ .../huawei_lte/translations/sv.json | 1 + .../components/hue/translations/sv.json | 10 +- .../huisbaasje/translations/sv.json | 8 +- .../humidifier/translations/sv.json | 1 + .../components/iaqualink/translations/sv.json | 6 +- .../components/icloud/translations/sv.json | 1 + .../components/ifttt/translations/sv.json | 1 + .../components/insteon/translations/sv.json | 18 +++ .../integration/translations/sv.json | 15 ++- .../intellifire/translations/sv.json | 14 +++ .../components/iotawatt/translations/sv.json | 10 +- .../components/iss/translations/sv.json | 22 ++++ .../kaleidescape/translations/sv.json | 22 +++- .../keenetic_ndms2/translations/sv.json | 5 +- .../components/knx/translations/sv.json | 14 ++- .../components/kodi/translations/sv.json | 12 +- .../components/konnected/translations/sv.json | 1 + .../kostal_plenticore/translations/sv.json | 2 + .../launch_library/translations/sv.json | 12 ++ .../components/lcn/translations/sv.json | 6 +- .../components/life360/translations/sv.json | 3 +- .../components/light/translations/sv.json | 1 + .../components/litejet/translations/sv.json | 22 ++++ .../components/local_ip/translations/sv.json | 1 + .../components/locative/translations/sv.json | 1 + .../components/luftdaten/translations/sv.json | 2 + .../lutron_caseta/translations/sv.json | 19 ++- .../components/mailgun/translations/sv.json | 1 + .../components/mazda/translations/sv.json | 4 +- .../media_player/translations/sv.json | 1 + .../components/met/translations/sv.json | 6 + .../met_eireann/translations/sv.json | 2 + .../meteoclimatic/translations/sv.json | 21 ++++ .../components/mill/translations/sv.json | 10 +- .../components/min_max/translations/sv.json | 14 ++- .../components/mjpeg/translations/sv.json | 23 +++- .../modem_callerid/translations/sv.json | 1 + .../modern_forms/translations/sv.json | 24 ++++ .../moehlenhoff_alpha2/translations/sv.json | 19 +++ .../components/moon/translations/sv.json | 13 +++ .../motion_blinds/translations/sv.json | 14 ++- .../components/motioneye/translations/sv.json | 5 + .../components/mqtt/translations/sv.json | 13 ++- .../components/mullvad/translations/sv.json | 5 + .../components/mysensors/translations/sv.json | 44 ++++++- .../components/nam/translations/sv.json | 16 ++- .../components/nanoleaf/translations/sv.json | 8 ++ .../components/nest/translations/de.json | 12 +- .../components/nest/translations/sv.json | 37 +++++- .../components/netatmo/translations/sv.json | 17 +++ .../components/netgear/translations/sv.json | 21 +++- .../components/nina/translations/sv.json | 25 ++++ .../nmap_tracker/translations/sv.json | 34 +++++- .../components/notion/translations/sv.json | 1 + .../components/nuki/translations/sv.json | 7 +- .../components/number/translations/sv.json | 8 ++ .../components/octoprint/translations/sv.json | 13 ++- .../components/omnilogic/translations/sv.json | 5 + .../ondilo_ico/translations/sv.json | 16 +++ .../components/onewire/translations/sv.json | 23 +++- .../components/onvif/translations/sv.json | 3 + .../open_meteo/translations/sv.json | 12 ++ .../opentherm_gw/translations/sv.json | 4 +- .../overkiz/translations/select.sv.json | 13 +++ .../overkiz/translations/sensor.sv.json | 37 ++++++ .../components/overkiz/translations/sv.json | 15 ++- .../ovo_energy/translations/sv.json | 9 +- .../components/owntracks/translations/de.json | 2 +- .../components/owntracks/translations/sv.json | 1 + .../panasonic_viera/translations/sv.json | 2 + .../components/peco/translations/sv.json | 14 +++ .../philips_js/translations/sv.json | 10 ++ .../components/picnic/translations/sv.json | 4 +- .../components/plaato/translations/de.json | 2 +- .../components/plaato/translations/sv.json | 1 + .../components/plex/translations/sv.json | 1 + .../components/plugwise/translations/sv.json | 2 + .../plum_lightpad/translations/sv.json | 7 ++ .../components/point/translations/sv.json | 3 +- .../components/poolsense/translations/sv.json | 18 +++ .../components/powerwall/translations/sv.json | 12 ++ .../components/profiler/translations/sv.json | 12 ++ .../components/prosegur/translations/sv.json | 12 ++ .../components/ps4/translations/sv.json | 6 + .../pure_energie/translations/sv.json | 23 ++++ .../components/pvoutput/translations/sv.json | 16 ++- .../radio_browser/translations/sv.json | 12 ++ .../rainmachine/translations/sv.json | 4 + .../components/remote/translations/sv.json | 1 + .../components/renault/translations/sv.json | 21 ++++ .../components/rfxtrx/translations/sv.json | 6 +- .../components/ridwell/translations/sv.json | 19 ++- .../components/risco/translations/sv.json | 15 +++ .../components/roku/translations/sv.json | 3 + .../rtsp_to_webrtc/translations/sv.json | 27 +++++ .../components/samsungtv/translations/sv.json | 10 +- .../components/season/translations/sv.json | 7 ++ .../components/sense/translations/sv.json | 19 ++- .../components/senseme/translations/sv.json | 23 ++++ .../components/sensibo/translations/sv.json | 11 ++ .../components/sensor/translations/sv.json | 5 + .../components/shelly/translations/sv.json | 24 +++- .../components/sia/translations/sv.json | 12 +- .../simplepush/translations/de.json | 4 + .../simplepush/translations/hu.json | 4 + .../simplepush/translations/ja.json | 3 + .../simplepush/translations/no.json | 4 + .../simplepush/translations/ru.json | 4 + .../simplepush/translations/sv.json | 4 + .../simplepush/translations/zh-Hant.json | 4 + .../simplisafe/translations/ca.json | 1 + .../simplisafe/translations/de.json | 3 +- .../simplisafe/translations/hu.json | 1 + .../simplisafe/translations/id.json | 2 +- .../simplisafe/translations/no.json | 3 +- .../simplisafe/translations/ru.json | 1 + .../simplisafe/translations/sv.json | 2 + .../simplisafe/translations/zh-Hant.json | 3 +- .../components/sleepiq/translations/sv.json | 16 +++ .../components/smhi/translations/sv.json | 3 + .../components/solaredge/translations/sv.json | 8 +- .../somfy_mylink/translations/sv.json | 11 ++ .../components/sonarr/translations/sv.json | 1 + .../components/songpal/translations/sv.json | 3 + .../components/spider/translations/sv.json | 7 +- .../components/spotify/translations/sv.json | 7 +- .../srp_energy/translations/sv.json | 4 + .../components/steamist/translations/sv.json | 18 ++- .../stookalert/translations/sv.json | 14 +++ .../components/subaru/translations/sv.json | 7 +- .../components/sun/translations/sv.json | 10 ++ .../surepetcare/translations/sv.json | 9 ++ .../components/switch/translations/sv.json | 1 + .../switch_as_x/translations/sv.json | 4 +- .../components/switchbot/translations/sv.json | 2 + .../components/syncthing/translations/sv.json | 4 + .../synology_dsm/translations/sv.json | 4 +- .../system_bridge/translations/sv.json | 21 +++- .../components/tag/translations/sv.json | 3 + .../components/tailscale/translations/sv.json | 16 ++- .../tellduslive/translations/sv.json | 3 +- .../tesla_wall_connector/translations/sv.json | 20 ++++ .../components/threshold/translations/sv.json | 20 +++- .../components/tile/translations/sv.json | 12 +- .../components/tod/translations/sv.json | 23 ++++ .../tolo/translations/select.sv.json | 8 ++ .../components/tolo/translations/sv.json | 22 ++++ .../tomorrowio/translations/sensor.sv.json | 27 +++++ .../tomorrowio/translations/sv.json | 23 +++- .../components/toon/translations/sv.json | 3 +- .../totalconnect/translations/sv.json | 3 + .../components/tplink/translations/sv.json | 6 + .../components/traccar/translations/sv.json | 1 + .../tractive/translations/sensor.sv.json | 10 ++ .../components/tradfri/translations/sv.json | 1 + .../translations/sv.json | 12 +- .../transmission/translations/sv.json | 1 + .../tuya/translations/select.sv.json | 109 +++++++++++++++++- .../tuya/translations/sensor.sv.json | 6 + .../components/tuya/translations/sv.json | 1 + .../components/twilio/translations/sv.json | 1 + .../components/twinkly/translations/sv.json | 3 + .../ukraine_alarm/translations/sv.json | 1 + .../components/unifi/translations/sv.json | 3 +- .../unifiprotect/translations/sv.json | 40 ++++++- .../components/update/translations/sv.json | 3 +- .../components/uptime/translations/sv.json | 13 +++ .../uptimerobot/translations/sensor.sv.json | 11 ++ .../uptimerobot/translations/sv.json | 1 + .../components/vallox/translations/sv.json | 22 ++++ .../components/vera/translations/sv.json | 3 + .../components/version/translations/sv.json | 26 +++++ .../components/vicare/translations/sv.json | 13 ++- .../components/vizio/translations/sv.json | 1 + .../vlc_telnet/translations/sv.json | 19 ++- .../components/vulcan/translations/id.json | 2 +- .../components/vulcan/translations/sv.json | 2 + .../components/watttime/translations/sv.json | 13 ++- .../waze_travel_time/translations/sv.json | 18 ++- .../components/webostv/translations/sv.json | 47 ++++++++ .../components/whirlpool/translations/sv.json | 5 +- .../components/whois/translations/sv.json | 9 ++ .../components/wiffi/translations/sv.json | 12 +- .../components/wilight/translations/sv.json | 15 +++ .../components/wiz/translations/sv.json | 33 ++++++ .../wolflink/translations/sensor.sv.json | 3 + .../xiaomi_ble/translations/sv.json | 14 ++- .../xiaomi_miio/translations/sv.json | 3 +- .../yale_smart_alarm/translations/sv.json | 27 +++++ .../translations/select.sv.json | 52 +++++++++ .../components/yeelight/translations/sv.json | 3 +- .../components/youless/translations/sv.json | 15 +++ .../components/zerproc/translations/sv.json | 13 +++ .../components/zha/translations/sv.json | 2 + .../zodiac/translations/sensor.sv.json | 18 +++ .../zoneminder/translations/sv.json | 17 ++- .../components/zwave_js/translations/sv.json | 54 ++++++++- .../components/zwave_me/translations/sv.json | 20 ++++ 313 files changed, 3494 insertions(+), 184 deletions(-) create mode 100644 homeassistant/components/androidtv/translations/sv.json create mode 100644 homeassistant/components/aseko_pool_live/translations/sv.json create mode 100644 homeassistant/components/azure_event_hub/translations/sv.json create mode 100644 homeassistant/components/balboa/translations/sv.json create mode 100644 homeassistant/components/button/translations/sv.json create mode 100644 homeassistant/components/climacell/translations/sv.json create mode 100644 homeassistant/components/cloudflare/translations/sv.json create mode 100644 homeassistant/components/cpuspeed/translations/sv.json create mode 100644 homeassistant/components/deutsche_bahn/translations/ca.json create mode 100644 homeassistant/components/deutsche_bahn/translations/de.json create mode 100644 homeassistant/components/deutsche_bahn/translations/hu.json create mode 100644 homeassistant/components/deutsche_bahn/translations/id.json create mode 100644 homeassistant/components/deutsche_bahn/translations/no.json create mode 100644 homeassistant/components/deutsche_bahn/translations/pt-BR.json create mode 100644 homeassistant/components/deutsche_bahn/translations/sv.json create mode 100644 homeassistant/components/deutsche_bahn/translations/zh-Hant.json create mode 100644 homeassistant/components/devolo_home_network/translations/sv.json create mode 100644 homeassistant/components/diagnostics/translations/sv.json create mode 100644 homeassistant/components/dlna_dms/translations/sv.json create mode 100644 homeassistant/components/dnsip/translations/sv.json create mode 100644 homeassistant/components/eafm/translations/sv.json create mode 100644 homeassistant/components/environment_canada/translations/sv.json create mode 100644 homeassistant/components/fivem/translations/sv.json create mode 100644 homeassistant/components/github/translations/sv.json create mode 100644 homeassistant/components/homekit_controller/translations/select.sv.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.de.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.hu.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.ja.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.sv.json create mode 100644 homeassistant/components/iss/translations/sv.json create mode 100644 homeassistant/components/launch_library/translations/sv.json create mode 100644 homeassistant/components/meteoclimatic/translations/sv.json create mode 100644 homeassistant/components/modern_forms/translations/sv.json create mode 100644 homeassistant/components/moehlenhoff_alpha2/translations/sv.json create mode 100644 homeassistant/components/moon/translations/sv.json create mode 100644 homeassistant/components/number/translations/sv.json create mode 100644 homeassistant/components/ondilo_ico/translations/sv.json create mode 100644 homeassistant/components/open_meteo/translations/sv.json create mode 100644 homeassistant/components/overkiz/translations/select.sv.json create mode 100644 homeassistant/components/peco/translations/sv.json create mode 100644 homeassistant/components/poolsense/translations/sv.json create mode 100644 homeassistant/components/profiler/translations/sv.json create mode 100644 homeassistant/components/pure_energie/translations/sv.json create mode 100644 homeassistant/components/radio_browser/translations/sv.json create mode 100644 homeassistant/components/rtsp_to_webrtc/translations/sv.json create mode 100644 homeassistant/components/stookalert/translations/sv.json create mode 100644 homeassistant/components/tag/translations/sv.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/sv.json create mode 100644 homeassistant/components/tolo/translations/select.sv.json create mode 100644 homeassistant/components/tolo/translations/sv.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.sv.json create mode 100644 homeassistant/components/tractive/translations/sensor.sv.json create mode 100644 homeassistant/components/uptime/translations/sv.json create mode 100644 homeassistant/components/uptimerobot/translations/sensor.sv.json create mode 100644 homeassistant/components/vallox/translations/sv.json create mode 100644 homeassistant/components/version/translations/sv.json create mode 100644 homeassistant/components/webostv/translations/sv.json create mode 100644 homeassistant/components/wilight/translations/sv.json create mode 100644 homeassistant/components/wiz/translations/sv.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.sv.json create mode 100644 homeassistant/components/youless/translations/sv.json create mode 100644 homeassistant/components/zerproc/translations/sv.json create mode 100644 homeassistant/components/zodiac/translations/sensor.sv.json create mode 100644 homeassistant/components/zwave_me/translations/sv.json diff --git a/homeassistant/components/accuweather/translations/sv.json b/homeassistant/components/accuweather/translations/sv.json index a87b6736271..4dea8a74f47 100644 --- a/homeassistant/components/accuweather/translations/sv.json +++ b/homeassistant/components/accuweather/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, + "create_entry": { + "default": "Vissa sensorer \u00e4r inte aktiverade som standard. Du kan aktivera dem i entitetsregistret efter integrationskonfigurationen.\n V\u00e4derprognos \u00e4r inte aktiverat som standard. Du kan aktivera det i integrationsalternativen." + }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_api_key": "Ogiltig API-nyckel", diff --git a/homeassistant/components/acmeda/translations/sv.json b/homeassistant/components/acmeda/translations/sv.json index 487295ecade..a968c430178 100644 --- a/homeassistant/components/acmeda/translations/sv.json +++ b/homeassistant/components/acmeda/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/adax/translations/sv.json b/homeassistant/components/adax/translations/sv.json index be36fec5fe3..ef22bdd620f 100644 --- a/homeassistant/components/adax/translations/sv.json +++ b/homeassistant/components/adax/translations/sv.json @@ -1,10 +1,34 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "heater_not_available": "V\u00e4rmare inte tillg\u00e4nglig. F\u00f6rs\u00f6k att \u00e5terst\u00e4lla v\u00e4rmaren genom att trycka p\u00e5 + och OK i n\u00e5gra sekunder.", + "heater_not_found": "V\u00e4rmare hittades inte. F\u00f6rs\u00f6k att flytta v\u00e4rmaren n\u00e4rmare Home Assistant-datorn.", + "invalid_auth": "Ogiltig autentisering" }, "error": { "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "cloud": { + "data": { + "account_id": "Konto-ID", + "password": "L\u00f6senord" + } + }, + "local": { + "data": { + "wifi_pswd": "Wi-Fi l\u00f6senord", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "\u00c5terst\u00e4ll v\u00e4rmaren genom att trycka p\u00e5 + och OK tills displayen visar 'Reset'. Tryck sedan och h\u00e5ll ner OK-knappen p\u00e5 v\u00e4rmaren tills den bl\u00e5 lysdioden b\u00f6rjar blinka innan du trycker p\u00e5 Skicka. Det kan ta n\u00e5gra minuter att konfigurera v\u00e4rmaren." + }, + "user": { + "data": { + "connection_type": "V\u00e4lj anslutningstyp" + }, + "description": "V\u00e4lj anslutningstyp. Lokalt kr\u00e4ver v\u00e4rmare med bluetooth" + } } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/sv.json b/homeassistant/components/airly/translations/sv.json index 05d56e9d88a..ac8ae0728df 100644 --- a/homeassistant/components/airly/translations/sv.json +++ b/homeassistant/components/airly/translations/sv.json @@ -21,6 +21,8 @@ }, "system_health": { "info": { + "can_reach_server": "N\u00e5 Airly-servern", + "requests_per_day": "Till\u00e5tna f\u00f6rfr\u00e5gningar per dag", "requests_remaining": "\u00c5terst\u00e5ende till\u00e5tna f\u00f6rfr\u00e5gningar" } } diff --git a/homeassistant/components/airnow/translations/sv.json b/homeassistant/components/airnow/translations/sv.json index 138764f44d9..320291a913f 100644 --- a/homeassistant/components/airnow/translations/sv.json +++ b/homeassistant/components/airnow/translations/sv.json @@ -5,13 +5,19 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "invalid_location": "Inga resultat hittades f\u00f6r den platsen", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Stationsradie (miles; valfritt)" + }, + "description": "F\u00f6r att generera API-nyckel g\u00e5 till https://docs.airnowapi.org/account/request/" } } } diff --git a/homeassistant/components/airzone/translations/sv.json b/homeassistant/components/airzone/translations/sv.json index daa1cb38499..1fe6a415693 100644 --- a/homeassistant/components/airzone/translations/sv.json +++ b/homeassistant/components/airzone/translations/sv.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_system_id": "Ogiltigt Airzone System ID" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "description": "St\u00e4ll in Airzone-integration." + } } } } \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/sv.json b/homeassistant/components/aladdin_connect/translations/sv.json index 867d5d1c5c7..0f9de2eb48c 100644 --- a/homeassistant/components/aladdin_connect/translations/sv.json +++ b/homeassistant/components/aladdin_connect/translations/sv.json @@ -13,6 +13,7 @@ "data": { "password": "L\u00f6senord" }, + "description": "Aladdin Connect-integrationen m\u00e5ste autentisera ditt konto igen", "title": "\u00c5terautenticera integration" }, "user": { diff --git a/homeassistant/components/alarmdecoder/translations/sv.json b/homeassistant/components/alarmdecoder/translations/sv.json index 0e8f0208b6c..4ff8037c318 100644 --- a/homeassistant/components/alarmdecoder/translations/sv.json +++ b/homeassistant/components/alarmdecoder/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "create_entry": { "default": "Ansluten till AlarmDecoder." }, diff --git a/homeassistant/components/almond/translations/sv.json b/homeassistant/components/almond/translations/sv.json index 6cccf60b2c2..c11af204012 100644 --- a/homeassistant/components/almond/translations/sv.json +++ b/homeassistant/components/almond/translations/sv.json @@ -3,7 +3,8 @@ "abort": { "cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.", "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond.", - "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambee/translations/sensor.sv.json b/homeassistant/components/ambee/translations/sensor.sv.json index a7c17b93906..d3280d4ebf4 100644 --- a/homeassistant/components/ambee/translations/sensor.sv.json +++ b/homeassistant/components/ambee/translations/sensor.sv.json @@ -1,6 +1,7 @@ { "state": { "ambee__risk": { + "high": "H\u00f6g", "low": "L\u00e5g", "moderate": "M\u00e5ttlig", "very high": "V\u00e4ldigt h\u00f6gt" diff --git a/homeassistant/components/ambee/translations/sv.json b/homeassistant/components/ambee/translations/sv.json index 77149dc7889..e31205118b9 100644 --- a/homeassistant/components/ambee/translations/sv.json +++ b/homeassistant/components/ambee/translations/sv.json @@ -3,6 +3,10 @@ "abort": { "reauth_successful": "\u00c5terautentisering lyckades" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel" + }, "step": { "reauth_confirm": { "data": { @@ -12,8 +16,12 @@ }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + }, + "description": "Konfigurera Ambee f\u00f6r att integrera med Home Assistant." } } }, diff --git a/homeassistant/components/amberelectric/translations/sv.json b/homeassistant/components/amberelectric/translations/sv.json index 7458627ef0a..fdf3161483f 100644 --- a/homeassistant/components/amberelectric/translations/sv.json +++ b/homeassistant/components/amberelectric/translations/sv.json @@ -1,8 +1,16 @@ { "config": { "step": { + "site": { + "data": { + "site_name": "Namn p\u00e5 platsen", + "site_nmi": "Plats NMI" + }, + "description": "V\u00e4lj NMI f\u00f6r den plats du vill l\u00e4gga till." + }, "user": { "data": { + "api_token": "API Token", "site_id": "Plats-ID" }, "description": "G\u00e5 till {api_url} f\u00f6r att skapa en API-nyckel" diff --git a/homeassistant/components/ambiclimate/translations/sv.json b/homeassistant/components/ambiclimate/translations/sv.json index e6d06553d77..02f3b80022f 100644 --- a/homeassistant/components/ambiclimate/translations/sv.json +++ b/homeassistant/components/ambiclimate/translations/sv.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken." + "access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken.", + "already_configured": "Konto har redan konfigurerats", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen." }, "create_entry": { "default": "Lyckad autentisering med Ambiclimate" diff --git a/homeassistant/components/androidtv/translations/sv.json b/homeassistant/components/androidtv/translations/sv.json new file mode 100644 index 00000000000..297daf9cc02 --- /dev/null +++ b/homeassistant/components/androidtv/translations/sv.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "invalid_unique_id": "Om\u00f6jligt att fastst\u00e4lla ett giltigt unikt ID f\u00f6r enheten" + }, + "error": { + "adbkey_not_file": "ADB-nyckelfil hittades inte", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "key_and_server": "Ange endast ADB-nyckel eller ADB-server", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "ADB-serverns IP-adress (l\u00e4mna tomt om du inte vill anv\u00e4nda den).", + "adb_server_port": "Port f\u00f6r ADB-servern", + "adbkey": "S\u00f6kv\u00e4g till din ADB-nyckelfil (l\u00e5t vara tomt f\u00f6r att automatiskt generera)", + "device_class": "Typ av enhet", + "host": "V\u00e4rd", + "port": "Port" + } + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Ogiltiga regler f\u00f6r tillst\u00e5ndsidentifiering" + }, + "step": { + "apps": { + "data": { + "app_delete": "Markera f\u00f6r att radera denna applikation", + "app_id": "Program-ID", + "app_name": "Programmets namn" + }, + "description": "Konfigurera program-ID {app_id}", + "title": "Konfigurera Android TV-appar" + }, + "init": { + "data": { + "apps": "Konfigurera applikationslista", + "exclude_unnamed_apps": "Undanta appar med ok\u00e4nt namn fr\u00e5n k\u00e4llistan", + "get_sources": "H\u00e4mta de appar som k\u00f6rs som en lista \u00f6ver k\u00e4llor", + "screencap": "Anv\u00e4nd sk\u00e4rmdump f\u00f6r albumomslag", + "state_detection_rules": "Konfigurera regler f\u00f6r tillst\u00e5ndsdetektering", + "turn_off_command": "Kommando f\u00f6r att st\u00e4nga av ADB-skalet (l\u00e4mna tomt som standard)", + "turn_on_command": "Kommando f\u00f6r att aktivera ADB-skalet (l\u00e4mna tomt som standard)" + } + }, + "rules": { + "data": { + "rule_delete": "Markera f\u00f6r att ta bort denna regel", + "rule_id": "Program-ID", + "rule_values": "F\u00f6rteckning \u00f6ver regler f\u00f6r detektering av tillst\u00e5nd (se dokumentationen)" + }, + "description": "Konfigurera identifieringsregel f\u00f6r app-id {rule_id}", + "title": "Konfigurera regler f\u00f6r detektering av Android TV-tillst\u00e5nd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/sv.json b/homeassistant/components/apple_tv/translations/sv.json index 28b6e2ed67b..2e421f154b0 100644 --- a/homeassistant/components/apple_tv/translations/sv.json +++ b/homeassistant/components/apple_tv/translations/sv.json @@ -1,10 +1,16 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "backoff": "Enheten accepterar inte parningsf\u00f6rfr\u00e5gningar f\u00f6r n\u00e4rvarande (du kan ha angett en ogiltig PIN-kod f\u00f6r m\u00e5nga g\u00e5nger), f\u00f6rs\u00f6k igen senare.", "device_did_not_pair": "Inget f\u00f6rs\u00f6k att avsluta parningsprocessen gjordes fr\u00e5n enheten.", + "device_not_found": "Enheten hittades inte under uppt\u00e4ckten, f\u00f6rs\u00f6k att l\u00e4gga till den igen.", + "inconsistent_device": "F\u00f6rv\u00e4ntade protokoll hittades inte under uppt\u00e4ckten. Detta indikerar normalt ett problem med multicast DNS (Zeroconf). F\u00f6rs\u00f6k att l\u00e4gga till enheten igen.", + "ipv6_not_supported": "IPv6 st\u00f6ds inte.", "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades", + "setup_failed": "Det gick inte att konfigurera enheten.", "unknown": "Ov\u00e4ntat fel" }, "error": { @@ -30,6 +36,14 @@ "description": "Parning kr\u00e4vs f\u00f6r protokollet ` {protocol} `. V\u00e4nligen ange PIN-koden som visas p\u00e5 sk\u00e4rmen. Inledande nollor ska utel\u00e4mnas, dvs ange 123 om den visade koden \u00e4r 0123.", "title": "Parkoppling" }, + "password": { + "description": "Ett l\u00f6senord kr\u00e4vs av ` {protocol} `. Detta st\u00f6ds inte \u00e4nnu, inaktivera l\u00f6senordet f\u00f6r att forts\u00e4tta.", + "title": "L\u00f6senord kr\u00e4vs" + }, + "protocol_disabled": { + "description": "Parkoppling kr\u00e4vs f\u00f6r ` {protocol} ` men det \u00e4r inaktiverat p\u00e5 enheten. Granska potentiella \u00e5tkomstbegr\u00e4nsningar (t.ex. till\u00e5t alla enheter i det lokala n\u00e4tverket att ansluta) p\u00e5 enheten. \n\n Du kan forts\u00e4tta utan att para detta protokoll, men vissa funktioner kommer att vara begr\u00e4nsade.", + "title": "Det g\u00e5r inte att koppla ihop" + }, "reconfigure": { "description": "Konfigurera om enheten f\u00f6r att \u00e5terst\u00e4lla dess funktionalitet.", "title": "Omkonfigurering av enheten" @@ -52,7 +66,8 @@ "init": { "data": { "start_off": "Sl\u00e5 inte p\u00e5 enheten n\u00e4r du startar Home Assistant" - } + }, + "description": "Konfigurera allm\u00e4nna enhetsinst\u00e4llningar" } } } diff --git a/homeassistant/components/aseko_pool_live/translations/sv.json b/homeassistant/components/aseko_pool_live/translations/sv.json new file mode 100644 index 00000000000..34edaf71822 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index f3b6cf8d552..0243aef828a 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kontot har redan konfigurerats" + "already_configured": "Kontot har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", diff --git a/homeassistant/components/aussie_broadband/translations/sv.json b/homeassistant/components/aussie_broadband/translations/sv.json index b159fb71b87..535bb55e74b 100644 --- a/homeassistant/components/aussie_broadband/translations/sv.json +++ b/homeassistant/components/aussie_broadband/translations/sv.json @@ -11,6 +11,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera l\u00f6senord f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "service": { "data": { "services": "Tj\u00e4nster" diff --git a/homeassistant/components/auth/translations/de.json b/homeassistant/components/auth/translations/de.json index 93cbf1073cc..e33536bcc1c 100644 --- a/homeassistant/components/auth/translations/de.json +++ b/homeassistant/components/auth/translations/de.json @@ -9,11 +9,11 @@ }, "step": { "init": { - "description": "Bitte w\u00e4hlen Sie einen der Benachrichtigungsdienste:", + "description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:", "title": "Einmal Passwort f\u00fcr Notify einrichten" }, "setup": { - "description": "Ein Einmal-Passwort wurde per **notify.{notify_service}** gesendet. Bitte geben Sie es unten ein:", + "description": "Ein Einmal-Passwort wurde per **notify.{notify_service}** gesendet. Bitte gib es unten ein:", "title": "\u00dcberpr\u00fcfe das Setup" } }, diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json index 4823ac2df6a..017247b4e1d 100644 --- a/homeassistant/components/awair/translations/sv.json +++ b/homeassistant/components/awair/translations/sv.json @@ -28,7 +28,8 @@ "data": { "access_token": "\u00c5tkomstnyckel", "email": "E-post" - } + }, + "description": "Du m\u00e5ste registrera dig f\u00f6r en Awair-utvecklar\u00e5tkomsttoken p\u00e5: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/azure_event_hub/translations/sv.json b/homeassistant/components/azure_event_hub/translations/sv.json new file mode 100644 index 00000000000..a01f17029e8 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/sv.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta med referenserna fr\u00e5n configuration.yaml, ta bort fr\u00e5n yaml och anv\u00e4nd konfigurationsfl\u00f6det.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Att ansluta med referenserna fr\u00e5n configuration.yaml misslyckades med ett ok\u00e4nt fel, ta bort fr\u00e5n yaml och anv\u00e4nd konfigurationsfl\u00f6det." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Event Hub Connection String" + }, + "description": "Ange anslutningsstr\u00e4ngen f\u00f6r: {event_hub_instance_name}", + "title": "Metod f\u00f6r anslutningsstr\u00e4ng" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hub Namnutrymme", + "event_hub_sas_key": "Event Hub SAS-nyckel", + "event_hub_sas_policy": "Event Hub SAS Policy" + }, + "description": "V\u00e4nligen ange SAS-uppgifterna (delad \u00e5tkomstsignatur) f\u00f6r: {event_hub_instance_name}", + "title": "Metod f\u00f6r SAS-autentiseringsuppgifter" + }, + "user": { + "data": { + "event_hub_instance_name": "Event Hub-instansnamn", + "use_connection_string": "Anv\u00e4nd anslutningsstr\u00e4ng" + }, + "title": "Konfigurera din Azure Event Hub-integration" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Intervall mellan att skicka batcher till hubben." + }, + "title": "Alternativ f\u00f6r Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/sv.json b/homeassistant/components/balboa/translations/sv.json new file mode 100644 index 00000000000..d74e7805e44 --- /dev/null +++ b/homeassistant/components/balboa/translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "title": "Anslut till Balboa Wi-Fi-enhet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "H\u00e5ll din Balboa Spa Clients tid synkroniserad med Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index eeaac07d691..b3f2529b766 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -2,6 +2,7 @@ "device_automation": { "condition_type": { "is_bat_low": "{entity_name}-batteriet \u00e4r l\u00e5gt", + "is_co": "{entity_name} uppt\u00e4cker kolmonoxid", "is_cold": "{entity_name} \u00e4r kall", "is_connected": "{entity_name} \u00e4r ansluten", "is_gas": "{entity_name} detekterar gas", @@ -11,6 +12,7 @@ "is_moist": "{entity_name} \u00e4r fuktig", "is_motion": "{entity_name} detekterar r\u00f6relse", "is_moving": "{entity_name} r\u00f6r sig", + "is_no_co": "{entity_name} uppt\u00e4cker inte kolmonoxid", "is_no_gas": "{entity_name} uppt\u00e4cker inte gas", "is_no_light": "{entity_name} uppt\u00e4cker inte ljus", "is_no_motion": "{entity_name} detekterar inte r\u00f6relse", @@ -31,6 +33,8 @@ "is_not_plugged_in": "{entity_name} \u00e4r urkopplad", "is_not_powered": "{entity_name} \u00e4r inte str\u00f6mf\u00f6rd", "is_not_present": "{entity_name} finns inte", + "is_not_running": "{entity_name} k\u00f6rs inte", + "is_not_tampered": "{entity_name} uppt\u00e4cker inte manipulering", "is_not_unsafe": "{entity_name} \u00e4r s\u00e4ker", "is_occupied": "{entity_name} \u00e4r upptagen", "is_off": "{entity_name} \u00e4r avst\u00e4ngd", @@ -43,12 +47,14 @@ "is_running": "{entity_name} k\u00f6rs", "is_smoke": "{entity_name} detekterar r\u00f6k", "is_sound": "{entity_name} uppt\u00e4cker ljud", + "is_tampered": "{entity_name} uppt\u00e4cker manipulering", "is_unsafe": "{entity_name} \u00e4r os\u00e4ker", "is_update": "{entity_name} har en uppdatering tillg\u00e4nglig", "is_vibration": "{entity_name} uppt\u00e4cker vibrationer" }, "trigger_type": { "bat_low": "{entity_name} batteri l\u00e5gt", + "co": "{entity_name} b\u00f6rjade detektera kolmonoxid", "cold": "{entity_name} blev kall", "connected": "{entity_name} ansluten", "gas": "{entity_name} b\u00f6rjade detektera gas", @@ -58,6 +64,7 @@ "moist": "{entity_name} blev fuktig", "motion": "{entity_name} b\u00f6rjade detektera r\u00f6relse", "moving": "{entity_name} b\u00f6rjade r\u00f6ra sig", + "no_co": "{entity_name} slutade detektera kolmonoxid", "no_gas": "{entity_name} slutade uppt\u00e4cka gas", "no_light": "{entity_name} slutade uppt\u00e4cka ljus", "no_motion": "{entity_name} slutade uppt\u00e4cka r\u00f6relse", @@ -78,6 +85,8 @@ "not_plugged_in": "{entity_name} urkopplad", "not_powered": "{entity_name} inte p\u00e5slagen", "not_present": "{entity_name} inte n\u00e4rvarande", + "not_running": "{entity_name} k\u00f6rs inte l\u00e4ngre", + "not_tampered": "{entity_name} slutade uppt\u00e4cka manipulering", "not_unsafe": "{entity_name} blev s\u00e4ker", "occupied": "{entity_name} blev upptagen", "opened": "{entity_name} \u00f6ppnades", @@ -85,8 +94,10 @@ "powered": "{entity_name} p\u00e5slagen", "present": "{entity_name} n\u00e4rvarande", "problem": "{entity_name} b\u00f6rjade uppt\u00e4cka problem", + "running": "{entity_name} b\u00f6rjade k\u00f6ras", "smoke": "{entity_name} b\u00f6rjade detektera r\u00f6k", "sound": "{entity_name} b\u00f6rjade uppt\u00e4cka ljud", + "tampered": "{entity_name} b\u00f6rjade uppt\u00e4cka manipulering", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5", "unsafe": "{entity_name} blev os\u00e4ker", @@ -95,10 +106,18 @@ } }, "device_class": { + "co": "kolmonoxid", "cold": "Kyla", + "gas": "gas", "heat": "v\u00e4rme", + "moisture": "fukt", "motion": "r\u00f6relse", - "power": "effekt" + "occupancy": "n\u00e4rvaro", + "power": "effekt", + "problem": "problem", + "smoke": "r\u00f6k", + "sound": "ljud", + "vibration": "vibration" }, "state": { "_": { @@ -113,6 +132,10 @@ "off": "Laddar inte", "on": "Laddar" }, + "carbon_monoxide": { + "off": "Rensa", + "on": "Detekterad" + }, "cold": { "off": "Normal", "on": "Kallt" diff --git a/homeassistant/components/blebox/translations/sv.json b/homeassistant/components/blebox/translations/sv.json index e521f4c6f4a..2f22cc7d372 100644 --- a/homeassistant/components/blebox/translations/sv.json +++ b/homeassistant/components/blebox/translations/sv.json @@ -13,6 +13,7 @@ "step": { "user": { "data": { + "host": "IP-adress", "port": "Port" }, "description": "St\u00e4ll in din BleBox f\u00f6r att integrera med Home Assistant.", diff --git a/homeassistant/components/blink/translations/sv.json b/homeassistant/components/blink/translations/sv.json index 282dc374e67..9d71443234e 100644 --- a/homeassistant/components/blink/translations/sv.json +++ b/homeassistant/components/blink/translations/sv.json @@ -4,6 +4,8 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/bond/translations/sv.json b/homeassistant/components/bond/translations/sv.json index e745283a84a..9d5223555b6 100644 --- a/homeassistant/components/bond/translations/sv.json +++ b/homeassistant/components/bond/translations/sv.json @@ -4,7 +4,10 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { - "old_firmware": "Gamla firmware som inte st\u00f6ds p\u00e5 Bond-enheten - uppgradera innan du forts\u00e4tter." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "old_firmware": "Gamla firmware som inte st\u00f6ds p\u00e5 Bond-enheten - uppgradera innan du forts\u00e4tter.", + "unknown": "Ov\u00e4ntat fel" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index f545bdcabfb..f47ee5c3a77 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -11,6 +11,9 @@ }, "step": { "authorize": { + "data": { + "pin": "Pin-kod" + }, "description": "Ange PIN-koden som visas p\u00e5 Sony Bravia TV. \n\n Om PIN-koden inte visas m\u00e5ste du avregistrera Home Assistant p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Inst\u00e4llningar f\u00f6r fj\u00e4rrenhet - > Avregistrera fj\u00e4rrenhet.", "title": "Auktorisera Sony Bravia TV" }, diff --git a/homeassistant/components/brunt/translations/sv.json b/homeassistant/components/brunt/translations/sv.json index 23c825f256f..8db2db05e9a 100644 --- a/homeassistant/components/brunt/translations/sv.json +++ b/homeassistant/components/brunt/translations/sv.json @@ -1,10 +1,28 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "V\u00e4nligen ange l\u00f6senordet igen f\u00f6r: {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Konfigurera din Brunt-integration" } } } diff --git a/homeassistant/components/button/translations/sv.json b/homeassistant/components/button/translations/sv.json new file mode 100644 index 00000000000..4f512af45d0 --- /dev/null +++ b/homeassistant/components/button/translations/sv.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Tryck p\u00e5 knappen {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} har tryckts" + } + }, + "title": "Knapp" +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/sv.json b/homeassistant/components/canary/translations/sv.json index 23c825f256f..7b57017ad3b 100644 --- a/homeassistant/components/canary/translations/sv.json +++ b/homeassistant/components/canary/translations/sv.json @@ -1,9 +1,29 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" + }, + "title": "Anslut till Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argument som skickas till ffmpeg f\u00f6r kameror", + "timeout": "Timeout f\u00f6r beg\u00e4ran (sekunder)" } } } diff --git a/homeassistant/components/cast/translations/sv.json b/homeassistant/components/cast/translations/sv.json index 49ec796088a..1bcc2ec4f72 100644 --- a/homeassistant/components/cast/translations/sv.json +++ b/homeassistant/components/cast/translations/sv.json @@ -26,8 +26,18 @@ "step": { "advanced_options": { "data": { - "ignore_cec": "Ignorera CEC" - } + "ignore_cec": "Ignorera CEC", + "uuid": "Till\u00e5tna UUID" + }, + "description": "Till\u00e5tna UUID - En kommaseparerad lista \u00f6ver UUID f\u00f6r Cast-enheter att l\u00e4gga till i Home Assistant. Anv\u00e4nd endast om du inte vill l\u00e4gga till alla tillg\u00e4ngliga cast-enheter.\n Ignorera CEC \u2013 En kommaseparerad lista \u00f6ver Chromecast-enheter som ska ignorera CEC-data f\u00f6r att fastst\u00e4lla den aktiva ing\u00e5ngen. Detta skickas till pychromecast.IGNORE_CEC.", + "title": "Avancerad Google Cast-konfiguration" + }, + "basic_options": { + "data": { + "known_hosts": "K\u00e4nad v\u00e4rdar" + }, + "description": "K\u00e4nda v\u00e4rdar - En kommaseparerad lista \u00f6ver v\u00e4rdnamn eller IP-adresser f\u00f6r cast-enheter, anv\u00e4nd om mDNS-uppt\u00e4ckt inte fungerar.", + "title": "Google Cast-konfiguration" } } } diff --git a/homeassistant/components/climacell/translations/sv.json b/homeassistant/components/climacell/translations/sv.json new file mode 100644 index 00000000000..2382ec64324 --- /dev/null +++ b/homeassistant/components/climacell/translations/sv.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Mellan NowCast-prognoser" + }, + "description": "Om du v\u00e4ljer att aktivera \"nowcast\"-prognosentiteten kan du konfigurera antalet minuter mellan varje prognos. Antalet prognoser som tillhandah\u00e5lls beror p\u00e5 antalet minuter som v\u00e4ljs mellan prognoserna.", + "title": "Uppdatera ClimaCell-alternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/sv.json b/homeassistant/components/cloud/translations/sv.json index 528e996272f..4711a333f49 100644 --- a/homeassistant/components/cloud/translations/sv.json +++ b/homeassistant/components/cloud/translations/sv.json @@ -10,6 +10,7 @@ "relayer_connected": "Vidarebefodrare Ansluten", "remote_connected": "Fj\u00e4rransluten", "remote_enabled": "Fj\u00e4rr\u00e5tkomst Aktiverad", + "remote_server": "Fj\u00e4rrserver", "subscription_expiration": "Prenumerationens utg\u00e5ng" } } diff --git a/homeassistant/components/cloudflare/translations/sv.json b/homeassistant/components/cloudflare/translations/sv.json new file mode 100644 index 00000000000..4d9f671a20d --- /dev/null +++ b/homeassistant/components/cloudflare/translations/sv.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_zone": "Ogiltig zon" + }, + "flow_title": "{name}", + "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token", + "description": "Autentisera p\u00e5 nytt med ditt Cloudflare-konto." + } + }, + "records": { + "data": { + "records": "Poster" + }, + "title": "V\u00e4lj de poster som ska uppdateras" + }, + "user": { + "data": { + "api_token": "API Token" + }, + "description": "Den h\u00e4r integrationen kr\u00e4ver en API-token som skapats med beh\u00f6righeter f\u00f6r Zone:Zone:Read och Zone:DNS:Edit f\u00f6r alla zoner i ditt konto.", + "title": "Anslut till Cloudflare" + }, + "zone": { + "data": { + "zone": "Zon" + }, + "title": "V\u00e4lj den zon som ska uppdateras" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/sv.json b/homeassistant/components/coinbase/translations/sv.json index c63ebf1c09f..83c5e7c3f0e 100644 --- a/homeassistant/components/coinbase/translations/sv.json +++ b/homeassistant/components/coinbase/translations/sv.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "invalid_auth_key": "API-inloggningsuppgifter avvisades av Coinbase p\u00e5 grund av en ogiltig API-nyckel.", + "invalid_auth_secret": "API-inloggningsuppgifter avvisades av Coinbase p\u00e5 grund av en ogiltig API-hemlighet.", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/cpuspeed/translations/sv.json b/homeassistant/components/cpuspeed/translations/sv.json new file mode 100644 index 00000000000..532d7a14fe9 --- /dev/null +++ b/homeassistant/components/cpuspeed/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "not_compatible": "Det g\u00e5r inte att f\u00e5 CPU-information, denna integration \u00e4r inte kompatibel med ditt system" + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?", + "title": "CPU klockfrekvens" + } + } + }, + "title": "CPU klockfrekvens" +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/sv.json b/homeassistant/components/deluge/translations/sv.json index 1a65fe29a6f..f77b1f5c552 100644 --- a/homeassistant/components/deluge/translations/sv.json +++ b/homeassistant/components/deluge/translations/sv.json @@ -1,11 +1,22 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", "port": "Port", - "username": "Anv\u00e4ndarnamn" - } + "username": "Anv\u00e4ndarnamn", + "web_port": "Webbport (f\u00f6r bes\u00f6kstj\u00e4nst)" + }, + "description": "F\u00f6r att kunna anv\u00e4nda denna integration m\u00e5ste du aktivera f\u00f6ljande alternativ i deluge-inst\u00e4llningarna: Daemon > Till\u00e5t fj\u00e4rrkontroller" } } } diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index 4ffda91d97c..19fc5a86e0b 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Prem ENVIAR per confirmar que s'ha canviat la font d'alimentaci\u00f3", + "title": "La font d'alimentaci\u00f3 s'ha de canviar" + } + } + }, + "title": "La font d'alimentaci\u00f3 no \u00e9s estable" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index 1ff83e4ed06..a5aa872fc5c 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Nyomja meg a MEHET gombot a t\u00e1pegys\u00e9g cser\u00e9j\u00e9nek meger\u0151s\u00edt\u00e9s\u00e9hez.", + "title": "A t\u00e1pegys\u00e9get ki kell cser\u00e9lni" + } + } + }, + "title": "A t\u00e1pegys\u00e9g nem m\u0171k\u00f6dik megb\u00edzhat\u00f3 m\u00f3don" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/id.json b/homeassistant/components/demo/translations/id.json index e8f827e2b86..7acf127caa8 100644 --- a/homeassistant/components/demo/translations/id.json +++ b/homeassistant/components/demo/translations/id.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tekan KIRIM untuk mengonfirmasi bahwa catu daya telah diganti", + "title": "Catu daya perlu diganti" + } + } + }, + "title": "Catu daya tidak stabil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Tekan Oke saat cairan blinker telah diisi ulang", + "description": "Tekan KIRIM saat cairan blinker telah diisi ulang", "title": "Cairan blinker perlu diisi ulang" } } diff --git a/homeassistant/components/demo/translations/no.json b/homeassistant/components/demo/translations/no.json index 7cbf50d76bb..396c3a366f6 100644 --- a/homeassistant/components/demo/translations/no.json +++ b/homeassistant/components/demo/translations/no.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Trykk SUBMIT for \u00e5 bekrefte at str\u00f8mforsyningen er byttet ut", + "title": "Str\u00f8mforsyningen m\u00e5 skiftes" + } + } + }, + "title": "Str\u00f8mforsyningen er ikke stabil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Trykk OK n\u00e5r blinklysv\u00e6ske er fylt p\u00e5 igjen", + "description": "Trykk SUBMIT n\u00e5r blinklysv\u00e6ske er fylt p\u00e5 igjen", "title": "Blinkerv\u00e6ske m\u00e5 etterfylles" } } diff --git a/homeassistant/components/demo/translations/pt-BR.json b/homeassistant/components/demo/translations/pt-BR.json index 26e903f345d..e38a9a1c7aa 100644 --- a/homeassistant/components/demo/translations/pt-BR.json +++ b/homeassistant/components/demo/translations/pt-BR.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pressione ENVIAR para confirmar que a fonte de alimenta\u00e7\u00e3o foi substitu\u00edda", + "title": "A fonte de alimenta\u00e7\u00e3o precisa ser substitu\u00edda" + } + } + }, + "title": "A fonte de alimenta\u00e7\u00e3o n\u00e3o \u00e9 est\u00e1vel" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/sv.json b/homeassistant/components/demo/translations/sv.json index d88f06a3f99..f192cb8b544 100644 --- a/homeassistant/components/demo/translations/sv.json +++ b/homeassistant/components/demo/translations/sv.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tryck p\u00e5 SKICKA f\u00f6r att bekr\u00e4fta att str\u00f6mf\u00f6rs\u00f6rjningen har bytts ut", + "title": "Str\u00f6mf\u00f6rs\u00f6rjningen m\u00e5ste bytas ut" + } + } + }, + "title": "Str\u00f6mf\u00f6rs\u00f6rjningen \u00e4r inte stabil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/zh-Hant.json b/homeassistant/components/demo/translations/zh-Hant.json index a984c9c62ca..60f465316a0 100644 --- a/homeassistant/components/demo/translations/zh-Hant.json +++ b/homeassistant/components/demo/translations/zh-Hant.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u6309\u4e0b\u300c\u50b3\u9001\u300d\u4ee5\u78ba\u8a8d\u96fb\u6e90\u4f9b\u61c9\u5df2\u7d93\u66f4\u63db", + "title": "\u96fb\u6e90\u4f9b\u61c9\u9700\u8981\u66f4\u63db" + } + } + }, + "title": "\u96fb\u6e90\u4f9b\u61c9\u4e0d\u7a69\u5b9a" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "\u65bc\u8a0a\u865f\u5291\u88dc\u5145\u5f8c\u6309\u4e0b OK", + "description": "\u65bc\u88dc\u5145\u8a0a\u865f\u5291\u5f8c\u6309\u4e0b\u300c\u50b3\u9001\u300d", "title": "\u8a0a\u865f\u5291\u9700\u8981\u88dc\u5145" } } diff --git a/homeassistant/components/denonavr/translations/sv.json b/homeassistant/components/denonavr/translations/sv.json index db692b7f835..288b5df5357 100644 --- a/homeassistant/components/denonavr/translations/sv.json +++ b/homeassistant/components/denonavr/translations/sv.json @@ -25,6 +25,9 @@ "user": { "data": { "host": "IP-adress" + }, + "data_description": { + "host": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt" } } } diff --git a/homeassistant/components/derivative/translations/sv.json b/homeassistant/components/derivative/translations/sv.json index 0f36236b1ae..5ea8260f731 100644 --- a/homeassistant/components/derivative/translations/sv.json +++ b/homeassistant/components/derivative/translations/sv.json @@ -5,12 +5,15 @@ "data": { "name": "Namn", "round": "Precision", + "source": "Ing\u00e5ngssensor", "time_window": "Tidsf\u00f6nster", "unit_prefix": "Metriskt prefix", "unit_time": "Tidsenhet" }, "data_description": { - "round": "Anger antal decimaler i resultatet." + "round": "Anger antal decimaler i resultatet.", + "time_window": "Om den \u00e4r inst\u00e4lld \u00e4r sensorns v\u00e4rde ett tidsviktat glidande medelv\u00e4rde av derivat inom detta f\u00f6nster.", + "unit_prefix": "Utdata kommer att skalas enligt det valda metriska prefixet och tidsenheten f\u00f6r derivatan." }, "description": "Skapa en sensor som ber\u00e4knar derivatan av en sensor", "title": "L\u00e4gg till derivatasensor" @@ -23,13 +26,15 @@ "data": { "name": "Namn", "round": "Precision", + "source": "Ing\u00e5ngssensor", "time_window": "Tidsf\u00f6nster", "unit_prefix": "Metriskt prefix", "unit_time": "Tidsenhet" }, "data_description": { "round": "Anger antal decimaler i resultatet.", - "unit_prefix": "." + "time_window": "Om den \u00e4r inst\u00e4lld \u00e4r sensorns v\u00e4rde ett tidsviktat glidande medelv\u00e4rde av derivat inom detta f\u00f6nster.", + "unit_prefix": "Utdata kommer att skalas enligt det valda metriska prefixet och tidsenheten f\u00f6r derivatan.." } } } diff --git a/homeassistant/components/deutsche_bahn/translations/ca.json b/homeassistant/components/deutsche_bahn/translations/ca.json new file mode 100644 index 00000000000..a4dc0724423 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/ca.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "La integraci\u00f3 Deutsche Bahn est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/de.json b/homeassistant/components/deutsche_bahn/translations/de.json new file mode 100644 index 00000000000..986aebfa7a6 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die Deutsche Bahn-Integration wird derzeit aus Home Assistant entfernt und wird ab Home Assistant 2022.11 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil sie auf Webscraping beruht, was nicht erlaubt ist.\n\nEntferne die YAML-Konfiguration der Deutschen Bahn aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Deutsche-Bahn-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/hu.json b/homeassistant/components/deutsche_bahn/translations/hu.json new file mode 100644 index 00000000000..5b1bf7e95e6 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A Deutsche Bahn integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a 2022.11-es Home Assistant-t\u00f3l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl, mivel webscrapingre t\u00e1maszkodik, ami nem megengedett.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Deutsche Bahn YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Deutsche Bahn integr\u00e1ci\u00f3 megsz\u00fcnik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/id.json b/homeassistant/components/deutsche_bahn/translations/id.json new file mode 100644 index 00000000000..2bd68daf49e --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasi Deutsche Bahn sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.11.\n\nIntegrasi ini dalam proses penghapusan, karena bergantung pada proses webscraping, yang tidak diizinkan.\n\nHapus konfigurasi Deutsche Bahn YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi Deutsche Bahn dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/no.json b/homeassistant/components/deutsche_bahn/translations/no.json new file mode 100644 index 00000000000..5852c5b716c --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahn-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2022.11. \n\n Integrasjonen blir fjernet, fordi den er avhengig av webscraping, noe som ikke er tillatt. \n\n Fjern Deutsche Bahn YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Deutsche Bahn-integrasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/pt-BR.json b/homeassistant/components/deutsche_bahn/translations/pt-BR.json new file mode 100644 index 00000000000..abf4f29a0e5 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o da Deutsche Bahn est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.11. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, pois depende de webscraping, o que n\u00e3o \u00e9 permitido. \n\n Remova a configura\u00e7\u00e3o YAML da Deutsche Bahn do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o da Deutsche Bahn est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/sv.json b/homeassistant/components/deutsche_bahn/translations/sv.json new file mode 100644 index 00000000000..7595e8bed60 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahn-integrationen v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.11. \n\n Integrationen tas bort, eftersom den f\u00f6rlitar sig p\u00e5 webbskrapning, vilket inte \u00e4r till\u00e5tet. \n\n Ta bort Deutsche Bahn YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Deutsche Bahn-integrationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/zh-Hant.json b/homeassistant/components/deutsche_bahn/translations/zh-Hant.json new file mode 100644 index 00000000000..fd60b2ae7fb --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahn \u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.11 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc\u4f7f\u7528\u4e86\u4e0d\u88ab\u5141\u8a31\u7684\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Deutsche Bahn YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Deutsche Bahn \u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json index 3081604f882..c2b34e47a0f 100644 --- a/homeassistant/components/devolo_home_control/translations/sv.json +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto har redan konfigurerats" + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "reauth_failed": "Anv\u00e4nd samma mydevolo-anv\u00e4ndare som tidigare." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_network/translations/sv.json b/homeassistant/components/devolo_home_network/translations/sv.json new file mode 100644 index 00000000000..097e9d826b9 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "home_control": "Devolo Home Control Central Unit fungerar inte med denna integration." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP-adress" + }, + "description": "Vill du starta konfigurationen?" + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till devolos hemn\u00e4tverksenhet med v\u00e4rdnamnet ` {host_name} ` till Home Assistant?", + "title": "Uppt\u00e4ckte devolo hemn\u00e4tverksenhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/sv.json b/homeassistant/components/dexcom/translations/sv.json index d82f098eb83..3dc87b0aaff 100644 --- a/homeassistant/components/dexcom/translations/sv.json +++ b/homeassistant/components/dexcom/translations/sv.json @@ -1,13 +1,30 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "error": { "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "password": "L\u00f6senord", + "server": "Server", "username": "Anv\u00e4ndarnamn" + }, + "description": "Ange Dexcom Share-uppgifter", + "title": "St\u00e4ll in Dexcom-integration" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "M\u00e5ttenhet" } } } diff --git a/homeassistant/components/diagnostics/translations/sv.json b/homeassistant/components/diagnostics/translations/sv.json new file mode 100644 index 00000000000..732e52ee843 --- /dev/null +++ b/homeassistant/components/diagnostics/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Diagnostik" +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/sv.json b/homeassistant/components/dialogflow/translations/sv.json index daf0f4f3ea6..bc2d5d27ecf 100644 --- a/homeassistant/components/dialogflow/translations/sv.json +++ b/homeassistant/components/dialogflow/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant instans m\u00e5ste kunna n\u00e5s fr\u00e5n Internet f\u00f6r att ta emot webhook meddelanden" }, diff --git a/homeassistant/components/discord/translations/sv.json b/homeassistant/components/discord/translations/sv.json index 38370827354..4526346ddf6 100644 --- a/homeassistant/components/discord/translations/sv.json +++ b/homeassistant/components/discord/translations/sv.json @@ -13,12 +13,14 @@ "reauth_confirm": { "data": { "api_token": "API Token" - } + }, + "description": "Se dokumentationen om hur du skaffar din Discord-botnyckel. \n\n {url}" }, "user": { "data": { "api_token": "API Token" - } + }, + "description": "Se dokumentationen om hur du skaffar din Discord-botnyckel. \n\n {url}" } } } diff --git a/homeassistant/components/dlna_dmr/translations/sv.json b/homeassistant/components/dlna_dmr/translations/sv.json index 8f95dda75db..dd2a1fb7491 100644 --- a/homeassistant/components/dlna_dmr/translations/sv.json +++ b/homeassistant/components/dlna_dmr/translations/sv.json @@ -1,10 +1,55 @@ { + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "alternative_integration": "Enheten st\u00f6ds b\u00e4ttre av en annan integration", + "cannot_connect": "Det gick inte att ansluta.", + "discovery_error": "Det gick inte att uppt\u00e4cka en matchande DLNA-enhet", + "incomplete_config": "Konfigurationen saknar en n\u00f6dv\u00e4ndig variabel", + "non_unique_id": "Flera enheter hittades med samma unika ID", + "not_dmr": "Enheten \u00e4r inte en digital mediarenderare som st\u00f6ds" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "not_dmr": "Enheten \u00e4r inte en digital mediarenderare som st\u00f6ds" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, + "import_turn_on": { + "description": "Sl\u00e5 p\u00e5 enheten och klicka p\u00e5 skicka f\u00f6r att forts\u00e4tta migreringen" + }, + "manual": { + "data": { + "url": "URL" + }, + "description": "URL till en XML-fil f\u00f6r enhetsbeskrivning", + "title": "Manuell DLNA DMR-enhetsanslutning" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "V\u00e4lj en enhet att konfigurera eller l\u00e4mna tomt f\u00f6r att ange en URL", + "title": "Uppt\u00e4ckte DLNA DMR-enheter" + } + } + }, "options": { + "error": { + "invalid_url": "Ogiltig URL" + }, "step": { "init": { "data": { - "browse_unfiltered": "Visa inkompatibla media n\u00e4r du surfar" - } + "browse_unfiltered": "Visa inkompatibla media n\u00e4r du surfar", + "callback_url_override": "URL f\u00f6r \u00e5teruppringning av h\u00e4ndelseavlyssnare", + "listen_port": "H\u00e4ndelseavlyssnarport (slumpm\u00e4ssig om inte inst\u00e4llt)", + "poll_availability": "Fr\u00e5ga efter om en enhet \u00e4r tillg\u00e4nglig" + }, + "title": "Konfiguration av DLNA Digital Media Renderer" } } } diff --git a/homeassistant/components/dlna_dms/translations/sv.json b/homeassistant/components/dlna_dms/translations/sv.json new file mode 100644 index 00000000000..6bf1cda2bd8 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "bad_ssdp": "SSDP-data saknar ett obligatoriskt v\u00e4rde", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_dms": "Enheten \u00e4r inte en mediaserver som st\u00f6ds" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "V\u00e4lj en enhet att konfigurera", + "title": "Uppt\u00e4ckta DLNA DMA-enheter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/sv.json b/homeassistant/components/dnsip/translations/sv.json new file mode 100644 index 00000000000..f5cc073d7a3 --- /dev/null +++ b/homeassistant/components/dnsip/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_hostname": "Ogiltigt v\u00e4rdnamn" + }, + "step": { + "user": { + "data": { + "hostname": "V\u00e4rdnamnet f\u00f6r vilket DNS-fr\u00e5gan ska utf\u00f6ras", + "resolver": "Resolver f\u00f6r IPV4-s\u00f6kning", + "resolver_ipv6": "Resolver f\u00f6r IPV6-s\u00f6kning" + } + } + } + }, + "options": { + "error": { + "invalid_resolver": "Ogiltig IP-adress f\u00f6r resolver" + }, + "step": { + "init": { + "data": { + "resolver": "Resolver f\u00f6r IPV4-s\u00f6kning", + "resolver_ipv6": "Resolver f\u00f6r IPV6-s\u00f6kning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json index d91cc55a3be..547af936d47 100644 --- a/homeassistant/components/doorbird/translations/sv.json +++ b/homeassistant/components/doorbird/translations/sv.json @@ -27,6 +27,9 @@ "init": { "data": { "events": "Kommaseparerad lista \u00f6ver h\u00e4ndelser." + }, + "data_description": { + "events": "L\u00e4gg till ett kommaseparerat h\u00e4ndelsenamn f\u00f6r varje h\u00e4ndelse du vill sp\u00e5ra. N\u00e4r du har angett dem h\u00e4r, anv\u00e4nd DoorBird-appen f\u00f6r att tilldela dem till en specifik h\u00e4ndelse. \n\n Exempel: n\u00e5gon_tryckte p\u00e5_knappen, r\u00f6relse" } } } diff --git a/homeassistant/components/dsmr/translations/sv.json b/homeassistant/components/dsmr/translations/sv.json index 7ad8f5f0b09..09eba96b2dc 100644 --- a/homeassistant/components/dsmr/translations/sv.json +++ b/homeassistant/components/dsmr/translations/sv.json @@ -35,7 +35,8 @@ "user": { "data": { "type": "Anslutningstyp" - } + }, + "title": "V\u00e4lj anslutningstyp" } } }, diff --git a/homeassistant/components/eafm/translations/sv.json b/homeassistant/components/eafm/translations/sv.json new file mode 100644 index 00000000000..8fdf2a9f064 --- /dev/null +++ b/homeassistant/components/eafm/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "no_stations": "Inga \u00f6vervakningsstationer f\u00f6r \u00f6versv\u00e4mningar hittades." + }, + "step": { + "user": { + "data": { + "station": "Station" + }, + "description": "V\u00e4lj den station du vill \u00f6vervaka", + "title": "Sp\u00e5ra en station f\u00f6r \u00f6vervakning av \u00f6versv\u00e4mningar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/sv.json b/homeassistant/components/ecobee/translations/sv.json index d78d5ba521e..84d699cc928 100644 --- a/homeassistant/components/ecobee/translations/sv.json +++ b/homeassistant/components/ecobee/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { "pin_request_failed": "Fel vid beg\u00e4ran av PIN-kod fr\u00e5n ecobee. kontrollera API-nyckeln \u00e4r korrekt.", "token_request_failed": "Fel vid beg\u00e4ran av tokens fr\u00e5n ecobee; v\u00e4nligen f\u00f6rs\u00f6k igen." diff --git a/homeassistant/components/efergy/translations/sv.json b/homeassistant/components/efergy/translations/sv.json index b163c1a520b..623afec73af 100644 --- a/homeassistant/components/efergy/translations/sv.json +++ b/homeassistant/components/efergy/translations/sv.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { - "invalid_auth": "Ogiltig autentisering" + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { diff --git a/homeassistant/components/elkm1/translations/sv.json b/homeassistant/components/elkm1/translations/sv.json index 305329feff4..003f12b54b3 100644 --- a/homeassistant/components/elkm1/translations/sv.json +++ b/homeassistant/components/elkm1/translations/sv.json @@ -3,6 +3,8 @@ "abort": { "address_already_configured": "En ElkM1 med denna adress \u00e4r redan konfigurerad", "already_configured": "En ElkM1 med detta prefix \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, @@ -11,19 +13,34 @@ "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{mac_address} ({host})", "step": { "discovered_connection": { "data": { + "password": "L\u00f6senord", + "protocol": "Protokoll", + "temperature_unit": "Temperaturenheten ElkM1 anv\u00e4nder.", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Anslut till det uppt\u00e4ckta systemet: {mac_address} ( {host} )", + "title": "Anslut till Elk-M1 Control" }, "manual_connection": { "data": { + "address": "IP-adressen eller dom\u00e4nen eller seriell port om anslutning via seriell.", + "password": "L\u00f6senord", + "prefix": "Ett unikt prefix (l\u00e4mna tomt om du bara har en ElkM1).", "protocol": "Protokoll", + "temperature_unit": "Temperaturenheten ElkM1 anv\u00e4nder.", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Adressstr\u00e4ngen m\u00e5ste ha formen \"address[:port]\" f\u00f6r \"s\u00e4ker\" och \"icke-s\u00e4ker\". Exempel: '192.168.1.1'. Porten \u00e4r valfri och har som standard 2101 f\u00f6r \"icke-s\u00e4ker\" och 2601 f\u00f6r \"s\u00e4ker\". F\u00f6r det seriella protokollet m\u00e5ste adressen vara i formen 'tty[:baud]'. Exempel: '/dev/ttyS1'. Bauden \u00e4r valfri och \u00e4r som standard 115200.", + "title": "Anslut till Elk-M1 Control" }, "user": { + "data": { + "device": "Enhet" + }, "description": "V\u00e4lj ett uppt\u00e4ckt system eller \"Manuell inmatning\" om inga enheter har uppt\u00e4ckts.", "title": "Anslut till Elk-M1 Control" } diff --git a/homeassistant/components/elmax/translations/sv.json b/homeassistant/components/elmax/translations/sv.json index 23c825f256f..cd87c97b3e3 100644 --- a/homeassistant/components/elmax/translations/sv.json +++ b/homeassistant/components/elmax/translations/sv.json @@ -1,10 +1,30 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "invalid_pin": "Den angivna pin-koden \u00e4r ogiltig", + "network_error": "Ett n\u00e4tverksfel uppstod", + "no_panel_online": "Ingen Elmax kontrollpanel online hittades.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "panels": { + "data": { + "panel_id": "Panel-ID", + "panel_name": "Panelnamn", + "panel_pin": "Pinkod" + }, + "description": "V\u00e4lj vilken panel du vill styra med denna integration. Observera att panelen m\u00e5ste vara p\u00e5 f\u00f6r att kunna konfigureras." + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen logga in p\u00e5 Elmax-molnet med dina uppgifter" } } } diff --git a/homeassistant/components/enphase_envoy/translations/sv.json b/homeassistant/components/enphase_envoy/translations/sv.json index 3889eae836b..5117a6fcd20 100644 --- a/homeassistant/components/enphase_envoy/translations/sv.json +++ b/homeassistant/components/enphase_envoy/translations/sv.json @@ -16,7 +16,8 @@ "host": "V\u00e4rd", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "F\u00f6r nyare modeller, ange anv\u00e4ndarnamnet \"envoy\" utan l\u00f6senord. F\u00f6r \u00e4ldre modeller, ange anv\u00e4ndarnamnet `installer` utan l\u00f6senord. F\u00f6r alla andra modeller, ange ett giltigt anv\u00e4ndarnamn och l\u00f6senord." } } } diff --git a/homeassistant/components/environment_canada/translations/sv.json b/homeassistant/components/environment_canada/translations/sv.json new file mode 100644 index 00000000000..8bd3af75502 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Stations-ID \u00e4r ogiltigt, saknas eller finns inte i stations-ID-databasen", + "cannot_connect": "Det gick inte att ansluta.", + "error_response": "Svar fr\u00e5n Environment Canada felaktigt", + "too_many_attempts": "Anslutningar till Environment Canada \u00e4r hastighetsbegr\u00e4nsade; F\u00f6rs\u00f6k igen om 60 sekunder.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "language": "Spr\u00e5k f\u00f6r v\u00e4derinformation", + "latitude": "Latitud", + "longitude": "Longitud", + "station": "V\u00e4derstations-ID" + }, + "description": "Antingen ett stations-ID eller latitud/longitud m\u00e5ste anges. Standardlatitud/longitud som anv\u00e4nds \u00e4r de v\u00e4rden som konfigurerats i din Home Assistant-installation. Den v\u00e4derstation som ligger n\u00e4rmast koordinaterna kommer att anv\u00e4ndas om du anger koordinater. Om en stationskod anv\u00e4nds m\u00e5ste den f\u00f6lja formatet: PP/kod, d\u00e4r PP \u00e4r tv\u00e5bokstavsprovinsen och kod \u00e4r stations-ID. Listan \u00f6ver stations-ID:n finns h\u00e4r: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. V\u00e4derinformation kan h\u00e4mtas p\u00e5 antingen engelska eller franska.", + "title": "Environment Canada: v\u00e4derplats och spr\u00e5k" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/sv.json b/homeassistant/components/epson/translations/sv.json index 45224016263..b933bf9485e 100644 --- a/homeassistant/components/epson/translations/sv.json +++ b/homeassistant/components/epson/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "powered_off": "\u00c4r projektorn p\u00e5slagen? Du m\u00e5ste sl\u00e5 p\u00e5 projektorn f\u00f6r den f\u00f6rsta konfigurationen." }, "step": { "user": { diff --git a/homeassistant/components/evil_genius_labs/translations/sv.json b/homeassistant/components/evil_genius_labs/translations/sv.json index d51f98c6100..77cc9e74352 100644 --- a/homeassistant/components/evil_genius_labs/translations/sv.json +++ b/homeassistant/components/evil_genius_labs/translations/sv.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Det gick inte att ansluta.", + "timeout": "Timeout uppr\u00e4ttar anslutning", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/ezviz/translations/sv.json b/homeassistant/components/ezviz/translations/sv.json index 971e996a103..2ea759456b7 100644 --- a/homeassistant/components/ezviz/translations/sv.json +++ b/homeassistant/components/ezviz/translations/sv.json @@ -1,29 +1,41 @@ { "config": { "abort": { + "already_configured_account": "Konto har redan konfigurerats", + "ezviz_cloud_account_missing": "Ezviz molnkonto saknas. V\u00e4nligen konfigurera om Ezviz molnkonto", "unknown": "Ov\u00e4ntat fel" }, "error": { - "cannot_connect": "Kunde inte ansluta" + "cannot_connect": "Kunde inte ansluta", + "invalid_auth": "Ogiltig autentisering", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress" }, + "flow_title": "{serial}", "step": { "confirm": { "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange RTSP-uppgifter f\u00f6r Ezviz-kamera {serial} med IP {ip_address}", + "title": "Uppt\u00e4ckte Ezviz Camera" }, "user": { "data": { "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till Ezviz Cloud" }, "user_custom_url": { "data": { "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange din regions URL manuellt", + "title": "Anslut till anpassad Ezviz URL" } } }, @@ -31,6 +43,7 @@ "step": { "init": { "data": { + "ffmpeg_arguments": "Argument som skickas till ffmpeg f\u00f6r kameror", "timeout": "Timeout f\u00f6r beg\u00e4ran (sekunder)" } } diff --git a/homeassistant/components/faa_delays/translations/sv.json b/homeassistant/components/faa_delays/translations/sv.json index bd797004301..cfa624c113d 100644 --- a/homeassistant/components/faa_delays/translations/sv.json +++ b/homeassistant/components/faa_delays/translations/sv.json @@ -1,14 +1,20 @@ { "config": { + "abort": { + "already_configured": "Denna flygplats \u00e4r redan konfigurerad." + }, "error": { "cannot_connect": "Kunde inte ansluta", + "invalid_airport": "Flygplatskoden \u00e4r inte giltig", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { "id": "Flygplats" - } + }, + "description": "Ange en amerikansk flygplatskod i IATA-format", + "title": "FAA f\u00f6rseningar" } } } diff --git a/homeassistant/components/fan/translations/sv.json b/homeassistant/components/fan/translations/sv.json index 31df690b766..d061669532f 100644 --- a/homeassistant/components/fan/translations/sv.json +++ b/homeassistant/components/fan/translations/sv.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} aktiverades" } diff --git a/homeassistant/components/fibaro/translations/sv.json b/homeassistant/components/fibaro/translations/sv.json index 89cfc8f6c3b..e35b7b41d46 100644 --- a/homeassistant/components/fibaro/translations/sv.json +++ b/homeassistant/components/fibaro/translations/sv.json @@ -11,7 +11,9 @@ "step": { "user": { "data": { + "import_plugins": "Importera enheter fr\u00e5n fibaro plugin?", "password": "L\u00f6senord", + "url": "URL i formatet http://HOST/api/", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/filesize/translations/sv.json b/homeassistant/components/filesize/translations/sv.json index c0b662beebe..4bc5b562d6a 100644 --- a/homeassistant/components/filesize/translations/sv.json +++ b/homeassistant/components/filesize/translations/sv.json @@ -2,6 +2,18 @@ "config": { "abort": { "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "not_allowed": "S\u00f6kv\u00e4gen \u00e4r inte till\u00e5ten", + "not_valid": "S\u00f6kv\u00e4gen \u00e4r inte giltig" + }, + "step": { + "user": { + "data": { + "file_path": "S\u00f6kv\u00e4g till filen" + } + } } - } + }, + "title": "Filstorlek" } \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/sv.json b/homeassistant/components/fireservicerota/translations/sv.json index 23c825f256f..79167d65334 100644 --- a/homeassistant/components/fireservicerota/translations/sv.json +++ b/homeassistant/components/fireservicerota/translations/sv.json @@ -1,8 +1,26 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "create_entry": { + "default": "Autentiserats" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { + "reauth": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Autentiseringstokens blev ogiltiga, logga in f\u00f6r att \u00e5terskapa dem." + }, "user": { "data": { + "password": "L\u00f6senord", + "url": "Webbplats", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/fivem/translations/sv.json b/homeassistant/components/fivem/translations/sv.json new file mode 100644 index 00000000000..01ce62450d5 --- /dev/null +++ b/homeassistant/components/fivem/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta. Kontrollera v\u00e4rden och porten och f\u00f6rs\u00f6k igen. Se ocks\u00e5 till att du k\u00f6r den senaste FiveM-servern.", + "invalid_game_name": "API:et f\u00f6r spelet du f\u00f6rs\u00f6ker ansluta till \u00e4r inte ett FiveM-spel.", + "unknown_error": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/sv.json b/homeassistant/components/flipr/translations/sv.json index ec835585137..84bdebc9ba9 100644 --- a/homeassistant/components/flipr/translations/sv.json +++ b/homeassistant/components/flipr/translations/sv.json @@ -22,7 +22,8 @@ "email": "E-post", "password": "L\u00f6senord" }, - "description": "Anslut med ditt Flipr-konto." + "description": "Anslut med ditt Flipr-konto.", + "title": "Anslut till Flipr" } } } diff --git a/homeassistant/components/flo/translations/sv.json b/homeassistant/components/flo/translations/sv.json index a07a2c509bc..f85a02855d6 100644 --- a/homeassistant/components/flo/translations/sv.json +++ b/homeassistant/components/flo/translations/sv.json @@ -4,11 +4,14 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json index 61dd2cd4ce7..72109df534c 100644 --- a/homeassistant/components/flunearyou/translations/de.json +++ b/homeassistant/components/flunearyou/translations/de.json @@ -16,5 +16,18 @@ "title": "Konfiguriere Grippe in deiner N\u00e4he" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Die externe Datenquelle, aus der die Integration von Flu Near You gespeist wird, ist nicht mehr verf\u00fcgbar; daher funktioniert die Integration nicht mehr.\n\nDr\u00fccke SUBMIT, um Flu Near You aus deiner Home Assistant-Instanz zu entfernen.", + "title": "Flu Near You entfernen" + } + } + }, + "title": "Flu Near You ist nicht mehr verf\u00fcgbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/hu.json b/homeassistant/components/flunearyou/translations/hu.json index a67bc91a2a1..efda42723b4 100644 --- a/homeassistant/components/flunearyou/translations/hu.json +++ b/homeassistant/components/flunearyou/translations/hu.json @@ -16,5 +16,18 @@ "title": "Flu Near You weboldal konfigur\u00e1l\u00e1sa" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "A Flu Near You integr\u00e1ci\u00f3t m\u0171k\u00f6dtet\u0151 k\u00fcls\u0151 adatforr\u00e1s m\u00e1r nem el\u00e9rhet\u0151, \u00edgy az integr\u00e1ci\u00f3 m\u00e1r nem m\u0171k\u00f6dik.\n\nNyomja meg a MEHET gombot a Flu Near You elt\u00e1vol\u00edt\u00e1s\u00e1hoz a Home Assistantb\u00f3l.", + "title": "Flu Near You elt\u00e1vol\u00edt\u00e1sa" + } + } + }, + "title": "Flu Near You m\u00e1r nem el\u00e9rhet\u0151" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ja.json b/homeassistant/components/flunearyou/translations/ja.json index 23df88d984b..116800656a6 100644 --- a/homeassistant/components/flunearyou/translations/ja.json +++ b/homeassistant/components/flunearyou/translations/ja.json @@ -16,5 +16,17 @@ "title": "\u8fd1\u304f\u306eFlu\u3092\u8a2d\u5b9a" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u8fd1\u304f\u306eFlu Near You\u3092\u524a\u9664" + } + } + }, + "title": "Flu Near You\u306f\u3001\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3057\u305f" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/no.json b/homeassistant/components/flunearyou/translations/no.json index b958aea2ab5..898889bc348 100644 --- a/homeassistant/components/flunearyou/translations/no.json +++ b/homeassistant/components/flunearyou/translations/no.json @@ -16,5 +16,18 @@ "title": "Konfigurere influensa i n\u00e6rheten av deg" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Den eksterne datakilden som driver Flu Near You-integrasjonen er ikke lenger tilgjengelig; dermed fungerer ikke integreringen lenger. \n\n Trykk SUBMIT for \u00e5 fjerne Flu Near You fra Home Assistant-forekomsten.", + "title": "Fjern influensa n\u00e6r deg" + } + } + }, + "title": "Flu Near You er ikke lenger tilgjengelig" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ru.json b/homeassistant/components/flunearyou/translations/ru.json index e4694cf31b9..d9f184e05a6 100644 --- a/homeassistant/components/flunearyou/translations/ru.json +++ b/homeassistant/components/flunearyou/translations/ru.json @@ -16,5 +16,18 @@ "title": "Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412\u043d\u0435\u0448\u043d\u0438\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u0430\u043d\u043d\u044b\u0445, \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u044e\u0449\u0438\u0439 \u0440\u0430\u0431\u043e\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Flu Near You, \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d.\n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c Flu Near You \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e Home Assistant.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Flu Near You" + } + } + }, + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Flu Near You \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/sv.json b/homeassistant/components/flunearyou/translations/sv.json index eb41d4ff78f..0ce55468d86 100644 --- a/homeassistant/components/flunearyou/translations/sv.json +++ b/homeassistant/components/flunearyou/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Dessa koordinater \u00e4r redan registrerade." }, + "error": { + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { @@ -13,5 +16,18 @@ "title": "Konfigurera influensa i n\u00e4rheten av dig" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Den externa datak\u00e4llan som driver Flu Near You-integrationen \u00e4r inte l\u00e4ngre tillg\u00e4nglig; allts\u00e5 fungerar inte integrationen l\u00e4ngre. \n\n Tryck p\u00e5 SUBMIT f\u00f6r att ta bort Flu Near You fr\u00e5n din Home Assistant-instans.", + "title": "Ta bort influensa n\u00e4ra dig" + } + } + }, + "title": "Influensa n\u00e4ra dig \u00e4r inte l\u00e4ngre tillg\u00e4nglig" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/zh-Hant.json b/homeassistant/components/flunearyou/translations/zh-Hant.json index f3273349f1f..42b975c451b 100644 --- a/homeassistant/components/flunearyou/translations/zh-Hant.json +++ b/homeassistant/components/flunearyou/translations/zh-Hant.json @@ -16,5 +16,18 @@ "title": "\u8a2d\u5b9a Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "The external data source powering the Flu Near You \u6574\u5408\u6240\u4f7f\u7528\u7684\u5916\u90e8\u8cc7\u6599\u4f86\u6e90\u5df2\u7d93\u7121\u6cd5\u4f7f\u7528\uff0c\u6574\u5408\u7121\u6cd5\u4f7f\u7528\u3002\n\n\u6309\u4e0b\u300c\u50b3\u9001\u300d\u4ee5\u79fb\u9664\u7531 Home Assistant \u79fb\u9664 Flu Near You\u3002", + "title": "\u79fb\u9664 Flu Near You" + } + } + }, + "title": "Flu Near You \u5df2\u7d93\u7121\u6cd5\u4f7f\u7528" + } } } \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/sv.json b/homeassistant/components/forecast_solar/translations/sv.json index 8a1e0911c8d..e21c5cc38fb 100644 --- a/homeassistant/components/forecast_solar/translations/sv.json +++ b/homeassistant/components/forecast_solar/translations/sv.json @@ -9,7 +9,8 @@ "longitude": "Longitud", "modules power": "Total maxeffekt (Watt) p\u00e5 dina solpaneler", "name": "Namn" - } + }, + "description": "Fyll i data f\u00f6r dina solpaneler. Se dokumentationen om ett f\u00e4lt \u00e4r otydligt." } } }, @@ -20,8 +21,11 @@ "api_key": "API-nyckel f\u00f6r Forecast.Solar (valfritt)", "azimuth": "Azimuth (360 grader, 0 = norr, 90 = \u00f6st, 180 = s\u00f6der, 270 = v\u00e4ster)", "damping": "D\u00e4mpningsfaktor: justerar resultaten p\u00e5 morgonen och kv\u00e4llen", - "declination": "Deklination (0 = horisontell, 90 = vertikal)" - } + "declination": "Deklination (0 = horisontell, 90 = vertikal)", + "inverter_size": "V\u00e4xelriktarens storlek (Watt)", + "modules power": "Total maxeffekt (Watt) p\u00e5 dina solpaneler" + }, + "description": "Dessa v\u00e4rden till\u00e5ter justering av Solar.Forecast-resultatet. Se dokumentationen om ett f\u00e4lt \u00e4r otydligt." } } } diff --git a/homeassistant/components/forked_daapd/translations/sv.json b/homeassistant/components/forked_daapd/translations/sv.json index 3e36427c525..e155ee1bd20 100644 --- a/homeassistant/components/forked_daapd/translations/sv.json +++ b/homeassistant/components/forked_daapd/translations/sv.json @@ -1,6 +1,15 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "not_forked_daapd": "Enheten \u00e4r inte en forked-daapd-server." + }, "error": { + "forbidden": "Kan inte ansluta. Kontrollera dina forked-daapd-n\u00e4tverksbeh\u00f6righeter.", + "unknown_error": "Ov\u00e4ntat fel", + "websocket_not_enabled": "forked-daapd server websocket inte aktiverat.", + "wrong_host_or_port": "Kan inte ansluta. Kontrollera v\u00e4rd och port.", + "wrong_password": "Felaktigt l\u00f6senord.", "wrong_server_type": "Forked-daapd-integrationen kr\u00e4ver en forked-daapd-server med version > = 27.0." }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/foscam/translations/sv.json b/homeassistant/components/foscam/translations/sv.json index 9de4793eabe..10974a23d65 100644 --- a/homeassistant/components/foscam/translations/sv.json +++ b/homeassistant/components/foscam/translations/sv.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "invalid_response": "Ogiltigt svar fr\u00e5n enheten", "unknown": "Ov\u00e4ntat fel" }, "step": { @@ -14,6 +15,7 @@ "host": "V\u00e4rd", "password": "L\u00f6senord", "port": "Port", + "rtsp_port": "RTSP-port", "stream": "Str\u00f6m", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/fritz/translations/sv.json b/homeassistant/components/fritz/translations/sv.json index f29f7f0ba78..6f1fd326587 100644 --- a/homeassistant/components/fritz/translations/sv.json +++ b/homeassistant/components/fritz/translations/sv.json @@ -10,7 +10,8 @@ "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "upnp_not_configured": "UPnP-inst\u00e4llningar saknas p\u00e5 enheten." }, "flow_title": "{name}", "step": { @@ -46,7 +47,8 @@ "step": { "init": { "data": { - "consider_home": "Sekunder att \u00f6verv\u00e4ga en enhet hemma" + "consider_home": "Sekunder att \u00f6verv\u00e4ga en enhet hemma", + "old_discovery": "Aktivera gammal uppt\u00e4cktsmetod" } } } diff --git a/homeassistant/components/fronius/translations/sv.json b/homeassistant/components/fronius/translations/sv.json index f341a6314ee..43d6170132b 100644 --- a/homeassistant/components/fronius/translations/sv.json +++ b/homeassistant/components/fronius/translations/sv.json @@ -1,17 +1,24 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress" }, "error": { "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Vill du l\u00e4gga till {device} i Home Assistant?" + }, "user": { "data": { "host": "V\u00e4rd" - } + }, + "description": "Konfigurera IP-adressen eller det lokala v\u00e4rdnamnet f\u00f6r din Fronius-enhet.", + "title": "Fronius SolarNet" } } } diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 9a4067bbcfc..616931824b9 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -5,9 +5,15 @@ "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "error": { + "already_exists": "Det finns redan en kamera med dessa URL-inst\u00e4llningar.", + "invalid_still_image": "URL returnerade inte en giltig stillbild", "malformed_url": "Ogiltig URL", + "no_still_image_or_stream_url": "Du m\u00e5ste ange \u00e5tminstone en stillbilds- eller stream-URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tna", + "stream_file_not_found": "Filen hittades inte n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6m (\u00e4r ffmpeg installerat?)", + "stream_http_not_found": "HTTP 404 Ej funnen n\u00e4r du f\u00f6rs\u00f6ker ansluta till str\u00f6mmen", "stream_io_error": "Inmatnings-/utg\u00e5ngsfel vid f\u00f6rs\u00f6k att ansluta till stream. Fel RTSP-transportprotokoll?", + "stream_no_route_to_host": "Kunde inte hitta v\u00e4rddatorn n\u00e4r jag f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", "stream_no_video": "Str\u00f6mmen har ingen video", "stream_not_permitted": "\u00c5tg\u00e4rden \u00e4r inte till\u00e5ten n\u00e4r du f\u00f6rs\u00f6ker ansluta till streamen. Fel RTSP-transportprotokoll?", "stream_unauthorised": "Auktoriseringen misslyckades n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", @@ -44,10 +50,22 @@ }, "options": { "error": { + "already_exists": "Det finns redan en kamera med dessa URL-inst\u00e4llningar.", + "invalid_still_image": "URL returnerade inte en giltig stillbild", "malformed_url": "Ogiltig URL", + "no_still_image_or_stream_url": "Du m\u00e5ste ange \u00e5tminstone en stillbilds- eller stream-URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tet", + "stream_file_not_found": "Filen hittades inte n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6m (\u00e4r ffmpeg installerat?)", + "stream_http_not_found": "HTTP 404 Ej funnen n\u00e4r du f\u00f6rs\u00f6ker ansluta till str\u00f6mmen", + "stream_io_error": "Inmatnings-/utg\u00e5ngsfel vid f\u00f6rs\u00f6k att ansluta till stream. Fel RTSP-transportprotokoll?", + "stream_no_route_to_host": "Kunde inte hitta v\u00e4rddatorn n\u00e4r jag f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", + "stream_no_video": "Str\u00f6mmen har ingen video", + "stream_not_permitted": "\u00c5tg\u00e4rden \u00e4r inte till\u00e5ten n\u00e4r du f\u00f6rs\u00f6ker ansluta till streamen. Fel RTSP-transportprotokoll?", + "stream_unauthorised": "Auktoriseringen misslyckades n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information.", - "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL" + "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL", + "unable_still_load": "Det g\u00e5r inte att ladda giltig bild fr\u00e5n stillbilds-URL (t.ex. ogiltig v\u00e4rd, URL eller autentiseringsfel). Granska loggen f\u00f6r mer information.", + "unknown": "Ov\u00e4ntat fel" }, "step": { "content_type": { @@ -59,6 +77,9 @@ "init": { "data": { "authentication": "Autentiseringen", + "framerate": "Bildfrekvens (Hz)", + "limit_refetch_to_url_change": "Begr\u00e4nsa \u00e5terh\u00e4mtning till \u00e4ndring av webbadress", + "password": "L\u00f6senord", "rtsp_transport": "RTSP transportprotokoll", "still_image_url": "URL f\u00f6r stillbild (t.ex. http://...)", "stream_source": "URL f\u00f6r str\u00f6mk\u00e4lla (t.ex. rtsp://...)", diff --git a/homeassistant/components/github/translations/de.json b/homeassistant/components/github/translations/de.json index 904d15774d9..0d195210744 100644 --- a/homeassistant/components/github/translations/de.json +++ b/homeassistant/components/github/translations/de.json @@ -5,7 +5,7 @@ "could_not_register": "Integration konnte nicht mit GitHub registriert werden" }, "progress": { - "wait_for_device": "1. \u00d6ffne {url}\n 2. F\u00fcge den folgenden Schl\u00fcssel ein, um die Integration zu autorisieren:\n ```\n {code}\n ```\n" + "wait_for_device": "1. \u00d6ffne {url}\n 2. F\u00fcge den folgenden Schl\u00fcssel ein, um die Integration zu autorisieren:\n ```\n {code}\n ```" }, "step": { "repositories": { diff --git a/homeassistant/components/github/translations/sv.json b/homeassistant/components/github/translations/sv.json new file mode 100644 index 00000000000..9f7a956f729 --- /dev/null +++ b/homeassistant/components/github/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "could_not_register": "Det gick inte att registrera integration med GitHub" + }, + "progress": { + "wait_for_device": "1. \u00d6ppna {url}\n 2.Klistra in f\u00f6ljande nyckel f\u00f6r att auktorisera integrationen:\n ```\n {code}\n ```\n" + }, + "step": { + "repositories": { + "data": { + "repositories": "V\u00e4lj de arkiv som ska sp\u00e5ras." + }, + "title": "Konfigurera arkiv" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/sv.json b/homeassistant/components/goodwe/translations/sv.json index 360862118d4..717b6bfaffc 100644 --- a/homeassistant/components/goodwe/translations/sv.json +++ b/homeassistant/components/goodwe/translations/sv.json @@ -11,7 +11,9 @@ "user": { "data": { "host": "IP-adress" - } + }, + "description": "Anslut till v\u00e4xelriktare", + "title": "GoodWe v\u00e4xelriktare" } } } diff --git a/homeassistant/components/google/translations/sv.json b/homeassistant/components/google/translations/sv.json index d1e92e9b8a0..499ea375547 100644 --- a/homeassistant/components/google/translations/sv.json +++ b/homeassistant/components/google/translations/sv.json @@ -5,8 +5,32 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "cannot_connect": "Det gick inte att ansluta.", + "code_expired": "Autentiseringskoden har l\u00f6pt ut eller autentiseringsinst\u00e4llningarna \u00e4r ogiltig, f\u00f6rs\u00f6k igen.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "oauth_error": "Mottog ogiltiga tokendata.", + "reauth_successful": "\u00c5terautentisering lyckades", "timeout_connect": "Timeout vid anslutningsf\u00f6rs\u00f6k" + }, + "create_entry": { + "default": "Autentiserats" + }, + "progress": { + "exchange": "F\u00f6r att l\u00e4nka ditt Google-konto bes\u00f6ker du [ {url} ]( {url} ) och anger koden: \n\n {user_code}" + }, + "step": { + "auth": { + "title": "L\u00e4nka Google-konto" + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "description": "Google Kalender-integrationen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" + } } }, "issues": { diff --git a/homeassistant/components/gpslogger/translations/sv.json b/homeassistant/components/gpslogger/translations/sv.json index f73ba63337d..794d5959541 100644 --- a/homeassistant/components/gpslogger/translations/sv.json +++ b/homeassistant/components/gpslogger/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index 93cbf568052..3382dc7a5ee 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -5,17 +5,34 @@ "data": { "all": "Alla entiteter", "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", "name": "Namn" }, + "description": "Om \"alla enheter\" \u00e4r aktiverat \u00e4r gruppens status endast aktiverad om alla medlemmar \u00e4r p\u00e5. Om \"alla enheter\" \u00e4r inaktiverat \u00e4r gruppens status p\u00e5 om n\u00e5gon medlem \u00e4r p\u00e5.", "title": "Ny grupp" }, "cover": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, "title": "L\u00e4gg till grupp" }, "fan": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, "title": "L\u00e4gg till grupp" }, "light": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, "title": "L\u00e4gg till grupp" }, "lock": { @@ -27,17 +44,31 @@ "title": "L\u00e4gg till grupp" }, "media_player": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, "title": "L\u00e4gg till grupp" }, "switch": { "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", "name": "Namn" }, "title": "L\u00e4gg till grupp" }, "user": { + "description": "Med grupper kan du skapa en ny enhet som representerar flera enheter av samma typ.", "menu_options": { - "lock": "L\u00e5sgrupp" + "binary_sensor": "Bin\u00e4r sensorgrupp", + "cover": "T\u00e4ckningsgrupp", + "fan": "Fl\u00e4ktgrupp", + "light": "Ljusgrupp", + "lock": "L\u00e5sgrupp", + "media_player": "Grupp av mediespelare", + "switch": "Brytargrupp" }, "title": "L\u00e4gg till grupp" } @@ -48,20 +79,50 @@ "binary_sensor": { "data": { "all": "Alla entiteter", - "entities": "Medlemmar" + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + }, + "description": "Om \"alla enheter\" \u00e4r aktiverat \u00e4r gruppens status endast aktiverad om alla medlemmar \u00e4r p\u00e5. Om \"alla enheter\" \u00e4r inaktiverat \u00e4r gruppens status p\u00e5 om n\u00e5gon medlem \u00e4r p\u00e5." + }, + "cover": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" } }, + "fan": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + } + }, + "light": { + "data": { + "all": "Alla entiteter", + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + }, + "description": "Om \"alla enheter\" \u00e4r aktiverat \u00e4r gruppens status endast aktiverad om alla medlemmar \u00e4r p\u00e5. Om \"alla enheter\" \u00e4r inaktiverat \u00e4r gruppens status p\u00e5 om n\u00e5gon medlem \u00e4r p\u00e5." + }, "lock": { "data": { "entities": "Medlemmar", "hide_members": "D\u00f6lj medlemmar" } }, + "media_player": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + } + }, "switch": { "data": { "all": "Alla entiteter", - "entities": "Medlemmar" - } + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + }, + "description": "Om \"alla enheter\" \u00e4r aktiverat \u00e4r gruppens status endast aktiverad om alla medlemmar \u00e4r p\u00e5. Om \"alla enheter\" \u00e4r inaktiverat \u00e4r gruppens status p\u00e5 om n\u00e5gon medlem \u00e4r p\u00e5." } } }, diff --git a/homeassistant/components/heos/translations/sv.json b/homeassistant/components/heos/translations/sv.json index 100f8fd83a5..f1538d3b780 100644 --- a/homeassistant/components/heos/translations/sv.json +++ b/homeassistant/components/heos/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hive/translations/de.json b/homeassistant/components/hive/translations/de.json index e40fd0f499f..e40c7a1adcb 100644 --- a/homeassistant/components/hive/translations/de.json +++ b/homeassistant/components/hive/translations/de.json @@ -24,7 +24,7 @@ "data": { "device_name": "Ger\u00e4tename" }, - "description": "Gib deine Hive-Konfiguration ein ", + "description": "Gib deine Hive-Konfiguration ein", "title": "Hive-Konfiguration." }, "reauth": { diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index caddbeaf7d2..474e595769f 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -19,6 +19,12 @@ }, "options": { "step": { + "accessory": { + "data": { + "entities": "Entitet" + }, + "title": "V\u00e4lj entiteten f\u00f6r tillbeh\u00f6ret" + }, "advanced": { "data": { "devices": "Enheter (utl\u00f6sare)" @@ -28,13 +34,30 @@ }, "cameras": { "data": { + "camera_audio": "Kameror som st\u00f6der ljud", "camera_copy": "Kameror som st\u00f6der inbyggda H.264-str\u00f6mmar" }, "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." }, + "exclude": { + "data": { + "entities": "Entiteter" + }, + "description": "Alla \" {domains} \"-enheter kommer att inkluderas f\u00f6rutom de exkluderade enheterna och kategoriserade enheterna.", + "title": "V\u00e4lj de enheter som ska exkluderas" + }, + "include": { + "data": { + "entities": "Entiteter" + }, + "description": "Alla \" {domains} \"-enheter kommer att inkluderas om inte specifika enheter har valts.", + "title": "V\u00e4lj de entiteter som ska inkluderas" + }, "init": { "data": { + "domains": "Dom\u00e4ner att inkludera", + "include_exclude_mode": "Inkluderingsl\u00e4ge", "mode": "HomeKit-l\u00e4ge" }, "description": "HomeKit kan konfigureras f\u00f6r att exponera en bro eller ett enskilt tillbeh\u00f6r. I tillbeh\u00f6rsl\u00e4get kan endast en enda enhet anv\u00e4ndas. Tillbeh\u00f6rsl\u00e4ge kr\u00e4vs f\u00f6r att mediaspelare med enhetsklassen TV ska fungera korrekt. Enheter i \"Dom\u00e4ner att inkludera\" kommer att inkluderas i HomeKit. Du kommer att kunna v\u00e4lja vilka enheter som ska inkluderas eller uteslutas fr\u00e5n listan p\u00e5 n\u00e4sta sk\u00e4rm.", diff --git a/homeassistant/components/homekit_controller/translations/select.sv.json b/homeassistant/components/homekit_controller/translations/select.sv.json new file mode 100644 index 00000000000..a97da943fda --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Borta", + "home": "Hemma", + "sleep": "Vilol\u00e4ge" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.de.json b/homeassistant/components/homekit_controller/translations/sensor.de.json new file mode 100644 index 00000000000..2aef45f5303 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.de.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router-f\u00e4hig", + "full": "Vollst\u00e4ndiges Endger\u00e4t", + "minimal": "Minimales Endger\u00e4t", + "none": "Keine", + "router_eligible": "Router-f\u00e4higes Endger\u00e4t", + "sleepy": "Sleepy Endger\u00e4t" + }, + "homekit_controller__thread_status": { + "border_router": "Border-Router", + "child": "Kind", + "detached": "Freistehend", + "disabled": "Deaktiviert", + "joining": "Beitreten", + "leader": "Anf\u00fchrer", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.hu.json b/homeassistant/components/homekit_controller/translations/sensor.hu.json new file mode 100644 index 00000000000..9c6de41a56b --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.hu.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Hat\u00e1rol\u00f3 \u00fatv\u00e1laszt\u00f3 k\u00e9pess\u00e9g", + "full": "Teljes k\u00e9pess\u00e9g\u0171 v\u00e9gberendez\u00e9s", + "minimal": "Minim\u00e1lis k\u00e9pess\u00e9g\u0171 v\u00e9gberendez\u00e9s", + "none": "Nincs", + "router_eligible": "\u00datv\u00e1laszt\u00f3ra alkalmas v\u00e9geszk\u00f6z", + "sleepy": "Alv\u00f3 v\u00e9gberendez\u00e9s" + }, + "homekit_controller__thread_status": { + "border_router": "Hat\u00e1rol\u00f3 \u00fatv\u00e1laszt\u00f3", + "child": "Gyermek", + "detached": "Lev\u00e1lasztva", + "disabled": "Letiltva", + "joining": "Csatlakoz\u00e1s", + "leader": "Vezet\u0151", + "router": "\u00datv\u00e1laszt\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.ja.json b/homeassistant/components/homekit_controller/translations/sensor.ja.json new file mode 100644 index 00000000000..a2f246f11b3 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.ja.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "\u5883\u754c(Border)\u30eb\u30fc\u30bf\u30fc\u5bfe\u5fdc", + "full": "\u30d5\u30eb\u30a8\u30f3\u30c9\u30c7\u30d0\u30a4\u30b9", + "minimal": "\u6700\u5c0f\u9650\u306e\u30a8\u30f3\u30c9 \u30c7\u30d0\u30a4\u30b9", + "none": "\u306a\u3057", + "router_eligible": "\u30eb\u30fc\u30bf\u30fc\u306e\u9069\u683c\u306a\u30a8\u30f3\u30c9 \u30c7\u30d0\u30a4\u30b9", + "sleepy": "\u30b9\u30ea\u30fc\u30d4\u30fc \u30a8\u30f3\u30c9 \u30c7\u30d0\u30a4\u30b9" + }, + "homekit_controller__thread_status": { + "border_router": "\u5883\u754c(Border)\u30eb\u30fc\u30bf\u30fc", + "child": "\u5b50(Child)", + "detached": "\u5207\u308a\u96e2\u3055\u308c\u305f", + "disabled": "\u7121\u52b9", + "joining": "\u63a5\u5408(Joining)", + "leader": "\u30ea\u30fc\u30c0\u30fc", + "router": "\u30eb\u30fc\u30bf\u30fc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.no.json b/homeassistant/components/homekit_controller/translations/sensor.no.json index f0323c0326a..997b3fe34e9 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.no.json +++ b/homeassistant/components/homekit_controller/translations/sensor.no.json @@ -7,6 +7,15 @@ "none": "Ingen", "router_eligible": "Ruterkvalifisert sluttenhet", "sleepy": "S\u00f8vnig sluttenhet" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "child": "Barn", + "detached": "Frakoblet", + "disabled": "Deaktivert", + "joining": "Blir med", + "leader": "Leder", + "router": "Ruter" } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.sv.json b/homeassistant/components/homekit_controller/translations/sensor.sv.json new file mode 100644 index 00000000000..61058201e61 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.sv.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Gr\u00e4nsrouter kapabel", + "full": "Fullst\u00e4ndig slutenhet", + "minimal": "Minimal slutenhet", + "none": "Ingen", + "router_eligible": "Routerkvalificerad slutenhet", + "sleepy": "S\u00f6mnig slutenhet" + }, + "homekit_controller__thread_status": { + "border_router": "Gr\u00e4nsrouter", + "child": "Barn", + "detached": "Frist\u00e5ende", + "disabled": "Inaktiverad", + "joining": "Ansluter sig till", + "leader": "Ledare", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json b/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json index 123469d79cf..78925b1ff0c 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json @@ -1,12 +1,21 @@ { "state": { "homekit_controller__thread_node_capabilities": { - "border_router_capable": "\u7db2\u8def\u6838\u5fc3\u80fd\u529b", + "border_router_capable": "Border Router \u80fd\u529b", "full": "\u6240\u6709\u7d42\u7aef\u88dd\u7f6e", "minimal": "\u6700\u4f4e\u7d42\u7aef\u88dd\u7f6e", "none": "\u7121", - "router_eligible": "\u4e2d\u9593\u5c64\u7d42\u7aef\u88dd\u7f6e", + "router_eligible": "Router \u7d42\u7aef\u88dd\u7f6e", "sleepy": "\u5f85\u547d\u7d42\u7aef\u88dd\u7f6e" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "child": "Child", + "detached": "Detached", + "disabled": "\u95dc\u9589", + "joining": "\u52a0\u5165", + "leader": "Leader", + "router": "Router" } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sv.json b/homeassistant/components/homekit_controller/translations/sv.json index 1766689ca69..aea1202b352 100644 --- a/homeassistant/components/homekit_controller/translations/sv.json +++ b/homeassistant/components/homekit_controller/translations/sv.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Felaktig HomeKit-kod. V\u00e4nligen kontrollera och f\u00f6rs\u00f6k igen.", + "insecure_setup_code": "Den beg\u00e4rda installationskoden \u00e4r os\u00e4ker p\u00e5 grund av dess triviala natur. Detta tillbeh\u00f6r uppfyller inte grundl\u00e4ggande s\u00e4kerhetskrav.", "max_peers_error": "Enheten nekade parningsf\u00f6rs\u00f6ket d\u00e5 det inte finns n\u00e5got parningsminnesutrymme kvar", "pairing_failed": "Ett ok\u00e4nt fel uppstod n\u00e4r parningsf\u00f6rs\u00f6ket gjordes med den h\u00e4r enheten. Det h\u00e4r kan vara ett tillf\u00e4lligt fel, eller s\u00e5 st\u00f6ds inte din enhet i nul\u00e4get.", "unable_to_pair": "Det g\u00e5r inte att para ihop, f\u00f6rs\u00f6k igen.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Till\u00e5t parning med os\u00e4kra installationskoder.", "pairing_code": "Parningskod" }, "description": "HomeKit Controller kommunicerar med {name} \u00f6ver det lokala n\u00e4tverket med hj\u00e4lp av en s\u00e4ker krypterad anslutning utan en separat HomeKit-kontroller eller iCloud. Ange din HomeKit-kopplingskod (i formatet XXX-XX-XXX) f\u00f6r att anv\u00e4nda detta tillbeh\u00f6r. Denna kod finns vanligtvis p\u00e5 sj\u00e4lva enheten eller i f\u00f6rpackningen.", diff --git a/homeassistant/components/homewizard/translations/sv.json b/homeassistant/components/homewizard/translations/sv.json index c9bc9cd66a9..554f347eee1 100644 --- a/homeassistant/components/homewizard/translations/sv.json +++ b/homeassistant/components/homewizard/translations/sv.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "api_not_enabled": "API:et \u00e4r inte aktiverat. Aktivera API i HomeWizard Energy App under inst\u00e4llningar", + "device_not_supported": "Den h\u00e4r enheten st\u00f6ds inte", + "invalid_discovery_parameters": "Uppt\u00e4ckte en API-version som inte st\u00f6ds", + "unknown_error": "Ov\u00e4ntat fel" + }, "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {product_type} ( {serial} ) p\u00e5 {ip_address} ?", + "title": "Bekr\u00e4fta" + }, "user": { + "data": { + "ip_address": "IP-adress" + }, + "description": "Ange IP-adressen f\u00f6r din HomeWizard Energy-enhet f\u00f6r att integrera med Home Assistant.", "title": "Konfigurera enhet" } } diff --git a/homeassistant/components/honeywell/translations/sv.json b/homeassistant/components/honeywell/translations/sv.json index 287e24372ba..6629effa6b6 100644 --- a/homeassistant/components/honeywell/translations/sv.json +++ b/homeassistant/components/honeywell/translations/sv.json @@ -12,5 +12,16 @@ "description": "Ange de autentiseringsuppgifter som anv\u00e4nds f\u00f6r att logga in p\u00e5 mytotalconnectcomfort.com." } } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Borta kylningstemperatur", + "away_heat_temperature": "Borta uppv\u00e4rmingstemperatur" + }, + "description": "Ytterligare Honeywell-konfigurationsalternativ. Temperaturen \u00e4r inst\u00e4lld i Fahrenheit." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/sv.json b/homeassistant/components/huawei_lte/translations/sv.json index 4efe112d468..b58dcb07da2 100644 --- a/homeassistant/components/huawei_lte/translations/sv.json +++ b/homeassistant/components/huawei_lte/translations/sv.json @@ -18,6 +18,7 @@ "user": { "data": { "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" }, "description": "Ange information om enhets\u00e5tkomst. Det \u00e4r valfritt att ange anv\u00e4ndarnamn och l\u00f6senord, men st\u00f6djer d\u00e5 fler integrationsfunktioner. \u00c5 andra sidan kan anv\u00e4ndning av en auktoriserad anslutning orsaka problem med att komma \u00e5t enhetens webbgr\u00e4nssnitt utanf\u00f6r Home Assistant medan integrationen \u00e4r aktiv och tv\u00e4rtom.", diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index f09232f92ab..519c35370fa 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -25,6 +25,12 @@ "link": { "description": "Tryck p\u00e5 knappen p\u00e5 bryggan f\u00f6r att registrera Philips Hue med Home Assistant. \n\n![Placering av knapp p\u00e5 brygga](/static/images/config_philips_hue.jpg)", "title": "L\u00e4nka hub" + }, + "manual": { + "data": { + "host": "V\u00e4rd" + }, + "title": "Manuellt konfigurera en Hue-brygga" } } }, @@ -63,7 +69,9 @@ "init": { "data": { "allow_hue_groups": "Till\u00e5t Hue-grupper", - "allow_hue_scenes": "Till\u00e5t Hue-scener" + "allow_hue_scenes": "Till\u00e5t Hue-scener", + "allow_unreachable": "Till\u00e5t o\u00e5tkomliga gl\u00f6dlampor att rapportera sitt tillst\u00e5nd korrekt", + "ignore_availability": "Ignorera anslutningsstatus f\u00f6r de givna enheterna" } } } diff --git a/homeassistant/components/huisbaasje/translations/sv.json b/homeassistant/components/huisbaasje/translations/sv.json index 4a6100815d6..6cc1e2b35ee 100644 --- a/homeassistant/components/huisbaasje/translations/sv.json +++ b/homeassistant/components/huisbaasje/translations/sv.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "cannot_connect": "Kunde inte ansluta" + "cannot_connect": "Kunde inte ansluta", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/humidifier/translations/sv.json b/homeassistant/components/humidifier/translations/sv.json index a500d9dc18e..985bb8eb820 100644 --- a/homeassistant/components/humidifier/translations/sv.json +++ b/homeassistant/components/humidifier/translations/sv.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "target_humidity_changed": "{entity_name} m\u00e5lfuktighet har \u00e4ndrats", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" diff --git a/homeassistant/components/iaqualink/translations/sv.json b/homeassistant/components/iaqualink/translations/sv.json index e697b5a02b9..bf65527243d 100644 --- a/homeassistant/components/iaqualink/translations/sv.json +++ b/homeassistant/components/iaqualink/translations/sv.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, "step": { "user": { diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index b0a052ecef4..bb07cc8291e 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -6,6 +6,7 @@ "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { + "invalid_auth": "Ogiltig autentisering", "send_verification_code": "Det gick inte att skicka verifieringskod", "validate_verification_code": "Det gick inte att verifiera verifieringskoden, v\u00e4lj en betrodd enhet och starta verifieringen igen" }, diff --git a/homeassistant/components/ifttt/translations/sv.json b/homeassistant/components/ifttt/translations/sv.json index 16ec51d62c2..0559bc82afa 100644 --- a/homeassistant/components/ifttt/translations/sv.json +++ b/homeassistant/components/ifttt/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json index 7b1692e7ec9..fa4c69f369a 100644 --- a/homeassistant/components/insteon/translations/sv.json +++ b/homeassistant/components/insteon/translations/sv.json @@ -1,9 +1,14 @@ { "config": { "abort": { + "cannot_connect": "Det gick inte att ansluta.", "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "select_single": "V\u00e4lj ett alternativ." + }, "flow_title": "{name}", "step": { "confirm_usb": { @@ -27,6 +32,13 @@ "description": "Konfigurera Insteon Hub version 2.", "title": "Insteon Hub version 2" }, + "plm": { + "data": { + "device": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "Konfigurera Insteon PowerLink-modemet (PLM).", + "title": "Insteon PLM" + }, "user": { "data": { "modem_type": "Modemtyp." @@ -36,9 +48,15 @@ } }, "options": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "input_error": "Ogiltiga poster, kontrollera dina v\u00e4rden.", + "select_single": "V\u00e4lj ett alternativ." + }, "step": { "add_override": { "data": { + "address": "Enhetsadress (dvs. 1a2b3c)", "cat": "Enhetskategori (dvs. 0x10)", "subcat": "Enhetsunderkategori (dvs. 0x0a)" }, diff --git a/homeassistant/components/integration/translations/sv.json b/homeassistant/components/integration/translations/sv.json index e0c12be775f..edba53d6310 100644 --- a/homeassistant/components/integration/translations/sv.json +++ b/homeassistant/components/integration/translations/sv.json @@ -2,17 +2,30 @@ "config": { "step": { "user": { + "data": { + "method": "Integrationsmetod", + "name": "Namn", + "round": "Precision", + "source": "Ing\u00e5ngssensor", + "unit_prefix": "Metriskt prefix", + "unit_time": "Tidsenhet" + }, "data_description": { "round": "Anger antal decimaler i resultatet.", "unit_prefix": "Utdata kommer att skalas enligt det valda metriska prefixet.", "unit_time": "Utg\u00e5ngen kommer att skalas enligt den valda tidsenheten." - } + }, + "description": "Skapa en sensor som ber\u00e4knar en Riemanns summa f\u00f6r att uppskatta integralen av en sensor.", + "title": "L\u00e4gg till Riemann summa integral sensor" } } }, "options": { "step": { "init": { + "data": { + "round": "Precision" + }, "data_description": { "round": "Anger antal decimaler i resultatet." } diff --git a/homeassistant/components/intellifire/translations/sv.json b/homeassistant/components/intellifire/translations/sv.json index 05685de670b..cb50a5ca6a0 100644 --- a/homeassistant/components/intellifire/translations/sv.json +++ b/homeassistant/components/intellifire/translations/sv.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", + "not_intellifire_device": "Inte en IntelliFire-enhet.", "reauth_successful": "Omautentiseringen lyckades" }, "error": { @@ -9,6 +10,7 @@ "cannot_connect": "Det gick inte att ansluta.", "iftapi_connect": "Fel vid anslutning till iftapi.net" }, + "flow_title": "{serial} ({host})", "step": { "api_config": { "data": { @@ -16,7 +18,19 @@ "username": "E-postadress" } }, + "dhcp_confirm": { + "description": "Vill du konfigurera {host}\nSerie: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "V\u00e4rd (IP-adress)" + }, + "description": "Lokal konfiguration" + }, "pick_device": { + "data": { + "host": "V\u00e4rd" + }, "description": "F\u00f6ljande IntelliFire-enheter uppt\u00e4cktes. V\u00e4lj vilken du vill konfigurera.", "title": "Val av enhet" } diff --git a/homeassistant/components/iotawatt/translations/sv.json b/homeassistant/components/iotawatt/translations/sv.json index 500a65ce603..acf1f77a107 100644 --- a/homeassistant/components/iotawatt/translations/sv.json +++ b/homeassistant/components/iotawatt/translations/sv.json @@ -2,12 +2,20 @@ "config": { "error": { "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "auth": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" + }, + "description": "IoTawatt-enheten kr\u00e4ver autentisering. Ange anv\u00e4ndarnamn och l\u00f6senord och klicka p\u00e5 knappen Skicka." + }, + "user": { + "data": { + "host": "V\u00e4rd" } } } diff --git a/homeassistant/components/iss/translations/sv.json b/homeassistant/components/iss/translations/sv.json new file mode 100644 index 00000000000..0962f763548 --- /dev/null +++ b/homeassistant/components/iss/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Latitud och longitud definieras inte i Home Assistant.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du konfigurera den internationella rymdstationen (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Visa p\u00e5 karta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/sv.json b/homeassistant/components/kaleidescape/translations/sv.json index c5cfbbb662a..6adb8993b5d 100644 --- a/homeassistant/components/kaleidescape/translations/sv.json +++ b/homeassistant/components/kaleidescape/translations/sv.json @@ -1,5 +1,25 @@ { "config": { - "flow_title": "{model} ({name})" + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "unknown": "Ov\u00e4ntat fel", + "unsupported": "Enhet som inte st\u00f6ds" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unsupported": "Enhet som inte st\u00f6ds" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Vill du st\u00e4lla in {model} -spelaren med namnet {name} ?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/sv.json b/homeassistant/components/keenetic_ndms2/translations/sv.json index 1e263250636..e0bce87762a 100644 --- a/homeassistant/components/keenetic_ndms2/translations/sv.json +++ b/homeassistant/components/keenetic_ndms2/translations/sv.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Konto har redan konfigurerats" + "already_configured": "Konto har redan konfigurerats", + "no_udn": "SSDP-uppt\u00e4cktsinformation har ingen UDN", + "not_keenetic_ndms2": "Den uppt\u00e4ckta enheten \u00e4r inte en Keenetic-router" }, "error": { "cannot_connect": "Det gick inte att ansluta." }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index 2c2d6f7d14d..8abd3264e18 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "file_not_found": "Den angivna `.knxkeys`-filen hittades inte i s\u00f6kv\u00e4gen config/.storage/knx/", "invalid_individual_address": "V\u00e4rdet matchar inte m\u00f6nstret f\u00f6r en individuell adress i KNX.\n'area.line.device'", "invalid_ip_address": "Ogiltig IPv4-adress.", @@ -9,16 +14,22 @@ "step": { "manual_tunnel": { "data": { + "host": "V\u00e4rd", + "local_ip": "Lokal IP f\u00f6r Home Assistant", + "port": "Port", "tunneling_type": "KNX tunneltyp" }, "data_description": { "host": "IP-adressen f\u00f6r KNX/IP-tunnelenheten.", "local_ip": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt.", "port": "Port p\u00e5 KNX/IP-tunnelenheten." - } + }, + "description": "Ange anslutningsinformationen f\u00f6r din tunnelenhet." }, "routing": { "data": { + "individual_address": "Individuell adress", + "local_ip": "Lokal IP f\u00f6r Home Assistant", "multicast_group": "Multicast-grupp", "multicast_port": "Multicast-port" }, @@ -79,6 +90,7 @@ "data": { "connection_type": "KNX anslutningstyp", "individual_address": "Enskild standardadress", + "local_ip": "Lokal IP f\u00f6r Home Assistant", "multicast_group": "Multicast-grupp", "multicast_port": "Multicast-port", "rate_limit": "Hastighetsgr\u00e4ns", diff --git a/homeassistant/components/kodi/translations/sv.json b/homeassistant/components/kodi/translations/sv.json index b65fb101fd2..8d242b673d8 100644 --- a/homeassistant/components/kodi/translations/sv.json +++ b/homeassistant/components/kodi/translations/sv.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "no_uuid": "Kodi-instansen har inte ett unikt ID. Detta beror troligen p\u00e5 en gammal Kodi-version (17.x eller l\u00e4gre). Du kan konfigurera integrationen manuellt eller uppgradera till en nyare Kodi-version.", + "unknown": "Ov\u00e4ntat fel" }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -24,8 +27,11 @@ }, "user": { "data": { - "host": "V\u00e4rd" - } + "host": "V\u00e4rd", + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat" + }, + "description": "Kodi anslutningsinformation. Se till att aktivera \"Till\u00e5t kontroll av Kodi via HTTP\" i System/Inst\u00e4llningar/N\u00e4tverk/Tj\u00e4nster." }, "ws_port": { "data": { diff --git a/homeassistant/components/konnected/translations/sv.json b/homeassistant/components/konnected/translations/sv.json index e8130afd424..a52dcb52cce 100644 --- a/homeassistant/components/konnected/translations/sv.json +++ b/homeassistant/components/konnected/translations/sv.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan.", + "cannot_connect": "Det gick inte att ansluta.", "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet", "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" }, diff --git a/homeassistant/components/kostal_plenticore/translations/sv.json b/homeassistant/components/kostal_plenticore/translations/sv.json index 70aba340c35..c0980850c66 100644 --- a/homeassistant/components/kostal_plenticore/translations/sv.json +++ b/homeassistant/components/kostal_plenticore/translations/sv.json @@ -5,11 +5,13 @@ }, "error": { "cannot_connect": "Kunde inte ansluta", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord" } } diff --git a/homeassistant/components/launch_library/translations/sv.json b/homeassistant/components/launch_library/translations/sv.json new file mode 100644 index 00000000000..a4d7bc415a6 --- /dev/null +++ b/homeassistant/components/launch_library/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du konfigurera Launch Library?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/sv.json b/homeassistant/components/lcn/translations/sv.json index 59b06895f48..a6f6f40edce 100644 --- a/homeassistant/components/lcn/translations/sv.json +++ b/homeassistant/components/lcn/translations/sv.json @@ -1,7 +1,11 @@ { "device_automation": { "trigger_type": { - "codelock": "kodl\u00e5skod mottagen" + "codelock": "kodl\u00e5skod mottagen", + "fingerprint": "Mottagen fingeravtryckskod.", + "send_keys": "skicka mottagna nycklar", + "transmitter": "Mottagen s\u00e4ndarkod", + "transponder": "transponderkod mottagen" } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index 77bb8815340..9f9168abdd2 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Konto har redan konfigurerats", "invalid_auth": "Ogiltig autentisering", - "reauth_successful": "\u00c5terautentisering lyckades" + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" }, "create_entry": { "default": "F\u00f6r att st\u00e4lla in avancerade alternativ, se [Life360 documentation]({docs_url})." diff --git a/homeassistant/components/light/translations/sv.json b/homeassistant/components/light/translations/sv.json index 1f32324d707..a68004c041e 100644 --- a/homeassistant/components/light/translations/sv.json +++ b/homeassistant/components/light/translations/sv.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} avst\u00e4ngd", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/litejet/translations/sv.json b/homeassistant/components/litejet/translations/sv.json index f865c5a2c6a..e0ffccbc1df 100644 --- a/homeassistant/components/litejet/translations/sv.json +++ b/homeassistant/components/litejet/translations/sv.json @@ -2,6 +2,28 @@ "config": { "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "open_failed": "Kan inte \u00f6ppna den angivna seriella porten." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Anslut LiteJets RS232-2-port till din dator och ange s\u00f6kv\u00e4gen till den seriella portenheten. \n\n LiteJet MCP m\u00e5ste konfigureras f\u00f6r 19,2 K baud, 8 databitar, 1 stoppbit, ingen paritet och att s\u00e4nda ett \"CR\" efter varje svar.", + "title": "Anslut till LiteJet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Standard\u00f6verg\u00e5ng (sekunder)" + }, + "title": "Konfigurera LiteJet" + } } } } \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/sv.json b/homeassistant/components/local_ip/translations/sv.json index 7f1b7ce637a..cb5a9eed725 100644 --- a/homeassistant/components/local_ip/translations/sv.json +++ b/homeassistant/components/local_ip/translations/sv.json @@ -5,6 +5,7 @@ }, "step": { "user": { + "description": "Vill du starta konfigurationen?", "title": "Lokal IP-adress" } } diff --git a/homeassistant/components/locative/translations/sv.json b/homeassistant/components/locative/translations/sv.json index 80da7825d40..905c85b7481 100644 --- a/homeassistant/components/locative/translations/sv.json +++ b/homeassistant/components/locative/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/luftdaten/translations/sv.json b/homeassistant/components/luftdaten/translations/sv.json index 63de146c6d7..de83a1788ee 100644 --- a/homeassistant/components/luftdaten/translations/sv.json +++ b/homeassistant/components/luftdaten/translations/sv.json @@ -1,6 +1,8 @@ { "config": { "error": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "invalid_sensor": "Sensor saknas eller \u00e4r ogiltig" }, "step": { diff --git a/homeassistant/components/lutron_caseta/translations/sv.json b/homeassistant/components/lutron_caseta/translations/sv.json index 3f8e2de7d0f..d72f71cef04 100644 --- a/homeassistant/components/lutron_caseta/translations/sv.json +++ b/homeassistant/components/lutron_caseta/translations/sv.json @@ -5,8 +5,15 @@ "cannot_connect": "Det gick inte att ansluta.", "not_lutron_device": "Uppt\u00e4ckt enhet \u00e4r inte en Lutron-enhet" }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "flow_title": "{name} ({host})", "step": { + "import_failed": { + "description": "Det gick inte att st\u00e4lla in bryggan (v\u00e4rd: {host} ) importerad fr\u00e5n configuration.yaml.", + "title": "Det gick inte att importera Cas\u00e9ta-bryggkonfigurationen." + }, "link": { "description": "F\u00f6r att para med {name} ( {host} ), efter att ha skickat in detta formul\u00e4r, tryck p\u00e5 den svarta knappen p\u00e5 baksidan av bryggan.", "title": "Para ihop med bryggan" @@ -24,6 +31,8 @@ "trigger_subtype": { "button_1": "F\u00f6rsta knappen", "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", "close_1": "St\u00e4ng 1", "close_2": "St\u00e4ng 2", "close_3": "St\u00e4ng 3", @@ -53,7 +62,15 @@ "raise_4": "H\u00f6j 4", "raise_all": "H\u00f6j alla", "stop": "Stoppa (favorit)", - "stop_1": "Stoppa 1" + "stop_1": "Stoppa 1", + "stop_2": "Stoppa 2", + "stop_3": "Stoppa 3", + "stop_4": "Stoppa 4", + "stop_all": "Stoppa alla" + }, + "trigger_type": { + "press": "\" {subtype} \" tryckt", + "release": "\"{subtyp}\" sl\u00e4pptes" } } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/sv.json b/homeassistant/components/mailgun/translations/sv.json index 3ca0139183c..68a8c5d5878 100644 --- a/homeassistant/components/mailgun/translations/sv.json +++ b/homeassistant/components/mailgun/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/mazda/translations/sv.json b/homeassistant/components/mazda/translations/sv.json index c71d7b4faa4..ec3c6339a6f 100644 --- a/homeassistant/components/mazda/translations/sv.json +++ b/homeassistant/components/mazda/translations/sv.json @@ -13,9 +13,11 @@ "step": { "user": { "data": { + "email": "E-post", "password": "L\u00f6senord", "region": "Region" - } + }, + "description": "Ange den e-postadress och det l\u00f6senord som du anv\u00e4nder f\u00f6r att logga in i MyMazda-mobilappen." } } } diff --git a/homeassistant/components/media_player/translations/sv.json b/homeassistant/components/media_player/translations/sv.json index e84edf74bb0..56fba12edf6 100644 --- a/homeassistant/components/media_player/translations/sv.json +++ b/homeassistant/components/media_player/translations/sv.json @@ -10,6 +10,7 @@ }, "trigger_type": { "buffering": "{entity_name} b\u00f6rjar buffra", + "changed_states": "{entity_name} \u00e4ndrade tillst\u00e5nd", "idle": "{entity_name} blir inaktiv", "paused": "{entity_name} \u00e4r pausad", "playing": "{entity_name} b\u00f6rjar spela", diff --git a/homeassistant/components/met/translations/sv.json b/homeassistant/components/met/translations/sv.json index fbea0183395..6ce86cca663 100644 --- a/homeassistant/components/met/translations/sv.json +++ b/homeassistant/components/met/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "no_home": "Inga hemkoordinater \u00e4r inst\u00e4llda i Home Assistant-konfigurationen" + }, + "error": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/met_eireann/translations/sv.json b/homeassistant/components/met_eireann/translations/sv.json index 754fa336a08..950802826ef 100644 --- a/homeassistant/components/met_eireann/translations/sv.json +++ b/homeassistant/components/met_eireann/translations/sv.json @@ -6,10 +6,12 @@ "step": { "user": { "data": { + "elevation": "H\u00f6jd", "latitude": "Latitud", "longitude": "Longitud", "name": "Namn" }, + "description": "Ange din plats f\u00f6r att anv\u00e4nda v\u00e4derdata fr\u00e5n Met \u00c9ireann Public Weather Forecast API", "title": "Plats" } } diff --git a/homeassistant/components/meteoclimatic/translations/sv.json b/homeassistant/components/meteoclimatic/translations/sv.json new file mode 100644 index 00000000000..8d6f17f635a --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "not_found": "Inga enheter hittades i n\u00e4tverket" + }, + "step": { + "user": { + "data": { + "code": "Stationskod" + }, + "data_description": { + "code": "Ser ut som ESCAT4300000043206B" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/sv.json b/homeassistant/components/mill/translations/sv.json index a7c8f4e0ea3..72ec5c09ed6 100644 --- a/homeassistant/components/mill/translations/sv.json +++ b/homeassistant/components/mill/translations/sv.json @@ -9,13 +9,21 @@ "step": { "cloud": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } }, + "local": { + "data": { + "ip_address": "IP-adress" + }, + "description": "Lokal IP-adress f\u00f6r enheten." + }, "user": { "data": { "connection_type": "V\u00e4lj anslutningstyp" - } + }, + "description": "V\u00e4lj anslutningstyp. Lokalt kr\u00e4ver generation 3 v\u00e4rmare" } } } diff --git a/homeassistant/components/min_max/translations/sv.json b/homeassistant/components/min_max/translations/sv.json index 7a48b0e631b..882dcd786b0 100644 --- a/homeassistant/components/min_max/translations/sv.json +++ b/homeassistant/components/min_max/translations/sv.json @@ -2,9 +2,16 @@ "config": { "step": { "user": { + "data": { + "entity_ids": "Indataentiteter", + "name": "Namn", + "round_digits": "Precision", + "type": "Statistisk egenskap" + }, "data_description": { "round_digits": "Styr antalet decimalsiffror i utg\u00e5ngen n\u00e4r statistikegenskapen \u00e4r medelv\u00e4rde eller median." }, + "description": "Skapa en sensor som ber\u00e4knar ett min-, max-, medelv\u00e4rde eller medianv\u00e4rde fr\u00e5n en lista med ing\u00e5ngssensorer.", "title": "L\u00e4gg till min / max / medelv\u00e4rde / mediansensor" } } @@ -13,12 +20,15 @@ "step": { "init": { "data": { - "round_digits": "Precision" + "entity_ids": "Indataentiteter", + "round_digits": "Precision", + "type": "Statistisk egenskap" }, "data_description": { "round_digits": "Styr antalet decimalsiffror i utg\u00e5ngen n\u00e4r statistikegenskapen \u00e4r medelv\u00e4rde eller median." } } } - } + }, + "title": "Min / max / medelv\u00e4rde / mediansensor" } \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/sv.json b/homeassistant/components/mjpeg/translations/sv.json index dd590fec4ba..74d669f60b2 100644 --- a/homeassistant/components/mjpeg/translations/sv.json +++ b/homeassistant/components/mjpeg/translations/sv.json @@ -1,21 +1,40 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { - "username": "Anv\u00e4ndarnamn" + "mjpeg_url": "MJPEG URL", + "name": "Namn", + "password": "L\u00f6senord", + "still_image_url": "URL f\u00f6r stillbild", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" } } } }, "options": { "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, "step": { "init": { "data": { - "username": "Anv\u00e4ndarnamn" + "mjpeg_url": "MJPEG URL", + "name": "Namn", + "password": "L\u00f6senord", + "still_image_url": "URL f\u00f6r stillbild", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/modem_callerid/translations/sv.json b/homeassistant/components/modem_callerid/translations/sv.json index 531a1029e21..4021e3f2e63 100644 --- a/homeassistant/components/modem_callerid/translations/sv.json +++ b/homeassistant/components/modem_callerid/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "no_devices_found": "Inga \u00e5terst\u00e5ende enheter hittades" }, diff --git a/homeassistant/components/modern_forms/translations/sv.json b/homeassistant/components/modern_forms/translations/sv.json new file mode 100644 index 00000000000..9058899ce38 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "St\u00e4ll in din Modern Forms-fl\u00e4kt f\u00f6r att integreras med Home Assistant." + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Modern Forms-fanen som heter ` {name} ` till Home Assistant?", + "title": "Uppt\u00e4ckte Modern Forms fl\u00e4ktenhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/sv.json b/homeassistant/components/moehlenhoff_alpha2/translations/sv.json new file mode 100644 index 00000000000..4b2c8fb1ce3 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sv.json b/homeassistant/components/moon/translations/sv.json new file mode 100644 index 00000000000..38aaa7157e6 --- /dev/null +++ b/homeassistant/components/moon/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, + "title": "M\u00e5nen" +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/sv.json b/homeassistant/components/motion_blinds/translations/sv.json index 1deca42bc26..10ed4870d6e 100644 --- a/homeassistant/components/motion_blinds/translations/sv.json +++ b/homeassistant/components/motion_blinds/translations/sv.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "connection_error": "Det gick inte att ansluta." }, "error": { "discovery_error": "Det gick inte att uppt\u00e4cka en Motion Gateway" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { @@ -17,7 +20,14 @@ "data": { "select_ip": "IP-adress" }, - "description": "K\u00f6r installationen igen om du vill ansluta ytterligare Motion Gateways" + "description": "K\u00f6r installationen igen om du vill ansluta ytterligare Motion Gateways", + "title": "V\u00e4lj den Motion Gateway som du vill ansluta" + }, + "user": { + "data": { + "host": "IP-adress" + }, + "description": "Anslut till din Motion Gateway, om IP-adressen inte \u00e4r inst\u00e4lld anv\u00e4nds automatisk uppt\u00e4ckt" } } }, diff --git a/homeassistant/components/motioneye/translations/sv.json b/homeassistant/components/motioneye/translations/sv.json index 38517e7571e..e8cbab545a7 100644 --- a/homeassistant/components/motioneye/translations/sv.json +++ b/homeassistant/components/motioneye/translations/sv.json @@ -11,6 +11,10 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "hassio_confirm": { + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till motionEye-tj\u00e4nsten som tillhandah\u00e5lls av till\u00e4gget: {addon} ?", + "title": "motionEye via Home Assistant-till\u00e4gget" + }, "user": { "data": { "admin_password": "Admin L\u00f6senord", @@ -26,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Mall f\u00f6r webbadress f\u00f6r str\u00f6m", "webhook_set": "Konfigurera motionEye webhooks f\u00f6r att rapportera h\u00e4ndelser till Home Assistant", "webhook_set_overwrite": "Skriv \u00f6ver ok\u00e4nda webhooks" } diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 30acc3c1098..46e3325f990 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -63,7 +63,8 @@ "port": "Port", "username": "Anv\u00e4ndarnamn" }, - "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker." + "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker.", + "title": "M\u00e4klaralternativ" }, "options": { "data": { @@ -72,9 +73,15 @@ "birth_qos": "F\u00f6delsemeddelande QoS", "birth_retain": "F\u00f6delsemeddelande beh\u00e5lls", "birth_topic": "\u00c4mne f\u00f6r f\u00f6delsemeddelande", - "discovery": "Aktivera uppt\u00e4ckt" + "discovery": "Aktivera uppt\u00e4ckt", + "will_enable": "Aktivera testament", + "will_payload": "Testamentets nyttolast", + "will_qos": "Testamentets QoS", + "will_retain": "Testamentets tid f\u00f6r sparande", + "will_topic": "Testamentets \u00e4mne" }, - "description": "Uppt\u00e4ckt - Om uppt\u00e4ckt \u00e4r aktiverat (rekommenderas), kommer Home Assistant automatiskt att uppt\u00e4cka enheter och enheter som publicerar sin konfiguration p\u00e5 MQTT-brokern. Om uppt\u00e4ckten \u00e4r inaktiverad m\u00e5ste all konfiguration g\u00f6ras manuellt.\n F\u00f6delsemeddelande - F\u00f6delsemeddelandet kommer att skickas varje g\u00e5ng Home Assistant (\u00e5ter)ansluter till MQTT-brokern.\n Kommer meddelande - Viljemeddelandet kommer att skickas varje g\u00e5ng Home Assistant f\u00f6rlorar sin anslutning till brokern, b\u00e5de i h\u00e4ndelse av en reng\u00f6ring (t.ex. Home Assistant st\u00e4ngs av) och i h\u00e4ndelse av en oren (t.ex. Home Assistant kraschar eller f\u00f6rlorar sin n\u00e4tverksanslutning) koppla ifr\u00e5n." + "description": "Uppt\u00e4ckt - Om uppt\u00e4ckt \u00e4r aktiverat (rekommenderas), kommer Home Assistant automatiskt att uppt\u00e4cka enheter och enheter som publicerar sin konfiguration p\u00e5 MQTT-brokern. Om uppt\u00e4ckten \u00e4r inaktiverad m\u00e5ste all konfiguration g\u00f6ras manuellt.\n F\u00f6delsemeddelande - F\u00f6delsemeddelandet kommer att skickas varje g\u00e5ng Home Assistant (\u00e5ter)ansluter till MQTT-brokern.\n Kommer meddelande - Viljemeddelandet kommer att skickas varje g\u00e5ng Home Assistant f\u00f6rlorar sin anslutning till brokern, b\u00e5de i h\u00e4ndelse av en reng\u00f6ring (t.ex. Home Assistant st\u00e4ngs av) och i h\u00e4ndelse av en oren (t.ex. Home Assistant kraschar eller f\u00f6rlorar sin n\u00e4tverksanslutning) koppla ifr\u00e5n.", + "title": "MQTT-alternativ" } } } diff --git a/homeassistant/components/mullvad/translations/sv.json b/homeassistant/components/mullvad/translations/sv.json index c5ad71d784d..4138bf60b8c 100644 --- a/homeassistant/components/mullvad/translations/sv.json +++ b/homeassistant/components/mullvad/translations/sv.json @@ -6,6 +6,11 @@ "error": { "cannot_connect": "Kunde inte ansluta", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "description": "Konfigurera Mullvad VPN-integration?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/sv.json b/homeassistant/components/mysensors/translations/sv.json index daebe65e30e..c41dc508691 100644 --- a/homeassistant/components/mysensors/translations/sv.json +++ b/homeassistant/components/mysensors/translations/sv.json @@ -31,7 +31,49 @@ "invalid_port": "Ogiltigt portnummer", "invalid_publish_topic": "Ogiltigt \u00e4mne f\u00f6r publicering", "invalid_serial": "Ogiltig serieport", - "mqtt_required": "MQTT-integrationen \u00e4r inte konfigurerad" + "invalid_subscribe_topic": "Ogiltigt \u00e4mne f\u00f6r prenumeration", + "invalid_version": "Ogiltig version av MySensors", + "mqtt_required": "MQTT-integrationen \u00e4r inte konfigurerad", + "not_a_number": "Ange ett nummer", + "port_out_of_range": "Portnummer m\u00e5ste vara minst 1 och h\u00f6gst 65535", + "same_topic": "\u00c4mnen f\u00f6r prenumeration och publicering \u00e4r desamma", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "persistensfil (l\u00e4mna tom f\u00f6r att automatiskt generera)", + "retain": "mqtt beh\u00e5ller", + "topic_in_prefix": "prefix f\u00f6r inmatnings\u00e4mnen (topic_in_prefix)", + "topic_out_prefix": "prefix f\u00f6r utdata\u00e4mnen (topic_out_prefix)", + "version": "MySensors version" + }, + "description": "MQTT-gateway-inst\u00e4llning" + }, + "gw_serial": { + "data": { + "baud_rate": "baudhastighet", + "device": "Serieport", + "persistence_file": "persistensfil (l\u00e4mna tom f\u00f6r att automatiskt generera)", + "version": "MySensors version" + }, + "description": "Seriell gateway-inst\u00e4llning" + }, + "gw_tcp": { + "data": { + "device": "IP-adress f\u00f6r gatewayen", + "persistence_file": "persistensfil (l\u00e4mna tom f\u00f6r att automatiskt generera)", + "tcp_port": "port", + "version": "MySensors version" + }, + "description": "Ethernet-gateway-inst\u00e4llning" + }, + "user": { + "data": { + "gateway_type": "Gateway typ" + }, + "description": "V\u00e4lj anslutningsmetod till gatewayen" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sv.json b/homeassistant/components/nam/translations/sv.json index 55d985814df..45b4410da7b 100644 --- a/homeassistant/components/nam/translations/sv.json +++ b/homeassistant/components/nam/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "device_unsupported": "Enheten st\u00f6ds ej", "reauth_successful": "\u00c5terautentisering lyckades", "reauth_unsuccessful": "\u00c5terautentiseringen misslyckades. Ta bort integrationen och konfigurera den igen." @@ -10,21 +11,30 @@ "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{host}", "step": { + "confirm_discovery": { + "description": "Vill du st\u00e4lla in Nettigo Air Monitor p\u00e5 {host} ?" + }, "credentials": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange anv\u00e4ndarnamn och l\u00f6senord." }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange r\u00e4tt anv\u00e4ndarnamn och l\u00f6senord f\u00f6r v\u00e4rden: {host}" }, "user": { "data": { "host": "V\u00e4rd" - } + }, + "description": "Konfigurera Nettigo Air Monitor integrationen." } } } diff --git a/homeassistant/components/nanoleaf/translations/sv.json b/homeassistant/components/nanoleaf/translations/sv.json index bbea90511da..6198fce87c6 100644 --- a/homeassistant/components/nanoleaf/translations/sv.json +++ b/homeassistant/components/nanoleaf/translations/sv.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Svep ned\u00e5t", + "swipe_left": "Svep \u00e5t v\u00e4nster", + "swipe_right": "Svep \u00e5t h\u00f6ger", + "swipe_up": "Svep upp\u00e5t" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 675b9087b6c..0836cc974bc 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Folgen Sie den [Anweisungen]({more_info_url}), um die Cloud-Konsole zu konfigurieren:\n\n1. Gehen Sie zum [OAuth-Zustimmungsbildschirm]({oauth_consent_url}) und konfigurieren Sie\n1. Gehen Sie zu [Credentials]({oauth_creds_url}) und klicken Sie auf **Create Credentials**.\n1. W\u00e4hlen Sie in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hlen Sie **Webanwendung** f\u00fcr den Anwendungstyp.\n1. F\u00fcgen Sie `{redirect_url}` unter *Authorized redirect URI* hinzu." + "description": "Folge den [Anweisungen]({more_info_url}), um die Cloud-Konsole zu konfigurieren:\n\n1. Gehe zum [OAuth-Zustimmungsbildschirm]({oauth_consent_url}) und konfiguriere\n1. Gehe zu [Credentials]({oauth_creds_url}) und klicke auf **Create Credentials**.\n1. W\u00e4hle in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **Webanwendung** f\u00fcr den Anwendungstyp.\n1. F\u00fcge `{redirect_url}` unter *Authorized redirect URI* hinzu." }, "config": { "abort": { @@ -34,29 +34,29 @@ "title": "Google-Konto verkn\u00fcpfen" }, "auth_upgrade": { - "description": "App Auth wurde von Google abgeschafft, um die Sicherheit zu verbessern, und Sie m\u00fcssen Ma\u00dfnahmen ergreifen, indem Sie neue Anmeldedaten f\u00fcr die Anwendung erstellen.\n\n\u00d6ffnen Sie die [Dokumentation]({more_info_url}) und folgen Sie den n\u00e4chsten Schritten, die Sie durchf\u00fchren m\u00fcssen, um den Zugriff auf Ihre Nest-Ger\u00e4te wiederherzustellen.", + "description": "App Auth wurde von Google abgeschafft, um die Sicherheit zu verbessern, und du musst Ma\u00dfnahmen ergreifen, indem du neue Anmeldedaten f\u00fcr die Anwendung erstellst.\n\n\u00d6ffnen die [Dokumentation]({more_info_url}) und folge den n\u00e4chsten Schritten, die du durchf\u00fchren musst, um den Zugriff auf deine Nest-Ger\u00e4te wiederherzustellen.", "title": "Nest: Einstellung der App-Authentifizierung" }, "cloud_project": { "data": { "cloud_project_id": "Google Cloud Projekt-ID" }, - "description": "Geben Sie unten die Cloud-Projekt-ID ein, z. B. *example-project-12345*. Siehe die [Google Cloud Console]({cloud_console_url}) oder die Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).", + "description": "Gib unten die Cloud-Projekt-ID ein, z. B. *example-project-12345*. Siehe die [Google Cloud Console]({cloud_console_url}) oder die Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).", "title": "Nest: Cloud-Projekt-ID eingeben" }, "create_cloud_project": { - "description": "Die Nest-Integration erm\u00f6glicht es Ihnen, Ihre Nest-Thermostate, -Kameras und -T\u00fcrklingeln \u00fcber die Smart Device Management API zu integrieren. Die SDM API **erfordert eine einmalige Einrichtungsgeb\u00fchr von US $5**. Siehe Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).\n\n1. Rufen Sie die [Google Cloud Console]({cloud_console_url}) auf.\n1. Wenn dies Ihr erstes Projekt ist, klicken Sie auf **Projekt erstellen** und dann auf **Neues Projekt**.\n1. Geben Sie Ihrem Cloud-Projekt einen Namen und klicken Sie dann auf **Erstellen**.\n1. Speichern Sie die Cloud Project ID, z. B. *example-project-12345*, da Sie diese sp\u00e4ter ben\u00f6tigen.\n1. Gehen Sie zur API-Bibliothek f\u00fcr [Smart Device Management API]({sdm_api_url}) und klicken Sie auf **Aktivieren**.\n1. Wechseln Sie zur API-Bibliothek f\u00fcr [Cloud Pub/Sub API]({pubsub_api_url}) und klicken Sie auf **Aktivieren**.\n\nFahren Sie fort, wenn Ihr Cloud-Projekt eingerichtet ist.", + "description": "Die Nest-Integration erm\u00f6glicht es dir, deine Nest-Thermostate, -Kameras und -T\u00fcrklingeln \u00fcber die Smart Device Management API zu integrieren. Die SDM API **erfordert eine einmalige Einrichtungsgeb\u00fchr von US $5**. Siehe Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).\n\n1. Rufe die [Google Cloud Console]({cloud_console_url}) auf.\n1. Wenn dies dein erstes Projekt ist, klicke auf **Projekt erstellen** und dann auf **Neues Projekt**.\n1. Gib deinem Cloud-Projekt einen Namen und klicke dann auf **Erstellen**.\n1. Speichere die Cloud Project ID, z. B. *example-project-12345*, da du diese sp\u00e4ter ben\u00f6tigst.\n1. Gehe zur API-Bibliothek f\u00fcr [Smart Device Management API]({sdm_api_url}) und klicke auf **Aktivieren**.\n1. Wechsele zur API-Bibliothek f\u00fcr [Cloud Pub/Sub API]({pubsub_api_url}) und klicke auf **Aktivieren**.\n\nFahre fort, wenn dein Cloud-Projekt eingerichtet ist.", "title": "Nest: Cloud-Projekt erstellen und konfigurieren" }, "device_project": { "data": { "project_id": "Ger\u00e4tezugriffsprojekt ID" }, - "description": "Erstellen Sie ein Nest Ger\u00e4tezugriffsprojekt, f\u00fcr dessen Einrichtung **eine Geb\u00fchr von 5 US-Dollar** anf\u00e4llt.\n1. Gehen Sie zur [Device Access Console]({device_access_console_url}) und durchlaufen Sie den Zahlungsablauf.\n1. Klicken Sie auf **Projekt erstellen**.\n1. Geben Sie Ihrem Device Access-Projekt einen Namen und klicken Sie auf **Weiter**.\n1. Geben Sie Ihre OAuth-Client-ID ein\n1. Aktivieren Sie Ereignisse, indem Sie auf **Aktivieren** und **Projekt erstellen** klicken.\n\nGeben Sie unten Ihre Ger\u00e4tezugriffsprojekt ID ein ([more info]({more_info_url})).\n", + "description": "Erstelle ein Nest Ger\u00e4tezugriffsprojekt, f\u00fcr dessen Einrichtung **eine Geb\u00fchr von 5 US-Dollar** anf\u00e4llt.\n1. Gehe zur [Device Access Console]({device_access_console_url}) und durchlaufe den Zahlungsablauf.\n1. Klicke auf **Projekt erstellen**.\n1. Gib deinem Device Access-Projekt einen Namen und klicke auf **Weiter**.\n1. Gib deine OAuth-Client-ID ein\n1. Aktiviere Ereignisse, indem du auf **Aktivieren** und **Projekt erstellen** klickst.\n\nGib unten deine Ger\u00e4tezugriffsprojekt ID ein ([more info]({more_info_url})).", "title": "Nest: Erstelle ein Ger\u00e4tezugriffsprojekt" }, "device_project_upgrade": { - "description": "Aktualisieren Sie das Nest Ger\u00e4tezugriffsprojekt mit Ihrer neuen OAuth Client ID ([more info]({more_info_url}))\n1. Gehen Sie zur [Ger\u00e4tezugriffskonsole]({device_access_console_url}).\n1. Klicken Sie auf das Papierkorbsymbol neben *OAuth Client ID*.\n1. Klicken Sie auf das \u00dcberlaufmen\u00fc und *Client ID hinzuf\u00fcgen*.\n1. Geben Sie Ihre neue OAuth-Client-ID ein und klicken Sie auf **Hinzuf\u00fcgen**.\n\nIhre OAuth-Client-ID lautet: `{client_id}`", + "description": "Aktualisiere das Nest Ger\u00e4tezugriffsprojekt mit deiner neuen OAuth Client ID ([more info]({more_info_url}))\n1. Gehe zur [Ger\u00e4tezugriffskonsole]({device_access_console_url}).\n1. Klicke auf das Papierkorbsymbol neben *OAuth Client ID*.\n1. Klicke auf das \u00dcberlaufmen\u00fc und *Client ID hinzuf\u00fcgen*.\n1. Gib deine neue OAuth-Client-ID ein und klicke auf **Hinzuf\u00fcgen**.\n\nDeine OAuth-Client-ID lautet: `{client_id}`", "title": "Nest: Aktualisiere das Ger\u00e4tezugriffsprojekt" }, "init": { diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 1e2082c4a32..3eae7e1c547 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -6,18 +6,31 @@ "abort": { "already_configured": "Konto har redan konfigurerats", "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", "reauth_successful": "\u00c5terautentisering lyckades", - "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown_authorize_url_generation": "Ok\u00e4nt fel vid generering av en auktoriserad URL." + }, + "create_entry": { + "default": "Autentiserats" }, "error": { + "bad_project_id": "Ange ett giltigt Cloud Project ID (kontrollera Cloud Console)", "internal_error": "Internt fel vid validering av kod", "invalid_pin": "Ogiltig Pin-kod", + "subscriber_error": "Ok\u00e4nt abonnentfel, se loggar", "timeout": "Timeout vid valididering av kod", - "unknown": "Ok\u00e4nt fel vid validering av kod" + "unknown": "Ok\u00e4nt fel vid validering av kod", + "wrong_project_id": "Ange ett giltigt Cloud Project ID (var samma som Device Access Project ID)" }, "step": { "auth": { + "data": { + "code": "\u00c5tkomstnyckel" + }, + "description": "F\u00f6r att l\u00e4nka ditt Google-konto, [auktorisera ditt konto]( {url} ). \n\n Efter auktorisering, kopiera och klistra in den medf\u00f6ljande Auth Token-koden nedan.", "title": "L\u00e4nka Google-konto" }, "auth_upgrade": { @@ -60,6 +73,16 @@ "description": "F\u00f6r att l\u00e4nka ditt Nest-konto, [autentisiera ditt konto]({url}). \n\nEfter autentisiering, klipp och klistra in den angivna pin-koden nedan.", "title": "L\u00e4nka Nest-konto" }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "pubsub": { + "data": { + "cloud_project_id": "Projekt-ID f\u00f6r Google Cloud" + }, + "description": "Bes\u00f6k [Cloud Console]( {url} ) f\u00f6r att hitta ditt Google Cloud Project ID.", + "title": "Konfigurera Google Cloud" + }, "reauth_confirm": { "description": "Nest-integreringen m\u00e5ste autentisera ditt konto igen", "title": "\u00c5terautenticera integration" @@ -73,5 +96,15 @@ "camera_sound": "Ljud uppt\u00e4ckt", "doorbell_chime": "D\u00f6rrklockan tryckt" } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Nest i configuration.yaml tas bort i Home Assistant 2022.10. \n\n Dina befintliga OAuth-applikationsuppgifter och \u00e5tkomstinst\u00e4llningar har importerats till anv\u00e4ndargr\u00e4nssnittet automatiskt. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Nest YAML-konfigurationen tas bort" + }, + "removed_app_auth": { + "description": "F\u00f6r att f\u00f6rb\u00e4ttra s\u00e4kerheten och minska risken f\u00f6r n\u00e4tfiske har Google fasat ut den autentiseringsmetod som anv\u00e4nds av Home Assistant. \n\n **Detta kr\u00e4ver \u00e5tg\u00e4rd fr\u00e5n dig f\u00f6r att l\u00f6sa** ([mer info]( {more_info_url} )) \n\n 1. Bes\u00f6k integrationssidan\n 1. Klicka p\u00e5 Konfigurera om p\u00e5 Nest-integreringen.\n 1. Home Assistant leder dig genom stegen f\u00f6r att uppgradera till webbautentisering. \n\n Se Nest [integreringsinstruktioner]( {documentation_url} ) f\u00f6r fels\u00f6kningsinformation.", + "title": "Nest-autentiseringsuppgifterna m\u00e5ste uppdateras" + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index cd91ea65c81..54e1a56ed11 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -22,7 +22,24 @@ }, "device_automation": { "trigger_subtype": { + "away": "borta", + "hg": "frostvakt", "schedule": "schema" + }, + "trigger_type": { + "alarm_started": "{entity_name} uppt\u00e4ckte ett larm", + "animal": "{entity_name} uppt\u00e4ckte ett djur", + "cancel_set_point": "{entity_name} har \u00e5terupptagit sitt schema", + "human": "{entity_name} uppt\u00e4ckte en m\u00e4nniska", + "movement": "{entity_name} uppt\u00e4ckte r\u00f6relse", + "outdoor": "{entity_name} uppt\u00e4ckte en utomhush\u00e4ndelse", + "person": "{entity_name} uppt\u00e4ckte en person", + "person_away": "{entity_name} uppt\u00e4ckte att en person har l\u00e4mnat", + "set_point": "M\u00e5ltemperaturen {entity_name} har st\u00e4llts in manuellt.", + "therm_mode": "{entity_name} bytte till \" {subtype} \"", + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5", + "vehicle": "{entity_name} uppt\u00e4ckt ett fordon" } }, "options": { diff --git a/homeassistant/components/netgear/translations/sv.json b/homeassistant/components/netgear/translations/sv.json index 2672bc03eef..5575242e6e8 100644 --- a/homeassistant/components/netgear/translations/sv.json +++ b/homeassistant/components/netgear/translations/sv.json @@ -1,10 +1,29 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "config": "Anslutnings- eller inloggningsfel: kontrollera din konfiguration" + }, "step": { "user": { "data": { + "host": "V\u00e4rd (Valfritt)", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn (frivilligt)" - } + }, + "description": "Standardv\u00e4rd: {host}\n Standardanv\u00e4ndarnamn: {username}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Tid f\u00f6r att betraktas som hemma (sekunder)" + }, + "description": "Ange valfria inst\u00e4llningar" } } } diff --git a/homeassistant/components/nina/translations/sv.json b/homeassistant/components/nina/translations/sv.json index 11f2e28deac..028b843fdb5 100644 --- a/homeassistant/components/nina/translations/sv.json +++ b/homeassistant/components/nina/translations/sv.json @@ -1,4 +1,29 @@ { + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "no_selection": "V\u00e4lj minst en stad/l\u00e4n", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Stad/l\u00e4n (A-D)", + "_e_to_h": "Stad/l\u00e4n (E-H)", + "_i_to_l": "Stad/l\u00e4n (I-L)", + "_m_to_q": "Stad/l\u00e4n (M-Q)", + "_r_to_u": "Stad/l\u00e4n (R-U)", + "_v_to_z": "Stad/l\u00e4n (V-Z)", + "corona_filter": "Ta bort Corona-varningar", + "slots": "Maximala varningar per stad/l\u00e4n" + }, + "title": "V\u00e4lj stad/l\u00e4n" + } + } + }, "options": { "error": { "cannot_connect": "Misslyckades att ansluta", diff --git a/homeassistant/components/nmap_tracker/translations/sv.json b/homeassistant/components/nmap_tracker/translations/sv.json index b69cd27fe52..e9bdf8dbdf9 100644 --- a/homeassistant/components/nmap_tracker/translations/sv.json +++ b/homeassistant/components/nmap_tracker/translations/sv.json @@ -1,12 +1,40 @@ { + "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, + "error": { + "invalid_hosts": "Ogiltiga v\u00e4rdar" + }, + "step": { + "user": { + "data": { + "exclude": "N\u00e4tverksadresser (kommaseparerade) f\u00f6r att utesluta fr\u00e5n skanning", + "home_interval": "Minsta antal minuter mellan skanningar av aktiva enheter (spar p\u00e5 batteri)", + "hosts": "N\u00e4tverksadresser (kommaseparerade) att skanna", + "scan_options": "R\u00e5konfigurerbara skanningsalternativ f\u00f6r Nmap" + }, + "description": "Konfigurera v\u00e4rdar som ska skannas av Nmap. N\u00e4tverksadresser och exkluderingar kan vara IP-adresser (192.168.1.1), IP-n\u00e4tverk (192.168.0.0/24) eller IP-intervall (192.168.1.0-32)." + } + } + }, "options": { + "error": { + "invalid_hosts": "Ogiltiga v\u00e4rdar" + }, "step": { "init": { "data": { "consider_home": "Sekunder att v\u00e4nta tills en enhetssp\u00e5rare markeras som inte hemma efter att den inte setts.", - "interval_seconds": "Skanningsintervall" - } + "exclude": "N\u00e4tverksadresser (kommaseparerade) f\u00f6r att utesluta fr\u00e5n skanning", + "home_interval": "Minsta antal minuter mellan skanningar av aktiva enheter (spar p\u00e5 batteri)", + "hosts": "N\u00e4tverksadresser (kommaseparerade) att skanna", + "interval_seconds": "Skanningsintervall", + "scan_options": "R\u00e5konfigurerbara skanningsalternativ f\u00f6r Nmap" + }, + "description": "Konfigurera v\u00e4rdar som ska skannas av Nmap. N\u00e4tverksadresser och exkluderingar kan vara IP-adresser (192.168.1.1), IP-n\u00e4tverk (192.168.0.0/24) eller IP-intervall (192.168.1.0-32)." } } - } + }, + "title": "Nmap sp\u00e5rare" } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/sv.json b/homeassistant/components/notion/translations/sv.json index b565872aacc..39fc56dbf66 100644 --- a/homeassistant/components/notion/translations/sv.json +++ b/homeassistant/components/notion/translations/sv.json @@ -5,6 +5,7 @@ "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/nuki/translations/sv.json b/homeassistant/components/nuki/translations/sv.json index 9a4ec6946b7..0d73cff0543 100644 --- a/homeassistant/components/nuki/translations/sv.json +++ b/homeassistant/components/nuki/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", @@ -9,7 +12,9 @@ "reauth_confirm": { "data": { "token": "\u00c5tkomstnyckel" - } + }, + "description": "Nuki-integrationen m\u00e5ste autentiseras p\u00e5 nytt med din brygga.", + "title": "\u00c5terautenticera integration" }, "user": { "data": { diff --git a/homeassistant/components/number/translations/sv.json b/homeassistant/components/number/translations/sv.json new file mode 100644 index 00000000000..02af44b1e7b --- /dev/null +++ b/homeassistant/components/number/translations/sv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Ange v\u00e4rde f\u00f6r {entity_name}" + } + }, + "title": "Nummer" +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/sv.json b/homeassistant/components/octoprint/translations/sv.json index af10a29c982..f3b9760aca2 100644 --- a/homeassistant/components/octoprint/translations/sv.json +++ b/homeassistant/components/octoprint/translations/sv.json @@ -6,12 +6,23 @@ "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ov\u00e4ntat fel" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "OctoPrint-skrivare: {host}", + "progress": { + "get_api_key": "\u00d6ppna OctoPrint UI och klicka p\u00e5 \"Till\u00e5t\" p\u00e5 \u00e5tkomstbeg\u00e4ran f\u00f6r \"Home Assistant\"." + }, "step": { "user": { "data": { "host": "V\u00e4rd", + "path": "S\u00f6kv\u00e4g till program", + "port": "Portnummer", "ssl": "Anv\u00e4nd SSL", - "username": "Anv\u00e4ndarnamn" + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/omnilogic/translations/sv.json b/homeassistant/components/omnilogic/translations/sv.json index 407893a7018..c572f6264b5 100644 --- a/homeassistant/components/omnilogic/translations/sv.json +++ b/homeassistant/components/omnilogic/translations/sv.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/ondilo_ico/translations/sv.json b/homeassistant/components/ondilo_ico/translations/sv.json new file mode 100644 index 00000000000..732124944cd --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen." + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/sv.json b/homeassistant/components/onewire/translations/sv.json index e4ce4947ca9..b1f66853ca5 100644 --- a/homeassistant/components/onewire/translations/sv.json +++ b/homeassistant/components/onewire/translations/sv.json @@ -1,21 +1,40 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { "host": "V\u00e4rd", "port": "Port" - } + }, + "title": "St\u00e4ll in serverdetaljer" } } }, "options": { + "error": { + "device_not_selected": "V\u00e4lj enheter att konfigurera" + }, "step": { + "configure_device": { + "data": { + "precision": "Sensorprecision" + }, + "description": "V\u00e4lj sensorprecision f\u00f6r {sensor_id}", + "title": "OneWire-sensorprecision" + }, "device_selection": { "data": { "clear_device_options": "Rensa alla enhetskonfigurationer", "device_selection": "V\u00e4lj enheter att konfigurera" - } + }, + "description": "V\u00e4lj vilka konfigurationssteg som ska bearbetas", + "title": "OneWire-enhetsalternativ" } } } diff --git a/homeassistant/components/onvif/translations/sv.json b/homeassistant/components/onvif/translations/sv.json index 579922f31e6..45fe62d156d 100644 --- a/homeassistant/components/onvif/translations/sv.json +++ b/homeassistant/components/onvif/translations/sv.json @@ -7,6 +7,9 @@ "no_mac": "Det gick inte att konfigurera unikt ID f\u00f6r ONVIF-enhet.", "onvif_error": "Problem med att konfigurera ONVIF enheten. Kolla loggarna f\u00f6r mer information." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "configure": { "data": { diff --git a/homeassistant/components/open_meteo/translations/sv.json b/homeassistant/components/open_meteo/translations/sv.json new file mode 100644 index 00000000000..8a7e60fcaa4 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zon" + }, + "description": "V\u00e4lj plats att anv\u00e4nda f\u00f6r v\u00e4derprognoser" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/sv.json b/homeassistant/components/opentherm_gw/translations/sv.json index 95ed092fece..1231a8343a7 100644 --- a/homeassistant/components/opentherm_gw/translations/sv.json +++ b/homeassistant/components/opentherm_gw/translations/sv.json @@ -2,7 +2,9 @@ "config": { "error": { "already_configured": "Gateway redan konfigurerad", - "id_exists": "Gateway-id finns redan" + "cannot_connect": "Det gick inte att ansluta.", + "id_exists": "Gateway-id finns redan", + "timeout_connect": "Timeout uppr\u00e4ttar anslutning" }, "step": { "init": { diff --git a/homeassistant/components/overkiz/translations/select.sv.json b/homeassistant/components/overkiz/translations/select.sv.json new file mode 100644 index 00000000000..bf0cd5dc9aa --- /dev/null +++ b/homeassistant/components/overkiz/translations/select.sv.json @@ -0,0 +1,13 @@ +{ + "state": { + "overkiz__memorized_simple_volume": { + "highest": "H\u00f6gsta", + "standard": "Standard" + }, + "overkiz__open_closed_pedestrian": { + "closed": "St\u00e4ngd", + "open": "\u00d6ppen", + "pedestrian": "Fotg\u00e4ngare" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.sv.json b/homeassistant/components/overkiz/translations/sensor.sv.json index 024e5fe1037..f7cfb3eb94f 100644 --- a/homeassistant/components/overkiz/translations/sensor.sv.json +++ b/homeassistant/components/overkiz/translations/sensor.sv.json @@ -1,5 +1,42 @@ { "state": { + "overkiz__battery": { + "full": "Full", + "low": "L\u00e5gt", + "normal": "Normal", + "verylow": "V\u00e4ldigt l\u00e5gt" + }, + "overkiz__discrete_rssi_level": { + "good": "Bra", + "low": "L\u00e5g", + "normal": "Normal", + "verylow": "Mycket l\u00e5g" + }, + "overkiz__priority_lock_originator": { + "external_gateway": "Extern gateway", + "local_user": "Lokal anv\u00e4ndare", + "lsc": "LSC", + "myself": "Jag sj\u00e4lv", + "rain": "Regn", + "saac": "SAAC", + "security": "S\u00e4kerhet", + "sfc": "SFC", + "temperature": "Temperatur", + "timer": "Timer", + "ups": "UPS", + "user": "Anv\u00e4ndare", + "wind": "Vind" + }, + "overkiz__sensor_defect": { + "dead": "D\u00f6d", + "low_battery": "L\u00e5g batteriniv\u00e5", + "maintenance_required": "Underh\u00e5ll kr\u00e4vs", + "no_defect": "Ingen defekt" + }, + "overkiz__sensor_room": { + "clean": "Ren", + "dirty": "Smutsig" + }, "overkiz__three_way_handle_direction": { "closed": "St\u00e4ngd", "open": "\u00d6ppen", diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json index d88861d15e0..b173a0bef99 100644 --- a/homeassistant/components/overkiz/translations/sv.json +++ b/homeassistant/components/overkiz/translations/sv.json @@ -1,17 +1,28 @@ { "config": { "abort": { + "already_configured": "Konto har redan konfigurerats", "reauth_successful": "\u00c5terautentisering lyckades", "reauth_wrong_account": "Du kan bara \u00e5terautentisera denna post med samma Overkiz-konto och hub" }, "error": { - "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd" + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "server_in_maintenance": "Servern ligger nere f\u00f6r underh\u00e5ll", + "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd", + "too_many_requests": "F\u00f6r m\u00e5nga f\u00f6rfr\u00e5gningar, f\u00f6rs\u00f6k igen senare", + "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "Gateway: {gateway_id}", "step": { "user": { "data": { + "host": "V\u00e4rd", + "hub": "Hubb", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Overkiz-plattformen anv\u00e4nds av olika leverant\u00f6rer som Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) och Atlantic (Cozytouch). Ange dina applikationsuppgifter och v\u00e4lj din hubb." } } } diff --git a/homeassistant/components/ovo_energy/translations/sv.json b/homeassistant/components/ovo_energy/translations/sv.json index bd443851b14..46fafdc3429 100644 --- a/homeassistant/components/ovo_energy/translations/sv.json +++ b/homeassistant/components/ovo_energy/translations/sv.json @@ -5,17 +5,22 @@ "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "{username}", "step": { "reauth": { "data": { "password": "L\u00f6senord" - } + }, + "description": "Autentisering misslyckades f\u00f6r OVO Energy. Ange dina aktuella uppgifter.", + "title": "\u00c5terautentisering" }, "user": { "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "St\u00e4ll in en OVO Energy-instans f\u00f6r att komma \u00e5t din energianv\u00e4ndning.", + "title": "L\u00e4gg till OVO Energikonto" } } } diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index d02571a6a50..f3a7542cdd5 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "\n\nUnter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen \u2192 Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links \u2192 Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." + "default": "Unter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen \u2192 Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links \u2192 Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/sv.json b/homeassistant/components/owntracks/translations/sv.json index 3c9146e49f2..0f3c8cade9c 100644 --- a/homeassistant/components/owntracks/translations/sv.json +++ b/homeassistant/components/owntracks/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "create_entry": { diff --git a/homeassistant/components/panasonic_viera/translations/sv.json b/homeassistant/components/panasonic_viera/translations/sv.json index a35794646b7..7375897d482 100644 --- a/homeassistant/components/panasonic_viera/translations/sv.json +++ b/homeassistant/components/panasonic_viera/translations/sv.json @@ -2,9 +2,11 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade. Kontrollera loggarna f\u00f6r mer information." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_pin_code": "Pin-kod du angav \u00e4r ogiltig" }, "step": { diff --git a/homeassistant/components/peco/translations/sv.json b/homeassistant/components/peco/translations/sv.json new file mode 100644 index 00000000000..b821234481e --- /dev/null +++ b/homeassistant/components/peco/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "county": "L\u00e4n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/sv.json b/homeassistant/components/philips_js/translations/sv.json index 290b8a9d82b..82c0206995b 100644 --- a/homeassistant/components/philips_js/translations/sv.json +++ b/homeassistant/components/philips_js/translations/sv.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_pin": "Ogiltig PIN-kod", + "pairing_failure": "Det g\u00e5r inte att para ihop: {error_id}", "unknown": "Ov\u00e4ntat fel" }, "step": { @@ -28,5 +29,14 @@ "trigger_type": { "turn_on": "Enheten uppmanas att sl\u00e5s p\u00e5" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Till\u00e5t anv\u00e4ndning av dataaviseringstj\u00e4nst." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/sv.json b/homeassistant/components/picnic/translations/sv.json index bff94714036..db0cc6e420a 100644 --- a/homeassistant/components/picnic/translations/sv.json +++ b/homeassistant/components/picnic/translations/sv.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", + "different_account": "Kontot b\u00f6r vara detsamma som anv\u00e4ndes f\u00f6r att st\u00e4lla in integrationen", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index d3e39738b02..fadf91bdbe8 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -47,7 +47,7 @@ "title": "Optionen f\u00fcr Plaato" }, "webhook": { - "description": "Webhook-Informationen:\n\n- URL: `{webhook_url}`\n- Methode: POST\n\n", + "description": "Webhook-Informationen:\n\n- URL: `{webhook_url}`\n- Methode: POST", "title": "Optionen f\u00fcr Plaato Airlock" } } diff --git a/homeassistant/components/plaato/translations/sv.json b/homeassistant/components/plaato/translations/sv.json index fe6b9055d06..ea08973ab07 100644 --- a/homeassistant/components/plaato/translations/sv.json +++ b/homeassistant/components/plaato/translations/sv.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index c79fdd881fc..81460b66c09 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -15,6 +15,7 @@ "not_found": "Plex-server hittades inte", "ssl_error": "Problem med SSL-certifikat" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { diff --git a/homeassistant/components/plugwise/translations/sv.json b/homeassistant/components/plugwise/translations/sv.json index 072006dd32e..379e1eb8a8c 100644 --- a/homeassistant/components/plugwise/translations/sv.json +++ b/homeassistant/components/plugwise/translations/sv.json @@ -7,8 +7,10 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "invalid_setup": "L\u00e4gg till din Adam ist\u00e4llet f\u00f6r din Anna, se Home Assistant Plugwise integrationsdokumentation f\u00f6r mer information", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plum_lightpad/translations/sv.json b/homeassistant/components/plum_lightpad/translations/sv.json index 26e9f2d6a49..4416244e366 100644 --- a/homeassistant/components/plum_lightpad/translations/sv.json +++ b/homeassistant/components/plum_lightpad/translations/sv.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "E-postadress" } } diff --git a/homeassistant/components/point/translations/sv.json b/homeassistant/components/point/translations/sv.json index 51627c78f3d..7fbf6d35c9f 100644 --- a/homeassistant/components/point/translations/sv.json +++ b/homeassistant/components/point/translations/sv.json @@ -4,7 +4,8 @@ "already_setup": "Du kan endast konfigurera ett Point-konto.", "authorize_url_timeout": "Timeout n\u00e4r genererar url f\u00f6r auktorisering.", "external_setup": "Point har lyckats med konfigurering ifr\u00e5n ett annat fl\u00f6de.", - "no_flows": "Du beh\u00f6ver konfigurera Point innan de kan autentisera med den. [L\u00e4s instruktioner](https://www.home-assistant.io/components/point/)." + "no_flows": "Du beh\u00f6ver konfigurera Point innan de kan autentisera med den. [L\u00e4s instruktioner](https://www.home-assistant.io/components/point/).", + "unknown_authorize_url_generation": "Ok\u00e4nt fel vid generering av en auktoriserad URL." }, "create_entry": { "default": "Lyckad autentisering med Minut f\u00f6r din(a) Point-enhet(er)" diff --git a/homeassistant/components/poolsense/translations/sv.json b/homeassistant/components/poolsense/translations/sv.json new file mode 100644 index 00000000000..6d40f08dd1f --- /dev/null +++ b/homeassistant/components/poolsense/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/sv.json b/homeassistant/components/powerwall/translations/sv.json index bc4dcc606ff..578eba75a05 100644 --- a/homeassistant/components/powerwall/translations/sv.json +++ b/homeassistant/components/powerwall/translations/sv.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { @@ -12,6 +13,17 @@ }, "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Vill du konfigurera {name} ({ip_address})?", + "title": "Anslut till powerwall" + }, + "reauth_confim": { + "data": { + "password": "L\u00f6senord" + }, + "description": "L\u00f6senordet \u00e4r vanligtvis de sista 5 tecknen i serienumret f\u00f6r Backup Gateway och kan hittas i Tesla-appen eller de sista 5 tecknen i l\u00f6senordet som finns innanf\u00f6r d\u00f6rren f\u00f6r Backup Gateway 2.", + "title": "Autentisera powerwall igen" + }, "user": { "data": { "ip_address": "IP-adress", diff --git a/homeassistant/components/profiler/translations/sv.json b/homeassistant/components/profiler/translations/sv.json new file mode 100644 index 00000000000..dda1c5ef70f --- /dev/null +++ b/homeassistant/components/profiler/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/sv.json b/homeassistant/components/prosegur/translations/sv.json index 4ae0ae62971..6c129cc5a64 100644 --- a/homeassistant/components/prosegur/translations/sv.json +++ b/homeassistant/components/prosegur/translations/sv.json @@ -1,13 +1,25 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { + "description": "Autentisera p\u00e5 nytt med Prosegur-konto.", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } }, "user": { "data": { + "country": "Land", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/ps4/translations/sv.json b/homeassistant/components/ps4/translations/sv.json index 26af627749d..f3382628633 100644 --- a/homeassistant/components/ps4/translations/sv.json +++ b/homeassistant/components/ps4/translations/sv.json @@ -23,12 +23,18 @@ "ip_address": "IP-adress", "name": "Namn", "region": "Region" + }, + "data_description": { + "code": "Navigera till \"Inst\u00e4llningar\" p\u00e5 din PlayStation 4-konsol. Navigera sedan till \"Mobile App Connection Settings\" och v\u00e4lj \"Add Device\" f\u00f6r att f\u00e5 fram PIN-koden." } }, "mode": { "data": { "ip_address": "IP-adress (l\u00e4mna tom om du anv\u00e4nder automatisk uppt\u00e4ckt).", "mode": "Konfigureringsl\u00e4ge" + }, + "data_description": { + "ip_address": "L\u00e4mna tomt om du v\u00e4ljer automatisk uppt\u00e4ckt." } } } diff --git a/homeassistant/components/pure_energie/translations/sv.json b/homeassistant/components/pure_energie/translations/sv.json new file mode 100644 index 00000000000..d85762bb0b4 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Pure Energie Meter (` {model} `) till Home Assistant?", + "title": "Uppt\u00e4ckte Pure Energie Meter-enhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvoutput/translations/sv.json b/homeassistant/components/pvoutput/translations/sv.json index 5ad5b5b6db4..75a03895a40 100644 --- a/homeassistant/components/pvoutput/translations/sv.json +++ b/homeassistant/components/pvoutput/translations/sv.json @@ -1,15 +1,25 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "reauth_confirm": { "data": { "api_key": "API-nyckel" - } + }, + "description": "F\u00f6r att autentisera p\u00e5 nytt med PVOutput m\u00e5ste du h\u00e4mta API-nyckeln p\u00e5 {account_url} ." }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "system_id": "System-ID" + }, + "description": "F\u00f6r att autentisera med PVOutput m\u00e5ste du h\u00e4mta API-nyckeln p\u00e5 {account_url} . \n\n System-ID:n f\u00f6r registrerade system listas p\u00e5 samma sida." } } } diff --git a/homeassistant/components/radio_browser/translations/sv.json b/homeassistant/components/radio_browser/translations/sv.json new file mode 100644 index 00000000000..69d8658d2ac --- /dev/null +++ b/homeassistant/components/radio_browser/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du l\u00e4gga till Radio Browser till Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/sv.json b/homeassistant/components/rainmachine/translations/sv.json index 40295676501..10e06693207 100644 --- a/homeassistant/components/rainmachine/translations/sv.json +++ b/homeassistant/components/rainmachine/translations/sv.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "Denna RainMachine-enhet \u00e4r redan konfigurerad" }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/remote/translations/sv.json b/homeassistant/components/remote/translations/sv.json index ef7a2ab0ad2..2fff0f39aa4 100644 --- a/homeassistant/components/remote/translations/sv.json +++ b/homeassistant/components/remote/translations/sv.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/renault/translations/sv.json b/homeassistant/components/renault/translations/sv.json index a3bea645b0f..484daf9b774 100644 --- a/homeassistant/components/renault/translations/sv.json +++ b/homeassistant/components/renault/translations/sv.json @@ -1,6 +1,27 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "kamereon_no_account": "Det gick inte att hitta Kamereon-kontot", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_credentials": "Ogiltig autentisering" + }, "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon konto-id" + }, + "title": "V\u00e4lj Kamereon-konto-id" + }, + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera ditt l\u00f6senord f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "locale": "Plats", diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json index be9e6e944a7..c35b96e9f44 100644 --- a/homeassistant/components/rfxtrx/translations/sv.json +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -60,7 +60,8 @@ "automatic_add": "Aktivera automatisk till\u00e4gg av enheter", "debug": "Aktivera fels\u00f6kning", "device": "V\u00e4lj enhet att konfigurera", - "event_code": "Ange h\u00e4ndelsekod att l\u00e4gga till" + "event_code": "Ange h\u00e4ndelsekod att l\u00e4gga till", + "protocols": "Protokoll" }, "title": "Rfxtrx-alternativ" }, @@ -71,7 +72,8 @@ "data_bit": "Antal databitar", "off_delay": "Avst\u00e4ngningsf\u00f6rdr\u00f6jning", "off_delay_enabled": "Aktivera avst\u00e4ngningsf\u00f6rdr\u00f6jning", - "replace_device": "V\u00e4lj enhet att ers\u00e4tta" + "replace_device": "V\u00e4lj enhet att ers\u00e4tta", + "venetian_blind_mode": "L\u00e4ge f\u00f6r persienner" }, "title": "Konfigurera enhetsalternativ" } diff --git a/homeassistant/components/ridwell/translations/sv.json b/homeassistant/components/ridwell/translations/sv.json index 23c825f256f..b62664b842d 100644 --- a/homeassistant/components/ridwell/translations/sv.json +++ b/homeassistant/components/ridwell/translations/sv.json @@ -1,10 +1,27 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ange l\u00f6senordet f\u00f6r {anv\u00e4ndarnamn} p\u00e5 nytt:", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange ditt anv\u00e4ndarnamn och l\u00f6senord:" } } } diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json index 662583561d0..f0194b4edf5 100644 --- a/homeassistant/components/risco/translations/sv.json +++ b/homeassistant/components/risco/translations/sv.json @@ -21,8 +21,23 @@ "options": { "step": { "ha_to_risco": { + "data": { + "armed_away": "Bortalarmat", + "armed_custom_bypass": "Larmat anpassad f\u00f6rbikoppling", + "armed_home": "Hemmalarmat", + "armed_night": "Nattlarmat" + }, + "description": "V\u00e4lj vilket l\u00e4ge du vill st\u00e4lla in ditt Risco-larm p\u00e5 n\u00e4r Home Assistant-larmet aktiveras", "title": "Mappa Home Assistant tillst\u00e5nd till Risco tillst\u00e5nd" }, + "init": { + "data": { + "code_arm_required": "Kr\u00e4v Pin-kod f\u00f6r att aktivera", + "code_disarm_required": "Kr\u00e4v Pin-kod f\u00f6r att avaktivera", + "scan_interval": "Hur ofta ska man fr\u00e5ga Risco (i sekunder)" + }, + "title": "Konfigurera alternativ" + }, "risco_to_ha": { "data": { "A": "Grupp A", diff --git a/homeassistant/components/roku/translations/sv.json b/homeassistant/components/roku/translations/sv.json index 0acdb41359d..a470c9455ad 100644 --- a/homeassistant/components/roku/translations/sv.json +++ b/homeassistant/components/roku/translations/sv.json @@ -10,6 +10,9 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name}?" + }, "user": { "data": { "host": "V\u00e4rd" diff --git a/homeassistant/components/rtsp_to_webrtc/translations/sv.json b/homeassistant/components/rtsp_to_webrtc/translations/sv.json new file mode 100644 index 00000000000..c748d2feb9c --- /dev/null +++ b/homeassistant/components/rtsp_to_webrtc/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "server_failure": "RTSPtoWebRTC-servern returnerade ett fel. Kontrollera loggar f\u00f6r mer information.", + "server_unreachable": "Det g\u00e5r inte att kommunicera med RTSPtoWebRTC-servern. Kontrollera loggar f\u00f6r mer information.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "invalid_url": "M\u00e5ste vara en giltig RTSPtoWebRTC-serveradress, t.ex. https://example.com", + "server_failure": "RTSPtoWebRTC-servern returnerade ett fel. Kontrollera loggar f\u00f6r mer information.", + "server_unreachable": "Det g\u00e5r inte att kommunicera med RTSPtoWebRTC-servern. Kontrollera loggar f\u00f6r mer information." + }, + "step": { + "hassio_confirm": { + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till RTSPtoWebRTC-servern som tillhandah\u00e5lls av till\u00e4gget: {addon} ?", + "title": "RTSPtoWebRTC via Home Assistant-till\u00e4gget" + }, + "user": { + "data": { + "server_url": "RTSPtoWebRTC server URL t.ex. https://example.com" + }, + "description": "RTSPtoWebRTC-integrationen kr\u00e4ver en server f\u00f6r att \u00f6vers\u00e4tta RTSP-str\u00f6mmar till WebRTC. Ange URL:en till RTSPtoWebRTC-servern.", + "title": "Konfigurera RTSPtoWebRTC" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index 67c8373d0e3..7de006b6a11 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -4,25 +4,33 @@ "already_configured": "Denna Samsung TV \u00e4r redan konfigurerad.", "already_in_progress": "Samsung TV-konfiguration p\u00e5g\u00e5r redan.", "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", + "cannot_connect": "Det gick inte att ansluta.", "id_missing": "Denna Samsung-enhet har inget serienummer.", "not_supported": "Denna Samsung enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte.", "reauth_successful": "\u00c5terautentisering lyckades", "unknown": "Ov\u00e4ntat fel" }, "error": { - "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant." + "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", + "invalid_pin": "PIN-koden \u00e4r ogiltig, f\u00f6rs\u00f6k igen." }, "flow_title": "{device}", "step": { "confirm": { "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." }, + "encrypted_pairing": { + "description": "Ange PIN-koden som visas p\u00e5 {device} ." + }, "pairing": { "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." }, "reauth_confirm": { "description": "N\u00e4r du har skickat, acceptera popup-f\u00f6nstret p\u00e5 {enheten} som beg\u00e4r auktorisering inom 30 sekunder eller ange PIN-koden." }, + "reauth_confirm_encrypted": { + "description": "Ange PIN-koden som visas p\u00e5 {device} ." + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress", diff --git a/homeassistant/components/season/translations/sv.json b/homeassistant/components/season/translations/sv.json index c0b662beebe..649789e560e 100644 --- a/homeassistant/components/season/translations/sv.json +++ b/homeassistant/components/season/translations/sv.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "type": "Typ av s\u00e4songsdefinition" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/sv.json b/homeassistant/components/sense/translations/sv.json index 76e41ccb1ba..f805dd483d8 100644 --- a/homeassistant/components/sense/translations/sv.json +++ b/homeassistant/components/sense/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", @@ -9,12 +10,26 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_validate": { + "data": { + "password": "L\u00f6senord" + }, + "description": "The Sense integration needs to re-authenticate your account {email}.", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "email": "E-postadress", - "password": "L\u00f6senord" + "password": "L\u00f6senord", + "timeout": "Timeout" }, "title": "Anslut till din Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "Verifieringskod" + }, + "title": "Sense Multifaktorsautentisering" } } } diff --git a/homeassistant/components/senseme/translations/sv.json b/homeassistant/components/senseme/translations/sv.json index 46631acc69a..ac7e8f6f76a 100644 --- a/homeassistant/components/senseme/translations/sv.json +++ b/homeassistant/components/senseme/translations/sv.json @@ -1,7 +1,30 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress" + }, + "flow_title": "{name} - {model} ({host})", + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} - {model} ({host})?" + }, + "manual": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Ange en IP-adress." + }, + "user": { + "data": { + "device": "Enhet" + }, + "description": "V\u00e4lj en enhet eller v\u00e4lj \"IP-adress\" f\u00f6r att manuellt ange en IP-adress." + } } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sv.json b/homeassistant/components/sensibo/translations/sv.json index 5ad5b5b6db4..04f12a04cd4 100644 --- a/homeassistant/components/sensibo/translations/sv.json +++ b/homeassistant/components/sensibo/translations/sv.json @@ -1,5 +1,16 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "incorrect_api_key": "Ogiltig API-nyckel f\u00f6r valt konto", + "invalid_auth": "Ogiltig autentisering", + "no_devices": "Inga enheter uppt\u00e4cktes", + "no_username": "Kunde inte h\u00e4mta anv\u00e4ndarnamn" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 24b137a420b..91f79c0c703 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Nuvarande {entity_name} skenbar effekt", "is_battery_level": "Nuvarande {entity_name} batteriniv\u00e5", "is_carbon_dioxide": "Nuvarande {entity_name} koncentration av koldioxid", "is_carbon_monoxide": "Nuvarande {entity_name} koncentration av kolmonoxid", @@ -29,15 +30,19 @@ "is_voltage": "Nuvarande {entity_name} sp\u00e4nning" }, "trigger_type": { + "apparent_power": "{entity_name} uppenbara effektf\u00f6r\u00e4ndringar", "battery_level": "{entity_name} batteriniv\u00e5 \u00e4ndras", "carbon_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koldioxidkoncentrationen", "carbon_monoxide": "{entity_name} f\u00f6r\u00e4ndringar av kolmonoxidkoncentrationen", + "current": "{entity_name} aktuella \u00e4ndringar", "energy": "Energif\u00f6r\u00e4ndringar", "frequency": "{entity_name} frekvens\u00e4ndringar", "gas": "{entity_name} gasf\u00f6r\u00e4ndringar", "humidity": "{entity_name} fuktighet \u00e4ndras", "illuminance": "{entity_name} belysning \u00e4ndras", "nitrogen_dioxide": "{entity_name} kv\u00e4vedioxidkoncentrationen f\u00f6r\u00e4ndras.", + "nitrogen_monoxide": "{entity_name} koncentrationen av kv\u00e4vemonoxid \u00e4ndras", + "nitrous_oxide": "{entity_name} f\u00f6r\u00e4ndringar i koncentrationen av lustgas", "ozone": "{entity_name} ozonkoncentrationen f\u00f6r\u00e4ndras", "pm1": "{entity_name} PM1-koncentrationsf\u00f6r\u00e4ndringar", "pm10": "{entity_name} PM10-koncentrations\u00e4ndringar", diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index d4f3f6f400e..62262c8558c 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -1,13 +1,20 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "unsupported_firmware": "Enheten anv\u00e4nder en firmwareversion som inte st\u00f6ds." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name}", "step": { + "confirm_discovery": { + "description": "Vill du konfigurera {model} p\u00e5 {host} ? \n\n Batteridrivna enheter som \u00e4r l\u00f6senordsskyddade m\u00e5ste v\u00e4ckas innan du forts\u00e4tter med installationen.\n Batteridrivna enheter som inte \u00e4r l\u00f6senordsskyddade kommer att l\u00e4ggas till n\u00e4r enheten vaknar, du kan nu manuellt v\u00e4cka enheten med en knapp p\u00e5 den eller v\u00e4nta p\u00e5 n\u00e4sta datauppdatering fr\u00e5n enheten." + }, "credentials": { "data": { "password": "L\u00f6senord", @@ -15,20 +22,33 @@ } }, "user": { + "data": { + "host": "V\u00e4rd" + }, "description": "F\u00f6re installationen m\u00e5ste batteridrivna enheter v\u00e4ckas, du kan nu v\u00e4cka enheten med en knapp p\u00e5 den." } } }, "device_automation": { "trigger_subtype": { + "button": "Knapp", + "button1": "F\u00f6rsta knappen", + "button2": "Andra knappen", + "button3": "Tredje knappen", "button4": "Fj\u00e4rde knappen" }, "trigger_type": { "btn_down": "\"{subtype}\" knappen nedtryckt", "btn_up": "\"{subtype}\" knappen uppsl\u00e4ppt", + "double": "\"{subtyp}\" dubbelklickad", "double_push": "{subtype} dubbeltryck", + "long": "{subtype} l\u00e5ngklickad", "long_push": "{subtype} l\u00e5ngtryck", - "single_push": "{subtyp} enkeltryck" + "long_single": "{subtype} l\u00e5ngklickad och sedan enkelklickad", + "single": "{subtype} enkelklickad", + "single_long": "{subtype} enkelklickad och sedan l\u00e5ngklickad", + "single_push": "{subtyp} enkeltryck", + "triple": "{subtype} trippelklickad" } } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/sv.json b/homeassistant/components/sia/translations/sv.json index fe4c471f302..9fac789fcfb 100644 --- a/homeassistant/components/sia/translations/sv.json +++ b/homeassistant/components/sia/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_account_format": "Kontot \u00e4r inte ett hexadecimalt v\u00e4rde, anv\u00e4nd endast 0-9 och AF.", "invalid_account_length": "Kontot har inte r\u00e4tt l\u00e4ngd, det m\u00e5ste vara mellan 3 och 16 tecken.", "invalid_key_format": "Nyckeln \u00e4r inte ett hexadecimalt v\u00e4rde, anv\u00e4nd endast 0-9 och AF.", "invalid_key_length": "Nyckeln har inte r\u00e4tt l\u00e4ngd, den m\u00e5ste vara 16, 24 eller 32 hexadecken.", @@ -12,18 +13,22 @@ "additional_account": { "data": { "account": "Konto-ID", + "additional_account": "Ytterligare konton", "encryption_key": "Krypteringsnyckel", - "ping_interval": "Pingintervall (min)" + "ping_interval": "Pingintervall (min)", + "zones": "Antal zoner f\u00f6r kontot" }, "title": "L\u00e4gg till ett annat konto till den aktuella porten." }, "user": { "data": { "account": "Konto-ID", + "additional_account": "Ytterligare konton", "encryption_key": "Krypteringsnyckel", "ping_interval": "Pingintervall (min)", "port": "Port", - "protocol": "Protokoll" + "protocol": "Protokoll", + "zones": "Antal zoner f\u00f6r kontot" }, "title": "Skapa en anslutning f\u00f6r SIA-baserade larmsystem." } @@ -33,7 +38,8 @@ "step": { "options": { "data": { - "ignore_timestamps": "Ignorera tidsst\u00e4mpelkontrollen f\u00f6r SIA-h\u00e4ndelserna" + "ignore_timestamps": "Ignorera tidsst\u00e4mpelkontrollen f\u00f6r SIA-h\u00e4ndelserna", + "zones": "Antal zoner f\u00f6r kontot" }, "description": "St\u00e4ll in alternativen f\u00f6r kontot: {account}", "title": "Alternativ f\u00f6r SIA-installationen." diff --git a/homeassistant/components/simplepush/translations/de.json b/homeassistant/components/simplepush/translations/de.json index 523ffda32bf..16e916f6d2f 100644 --- a/homeassistant/components/simplepush/translations/de.json +++ b/homeassistant/components/simplepush/translations/de.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Die Konfiguration von Simplepush mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Simplepush-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", "title": "Die Simplepush YAML-Konfiguration wird entfernt" + }, + "removed_yaml": { + "description": "Die Konfiguration von Simplepush mittels YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird vom Home Assistant nicht verwendet.\n\nEntferne die Simplepush-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Simplepush YAML-Konfiguration wurde entfernt" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/hu.json b/homeassistant/components/simplepush/translations/hu.json index b1f7b519dc5..b5809898fb1 100644 --- a/homeassistant/components/simplepush/translations/hu.json +++ b/homeassistant/components/simplepush/translations/hu.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "A Simplepush YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Simplepush YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", "title": "A Simplepush YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "removed_yaml": { + "description": "A Simplepush YAML-ban t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Simplepush YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Simplepush YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index 398fa3d63b0..c407990aa4d 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -22,6 +22,9 @@ "deprecated_yaml": { "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "removed_yaml": { + "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/no.json b/homeassistant/components/simplepush/translations/no.json index af15a931acc..453632f348e 100644 --- a/homeassistant/components/simplepush/translations/no.json +++ b/homeassistant/components/simplepush/translations/no.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Konfigurering av Simplepush med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", "title": "Simplepush YAML-konfigurasjonen blir fjernet" + }, + "removed_yaml": { + "description": "Konfigurering av Simplepush med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Simplepush YAML-konfigurasjonen er fjernet" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json index 2ddcba76929..e615a4c4c72 100644 --- a/homeassistant/components/simplepush/translations/ru.json +++ b/homeassistant/components/simplepush/translations/ru.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "removed_yaml": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Simplepush \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/sv.json b/homeassistant/components/simplepush/translations/sv.json index 9ed6908f0c3..2572b2cce75 100644 --- a/homeassistant/components/simplepush/translations/sv.json +++ b/homeassistant/components/simplepush/translations/sv.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Konfigurering av Simplepush med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Simplepush YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", "title": "Simplepush YAML-konfigurationen tas bort" + }, + "removed_yaml": { + "description": "Konfigurering av Simplepush med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort Simplepush YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Simplepush YAML-konfigurationen har tagits bort" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/zh-Hant.json b/homeassistant/components/simplepush/translations/zh-Hant.json index 15cd0bedb37..2e5b3576135 100644 --- a/homeassistant/components/simplepush/translations/zh-Hant.json +++ b/homeassistant/components/simplepush/translations/zh-Hant.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Simplepush \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Simplepush YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + }, + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Simplepush \u7684\u529f\u80fd\u5df2\u7d93\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Simplepush YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index dbb3d0e4207..9f44ba9ade9 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Compte ja est\u00e0 registrat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_auth_code_length": "Els codis d'autoritzaci\u00f3 de SimpliSafe tenen una longitud de 45 car\u00e0cters", "unknown": "Error inesperat" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 4788f2201b4..3f9eae5f187 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Konto bereits registriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_auth_code_length": "SimpliSafe Autorisierungscodes sind 45 Zeichen lang", "unknown": "Unerwarteter Fehler" }, "progress": { @@ -34,7 +35,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "SimpliSafe authentifiziert Benutzer \u00fcber seine Web-App. Aufgrund technischer Einschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; Bitte stelle sicher, dass Du die [Dokumentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) lesen, bevor Sie beginnen. \n\n Wenn Sie fertig sind, klicke [hier]({url}), um die SimpliSafe-Web-App zu \u00f6ffnen und Ihre Anmeldeinformationen einzugeben. Wenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gebe den Autorisierungscode von der SimpliSafe-Web-App-URL ein." + "description": "SimpliSafe authentifiziert die Benutzer \u00fcber seine Web-App. Aufgrund technischer Beschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; bitte stelle sicher, dass du die [Dokumentation] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) liest, bevor du beginnst.\n\nWenn du bereit bist, klicke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben. Wenn du dich bereits bei SimpliSafe in deinem Browser angemeldet hast, kannst du eine neue Registerkarte \u00f6ffnen und dann die oben genannte URL in diese Registerkarte kopieren/einf\u00fcgen.\n\nWenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gib den Autorisierungscode von der URL \"com.simplisafe.mobile\" ein." } } }, diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 5b2e898a1e5..78c32522a3d 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_auth_code_length": "A SimpliSafe enged\u00e9lyez\u00e9si k\u00f3dok 45 karakter hossz\u00faak.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 4d5ddf18ed6..4bb637e3f08 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -35,7 +35,7 @@ "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "SimpliSafe mengautentikasi pengguna melalui aplikasi webnya. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan bahwa Anda membaca [dokumentasi] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) sebelum memulai.\n\nJika sudah siap, klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan memasukkan kredensial Anda. Setelah proses selesai, kembali ke sini dan masukkan kode otorisasi dari URL aplikasi web SimpliSafe." + "description": "SimpliSafe mengautentikasi pengguna melalui aplikasi webnya. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan bahwa Anda membaca [dokumentasi] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) sebelum memulai.\n\nJika sudah siap, klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan memasukkan kredensial Anda. Jika Anda sedang masuk ke SimpliSafe di browser, Anda mungkin harus membuka tab baru, kemudian menyalin-tempel URL di atas di tab baru tersebut.\n\nSetelah proses selesai, kembali ke sini dan masukkan kode otorisasi dari URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index a0335228b2d..2997ca5dba5 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Konto er allerede registrert", "invalid_auth": "Ugyldig godkjenning", + "invalid_auth_code_length": "SimpliSafe-autorisasjonskoder er p\u00e5 45 tegn", "unknown": "Uventet feil" }, "progress": { @@ -34,7 +35,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "SimpliSafe autentiserer brukere via sin nettapp. P\u00e5 grunn av tekniske begrensninger er det et manuelt trinn p\u00e5 slutten av denne prosessen; s\u00f8rg for at du leser [dokumentasjonen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) f\u00f8r du starter. \n\n N\u00e5r du er klar, klikk [her]( {url} ) for \u00e5 \u00e5pne SimpliSafe-nettappen og angi legitimasjonen din. N\u00e5r prosessen er fullf\u00f8rt, g\u00e5 tilbake hit og skriv inn autorisasjonskoden fra SimpliSafe-nettappens URL." + "description": "SimpliSafe autentiserer brukere via sin nettapp. P\u00e5 grunn av tekniske begrensninger er det et manuelt trinn p\u00e5 slutten av denne prosessen; s\u00f8rg for at du leser [dokumentasjonen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) f\u00f8r du starter. \n\n N\u00e5r du er klar, klikk [her]( {url} ) for \u00e5 \u00e5pne SimpliSafe-nettappen og angi legitimasjonen din. Hvis du allerede har logget p\u00e5 SimpliSafe i nettleseren din, kan det v\u00e6re lurt \u00e5 \u00e5pne en ny fane, og deretter kopiere/lime inn URL-en ovenfor i den fanen. \n\n N\u00e5r prosessen er fullf\u00f8rt, g\u00e5 tilbake hit og skriv inn autorisasjonskoden fra `com.simplisafe.mobile` URL." } } }, diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 1b88417dd27..863a67c3b89 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "\u0423\u0447\u0451\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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_auth_code_length": "\u041a\u043e\u0434\u044b \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 SimpliSafe \u0441\u043e\u0441\u0442\u043e\u044f\u0442 \u0438\u0437 45 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index 2e24eb856bd..61e00432950 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -8,6 +8,8 @@ }, "error": { "identifier_exists": "Kontot \u00e4r redan registrerat", + "invalid_auth": "Ogiltig autentisering", + "invalid_auth_code_length": "SimpliSafe auktoriseringskoder \u00e4r 45 tecken l\u00e5nga", "unknown": "Ov\u00e4ntat fel" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 99f08eb14d4..fe6a3fa5ce0 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_auth_code_length": "SimpliSafe \u8a8d\u8b49\u78bc\u70ba 45 \u500b\u5b57\u5143\u9577", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "progress": { @@ -34,7 +35,7 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "SimpliSafe \u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u5176\u4f7f\u7528\u8005\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3002\n\n\u6e96\u5099\u5c31\u7dd2\u5f8c\u3001\u9ede\u9078 [\u6b64\u8655]({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\u5b8c\u6210\u5f8c\u56de\u5230\u9019\u88e1\u4e26\u8f38\u5165\u7531 SimpliSafe Web App \u6240\u53d6\u7684\u8a8d\u8b49\u78bc\u3002" + "description": "SimpliSafe \u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u5176\u4f7f\u7528\u8005\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3002\n\n\u6e96\u5099\u5c31\u7dd2\u5f8c\u3001\u9ede\u9078 [\u6b64\u8655]({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\u5047\u5982\u5df2\u7d93\u65bc\u700f\u89bd\u5668\u4e2d\u767b\u5165 SimpliSafe\uff0c\u53ef\u80fd\u9700\u8981\u958b\u555f\u65b0\u9801\u9762\u3001\u7136\u5f8c\u65bc\u9801\u9762\u7db2\u5740\u8907\u88fd/\u8cbc\u4e0a\u4e0a\u65b9 URL\u3002\n\n\u5b8c\u6210\u5f8c\u56de\u5230\u9019\u88e1\u4e26\u8f38\u5165\u7531 `com.simplisafe.mobile` \u6240\u53d6\u5f97\u7684\u8a8d\u8b49\u78bc\u3002" } } }, diff --git a/homeassistant/components/sleepiq/translations/sv.json b/homeassistant/components/sleepiq/translations/sv.json index 23c825f256f..e22bee20046 100644 --- a/homeassistant/components/sleepiq/translations/sv.json +++ b/homeassistant/components/sleepiq/translations/sv.json @@ -1,8 +1,24 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "SleepIQ-integrationen m\u00e5ste autentisera ditt konto {username} igen.", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/smhi/translations/sv.json b/homeassistant/components/smhi/translations/sv.json index 0bb597e393b..f9aac9e7d2b 100644 --- a/homeassistant/components/smhi/translations/sv.json +++ b/homeassistant/components/smhi/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "error": { "wrong_location": "Plats i Sverige endast" }, diff --git a/homeassistant/components/solaredge/translations/sv.json b/homeassistant/components/solaredge/translations/sv.json index df7408b43c2..e956870d013 100644 --- a/homeassistant/components/solaredge/translations/sv.json +++ b/homeassistant/components/solaredge/translations/sv.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "invalid_api_key": "Ogiltig API-nyckel" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "could_not_connect": "Det gick inte att ansluta till solaredge API", + "invalid_api_key": "Ogiltig API-nyckel", + "site_not_active": "Webbplatsen \u00e4r inte aktiv" }, "step": { "user": { diff --git a/homeassistant/components/somfy_mylink/translations/sv.json b/homeassistant/components/somfy_mylink/translations/sv.json index 6cacc18d1a9..75b4d6abd88 100644 --- a/homeassistant/components/somfy_mylink/translations/sv.json +++ b/homeassistant/components/somfy_mylink/translations/sv.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "flow_title": "{mac} ({ip})", "step": { "user": { @@ -24,6 +32,9 @@ "title": "Konfigurera MyLink-alternativ" }, "target_config": { + "data": { + "reverse": "Locket \u00e4r omv\u00e4nt" + }, "description": "Konfigurera alternativ f\u00f6r ` {target_name} `", "title": "Konfigurera MyLink Cover" } diff --git a/homeassistant/components/sonarr/translations/sv.json b/homeassistant/components/sonarr/translations/sv.json index c17fa818ed8..01dfc35be20 100644 --- a/homeassistant/components/sonarr/translations/sv.json +++ b/homeassistant/components/sonarr/translations/sv.json @@ -18,6 +18,7 @@ "user": { "data": { "api_key": "API nyckel", + "url": "URL", "verify_ssl": "Verifiera SSL-certifikat" } } diff --git a/homeassistant/components/songpal/translations/sv.json b/homeassistant/components/songpal/translations/sv.json index ea2b5e2d715..d67bad837e9 100644 --- a/homeassistant/components/songpal/translations/sv.json +++ b/homeassistant/components/songpal/translations/sv.json @@ -4,6 +4,9 @@ "already_configured": "Enheten \u00e4r redan konfigurerad", "not_songpal_device": "Inte en Songpal-enhet" }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { diff --git a/homeassistant/components/spider/translations/sv.json b/homeassistant/components/spider/translations/sv.json index d078c6d4110..678d8895de8 100644 --- a/homeassistant/components/spider/translations/sv.json +++ b/homeassistant/components/spider/translations/sv.json @@ -4,13 +4,16 @@ "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "error": { - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Logga in med mijn.ithodaalderop.nl-konto" } } } diff --git a/homeassistant/components/spotify/translations/sv.json b/homeassistant/components/spotify/translations/sv.json index 08c7e415edf..3dbcb9dc85a 100644 --- a/homeassistant/components/spotify/translations/sv.json +++ b/homeassistant/components/spotify/translations/sv.json @@ -3,7 +3,8 @@ "abort": { "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", "missing_configuration": "Spotify-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", - "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "reauth_account_mismatch": "Spotify-kontot som autentiserats med matchar inte kontot som kr\u00e4vs f\u00f6r omautentisering." }, "create_entry": { "default": "Lyckad autentisering med Spotify." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod." + }, + "reauth_confirm": { + "description": "Spotify-integrationen m\u00e5ste autentiseras p\u00e5 nytt med Spotify f\u00f6r konto: {account}", + "title": "\u00c5terautenticera integration" } } }, diff --git a/homeassistant/components/srp_energy/translations/sv.json b/homeassistant/components/srp_energy/translations/sv.json index b2068a4d12d..3f501296ccb 100644 --- a/homeassistant/components/srp_energy/translations/sv.json +++ b/homeassistant/components/srp_energy/translations/sv.json @@ -5,11 +5,15 @@ }, "error": { "cannot_connect": "Kan inte ansluta", + "invalid_account": "Konto-ID ska vara ett 9-siffrigt nummer", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "id": "Konto-id", + "is_tou": "\u00c4r plan f\u00f6r anv\u00e4ndningstid", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/steamist/translations/sv.json b/homeassistant/components/steamist/translations/sv.json index c20adb3f64d..a37f088921f 100644 --- a/homeassistant/components/steamist/translations/sv.json +++ b/homeassistant/components/steamist/translations/sv.json @@ -1,17 +1,31 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_steamist_device": "Inte en steamist enhet" }, "error": { "cannot_connect": "Kunde inte ansluta", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name} ({ipaddress})", "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} ({ipaddress})?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, "user": { "data": { "host": "V\u00e4rd" - } + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." } } } diff --git a/homeassistant/components/stookalert/translations/sv.json b/homeassistant/components/stookalert/translations/sv.json new file mode 100644 index 00000000000..505ccef6044 --- /dev/null +++ b/homeassistant/components/stookalert/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "province": "Provins" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/sv.json b/homeassistant/components/subaru/translations/sv.json index e89f29fc04a..69a01c5ea25 100644 --- a/homeassistant/components/subaru/translations/sv.json +++ b/homeassistant/components/subaru/translations/sv.json @@ -10,7 +10,8 @@ "cannot_connect": "Det gick inte att ansluta.", "incorrect_pin": "Felaktig PIN-kod", "incorrect_validation_code": "Felaktig valideringkod", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "two_factor_request_failed": "Beg\u00e4ran om 2FA-kod misslyckades, f\u00f6rs\u00f6k igen" }, "step": { "pin": { @@ -21,6 +22,9 @@ "title": "Subaru Starlink-konfiguration" }, "two_factor": { + "data": { + "contact_method": "V\u00e4lj en kontaktmetod:" + }, "description": "Tv\u00e5faktorautentisering kr\u00e4vs", "title": "Subaru Starlink-konfiguration" }, @@ -28,6 +32,7 @@ "data": { "validation_code": "Valideringskod" }, + "description": "Ange mottagen valideringskod", "title": "Subaru Starlink-konfiguration" }, "user": { diff --git a/homeassistant/components/sun/translations/sv.json b/homeassistant/components/sun/translations/sv.json index 7494630ac88..ffc1bacf65a 100644 --- a/homeassistant/components/sun/translations/sv.json +++ b/homeassistant/components/sun/translations/sv.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, "state": { "_": { "above_horizon": "Ovanf\u00f6r horisonten", diff --git a/homeassistant/components/surepetcare/translations/sv.json b/homeassistant/components/surepetcare/translations/sv.json index 23c825f256f..939b543adea 100644 --- a/homeassistant/components/surepetcare/translations/sv.json +++ b/homeassistant/components/surepetcare/translations/sv.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/switch/translations/sv.json b/homeassistant/components/switch/translations/sv.json index a2cd74434eb..6a87682ae5d 100644 --- a/homeassistant/components/switch/translations/sv.json +++ b/homeassistant/components/switch/translations/sv.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/switch_as_x/translations/sv.json b/homeassistant/components/switch_as_x/translations/sv.json index d21eefa5a1a..95ea5abe410 100644 --- a/homeassistant/components/switch_as_x/translations/sv.json +++ b/homeassistant/components/switch_as_x/translations/sv.json @@ -3,8 +3,10 @@ "step": { "user": { "data": { + "entity_id": "Brytare", "target_domain": "Typ" - } + }, + "description": "V\u00e4lj en str\u00f6mbrytare som du vill visa i Home Assistant som lampa, skal eller n\u00e5got annat. Den ursprungliga switchen kommer att d\u00f6ljas." } } }, diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json index f2c86841487..717b00d3d6c 100644 --- a/homeassistant/components/switchbot/translations/sv.json +++ b/homeassistant/components/switchbot/translations/sv.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "no_unconfigured_devices": "Inga okonfigurerade enheter hittades.", "switchbot_unsupported_type": "Switchbot-typ som inte st\u00f6ds.", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/syncthing/translations/sv.json b/homeassistant/components/syncthing/translations/sv.json index a77f97f91e7..1ba91de2f70 100644 --- a/homeassistant/components/syncthing/translations/sv.json +++ b/homeassistant/components/syncthing/translations/sv.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, "step": { diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index 95a8af99f63..5813be31352 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -32,8 +32,10 @@ }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Synology DSM \u00c5terautenticera integration" }, "user": { "data": { diff --git a/homeassistant/components/system_bridge/translations/sv.json b/homeassistant/components/system_bridge/translations/sv.json index 5fa635734cb..fdd4c4c0b67 100644 --- a/homeassistant/components/system_bridge/translations/sv.json +++ b/homeassistant/components/system_bridge/translations/sv.json @@ -1,15 +1,30 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", "step": { "authenticate": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Ange API-nyckeln som du st\u00e4llt in i din konfiguration f\u00f6r {name} ." }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "host": "V\u00e4rd", + "port": "Port" + }, + "description": "Ange dina anslutningsuppgifter." } } } diff --git a/homeassistant/components/tag/translations/sv.json b/homeassistant/components/tag/translations/sv.json new file mode 100644 index 00000000000..421db629ef8 --- /dev/null +++ b/homeassistant/components/tag/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Tagg" +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/sv.json b/homeassistant/components/tailscale/translations/sv.json index 5ad5b5b6db4..40780c30f16 100644 --- a/homeassistant/components/tailscale/translations/sv.json +++ b/homeassistant/components/tailscale/translations/sv.json @@ -1,15 +1,25 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "reauth_confirm": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Tailscale API-tokens \u00e4r giltiga i 90 dagar. Du kan skapa en ny Tailscale API-nyckel p\u00e5 https://login.tailscale.com/admin/settings/authkeys." }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "tailnet": "Tailnet" + }, + "description": "Denna integration \u00f6vervakar ditt Tailscale-n\u00e4tverk, den **G\u00d6R INTE** din Home Assistant tillg\u00e4nglig via Tailscale VPN. \n\n F\u00f6r att autentisera med Tailscale m\u00e5ste du skapa en API-nyckel p\u00e5 {authkeys_url} . \n\n Ett Tailnet \u00e4r namnet p\u00e5 ditt Tailscale-n\u00e4tverk. Du hittar den i det \u00f6vre v\u00e4nstra h\u00f6rnet i Tailscale Admin Panel (bredvid Tailscale-logotypen)." } } } diff --git a/homeassistant/components/tellduslive/translations/sv.json b/homeassistant/components/tellduslive/translations/sv.json index 25d33f3afdf..709100f1b57 100644 --- a/homeassistant/components/tellduslive/translations/sv.json +++ b/homeassistant/components/tellduslive/translations/sv.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", - "unknown": "Ok\u00e4nt fel intr\u00e4ffade" + "unknown": "Ok\u00e4nt fel intr\u00e4ffade", + "unknown_authorize_url_generation": "Ok\u00e4nt fel vid generering av en auktoriserad URL." }, "error": { "invalid_auth": "Ogiltig autentisering" diff --git a/homeassistant/components/tesla_wall_connector/translations/sv.json b/homeassistant/components/tesla_wall_connector/translations/sv.json new file mode 100644 index 00000000000..099c5aecafb --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "title": "Konfigurera Tesla Wall Connector" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/sv.json b/homeassistant/components/threshold/translations/sv.json index dd247031a4e..aaef886e1a6 100644 --- a/homeassistant/components/threshold/translations/sv.json +++ b/homeassistant/components/threshold/translations/sv.json @@ -5,16 +5,32 @@ }, "step": { "user": { + "data": { + "entity_id": "Ing\u00e5ngssensor", + "hysteresis": "Hysteres", + "lower": "L\u00e4gre gr\u00e4ns", + "name": "Namn", + "upper": "\u00d6vre gr\u00e4ns" + }, + "description": "Skapa en bin\u00e4r sensor som sl\u00e5s p\u00e5 och av beroende p\u00e5 v\u00e4rdet p\u00e5 en sensor \n\n Endast nedre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r mindre \u00e4n den nedre gr\u00e4nsen.\n Endast \u00f6vre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r st\u00f6rre \u00e4n den \u00f6vre gr\u00e4nsen.\n B\u00e5de undre och \u00f6vre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r inom omr\u00e5det [nedre gr\u00e4ns .. \u00f6vre gr\u00e4ns].", "title": "L\u00e4gg till gr\u00e4nsv\u00e4rdessensor" } } }, "options": { + "error": { + "need_lower_upper": "Undre och \u00f6vre gr\u00e4ns kan inte vara tomma" + }, "step": { "init": { "data": { - "lower": "Undre gr\u00e4ns" - } + "entity_id": "Ing\u00e5ngssensor", + "hysteresis": "Hysteres", + "lower": "Undre gr\u00e4ns", + "name": "Namn", + "upper": "\u00d6vre gr\u00e4ns" + }, + "description": "Endast nedre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r mindre \u00e4n den nedre gr\u00e4nsen.\n Endast \u00f6vre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r st\u00f6rre \u00e4n den \u00f6vre gr\u00e4nsen.\n B\u00e5de undre och \u00f6vre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r inom omr\u00e5det [nedre gr\u00e4ns .. \u00f6vre gr\u00e4ns]." } } }, diff --git a/homeassistant/components/tile/translations/sv.json b/homeassistant/components/tile/translations/sv.json index b3a1bc0e169..e9a1392a842 100644 --- a/homeassistant/components/tile/translations/sv.json +++ b/homeassistant/components/tile/translations/sv.json @@ -1,9 +1,19 @@ { "config": { "abort": { - "already_configured": "Konto har redan konfigurerats" + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "title": "Autentisera panelen igen" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/tod/translations/sv.json b/homeassistant/components/tod/translations/sv.json index 79e74cf3935..4441f7ed5da 100644 --- a/homeassistant/components/tod/translations/sv.json +++ b/homeassistant/components/tod/translations/sv.json @@ -1,3 +1,26 @@ { + "config": { + "step": { + "user": { + "data": { + "after_time": "P\u00e5 tid", + "before_time": "Av tid", + "name": "Namn" + }, + "description": "Skapa en bin\u00e4r sensor som sl\u00e5s p\u00e5 eller av beroende p\u00e5 tiden.", + "title": "L\u00e4gg till sensorer f\u00f6r tider p\u00e5 dagen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after_time": "P\u00e5 tid", + "before_time": "Av tid" + } + } + } + }, "title": "Tider p\u00e5 dagen sensor" } \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.sv.json b/homeassistant/components/tolo/translations/select.sv.json new file mode 100644 index 00000000000..cab1d17ca34 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.sv.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "Automatisk", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/sv.json b/homeassistant/components/tolo/translations/sv.json new file mode 100644 index 00000000000..415e3da5dbf --- /dev/null +++ b/homeassistant/components/tolo/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Ange v\u00e4rdnamnet eller IP-adressen f\u00f6r din TOLO Sauna-enhet." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.sv.json b/homeassistant/components/tomorrowio/translations/sensor.sv.json new file mode 100644 index 00000000000..04886f124dd --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.sv.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Bra", + "hazardous": "Farlig", + "moderate": "M\u00e5ttlig", + "unhealthy": "Oh\u00e4lsosam", + "unhealthy_for_sensitive_groups": "Oh\u00e4lsosamt f\u00f6r k\u00e4nsliga grupper", + "very_unhealthy": "Mycket oh\u00e4lsosamt" + }, + "tomorrowio__pollen_index": { + "high": "H\u00f6gt", + "low": "L\u00e5g", + "medium": "Medium", + "none": "Inget", + "very_high": "V\u00e4ldigt h\u00f6gt", + "very_low": "V\u00e4ldigt l\u00e5gt" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Underkylt regn", + "ice_pellets": "Hagel", + "none": "Inget", + "rain": "Regn", + "snow": "Sn\u00f6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sv.json b/homeassistant/components/tomorrowio/translations/sv.json index 15498795844..64851f5d0ae 100644 --- a/homeassistant/components/tomorrowio/translations/sv.json +++ b/homeassistant/components/tomorrowio/translations/sv.json @@ -1,11 +1,30 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel", + "rate_limited": "F\u00f6r n\u00e4rvarande \u00e4r hastigheten begr\u00e4nsad, v\u00e4nligen f\u00f6rs\u00f6k igen senare.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { "api_key": "API-nyckel", - "location": "Plats" - } + "location": "Plats", + "name": "Namn" + }, + "description": "F\u00f6r att f\u00e5 en API-nyckel, registrera dig p\u00e5 [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuter mellan NowCast-prognoser" + }, + "description": "Om du v\u00e4ljer att aktivera \"nowcast\"-prognosentiteten kan du konfigurera antalet minuter mellan varje prognos. Antalet prognoser som tillhandah\u00e5lls beror p\u00e5 antalet minuter som v\u00e4ljs mellan prognoserna.", + "title": "Uppdatera Tomorrow.io-alternativen" } } } diff --git a/homeassistant/components/toon/translations/sv.json b/homeassistant/components/toon/translations/sv.json index 95864e9bbe3..fd177e7bcfc 100644 --- a/homeassistant/components/toon/translations/sv.json +++ b/homeassistant/components/toon/translations/sv.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", "no_agreements": "Det h\u00e4r kontot har inga Toon-sk\u00e4rmar.", - "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "unknown_authorize_url_generation": "Ok\u00e4nt fel vid generering av en auktoriserad URL." }, "step": { "agreement": { diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json index fb9e0feb4e0..b06dc765a4b 100644 --- a/homeassistant/components/totalconnect/translations/sv.json +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -11,6 +11,9 @@ }, "step": { "locations": { + "data": { + "usercode": "Anv\u00e4ndarkod" + }, "description": "Ange anv\u00e4ndarkoden f\u00f6r denna anv\u00e4ndare p\u00e5 plats {location_id}", "title": "Anv\u00e4ndarkoder f\u00f6r plats" }, diff --git a/homeassistant/components/tplink/translations/sv.json b/homeassistant/components/tplink/translations/sv.json index 923647633d9..2bcc7d3137a 100644 --- a/homeassistant/components/tplink/translations/sv.json +++ b/homeassistant/components/tplink/translations/sv.json @@ -16,6 +16,12 @@ "data": { "device": "Enhet" } + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." } } } diff --git a/homeassistant/components/traccar/translations/sv.json b/homeassistant/components/traccar/translations/sv.json index b6858100aa4..0b35f9aab56 100644 --- a/homeassistant/components/traccar/translations/sv.json +++ b/homeassistant/components/traccar/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/tractive/translations/sensor.sv.json b/homeassistant/components/tractive/translations/sensor.sv.json new file mode 100644 index 00000000000..7fde66fac96 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.sv.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Rapporterar inte", + "operational": "Operativ", + "system_shutdown_user": "Anv\u00e4ndare av systemavst\u00e4ngning", + "system_startup": "Systemstart" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/sv.json b/homeassistant/components/tradfri/translations/sv.json index 69d7f7ba3c5..610cc12014c 100644 --- a/homeassistant/components/tradfri/translations/sv.json +++ b/homeassistant/components/tradfri/translations/sv.json @@ -5,6 +5,7 @@ "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r bryggan p\u00e5g\u00e5r redan." }, "error": { + "cannot_authenticate": "Det g\u00e5r inte att autentisera, \u00e4r Gateway kopplad till en annan server, t.ex. Homekit?", "cannot_connect": "Det gick inte att ansluta till gatewayen.", "invalid_key": "Misslyckades med att registrera den angivna nyckeln. Om det h\u00e4r h\u00e4nder, f\u00f6rs\u00f6k starta om gatewayen igen.", "timeout": "Timeout vid valididering av kod" diff --git a/homeassistant/components/trafikverket_weatherstation/translations/sv.json b/homeassistant/components/trafikverket_weatherstation/translations/sv.json index f4a63bb449d..f1a5b2dfb41 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/sv.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/sv.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_station": "Det gick inte att hitta en v\u00e4derstation med det angivna namnet", + "more_stations": "Hittade flera v\u00e4derstationer med det angivna namnet" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "station": "Station" } } } diff --git a/homeassistant/components/transmission/translations/sv.json b/homeassistant/components/transmission/translations/sv.json index 79b40b78ff5..859d042dbbc 100644 --- a/homeassistant/components/transmission/translations/sv.json +++ b/homeassistant/components/transmission/translations/sv.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", + "invalid_auth": "Ogiltig autentisering", "name_exists": "Namnet finns redan" }, "step": { diff --git a/homeassistant/components/tuya/translations/select.sv.json b/homeassistant/components/tuya/translations/select.sv.json index c34c40ebacd..ec08db5924b 100644 --- a/homeassistant/components/tuya/translations/select.sv.json +++ b/homeassistant/components/tuya/translations/select.sv.json @@ -1,7 +1,74 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Inaktiverad", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automatisk", + "1": "Av", + "2": "P\u00e5" + }, + "tuya__countdown": { + "1h": "1 timme", + "2h": "2 timmar", + "3h": "3 timmar", + "4h": "4 timmar", + "5h": "5 timmar", + "6h": "6 timmar", + "cancel": "Avbryt" + }, + "tuya__curtain_mode": { + "morning": "Morgon", + "night": "Natt" + }, + "tuya__curtain_motor_mode": { + "back": "Tillbaka", + "forward": "Fram\u00e5t" + }, + "tuya__decibel_sensitivity": { + "0": "L\u00e5g k\u00e4nslighet", + "1": "H\u00f6g k\u00e4nslighet" + }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, + "tuya__fingerbot_mode": { + "click": "Tryck", + "switch": "Knapp" + }, + "tuya__humidifier_level": { + "level_1": "Niv\u00e5 1", + "level_10": "Niv\u00e5 10", + "level_2": "Niv\u00e5 2", + "level_3": "Niv\u00e5 3", + "level_4": "Niv\u00e5 4", + "level_5": "Niv\u00e5 5", + "level_6": "Niv\u00e5 6", + "level_7": "Niv\u00e5 7", + "level_8": "Niv\u00e5 8", + "level_9": "Niv\u00e5 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Hum\u00f6r 1", + "2": "Hum\u00f6r 2", + "3": "Hum\u00f6r 3", + "4": "Hum\u00f6r 4", + "5": "Hum\u00f6r 5" + }, "tuya__humidifier_spray_mode": { - "humidity": "Luftfuktighet" + "auto": "Automatisk", + "health": "H\u00e4lsa", + "humidity": "Luftfuktighet", + "sleep": "S\u00f6mnl\u00e4ge", + "work": "Arbetsl\u00e4ge" + }, + "tuya__ipc_work_mode": { + "0": "L\u00e5geffektl\u00e4ge", + "1": "Kontinuerligt arbetsl\u00e4ge" }, "tuya__led_type": { "halogen": "Halogen", @@ -13,6 +80,15 @@ "pos": "Ange omkopplarens plats", "relay": "Indikera p\u00e5/av-l\u00e4ge" }, + "tuya__motion_sensitivity": { + "0": "L\u00e5g k\u00e4nslighet", + "1": "Medium k\u00e4nslighet", + "2": "H\u00f6g k\u00e4nslighet" + }, + "tuya__record_mode": { + "1": "Spela bara in h\u00e4ndelser", + "2": "Kontinuerlig inspelning" + }, "tuya__relay_status": { "last": "Kom ih\u00e5g senaste tillst\u00e5ndet", "memory": "Kom ih\u00e5g senaste tillst\u00e5ndet", @@ -21,8 +97,37 @@ "power_off": "Av", "power_on": "P\u00e5" }, + "tuya__vacuum_cistern": { + "closed": "St\u00e4ngd", + "high": "H\u00f6gt", + "low": "L\u00e5gt", + "middle": "Mitten" + }, + "tuya__vacuum_collection": { + "large": "Stor", + "middle": "Mitten", + "small": "Liten" + }, "tuya__vacuum_mode": { - "standby": "Standby" + "bow": "B\u00e5ge", + "chargego": "\u00c5terg\u00e5 till dockan", + "left_bow": "B\u00e5ge v\u00e4nster", + "left_spiral": "Spiral v\u00e4nster", + "mop": "Moppa", + "part": "Del", + "partial_bow": "B\u00e5ge delvis", + "pick_zone": "V\u00e4lj zon", + "point": "Punkt", + "pose": "Pose", + "random": "Slumpm\u00e4ssig", + "right_bow": "B\u00e5ge \u00e5t h\u00f6ger", + "right_spiral": "Spiral h\u00f6ger", + "single": "Enkel", + "smart": "Smart", + "spiral": "Spiral", + "standby": "Standby", + "wall_follow": "F\u00f6lj v\u00e4ggen", + "zone": "Zon" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.sv.json b/homeassistant/components/tuya/translations/sensor.sv.json index a3dd5ff28df..8bc39f828d5 100644 --- a/homeassistant/components/tuya/translations/sensor.sv.json +++ b/homeassistant/components/tuya/translations/sensor.sv.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Bra", + "great": "Mycket bra", + "mild": "Mild", + "severe": "Allvarlig" + }, "tuya__status": { "boiling_temp": "Koktemperatur", "cooling": "Kyler", diff --git a/homeassistant/components/tuya/translations/sv.json b/homeassistant/components/tuya/translations/sv.json index 1add4cb2e2d..665f781d726 100644 --- a/homeassistant/components/tuya/translations/sv.json +++ b/homeassistant/components/tuya/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_auth": "Ogiltig autentisering", "login_error": "Inloggningsfel ( {code} ): {msg}" }, "step": { diff --git a/homeassistant/components/twilio/translations/sv.json b/homeassistant/components/twilio/translations/sv.json index 8bb70f9cd64..c4e94b50741 100644 --- a/homeassistant/components/twilio/translations/sv.json +++ b/homeassistant/components/twilio/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/twinkly/translations/sv.json b/homeassistant/components/twinkly/translations/sv.json index 9b541d7ceac..e997d3520bf 100644 --- a/homeassistant/components/twinkly/translations/sv.json +++ b/homeassistant/components/twinkly/translations/sv.json @@ -7,6 +7,9 @@ "cannot_connect": "Det gick inte att ansluta." }, "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} - {model} ({host})?" + }, "user": { "data": { "host": "V\u00e4rd" diff --git a/homeassistant/components/ukraine_alarm/translations/sv.json b/homeassistant/components/ukraine_alarm/translations/sv.json index 3e8c6255251..433b8d25211 100644 --- a/homeassistant/components/ukraine_alarm/translations/sv.json +++ b/homeassistant/components/ukraine_alarm/translations/sv.json @@ -5,6 +5,7 @@ "cannot_connect": "Det gick inte att ansluta.", "max_regions": "Max 5 regioner kan konfigureras", "rate_limit": "F\u00f6r mycket f\u00f6rfr\u00e5gningar", + "timeout": "Timeout uppr\u00e4ttar anslutning", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index cdc66ea5126..8ab224c7ab7 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -67,7 +67,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter" + "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter", + "allow_uptime_sensors": "Upptidssensorer f\u00f6r n\u00e4tverksklienter" }, "description": "Konfigurera statistiksensorer", "title": "UniFi-inst\u00e4llningar 2/3" diff --git a/homeassistant/components/unifiprotect/translations/sv.json b/homeassistant/components/unifiprotect/translations/sv.json index 702dcbecdb7..81afcc7c14b 100644 --- a/homeassistant/components/unifiprotect/translations/sv.json +++ b/homeassistant/components/unifiprotect/translations/sv.json @@ -1,22 +1,56 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "discovery_started": "Uppt\u00e4ckten startades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "protect_version": "Minsta n\u00f6dv\u00e4ndiga version \u00e4r v1.20.0. Uppgradera UniFi Protect och f\u00f6rs\u00f6k sedan igen." + }, + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" }, + "description": "Vill du konfigurera {name} ({ip_address})? Du beh\u00f6ver en lokal anv\u00e4ndare skapad i din UniFi OS-konsol f\u00f6r att logga in med. Ubiquiti Cloud-anv\u00e4ndare kommer inte att fungera. F\u00f6r mer information: {local_user_documentation_url}", "title": "UniFi Protect uppt\u00e4ckt" }, "reauth_confirm": { "data": { + "host": "IP/v\u00e4rd f\u00f6r UniFi Protect Server", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "UniFi Protect Reauth" }, "user": { "data": { - "username": "Anv\u00e4ndarnamn" + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" }, - "description": "Du beh\u00f6ver en lokal anv\u00e4ndare skapad i din UniFi OS-konsol f\u00f6r att logga in med. Ubiquiti Cloud-anv\u00e4ndare kommer inte att fungera. F\u00f6r mer information: {local_user_documentation_url}" + "description": "Du beh\u00f6ver en lokal anv\u00e4ndare skapad i din UniFi OS-konsol f\u00f6r att logga in med. Ubiquiti Cloud-anv\u00e4ndare kommer inte att fungera. F\u00f6r mer information: {local_user_documentation_url}", + "title": "Installation av UniFi Protect" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "Realtidsm\u00e4tningar (VARNING: \u00d6kar CPU-anv\u00e4ndningen avsev\u00e4rt)", + "disable_rtsp": "Inaktivera RTSP-str\u00f6mmen", + "override_connection_host": "\u00c5sidos\u00e4tt anslutningsv\u00e4rd" + }, + "description": "Alternativet Realtidsm\u00e4tv\u00e4rden b\u00f6r endast aktiveras om du har aktiverat diagnostiksensorerna och vill att de ska uppdateras i realtid. Om det inte \u00e4r aktiverat kommer de bara att uppdateras en g\u00e5ng var 15:e minut.", + "title": "UniFi Protect-alternativ" } } } diff --git a/homeassistant/components/update/translations/sv.json b/homeassistant/components/update/translations/sv.json index 53ecdb21377..c68c6d0f1c3 100644 --- a/homeassistant/components/update/translations/sv.json +++ b/homeassistant/components/update/translations/sv.json @@ -5,5 +5,6 @@ "turned_off": "{entity_name} blev uppdaterad", "turned_on": "{entity_name} har en uppdatering tillg\u00e4nglig" } - } + }, + "title": "Uppdatera" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/sv.json b/homeassistant/components/uptime/translations/sv.json new file mode 100644 index 00000000000..0d9e03ec575 --- /dev/null +++ b/homeassistant/components/uptime/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, + "title": "Upptid" +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.sv.json b/homeassistant/components/uptimerobot/translations/sensor.sv.json new file mode 100644 index 00000000000..b0d99120a3c --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.sv.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Nere", + "not_checked_yet": "Inte kontrollerat \u00e4n", + "pause": "Pausa", + "seems_down": "Verkar nere", + "up": "Uppe" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sv.json b/homeassistant/components/uptimerobot/translations/sv.json index d9db1fc1deb..b0bb7b6e72a 100644 --- a/homeassistant/components/uptimerobot/translations/sv.json +++ b/homeassistant/components/uptimerobot/translations/sv.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_api_key": "Ogiltig API-nyckel", + "not_main_key": "Fel API-nyckeltyp uppt\u00e4ckt, anv\u00e4nd \"huvud\" API-nyckeln", "reauth_failed_matching_account": "API-nyckeln du angav matchar inte konto-id:t f\u00f6r befintlig konfiguration.", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/vallox/translations/sv.json b/homeassistant/components/vallox/translations/sv.json new file mode 100644 index 00000000000..b3e55ae100f --- /dev/null +++ b/homeassistant/components/vallox/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/sv.json b/homeassistant/components/vera/translations/sv.json index 8c7335c2b13..5e28c1db392 100644 --- a/homeassistant/components/vera/translations/sv.json +++ b/homeassistant/components/vera/translations/sv.json @@ -9,6 +9,9 @@ "exclude": "Vera enhets-id att utesluta fr\u00e5n Home Assistant.", "lights": "Vera switch enhets-ID att behandla som lampor i Home Assistant.", "vera_controller_url": "Controller-URL" + }, + "data_description": { + "vera_controller_url": "Det ska se ut s\u00e5 h\u00e4r: http://192.168.1.161:3480" } } } diff --git a/homeassistant/components/version/translations/sv.json b/homeassistant/components/version/translations/sv.json new file mode 100644 index 00000000000..3db6f23dbfc --- /dev/null +++ b/homeassistant/components/version/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "version_source": "Versionsk\u00e4lla" + }, + "description": "V\u00e4lj k\u00e4llan du vill sp\u00e5ra versioner fr\u00e5n", + "title": "V\u00e4lj installationstyp" + }, + "version_source": { + "data": { + "beta": "Inkludera betaversioner", + "board": "Vilket kort som ska sp\u00e5ras", + "channel": "Vilken kanal ska sp\u00e5ras", + "image": "Vilken bild ska sp\u00e5ras" + }, + "description": "Konfigurera {version_source} versionssp\u00e5rning", + "title": "Konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/sv.json b/homeassistant/components/vicare/translations/sv.json index 80588906063..cd9c40f60c1 100644 --- a/homeassistant/components/vicare/translations/sv.json +++ b/homeassistant/components/vicare/translations/sv.json @@ -1,11 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "client_id": "API-nyckel", + "heating_type": "V\u00e4rmel\u00e4ge", + "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "description": "Konfigurera ViCare-integration. F\u00f6r att generera API-nyckel g\u00e5 till https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vizio/translations/sv.json b/homeassistant/components/vizio/translations/sv.json index dc1f5514d80..62e74a67ed1 100644 --- a/homeassistant/components/vizio/translations/sv.json +++ b/homeassistant/components/vizio/translations/sv.json @@ -7,6 +7,7 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", + "complete_pairing_failed": "Det gick inte att slutf\u00f6ra ihopkopplingen. Se till att PIN-koden du angav \u00e4r korrekt och att TV:n fortfarande \u00e4r str\u00f6msatt och ansluten till n\u00e4tverket innan du skickar in igen.", "existing_config_entry_found": "En befintlig St\u00e4ll in Vizio SmartCast-klient konfigurationspost med samma serienummer har redan konfigurerats. Du m\u00e5ste ta bort den befintliga posten f\u00f6r att konfigurera denna." }, "step": { diff --git a/homeassistant/components/vlc_telnet/translations/sv.json b/homeassistant/components/vlc_telnet/translations/sv.json index f8a77ff3086..bc13817a21a 100644 --- a/homeassistant/components/vlc_telnet/translations/sv.json +++ b/homeassistant/components/vlc_telnet/translations/sv.json @@ -1,17 +1,34 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Vill du ansluta till till\u00e4gget {addon} ?" }, + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ange r\u00e4tt l\u00f6senord f\u00f6r v\u00e4rden: {host}" + }, "user": { "data": { - "host": "V\u00e4rd" + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port" } } } diff --git a/homeassistant/components/vulcan/translations/id.json b/homeassistant/components/vulcan/translations/id.json index 6aa1e0d7fa6..eab39d53811 100644 --- a/homeassistant/components/vulcan/translations/id.json +++ b/homeassistant/components/vulcan/translations/id.json @@ -3,7 +3,7 @@ "abort": { "all_student_already_configured": "Semua siswa telah ditambahkan.", "already_configured": "Siswa tersebut telah ditambahkan.", - "no_matching_entries": "Tidak ditemukan entri yang cocok, harap gunakan akun lain atau hapus integrasi dengan siswa yang ketinggalan zaman..", + "no_matching_entries": "Tidak ditemukan entri yang cocok. Gunakan akun lain atau hapus integrasi dengan data siswa yang usang.", "reauth_successful": "Otorisasi ulang berhasil" }, "error": { diff --git a/homeassistant/components/vulcan/translations/sv.json b/homeassistant/components/vulcan/translations/sv.json index cbe2d7174b1..29bedcf709c 100644 --- a/homeassistant/components/vulcan/translations/sv.json +++ b/homeassistant/components/vulcan/translations/sv.json @@ -7,6 +7,8 @@ "reauth_successful": "Reautentisering lyckades" }, "error": { + "cannot_connect": "Anslutningsfel - kontrollera din internetanslutning", + "expired_credentials": "Utg\u00e5ngna uppgifter - v\u00e4nligen skapa nya p\u00e5 Vulcans mobilapps registreringssida", "expired_token": "Token som har upph\u00f6rt att g\u00e4lla \u2013 generera en ny token", "invalid_pin": "Ogiltig pin", "invalid_symbol": "Ogiltig symbol", diff --git a/homeassistant/components/watttime/translations/sv.json b/homeassistant/components/watttime/translations/sv.json index f8a3d0b7fcf..6df3193bbea 100644 --- a/homeassistant/components/watttime/translations/sv.json +++ b/homeassistant/components/watttime/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "invalid_auth": "Ogiltig autentisering", @@ -13,7 +14,8 @@ "data": { "latitude": "Latitud", "longitude": "Longitud" - } + }, + "description": "Ange latitud och longitud f\u00f6r att \u00f6vervaka:" }, "location": { "data": { @@ -21,6 +23,13 @@ }, "description": "V\u00e4lj en plats att \u00f6vervaka:" }, + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ange l\u00f6senordet f\u00f6r {anv\u00e4ndarnamn} p\u00e5 nytt:", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/waze_travel_time/translations/sv.json b/homeassistant/components/waze_travel_time/translations/sv.json index 05407519a87..4b3da3f69d8 100644 --- a/homeassistant/components/waze_travel_time/translations/sv.json +++ b/homeassistant/components/waze_travel_time/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Kunde inte ansluta" }, @@ -10,7 +13,8 @@ "name": "Namn", "origin": "Ursprung", "region": "Region" - } + }, + "description": "F\u00f6r Ursprung och Destination anger du adressen eller GPS-koordinaterna f\u00f6r platsen (GPS-koordinaterna m\u00e5ste avgr\u00e4nsas med kommatecken). Du kan ocks\u00e5 ange ett enhets-id som tillhandah\u00e5ller denna information i dess tillst\u00e5nd, ett enhets-ID med latitud- och longitudattribut eller zonv\u00e4nligt namn." } } }, @@ -18,10 +22,18 @@ "step": { "init": { "data": { + "avoid_ferries": "Undvik f\u00e4rjor?", + "avoid_subscription_roads": "Undvik v\u00e4gar som beh\u00f6ver en vinjett / prenumeration?", + "avoid_toll_roads": "Undvika avgiftsbelagda v\u00e4gar?", + "excl_filter": "Delstr\u00e4ng INTE i beskrivning av vald rutt", + "incl_filter": "Delstr\u00e4ng i beskrivningen av den valda rutten", + "realtime": "Restid i realtid?", "units": "Enheter", "vehicle_type": "Fordonstyp" - } + }, + "description": "Med \"delstr\u00e4ng\"-ing\u00e5ngarna kan du tvinga integrationen att anv\u00e4nda en viss rutt eller undvika en viss rutt i dess tidsreseber\u00e4kning." } } - } + }, + "title": "Waze restid" } \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/sv.json b/homeassistant/components/webostv/translations/sv.json new file mode 100644 index 00000000000..dcd01faf6a8 --- /dev/null +++ b/homeassistant/components/webostv/translations/sv.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "error_pairing": "Ansluten till LG webOS TV men inte ihopkopplad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta. Sl\u00e5 p\u00e5 din TV eller kontrollera ip-adressen" + }, + "flow_title": "LG webOS Smart TV", + "step": { + "pairing": { + "description": "Klicka p\u00e5 skicka och acceptera parningsf\u00f6rfr\u00e5gan p\u00e5 din TV. \n\n ![Image](/static/images/config_webos.png)", + "title": "Koppling av webOS TV" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + }, + "description": "Sl\u00e5 p\u00e5 TV:n, fyll i f\u00f6ljande f\u00e4lt och klicka p\u00e5 skicka.", + "title": "Anslut till webOS TV" + } + } + }, + "device_automation": { + "trigger_type": { + "webostv.turn_on": "Enheten uppmanas att sl\u00e5s p\u00e5" + } + }, + "options": { + "error": { + "cannot_retrieve": "Det gick inte att h\u00e4mta k\u00e4lllistan. Se till att enheten \u00e4r p\u00e5slagen", + "script_not_found": "Skriptet hittades inte" + }, + "step": { + "init": { + "data": { + "sources": "K\u00e4lllista" + }, + "description": "V\u00e4lj aktiverade k\u00e4llor", + "title": "Alternativ f\u00f6r webOS Smart TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/sv.json b/homeassistant/components/whirlpool/translations/sv.json index f7461922566..72e401bd4f0 100644 --- a/homeassistant/components/whirlpool/translations/sv.json +++ b/homeassistant/components/whirlpool/translations/sv.json @@ -1,11 +1,14 @@ { "config": { "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/whois/translations/sv.json b/homeassistant/components/whois/translations/sv.json index c5d4a425a5b..524f5261475 100644 --- a/homeassistant/components/whois/translations/sv.json +++ b/homeassistant/components/whois/translations/sv.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "unexpected_response": "Ov\u00e4ntat svar fr\u00e5n whois server", + "unknown_date_format": "Ok\u00e4nt datumformat i whois-serverns svar", + "unknown_tld": "Den givna toppdom\u00e4nen \u00e4r ok\u00e4nd eller inte tillg\u00e4nglig f\u00f6r denna integration", + "whois_command_failed": "Whois-kommandot misslyckades: kunde inte h\u00e4mta whois-information" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wiffi/translations/sv.json b/homeassistant/components/wiffi/translations/sv.json index 1fdd2ec4aa7..8916c3a34cd 100644 --- a/homeassistant/components/wiffi/translations/sv.json +++ b/homeassistant/components/wiffi/translations/sv.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "addr_in_use": "Serverporten \u00e4r upptagen." + "addr_in_use": "Serverporten \u00e4r upptagen.", + "already_configured": "Serverporten \u00e4r redan konfigurerad.", + "start_server_failed": "Det gick inte att starta servern." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "title": "Konfigurera TCP-server f\u00f6r WIFFI-enheter" + } } }, "options": { diff --git a/homeassistant/components/wilight/translations/sv.json b/homeassistant/components/wilight/translations/sv.json new file mode 100644 index 00000000000..5e2fe1c7707 --- /dev/null +++ b/homeassistant/components/wilight/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "not_supported_device": "Denna WiLight st\u00f6ds f\u00f6r n\u00e4rvarande inte", + "not_wilight_device": "Denna enhet \u00e4r inte en WiLight" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "F\u00f6ljande komponenter st\u00f6ds: {components}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/sv.json b/homeassistant/components/wiz/translations/sv.json new file mode 100644 index 00000000000..05fe0b3ece1 --- /dev/null +++ b/homeassistant/components/wiz/translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "error": { + "bulb_time_out": "Kan inte ansluta till lampan. Kanske \u00e4r lampan offline eller s\u00e5 har du angett fel IP-nummer. Sl\u00e5 p\u00e5 lampan och f\u00f6rs\u00f6k igen!", + "cannot_connect": "Det gick inte att ansluta.", + "no_ip": "Inte en giltig IP-adress.", + "no_wiz_light": "Lampan kan inte anslutas via WiZ Platform-integration.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({host})", + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, + "user": { + "data": { + "host": "IP-adress" + }, + "description": "Om du l\u00e4mnar IP-adressen tom kommer uppt\u00e4ckten att anv\u00e4ndas f\u00f6r att hitta enheter." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.sv.json b/homeassistant/components/wolflink/translations/sensor.sv.json index faf7a2b9673..7a10945fc91 100644 --- a/homeassistant/components/wolflink/translations/sensor.sv.json +++ b/homeassistant/components/wolflink/translations/sensor.sv.json @@ -11,6 +11,7 @@ "at_frostschutz": "OT frostskydd", "aus": "Inaktiverad", "auto": "Automatiskt", + "auto_off_cool": "AutoOffCool", "auto_on_cool": "AutoOnCool", "automatik_aus": "Automatisk avst\u00e4ngning", "automatik_ein": "Automatiskt p\u00e5", @@ -18,6 +19,7 @@ "betrieb_ohne_brenner": "Arbetar utan br\u00e4nnare", "cooling": "Kyler", "deaktiviert": "Inaktiv", + "dhw_prior": "DHWPrior", "eco": "Eco", "ein": "Aktiverad", "estrichtrocknung": "Avj\u00e4mningstorkning", @@ -72,6 +74,7 @@ "test": "Test", "tpw": "TPW", "urlaubsmodus": "Semesterl\u00e4ge", + "ventilprufung": "Test av ventiler", "vorspulen": "Ing\u00e5ngssk\u00f6ljning", "warmwasser": "Varmvatten", "warmwasser_schnellstart": "Snabbstart f\u00f6r varmvatten", diff --git a/homeassistant/components/xiaomi_ble/translations/sv.json b/homeassistant/components/xiaomi_ble/translations/sv.json index 68373aca702..85dd1bdf557 100644 --- a/homeassistant/components/xiaomi_ble/translations/sv.json +++ b/homeassistant/components/xiaomi_ble/translations/sv.json @@ -6,13 +6,22 @@ "decryption_failed": "Den tillhandah\u00e5llna bindningsnyckeln fungerade inte, sensordata kunde inte dekrypteras. Kontrollera den och f\u00f6rs\u00f6k igen.", "expected_24_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 24 tecken.", "expected_32_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 32 tecken.", - "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "decryption_failed": "Den tillhandah\u00e5llna bindningsnyckeln fungerade inte, sensordata kunde inte dekrypteras. Kontrollera den och f\u00f6rs\u00f6k igen.", + "expected_24_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 24 tecken.", + "expected_32_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 32 tecken." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Vill du s\u00e4tta upp {name}?" }, + "confirm_slow": { + "description": "Det har inte s\u00e4nts fr\u00e5n den h\u00e4r enheten under den senaste minuten s\u00e5 vi \u00e4r inte s\u00e4kra p\u00e5 om den h\u00e4r enheten anv\u00e4nder kryptering eller inte. Det kan bero p\u00e5 att enheten anv\u00e4nder ett l\u00e5ngsamt s\u00e4ndningsintervall. Bekr\u00e4fta att l\u00e4gga till den h\u00e4r enheten \u00e4nd\u00e5, n\u00e4sta g\u00e5ng en s\u00e4ndning tas emot kommer du att uppmanas att ange dess bindnyckel om det beh\u00f6vs." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindningsnyckel" @@ -25,6 +34,9 @@ }, "description": "De sensordata som s\u00e4nds av sensorn \u00e4r krypterade. F\u00f6r att dekryptera dem beh\u00f6ver vi en hexadecimal bindningsnyckel med 24 tecken." }, + "slow_confirm": { + "description": "Det har inte s\u00e4nts fr\u00e5n den h\u00e4r enheten under den senaste minuten s\u00e5 vi \u00e4r inte s\u00e4kra p\u00e5 om den h\u00e4r enheten anv\u00e4nder kryptering eller inte. Det kan bero p\u00e5 att enheten anv\u00e4nder ett l\u00e5ngsamt s\u00e4ndningsintervall. Bekr\u00e4fta att l\u00e4gga till den h\u00e4r enheten \u00e4nd\u00e5, n\u00e4sta g\u00e5ng en s\u00e4ndning tas emot kommer du att uppmanas att ange dess bindnyckel om det beh\u00f6vs." + }, "user": { "data": { "address": "Enhet" diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json index 37452a61e79..83e3ba16b32 100644 --- a/homeassistant/components/xiaomi_miio/translations/sv.json +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -12,7 +12,8 @@ "cloud_credentials_incomplete": "Cloud-uppgifterna \u00e4r ofullst\u00e4ndiga, v\u00e4nligen fyll i anv\u00e4ndarnamn, l\u00f6senord och land", "cloud_login_error": "Kunde inte logga in p\u00e5 Xiaomi Miio Cloud, kontrollera anv\u00e4ndaruppgifterna.", "cloud_no_devices": "Inga enheter hittades i detta Xiaomi Miio molnkonto.", - "unknown_device": "Enhetsmodellen \u00e4r inte k\u00e4nd, kan inte st\u00e4lla in enheten med hj\u00e4lp av konfigurationsfl\u00f6de." + "unknown_device": "Enhetsmodellen \u00e4r inte k\u00e4nd, kan inte st\u00e4lla in enheten med hj\u00e4lp av konfigurationsfl\u00f6de.", + "wrong_token": "Kontrollsummafel, fel token" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/yale_smart_alarm/translations/sv.json b/homeassistant/components/yale_smart_alarm/translations/sv.json index 8a60ea1a5dc..316c0587b3e 100644 --- a/homeassistant/components/yale_smart_alarm/translations/sv.json +++ b/homeassistant/components/yale_smart_alarm/translations/sv.json @@ -1,16 +1,43 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "reauth_confirm": { "data": { + "area_id": "Omr\u00e5des-ID", + "name": "Namn", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } }, "user": { "data": { + "area_id": "Omr\u00e5des-ID", + "name": "Namn", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } } + }, + "options": { + "error": { + "code_format_mismatch": "Koden matchar inte det antal siffror som kr\u00e4vs" + }, + "step": { + "init": { + "data": { + "code": "Standardkod f\u00f6r l\u00e5s, anv\u00e4nds om inget anges", + "lock_code_digits": "Antal siffror i PIN-kod f\u00f6r l\u00e5s" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.sv.json b/homeassistant/components/yamaha_musiccast/translations/select.sv.json new file mode 100644 index 00000000000..2c1897b9903 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.sv.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Automatiskt" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Automatiskt", + "bypass": "F\u00f6rbikoppling", + "manual": "Manuell" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Ljudsynkronisering", + "audio_sync_off": "Ljudsynkronisering av", + "audio_sync_on": "Ljudsynkronisering p\u00e5", + "balanced": "Balanserad", + "lip_sync": "L\u00e4ppsynkronisering" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Komprimerad", + "uncompressed": "Okomprimerad" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Hastighet", + "stability": "Stabilitet", + "standard": "Standard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minuter", + "30 min": "30 minuter", + "60 min": "60 minuter", + "90 min": "90 minuter", + "off": "Av" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Automatiskt", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x-spel", + "dolby_pl2x_movie": "Dolby ProLogic 2x film", + "dolby_pl2x_music": "Dolby ProLogic 2x Musik", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Musik", + "dts_neural_x": "DTS Neural:X", + "toggle": "V\u00e4xla" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Automatiskt", + "bypass": "F\u00f6rbikoppling", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/sv.json b/homeassistant/components/yeelight/translations/sv.json index 7a8780c0bf1..18b5f3ce4a9 100644 --- a/homeassistant/components/yeelight/translations/sv.json +++ b/homeassistant/components/yeelight/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" }, "error": { "cannot_connect": "Det gick inte att ansluta." diff --git a/homeassistant/components/youless/translations/sv.json b/homeassistant/components/youless/translations/sv.json new file mode 100644 index 00000000000..45224016263 --- /dev/null +++ b/homeassistant/components/youless/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/sv.json b/homeassistant/components/zerproc/translations/sv.json new file mode 100644 index 00000000000..18a80850e45 --- /dev/null +++ b/homeassistant/components/zerproc/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index ee189ef1463..57be69d4f9f 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -47,6 +47,8 @@ }, "zha_options": { "always_prefer_xy_color_mode": "F\u00f6redrar alltid XY-f\u00e4rgl\u00e4ge", + "consider_unavailable_battery": "\u00d6verv\u00e4g att batteridrivna enheter inte \u00e4r tillg\u00e4ngliga efter (sekunder)", + "consider_unavailable_mains": "Anse att n\u00e4tdrivna enheter inte \u00e4r tillg\u00e4ngliga efter (sekunder)", "default_light_transition": "Standard ljus\u00f6verg\u00e5ngstid (sekunder)", "enable_identify_on_join": "Aktivera identifieringseffekt n\u00e4r enheter ansluter till n\u00e4tverket", "enhanced_light_transition": "Aktivera f\u00f6rb\u00e4ttrad ljusf\u00e4rg/temperatur\u00f6verg\u00e5ng fr\u00e5n ett avst\u00e4ngt l\u00e4ge", diff --git a/homeassistant/components/zodiac/translations/sensor.sv.json b/homeassistant/components/zodiac/translations/sensor.sv.json new file mode 100644 index 00000000000..ca874c92c00 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.sv.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Vattumannen", + "aries": "V\u00e4duren", + "cancer": "Kr\u00e4ftan", + "capricorn": "Stenbocken", + "gemini": "Tvillingarna", + "leo": "Lejonet", + "libra": "V\u00e5gen", + "pisces": "Fiskarna", + "sagittarius": "Skytten", + "scorpio": "Skorpionen", + "taurus": "Oxen", + "virgo": "Jungfrun" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/sv.json b/homeassistant/components/zoneminder/translations/sv.json index f3e7d2891bd..07a162a321f 100644 --- a/homeassistant/components/zoneminder/translations/sv.json +++ b/homeassistant/components/zoneminder/translations/sv.json @@ -2,21 +2,32 @@ "config": { "abort": { "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt.", - "connection_error": "Det gick inte att ansluta till en ZoneMinder-server." + "cannot_connect": "Det gick inte att ansluta.", + "connection_error": "Det gick inte att ansluta till en ZoneMinder-server.", + "invalid_auth": "Ogiltig autentisering" }, "create_entry": { "default": "ZoneMinder-server har lagts till." }, "error": { "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt.", - "connection_error": "Det gick inte att ansluta till en ZoneMinder-server." + "cannot_connect": "Det gick inte att ansluta.", + "connection_error": "Det gick inte att ansluta till en ZoneMinder-server.", + "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "ZoneMinder", "step": { "user": { "data": { + "host": "V\u00e4rd och port (ex 10.10.0.4:8010)", + "password": "L\u00f6senord", + "path": "ZM-s\u00f6kv\u00e4g", + "path_zms": "ZMS-s\u00f6kv\u00e4g", + "ssl": "Anv\u00e4nd ett SSL certifikat", "username": "Anv\u00e4ndarnamn", "verify_ssl": "Verifiera SSL-certifikat" - } + }, + "title": "L\u00e4gg till ZoneMinder Server." } } } diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index 65ddbb3dce8..b619c54026b 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -1,29 +1,66 @@ { "config": { "abort": { + "addon_get_discovery_info_failed": "Det gick inte att h\u00e4mta Z-Wave JS-till\u00e4ggsuppt\u00e4cktsinformation.", + "addon_info_failed": "Det gick inte att h\u00e4mta Z-Wave JS-till\u00e4ggsinformation.", + "addon_install_failed": "Det gick inte att installera Z-Wave JS-till\u00e4gget.", + "addon_set_config_failed": "Det gick inte att st\u00e4lla in Z-Wave JS-konfiguration.", + "addon_start_failed": "Det gick inte att starta Z-Wave JS-till\u00e4gget.", "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", "discovery_requires_supervisor": "Uppt\u00e4ckt kr\u00e4ver \u00f6vervakaren.", "not_zwave_device": "Uppt\u00e4ckt enhet \u00e4r inte en Z-Wave-enhet." }, "error": { + "addon_start_failed": "Det gick inte att starta Z-Wave JS-till\u00e4gget. Kontrollera konfigurationen.", "cannot_connect": "Det gick inte att ansluta.", "invalid_ws_url": "Ogiltig websocket-URL", "unknown": "Ov\u00e4ntat fel" }, "flow_title": "{name}", + "progress": { + "install_addon": "V\u00e4nta medan installationen av Z-Wave JS-till\u00e4gget slutf\u00f6rs. Detta kan ta flera minuter.", + "start_addon": "V\u00e4nta medan Z-Wave JS-till\u00e4ggsstarten slutf\u00f6rs. Detta kan ta n\u00e5gra sekunder." + }, "step": { "configure_addon": { "data": { "s0_legacy_key": "S0-nyckel (\u00e4ldre)", "s2_access_control_key": "S2-\u00e5tkomstkontrollnyckel", "s2_authenticated_key": "S2 Autentiserad nyckel", - "s2_unauthenticated_key": "S2 oautentiserad nyckel" + "s2_unauthenticated_key": "S2 oautentiserad nyckel", + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "Till\u00e4gget genererar s\u00e4kerhetsnycklar om dessa f\u00e4lt l\u00e4mnas tomma.", + "title": "G\u00e5 in i konfigurationen f\u00f6r till\u00e4gget Z-Wave JS" + }, + "hassio_confirm": { + "title": "Konfigurera Z-Wave JS-integration med till\u00e4gget Z-Wave JS" + }, + "install_addon": { + "title": "Z-Wave JS-till\u00e4ggsinstallationen har startat" + }, + "manual": { + "data": { + "url": "URL" } }, + "on_supervisor": { + "data": { + "use_addon": "Anv\u00e4nd Z-Wave JS Supervisor-till\u00e4gget" + }, + "description": "Vill du anv\u00e4nda Z-Wave JS Supervisor-till\u00e4gget?", + "title": "V\u00e4lj anslutningsmetod" + }, + "start_addon": { + "title": "Z-Wave JS-till\u00e4gget startar." + }, "usb_confirm": { "description": "Vill du konfigurera {name} med Z-Wave JS till\u00e4gget?" }, "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Z-Wave JS Server med hem-ID {home_id} finns p\u00e5 {url} till Home Assistant?", "title": "Uppt\u00e4ckte Z-Wave JS Server" } } @@ -56,7 +93,11 @@ }, "options": { "abort": { + "addon_get_discovery_info_failed": "Det gick inte att h\u00e4mta Z-Wave JS-till\u00e4ggsuppt\u00e4cktsinformation.", + "addon_info_failed": "Det gick inte att h\u00e4mta Z-Wave JS-till\u00e4ggsinformation.", "addon_install_failed": "Det gick inte att installera Z-Wave JS-till\u00e4gget.", + "addon_set_config_failed": "Det gick inte att st\u00e4lla in Z-Wave JS-konfiguration.", + "addon_start_failed": "Det gick inte att starta Z-Wave JS-till\u00e4gget.", "already_configured": "Enheten \u00e4r redan konfigurerad", "cannot_connect": "Det gick inte att ansluta.", "different_device": "Den anslutna USB-enheten \u00e4r inte densamma som tidigare konfigurerats f\u00f6r den h\u00e4r konfigurationsposten. Skapa ist\u00e4llet en ny konfigurationspost f\u00f6r den nya enheten." @@ -80,7 +121,9 @@ "s2_authenticated_key": "S2 Autentiserad nyckel", "s2_unauthenticated_key": "S2 oautentiserad nyckel", "usb_path": "USB-enhetens s\u00f6kv\u00e4g" - } + }, + "description": "Till\u00e4gget genererar s\u00e4kerhetsnycklar om dessa f\u00e4lt l\u00e4mnas tomma.", + "title": "G\u00e5 in i konfigurationen f\u00f6r till\u00e4gget Z-Wave JS" }, "install_addon": { "title": "Z-Wave JS-till\u00e4ggsinstallationen har startat" @@ -91,7 +134,14 @@ } }, "on_supervisor": { + "data": { + "use_addon": "Anv\u00e4nd Z-Wave JS Supervisor-till\u00e4gget" + }, + "description": "Vill du anv\u00e4nda Z-Wave JS Supervisor-till\u00e4gget?", "title": "V\u00e4lj anslutningsmetod" + }, + "start_addon": { + "title": "Z-Wave JS-till\u00e4gget startar." } } } diff --git a/homeassistant/components/zwave_me/translations/sv.json b/homeassistant/components/zwave_me/translations/sv.json new file mode 100644 index 00000000000..3012a7f3ef6 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "no_valid_uuid_set": "Ingen giltig UUID-upps\u00e4ttning" + }, + "error": { + "no_valid_uuid_set": "Ingen giltig UUID-upps\u00e4ttning" + }, + "step": { + "user": { + "data": { + "token": "API Token", + "url": "URL" + }, + "description": "Mata in IP-adress med port och \u00e5tkomsttoken f\u00f6r Z-Way-servern. F\u00f6r att f\u00e5 token, g\u00e5 till Z-Way-anv\u00e4ndargr\u00e4nssnittet Smart Home UI > Meny > Inst\u00e4llningar > Anv\u00e4ndare > Administrat\u00f6r > API-token. \n\n Exempel p\u00e5 anslutning till Z-Way i det lokala n\u00e4tverket:\n URL: {local_url}\n Token: {local_token} \n\n Exempel p\u00e5 anslutning till Z-Way via fj\u00e4rr\u00e5tkomst find.z-wave.me:\n URL: {find_url}\n Token: {find_token} \n\n Exempel p\u00e5 anslutning till Z-Way med en statisk offentlig IP-adress:\n URL: {remote_url}\n Token: {local_token} \n\n N\u00e4r du ansluter via find.z-wave.me m\u00e5ste du anv\u00e4nda en token med ett globalt scope (logga in p\u00e5 Z-Way via find.z-wave.me f\u00f6r detta)." + } + } + } +} \ No newline at end of file From 7c6a64c348e541f48c50d0d504aa37844c14a743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Fri, 5 Aug 2022 17:30:45 -0700 Subject: [PATCH 190/903] Bump aiobafi6 to 0.7.2 to unblock #76328 (#76330) --- homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 15e0272b2b0..7462a64e770 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.7.0"], + "requirements": ["aiobafi6==0.7.2"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index b638e66be56..c3f23dfba96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -128,7 +128,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.7.0 +aiobafi6==0.7.2 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f4cd646e4e..f8e23870756 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.7.0 +aiobafi6==0.7.2 # homeassistant.components.aws aiobotocore==2.1.0 From 76f137eb758910bced2ae3a59651951e88b48479 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Aug 2022 00:39:33 -1000 Subject: [PATCH 191/903] Bump yalexs to 1.2.1 (#76339) Changelog: https://github.com/bdraco/yalexs/compare/v1.1.25...v1.2.1 --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 34bd4843f32..418fa6920ad 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.25"], + "requirements": ["yalexs==1.2.1"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index c3f23dfba96..e654833389e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2496,7 +2496,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.25 +yalexs==1.2.1 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8e23870756..1c490f24ea7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1682,7 +1682,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.25 +yalexs==1.2.1 # homeassistant.components.yeelight yeelight==0.7.10 From adce55b4dbe1926a4f6d2e545c487565969b9858 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Aug 2022 00:46:27 -1000 Subject: [PATCH 192/903] Bump pySwitchbot to 0.18.4 (#76322) * Bump pySwitchbot to 0.18.3 Fixes #76321 Changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.17.3...0.18.3 * bump --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index f01eae4a938..b413b44d605 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.17.3"], + "requirements": ["PySwitchbot==0.18.4"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index e654833389e..7abfad4db5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.3 +PySwitchbot==0.18.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c490f24ea7..75ce10222f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.3 +PySwitchbot==0.18.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From c580bce879b6c2f68c4ea45707b5a05ee88c6ecc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Aug 2022 08:10:26 -1000 Subject: [PATCH 193/903] Move HKC entity classes into entity.py (#76333) --- .../components/homekit_controller/__init__.py | 198 +---------------- .../homekit_controller/alarm_control_panel.py | 3 +- .../homekit_controller/binary_sensor.py | 3 +- .../components/homekit_controller/button.py | 3 +- .../components/homekit_controller/camera.py | 3 +- .../components/homekit_controller/climate.py | 3 +- .../components/homekit_controller/cover.py | 3 +- .../components/homekit_controller/entity.py | 203 ++++++++++++++++++ .../components/homekit_controller/fan.py | 3 +- .../homekit_controller/humidifier.py | 3 +- .../components/homekit_controller/light.py | 3 +- .../components/homekit_controller/lock.py | 3 +- .../homekit_controller/media_player.py | 3 +- .../components/homekit_controller/number.py | 3 +- .../components/homekit_controller/select.py | 3 +- .../components/homekit_controller/sensor.py | 3 +- .../components/homekit_controller/switch.py | 3 +- 17 files changed, 235 insertions(+), 211 deletions(-) create mode 100644 homeassistant/components/homekit_controller/entity.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index b2ccad9a457..3a5ba42848c 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio import logging -from typing import Any import aiohomekit from aiohomekit.exceptions import ( @@ -11,216 +10,23 @@ from aiohomekit.exceptions import ( AccessoryNotFoundError, EncryptionError, ) -from aiohomekit.model import Accessory -from aiohomekit.model.characteristics import ( - Characteristic, - CharacteristicPermissions, - CharacteristicsTypes, -) -from aiohomekit.model.services import Service, ServicesTypes from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_IDENTIFIERS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid -from .connection import HKDevice, valid_serial_number +from .connection import HKDevice from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .storage import EntityMapStorage, async_get_entity_storage -from .utils import async_get_controller, folded_name +from .utils import async_get_controller _LOGGER = logging.getLogger(__name__) -class HomeKitEntity(Entity): - """Representation of a Home Assistant HomeKit device.""" - - _attr_should_poll = False - - def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None: - """Initialise a generic HomeKit device.""" - self._accessory = accessory - self._aid = devinfo["aid"] - self._iid = devinfo["iid"] - self._char_name: str | None = None - self._features = 0 - self.setup() - - super().__init__() - - @property - def accessory(self) -> Accessory: - """Return an Accessory model that this entity is attached to.""" - return self._accessory.entity_map.aid(self._aid) - - @property - def accessory_info(self) -> Service: - """Information about the make and model of an accessory.""" - return self.accessory.services.first( - service_type=ServicesTypes.ACCESSORY_INFORMATION - ) - - @property - def service(self) -> Service: - """Return a Service model that this entity is attached to.""" - return self.accessory.services.iid(self._iid) - - async def async_added_to_hass(self) -> None: - """Entity added to hass.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self._accessory.signal_state_updated, - self.async_write_ha_state, - ) - ) - - self._accessory.add_pollable_characteristics(self.pollable_characteristics) - await self._accessory.add_watchable_characteristics( - self.watchable_characteristics - ) - - async def async_will_remove_from_hass(self) -> None: - """Prepare to be removed from hass.""" - self._accessory.remove_pollable_characteristics(self._aid) - self._accessory.remove_watchable_characteristics(self._aid) - - async def async_put_characteristics(self, characteristics: dict[str, Any]) -> None: - """ - Write characteristics to the device. - - A characteristic type is unique within a service, but in order to write - to a named characteristic on a bridge we need to turn its type into - an aid and iid, and send it as a list of tuples, which is what this - helper does. - - E.g. you can do: - - await entity.async_put_characteristics({ - CharacteristicsTypes.ON: True - }) - """ - payload = self.service.build_update(characteristics) - return await self._accessory.put_characteristics(payload) - - def setup(self) -> None: - """Configure an entity based on its HomeKit characteristics metadata.""" - self.pollable_characteristics: list[tuple[int, int]] = [] - self.watchable_characteristics: list[tuple[int, int]] = [] - - char_types = self.get_characteristic_types() - - # Setup events and/or polling for characteristics directly attached to this entity - for char in self.service.characteristics.filter(char_types=char_types): - self._setup_characteristic(char) - - # Setup events and/or polling for characteristics attached to sub-services of this - # entity (like an INPUT_SOURCE). - for service in self.accessory.services.filter(parent_service=self.service): - for char in service.characteristics.filter(char_types=char_types): - self._setup_characteristic(char) - - def _setup_characteristic(self, char: Characteristic) -> None: - """Configure an entity based on a HomeKit characteristics metadata.""" - # Build up a list of (aid, iid) tuples to poll on update() - if CharacteristicPermissions.paired_read in char.perms: - self.pollable_characteristics.append((self._aid, char.iid)) - - # Build up a list of (aid, iid) tuples to subscribe to - if CharacteristicPermissions.events in char.perms: - self.watchable_characteristics.append((self._aid, char.iid)) - - if self._char_name is None: - self._char_name = char.service.value(CharacteristicsTypes.NAME) - - @property - def unique_id(self) -> str: - """Return the ID of this device.""" - info = self.accessory_info - serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) - if valid_serial_number(serial): - return f"homekit-{serial}-{self._iid}" - # Some accessories do not have a serial number - return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" - - @property - def default_name(self) -> str | None: - """Return the default name of the device.""" - return None - - @property - def name(self) -> str | None: - """Return the name of the device if any.""" - accessory_name = self.accessory.name - # If the service has a name char, use that, if not - # fallback to the default name provided by the subclass - device_name = self._char_name or self.default_name - folded_device_name = folded_name(device_name or "") - folded_accessory_name = folded_name(accessory_name) - if device_name: - # Sometimes the device name includes the accessory - # name already like My ecobee Occupancy / My ecobee - if folded_device_name.startswith(folded_accessory_name): - return device_name - if ( - folded_accessory_name not in folded_device_name - and folded_device_name not in folded_accessory_name - ): - return f"{accessory_name} {device_name}" - return accessory_name - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._accessory.available and self.service.available - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return self._accessory.device_info_for_accessory(self.accessory) - - def get_characteristic_types(self) -> list[str]: - """Define the homekit characteristics the entity cares about.""" - raise NotImplementedError - - -class AccessoryEntity(HomeKitEntity): - """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic.""" - - @property - def unique_id(self) -> str: - """Return the ID of this device.""" - serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) - return f"homekit-{serial}-aid:{self._aid}" - - -class CharacteristicEntity(HomeKitEntity): - """ - A HomeKit entity that is related to an single characteristic rather than a whole service. - - This is typically used to expose additional sensor, binary_sensor or number entities that don't belong with - the service entity. - """ - - def __init__( - self, accessory: HKDevice, devinfo: ConfigType, char: Characteristic - ) -> None: - """Initialise a generic single characteristic HomeKit entity.""" - self._char = char - super().__init__(accessory, devinfo) - - @property - def unique_id(self) -> str: - """Return the ID of this device.""" - serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) - return f"homekit-{serial}-aid:{self._aid}-sid:{self._char.service.iid}-cid:{self._char.iid}" - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a HomeKit connection on a config entry.""" conn = HKDevice(hass, entry, entry.data) diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index c7e499b6e89..204fa1bb3f8 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -22,7 +22,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity ICON = "mdi:security" diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 5efd0915cb0..11c81e7e251 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -12,7 +12,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity): diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index e9c85dbe876..d5a8bc733ad 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -21,8 +21,9 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from . import KNOWN_DEVICES, CharacteristicEntity +from . import KNOWN_DEVICES from .connection import HKDevice +from .entity import CharacteristicEntity @dataclass diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index 0f0dd4f9050..510c0c2f522 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -9,7 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, AccessoryEntity +from . import KNOWN_DEVICES +from .entity import AccessoryEntity class HomeKitCamera(AccessoryEntity, Camera): diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index b76ed1ea6a9..7254363e835 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -38,7 +38,8 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 4e8af03bba0..6cbc623596e 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -18,7 +18,8 @@ from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_O from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity STATE_STOPPED = "stopped" diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py new file mode 100644 index 00000000000..ad99e65f2d8 --- /dev/null +++ b/homeassistant/components/homekit_controller/entity.py @@ -0,0 +1,203 @@ +"""Homekit Controller entities.""" +from __future__ import annotations + +from typing import Any + +from aiohomekit.model import Accessory +from aiohomekit.model.characteristics import ( + Characteristic, + CharacteristicPermissions, + CharacteristicsTypes, +) +from aiohomekit.model.services import Service, ServicesTypes + +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.typing import ConfigType + +from .connection import HKDevice, valid_serial_number +from .utils import folded_name + + +class HomeKitEntity(Entity): + """Representation of a Home Assistant HomeKit device.""" + + _attr_should_poll = False + + def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None: + """Initialise a generic HomeKit device.""" + self._accessory = accessory + self._aid = devinfo["aid"] + self._iid = devinfo["iid"] + self._char_name: str | None = None + self._features = 0 + self.setup() + + super().__init__() + + @property + def accessory(self) -> Accessory: + """Return an Accessory model that this entity is attached to.""" + return self._accessory.entity_map.aid(self._aid) + + @property + def accessory_info(self) -> Service: + """Information about the make and model of an accessory.""" + return self.accessory.services.first( + service_type=ServicesTypes.ACCESSORY_INFORMATION + ) + + @property + def service(self) -> Service: + """Return a Service model that this entity is attached to.""" + return self.accessory.services.iid(self._iid) + + async def async_added_to_hass(self) -> None: + """Entity added to hass.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._accessory.signal_state_updated, + self.async_write_ha_state, + ) + ) + + self._accessory.add_pollable_characteristics(self.pollable_characteristics) + await self._accessory.add_watchable_characteristics( + self.watchable_characteristics + ) + + async def async_will_remove_from_hass(self) -> None: + """Prepare to be removed from hass.""" + self._accessory.remove_pollable_characteristics(self._aid) + self._accessory.remove_watchable_characteristics(self._aid) + + async def async_put_characteristics(self, characteristics: dict[str, Any]) -> None: + """ + Write characteristics to the device. + + A characteristic type is unique within a service, but in order to write + to a named characteristic on a bridge we need to turn its type into + an aid and iid, and send it as a list of tuples, which is what this + helper does. + + E.g. you can do: + + await entity.async_put_characteristics({ + CharacteristicsTypes.ON: True + }) + """ + payload = self.service.build_update(characteristics) + return await self._accessory.put_characteristics(payload) + + def setup(self) -> None: + """Configure an entity based on its HomeKit characteristics metadata.""" + self.pollable_characteristics: list[tuple[int, int]] = [] + self.watchable_characteristics: list[tuple[int, int]] = [] + + char_types = self.get_characteristic_types() + + # Setup events and/or polling for characteristics directly attached to this entity + for char in self.service.characteristics.filter(char_types=char_types): + self._setup_characteristic(char) + + # Setup events and/or polling for characteristics attached to sub-services of this + # entity (like an INPUT_SOURCE). + for service in self.accessory.services.filter(parent_service=self.service): + for char in service.characteristics.filter(char_types=char_types): + self._setup_characteristic(char) + + def _setup_characteristic(self, char: Characteristic) -> None: + """Configure an entity based on a HomeKit characteristics metadata.""" + # Build up a list of (aid, iid) tuples to poll on update() + if CharacteristicPermissions.paired_read in char.perms: + self.pollable_characteristics.append((self._aid, char.iid)) + + # Build up a list of (aid, iid) tuples to subscribe to + if CharacteristicPermissions.events in char.perms: + self.watchable_characteristics.append((self._aid, char.iid)) + + if self._char_name is None: + self._char_name = char.service.value(CharacteristicsTypes.NAME) + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + info = self.accessory_info + serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) + if valid_serial_number(serial): + return f"homekit-{serial}-{self._iid}" + # Some accessories do not have a serial number + return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" + + @property + def default_name(self) -> str | None: + """Return the default name of the device.""" + return None + + @property + def name(self) -> str | None: + """Return the name of the device if any.""" + accessory_name = self.accessory.name + # If the service has a name char, use that, if not + # fallback to the default name provided by the subclass + device_name = self._char_name or self.default_name + folded_device_name = folded_name(device_name or "") + folded_accessory_name = folded_name(accessory_name) + if device_name: + # Sometimes the device name includes the accessory + # name already like My ecobee Occupancy / My ecobee + if folded_device_name.startswith(folded_accessory_name): + return device_name + if ( + folded_accessory_name not in folded_device_name + and folded_device_name not in folded_accessory_name + ): + return f"{accessory_name} {device_name}" + return accessory_name + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._accessory.available and self.service.available + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return self._accessory.device_info_for_accessory(self.accessory) + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity cares about.""" + raise NotImplementedError + + +class AccessoryEntity(HomeKitEntity): + """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic.""" + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-aid:{self._aid}" + + +class CharacteristicEntity(HomeKitEntity): + """ + A HomeKit entity that is related to an single characteristic rather than a whole service. + + This is typically used to expose additional sensor, binary_sensor or number entities that don't belong with + the service entity. + """ + + def __init__( + self, accessory: HKDevice, devinfo: ConfigType, char: Characteristic + ) -> None: + """Initialise a generic single characteristic HomeKit entity.""" + self._char = char + super().__init__(accessory, devinfo) + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-aid:{self._aid}-sid:{self._char.service.iid}-cid:{self._char.iid}" diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 159a1d936fa..03f4dade674 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -20,7 +20,8 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity # 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that # its consistent with homeassistant.components.homekit. diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 1676999ad78..ebba525e0c9 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -21,7 +21,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity HK_MODE_TO_HA = { 0: "off", diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index d882f6790f7..010411c60d0 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -17,7 +17,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity async def async_setup_entry( diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 248bb93a68f..8e8919ae4f8 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -17,7 +17,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity CURRENT_STATE_MAP = { 0: STATE_UNLOCKED, diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index fbdf800edf8..092652ed17d 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -28,7 +28,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 7a6d0a01ab6..2987c82e829 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -20,8 +20,9 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from . import KNOWN_DEVICES, CharacteristicEntity +from . import KNOWN_DEVICES from .connection import HKDevice +from .entity import CharacteristicEntity NUMBER_ENTITIES: dict[str, NumberEntityDescription] = { CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: NumberEntityDescription( diff --git a/homeassistant/components/homekit_controller/select.py b/homeassistant/components/homekit_controller/select.py index 681f24b9ab8..a22f79d675b 100644 --- a/homeassistant/components/homekit_controller/select.py +++ b/homeassistant/components/homekit_controller/select.py @@ -8,8 +8,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, CharacteristicEntity +from . import KNOWN_DEVICES from .const import DEVICE_CLASS_ECOBEE_MODE +from .entity import CharacteristicEntity _ECOBEE_MODE_TO_TEXT = { 0: "home", diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index a6810c10d99..04856a60347 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -32,8 +32,9 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity +from . import KNOWN_DEVICES from .connection import HKDevice +from .entity import CharacteristicEntity, HomeKitEntity from .utils import folded_name diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index be6c3b8bfe0..c537233de7e 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -19,8 +19,9 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity +from . import KNOWN_DEVICES from .connection import HKDevice +from .entity import CharacteristicEntity, HomeKitEntity OUTLET_IN_USE = "outlet_in_use" From 1aa0e64354d1dffd5e948da803e4d379ee39443c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 6 Aug 2022 21:45:44 +0100 Subject: [PATCH 194/903] Update gree to use the network component to set discovery interfaces (#75812) --- homeassistant/components/gree/__init__.py | 4 +++- homeassistant/components/gree/config_flow.py | 6 +++++- homeassistant/components/gree/manifest.json | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/common.py | 2 +- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index d4a929f1642..ff3438ed53f 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from homeassistant.components.network import async_get_ipv4_broadcast_addresses from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -32,7 +33,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def _async_scan_update(_=None): - await gree_discovery.discovery.scan() + bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass)) + await gree_discovery.discovery.scan(0, bcast_ifaces=bcast_addr) _LOGGER.debug("Scanning network for Gree devices") await _async_scan_update() diff --git a/homeassistant/components/gree/config_flow.py b/homeassistant/components/gree/config_flow.py index d317fe6d873..58f83cd4486 100644 --- a/homeassistant/components/gree/config_flow.py +++ b/homeassistant/components/gree/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Gree.""" from greeclimate.discovery import Discovery +from homeassistant.components.network import async_get_ipv4_broadcast_addresses from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow @@ -10,7 +11,10 @@ from .const import DISCOVERY_TIMEOUT, DOMAIN async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" gree_discovery = Discovery(DISCOVERY_TIMEOUT) - devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT) + bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass)) + devices = await gree_discovery.scan( + wait_for=DISCOVERY_TIMEOUT, bcast_ifaces=bcast_addr + ) return len(devices) > 0 diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 1b2c8dd6a2a..97c0ec1780c 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,8 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==1.2.0"], + "requirements": ["greeclimate==1.3.0"], + "dependencies": ["network"], "codeowners": ["@cmroche"], "iot_class": "local_polling", "loggers": ["greeclimate"] diff --git a/requirements_all.txt b/requirements_all.txt index 7abfad4db5e..ead73586d7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -772,7 +772,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.2.0 +greeclimate==1.3.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75ce10222f1..e27714f81f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -564,7 +564,7 @@ googlemaps==2.5.1 govee-ble==0.12.6 # homeassistant.components.gree -greeclimate==1.2.0 +greeclimate==1.3.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index c7db03b118f..cd8a2d6ee28 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -28,7 +28,7 @@ class FakeDiscovery: """Add an event listener.""" self._listeners.append(listener) - async def scan(self, wait_for: int = 0): + async def scan(self, wait_for: int = 0, bcast_ifaces=None): """Search for devices, return mocked data.""" self.scan_count += 1 _LOGGER.info("CALLED SCAN %d TIMES", self.scan_count) From e864b82c036d38b8d793c7094d7f19ccfff6a63f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 Aug 2022 01:22:50 +0200 Subject: [PATCH 195/903] Improve mysensors config flow (#75122) * Improve mysensors config flow * Improve form input order * Update flow tests --- .../components/mysensors/config_flow.py | 112 ++++++++--------- homeassistant/components/mysensors/const.py | 5 - .../components/mysensors/strings.json | 10 +- .../components/mysensors/translations/en.json | 10 +- .../components/mysensors/test_config_flow.py | 113 +++++++----------- 5 files changed, 113 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 5409e3c9a85..04e95f1dad3 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -20,13 +20,13 @@ from homeassistant.components.mqtt import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv from .const import ( CONF_BAUD_RATE, CONF_DEVICE, CONF_GATEWAY_TYPE, - CONF_GATEWAY_TYPE_ALL, CONF_GATEWAY_TYPE_MQTT, CONF_GATEWAY_TYPE_SERIAL, CONF_GATEWAY_TYPE_TCP, @@ -45,6 +45,15 @@ DEFAULT_BAUD_RATE = 115200 DEFAULT_TCP_PORT = 5003 DEFAULT_VERSION = "1.4" +_PORT_SELECTOR = vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=65535, mode=selector.NumberSelectorMode.BOX + ), + ), + vol.Coerce(int), +) + def is_persistence_file(value: str) -> str: """Validate that persistence file path ends in either .pickle or .json.""" @@ -119,51 +128,34 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, str] | None = None ) -> FlowResult: """Create a config entry from frontend user input.""" - schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)} - schema = vol.Schema(schema) - errors = {} - - if user_input is not None: - 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: - # Naive check that doesn't consider config entry state. - if MQTT_DOMAIN in self.hass.config.components: - return await self.async_step_gw_mqtt(input_pass) - - errors["base"] = "mqtt_required" - 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, errors=errors) + return self.async_show_menu( + step_id="select_gateway_type", + menu_options=["gw_serial", "gw_tcp", "gw_mqtt"], + ) async def async_step_gw_serial( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Create config entry for a serial gateway.""" + gw_type = self._gw_type = CONF_GATEWAY_TYPE_SERIAL errors: dict[str, str] = {} + if user_input is not None: - errors.update( - await self.validate_common(CONF_GATEWAY_TYPE_SERIAL, errors, user_input) - ) + errors.update(await self.validate_common(gw_type, errors, user_input)) if not errors: return self._async_create_entry(user_input) user_input = user_input or {} - schema = _get_schema_common(user_input) - schema[ + schema = { + vol.Required( + CONF_DEVICE, default=user_input.get(CONF_DEVICE, "/dev/ttyACM0") + ): str, vol.Required( CONF_BAUD_RATE, default=user_input.get(CONF_BAUD_RATE, DEFAULT_BAUD_RATE), - ) - ] = cv.positive_int - schema[ - vol.Required( - CONF_DEVICE, default=user_input.get(CONF_DEVICE, "/dev/ttyACM0") - ) - ] = str + ): cv.positive_int, + } + schema.update(_get_schema_common(user_input)) schema = vol.Schema(schema) return self.async_show_form( @@ -174,30 +166,24 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """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" + gw_type = self._gw_type = CONF_GATEWAY_TYPE_TCP + errors: dict[str, str] = {} - errors.update( - await self.validate_common(CONF_GATEWAY_TYPE_TCP, errors, user_input) - ) + if user_input is not None: + errors.update(await self.validate_common(gw_type, errors, user_input)) if not errors: return self._async_create_entry(user_input) 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[ + schema = { + vol.Required( + CONF_DEVICE, default=user_input.get(CONF_DEVICE, "127.0.0.1") + ): str, vol.Optional( CONF_TCP_PORT, default=user_input.get(CONF_TCP_PORT, DEFAULT_TCP_PORT) - ) - ] = vol.Coerce(int) + ): _PORT_SELECTOR, + } + schema.update(_get_schema_common(user_input)) schema = vol.Schema(schema) return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors) @@ -214,7 +200,13 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Create a config entry for a mqtt gateway.""" - errors = {} + # Naive check that doesn't consider config entry state. + if MQTT_DOMAIN not in self.hass.config.components: + return self.async_abort(reason="mqtt_required") + + gw_type = self._gw_type = CONF_GATEWAY_TYPE_MQTT + errors: dict[str, str] = {} + if user_input is not None: user_input[CONF_DEVICE] = MQTT_COMPONENT @@ -239,27 +231,21 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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) - ) + errors.update(await self.validate_common(gw_type, errors, user_input)) if not errors: return self._async_create_entry(user_input) 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[ + schema = { vol.Required( CONF_TOPIC_IN_PREFIX, default=user_input.get(CONF_TOPIC_IN_PREFIX, "") - ) - ] = str - schema[ + ): str, vol.Required( CONF_TOPIC_OUT_PREFIX, default=user_input.get(CONF_TOPIC_OUT_PREFIX, "") - ) - ] = str + ): str, + vol.Required(CONF_RETAIN, default=user_input.get(CONF_RETAIN, True)): bool, + } + schema.update(_get_schema_common(user_input)) schema = vol.Schema(schema) return self.async_show_form( diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 32e2110dd95..42df81ae526 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -22,11 +22,6 @@ 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: Final = "mysensors" MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}" diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index d7722e565cb..dc5dc76c7ae 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -7,6 +7,14 @@ }, "description": "Choose connection method to the gateway" }, + "select_gateway_type": { + "description": "Select which gateway to configure.", + "menu_options": { + "gw_mqtt": "Configure an MQTT gateway", + "gw_serial": "Configure a serial gateway", + "gw_tcp": "Configure a TCP gateway" + } + }, "gw_tcp": { "description": "Ethernet gateway setup", "data": { @@ -51,7 +59,6 @@ "invalid_serial": "Invalid serial port", "invalid_device": "Invalid device", "invalid_version": "Invalid MySensors version", - "mqtt_required": "The MQTT integration is not set up", "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%]" @@ -71,6 +78,7 @@ "invalid_serial": "Invalid serial port", "invalid_device": "Invalid device", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "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/translations/en.json b/homeassistant/components/mysensors/translations/en.json index 5ec81c22186..b85a28fb7d3 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -14,6 +14,7 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "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", @@ -33,7 +34,6 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", - "mqtt_required": "The MQTT integration is not set up", "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", @@ -68,6 +68,14 @@ }, "description": "Ethernet gateway setup" }, + "select_gateway_type": { + "description": "Select which gateway to configure.", + "menu_options": { + "gw_mqtt": "Configure an MQTT gateway", + "gw_serial": "Configure a serial gateway", + "gw_tcp": "Configure a TCP gateway" + } + }, "user": { "data": { "gateway_type": "Gateway type" diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index e7808162043..e14059c4e4f 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -24,28 +24,32 @@ from homeassistant.components.mysensors.const import ( ConfGatewayType, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import FlowResult, FlowResultType from tests.common import MockConfigEntry +GATEWAY_TYPE_TO_STEP = { + CONF_GATEWAY_TYPE_TCP: "gw_tcp", + CONF_GATEWAY_TYPE_SERIAL: "gw_serial", + CONF_GATEWAY_TYPE_MQTT: "gw_mqtt", +} + async def get_form( - hass: HomeAssistant, gatway_type: ConfGatewayType, expected_step_id: str + hass: HomeAssistant, gateway_type: ConfGatewayType, expected_step_id: str ) -> FlowResult: """Get a form for the given gateway type.""" - stepuser = await hass.config_entries.flow.async_init( + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert stepuser["type"] == "form" - assert not stepuser["errors"] + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( - stepuser["flow_id"], - {CONF_GATEWAY_TYPE: gatway_type}, + result["flow_id"], {"next_step_id": GATEWAY_TYPE_TO_STEP[gateway_type]} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] == FlowResultType.FORM assert result["step_id"] == expected_step_id return result @@ -62,7 +66,7 @@ async def test_config_mqtt(hass: HomeAssistant, mqtt: None) -> None: "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( flow_id, { CONF_RETAIN: True, @@ -73,11 +77,11 @@ async def test_config_mqtt(hass: HomeAssistant, mqtt: None) -> None: ) 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"] == { + if "errors" in result: + assert not result["errors"] + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "mqtt" + assert result["data"] == { CONF_DEVICE: "mqtt", CONF_RETAIN: True, CONF_TOPIC_IN_PREFIX: "bla", @@ -91,20 +95,19 @@ async def test_config_mqtt(hass: HomeAssistant, mqtt: None) -> None: async def test_missing_mqtt(hass: HomeAssistant) -> None: """Test configuring a mqtt gateway without mqtt integration setup.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" - assert not result["errors"] + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT}, + {"next_step_id": GATEWAY_TYPE_TO_STEP[CONF_GATEWAY_TYPE_MQTT]}, ) - assert result["step_id"] == "user" - assert result["type"] == "form" - assert result["errors"] == {"base": "mqtt_required"} + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "mqtt_required" async def test_config_serial(hass: HomeAssistant) -> None: @@ -123,7 +126,7 @@ async def test_config_serial(hass: HomeAssistant) -> None: "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( flow_id, { CONF_BAUD_RATE: 115200, @@ -133,11 +136,11 @@ async def test_config_serial(hass: HomeAssistant) -> None: ) 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"] == { + if "errors" in result: + assert not result["errors"] + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "/dev/ttyACM0" + assert result["data"] == { CONF_DEVICE: "/dev/ttyACM0", CONF_BAUD_RATE: 115200, CONF_VERSION: "2.4", @@ -160,7 +163,7 @@ async def test_config_tcp(hass: HomeAssistant) -> None: "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( flow_id, { CONF_TCP_PORT: 5003, @@ -170,11 +173,11 @@ async def test_config_tcp(hass: HomeAssistant) -> None: ) 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"] == { + if "errors" in result: + assert not result["errors"] + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "127.0.0.1" + assert result["data"] == { CONF_DEVICE: "127.0.0.1", CONF_TCP_PORT: 5003, CONF_VERSION: "2.4", @@ -197,7 +200,7 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( flow_id, { CONF_TCP_PORT: 5003, @@ -207,9 +210,9 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" - assert "errors" in result2 - errors = result2["errors"] + assert result["type"] == FlowResultType.FORM + assert "errors" in result + errors = result["errors"] assert errors assert errors.get("base") == "cannot_connect" assert len(mock_setup.mock_calls) == 0 @@ -219,28 +222,6 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: @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", @@ -382,15 +363,15 @@ async def test_config_invalid( "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = 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 - errors = result2["errors"] + assert result["type"] == FlowResultType.FORM + assert "errors" in result + errors = result["errors"] assert errors assert err_field in errors assert errors[err_field] == err_string @@ -681,10 +662,8 @@ async def test_duplicate( MockConfigEntry(domain=DOMAIN, data=first_input).add_to_hass(hass) second_gateway_type = second_input.pop(CONF_GATEWAY_TYPE) - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={CONF_GATEWAY_TYPE: second_gateway_type}, - context={"source": config_entries.SOURCE_USER}, + result = await get_form( + hass, second_gateway_type, GATEWAY_TYPE_TO_STEP[second_gateway_type] ) result = await hass.config_entries.flow.async_configure( result["flow_id"], From 953d9eb9c86f5065067d35a60b50e78650077d29 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 Aug 2022 01:23:18 +0200 Subject: [PATCH 196/903] Bump aioopenexchangerates to 0.4.0 (#76356) --- homeassistant/components/openexchangerates/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index a795eaf8d5e..f2377478c5f 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -2,7 +2,7 @@ "domain": "openexchangerates", "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", - "requirements": ["aioopenexchangerates==0.3.0"], + "requirements": ["aioopenexchangerates==0.4.0"], "codeowners": ["@MartinHjelmare"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index ead73586d7a..5dbce196182 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,7 +220,7 @@ aionotion==3.0.2 aiooncue==0.3.4 # homeassistant.components.openexchangerates -aioopenexchangerates==0.3.0 +aioopenexchangerates==0.4.0 # homeassistant.components.acmeda aiopulse==0.4.3 From 4b53b920cb4c086fe8eb47f96f8fa3dafb9d9fa1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 7 Aug 2022 00:29:25 +0000 Subject: [PATCH 197/903] [ci skip] Translation update --- homeassistant/components/demo/translations/de.json | 13 ++++++++++++- homeassistant/components/demo/translations/fr.json | 10 ++++++++++ homeassistant/components/demo/translations/ru.json | 11 +++++++++++ .../components/deutsche_bahn/translations/fr.json | 7 +++++++ .../components/deutsche_bahn/translations/ru.json | 8 ++++++++ .../components/mysensors/translations/en.json | 1 + 6 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/deutsche_bahn/translations/fr.json create mode 100644 homeassistant/components/deutsche_bahn/translations/ru.json diff --git a/homeassistant/components/demo/translations/de.json b/homeassistant/components/demo/translations/de.json index ab06043d52c..8f5950f49f8 100644 --- a/homeassistant/components/demo/translations/de.json +++ b/homeassistant/components/demo/translations/de.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Dr\u00fccke SENDEN, um zu best\u00e4tigen, dass das Netzteil ausgetauscht wurde", + "title": "Das Netzteil muss ausgetauscht werden" + } + } + }, + "title": "Das Netzteil ist nicht stabil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Dr\u00fccke OK, wenn die Blinkerfl\u00fcssigkeit nachgef\u00fcllt wurde.", + "description": "Dr\u00fccke SENDEN, wenn die Blinkerfl\u00fcssigkeit nachgef\u00fcllt wurde", "title": "Blinkerfl\u00fcssigkeit muss nachgef\u00fcllt werden" } } diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index c5a150d8cb6..a09a7a1fd2f 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -1,5 +1,15 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "title": "L'alimentation \u00e9lectrique doit \u00eatre remplac\u00e9e" + } + } + }, + "title": "L'alimentation \u00e9lectrique n'est pas stable" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index 3c919e12d84..29b4229dacb 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\" \u0434\u043b\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u0437\u0430\u043c\u0435\u043d\u044b \u0431\u043b\u043e\u043a\u0430 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0438\u0442\u0430\u043d\u0438\u044f" + } + } + }, + "title": "\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0438\u0442\u0430\u043d\u0438\u044f \u043d\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u0435\u043d" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/deutsche_bahn/translations/fr.json b/homeassistant/components/deutsche_bahn/translations/fr.json new file mode 100644 index 00000000000..a3e3b26fa78 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/fr.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "L'int\u00e9gration Deutsche Bahn est en cours de suppression" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/ru.json b/homeassistant/components/deutsche_bahn/translations/ru.json new file mode 100644 index 00000000000..2cfbb695fc3 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.11. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index b85a28fb7d3..081ae3a2b95 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -34,6 +34,7 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "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", From db3e21df86d299d59e0c8ea4a3aeea1627837abc Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 7 Aug 2022 11:02:32 +1000 Subject: [PATCH 198/903] Update aiolifx to version 0.8.2 (#76367) --- homeassistant/components/lifx/__init__.py | 2 +- homeassistant/components/lifx/config_flow.py | 2 +- homeassistant/components/lifx/coordinator.py | 2 +- homeassistant/components/lifx/manifest.json | 6 +----- requirements_all.txt | 5 +---- requirements_test_all.txt | 5 +---- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 3faf69483d5..9d4e2d5facf 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -8,7 +8,7 @@ import socket from typing import Any from aiolifx.aiolifx import Light -from aiolifx_connection import LIFXConnection +from aiolifx.connection import LIFXConnection import voluptuous as vol from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index 30b42e640f8..daa917dc847 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -6,7 +6,7 @@ import socket from typing import Any from aiolifx.aiolifx import Light -from aiolifx_connection import LIFXConnection +from aiolifx.connection import LIFXConnection import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 87ba46e94d1..bb3dd60b326 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -7,7 +7,7 @@ from functools import partial from typing import cast from aiolifx.aiolifx import Light -from aiolifx_connection import LIFXConnection +from aiolifx.connection import LIFXConnection from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index ebc4d73ce5d..83408f87bb5 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,11 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": [ - "aiolifx==0.8.1", - "aiolifx_effects==0.2.2", - "aiolifx-connection==1.0.0" - ], + "requirements": ["aiolifx==0.8.2", "aiolifx_effects==0.2.2"], "quality_scale": "platinum", "dependencies": ["network"], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 5dbce196182..8dbcab9d520 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,10 +187,7 @@ aiokafka==0.7.2 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx-connection==1.0.0 - -# homeassistant.components.lifx -aiolifx==0.8.1 +aiolifx==0.8.2 # homeassistant.components.lifx aiolifx_effects==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e27714f81f8..eba56137019 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,10 +165,7 @@ aiohue==4.4.2 aiokafka==0.7.2 # homeassistant.components.lifx -aiolifx-connection==1.0.0 - -# homeassistant.components.lifx -aiolifx==0.8.1 +aiolifx==0.8.2 # homeassistant.components.lifx aiolifx_effects==0.2.2 From 74cfdc6c1fa1e9fb4fa7acb6b4d06401f7ae66b6 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 7 Aug 2022 13:28:30 +1000 Subject: [PATCH 199/903] Add identify and restart button entities to the LIFX integration (#75568) --- homeassistant/components/lifx/__init__.py | 3 +- homeassistant/components/lifx/button.py | 77 +++++++++++ homeassistant/components/lifx/const.py | 15 +++ homeassistant/components/lifx/coordinator.py | 29 +++- homeassistant/components/lifx/entity.py | 28 ++++ homeassistant/components/lifx/light.py | 30 +---- tests/components/lifx/__init__.py | 5 +- tests/components/lifx/test_button.py | 132 +++++++++++++++++++ 8 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/lifx/button.py create mode 100644 homeassistant/components/lifx/entity.py create mode 100644 tests/components/lifx/test_button.py diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 9d4e2d5facf..ec54382ec40 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -57,7 +57,7 @@ CONFIG_SCHEMA = vol.All( ) -PLATFORMS = [Platform.LIGHT] +PLATFORMS = [Platform.BUTTON, Platform.LIGHT] DISCOVERY_INTERVAL = timedelta(minutes=15) MIGRATION_INTERVAL = timedelta(minutes=5) @@ -173,7 +173,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LIFX from a config entry.""" - if async_entry_is_legacy(entry): return True diff --git a/homeassistant/components/lifx/button.py b/homeassistant/components/lifx/button.py new file mode 100644 index 00000000000..6d3f4fe51bf --- /dev/null +++ b/homeassistant/components/lifx/button.py @@ -0,0 +1,77 @@ +"""Button entity for LIFX devices..""" +from __future__ import annotations + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, IDENTIFY, RESTART +from .coordinator import LIFXUpdateCoordinator +from .entity import LIFXEntity + +RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription( + key=RESTART, + name="Restart", + device_class=ButtonDeviceClass.RESTART, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, +) + +IDENTIFY_BUTTON_DESCRIPTION = ButtonEntityDescription( + key=IDENTIFY, + name="Identify", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LIFX from a config entry.""" + domain_data = hass.data[DOMAIN] + coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id] + async_add_entities( + cls(coordinator) for cls in (LIFXRestartButton, LIFXIdentifyButton) + ) + + +class LIFXButton(LIFXEntity, ButtonEntity): + """Base LIFX button.""" + + _attr_has_entity_name: bool = True + + def __init__(self, coordinator: LIFXUpdateCoordinator) -> None: + """Initialise a LIFX button.""" + super().__init__(coordinator) + self._attr_unique_id = ( + f"{coordinator.serial_number}_{self.entity_description.key}" + ) + + +class LIFXRestartButton(LIFXButton): + """LIFX restart button.""" + + entity_description = RESTART_BUTTON_DESCRIPTION + + async def async_press(self) -> None: + """Restart the bulb on button press.""" + self.bulb.set_reboot() + + +class LIFXIdentifyButton(LIFXButton): + """LIFX identify button.""" + + entity_description = IDENTIFY_BUTTON_DESCRIPTION + + async def async_press(self) -> None: + """Identify the bulb by flashing it when the button is pressed.""" + await self.coordinator.async_identify_bulb() diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index ec756c2091f..f6ec653c994 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -14,6 +14,21 @@ UNAVAILABLE_GRACE = 90 CONF_SERIAL = "serial" +IDENTIFY_WAVEFORM = { + "transient": True, + "color": [0, 0, 1, 3500], + "skew_ratio": 0, + "period": 1000, + "cycles": 3, + "waveform": 1, + "set_hue": True, + "set_saturation": True, + "set_brightness": True, + "set_kelvin": True, +} +IDENTIFY = "identify" +RESTART = "restart" + DATA_LIFX_MANAGER = "lifx_manager" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index bb3dd60b326..1f3f49368ca 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta from functools import partial -from typing import cast +from typing import Any, cast from aiolifx.aiolifx import Light from aiolifx.connection import LIFXConnection @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( _LOGGER, + IDENTIFY_WAVEFORM, MESSAGE_RETRIES, MESSAGE_TIMEOUT, TARGET_ANY, @@ -75,6 +76,24 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.device.host_firmware_version, ) + @property + def label(self) -> str: + """Return the label of the bulb.""" + return cast(str, self.device.label) + + async def async_identify_bulb(self) -> None: + """Identify the device by flashing it three times.""" + bulb: Light = self.device + if bulb.power_level: + # just flash the bulb for three seconds + await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM) + return + # Turn the bulb on first, flash for 3 seconds, then turn off + await self.async_set_power(state=True, duration=1) + await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM) + await asyncio.sleep(3) + await self.async_set_power(state=False, duration=1) + async def _async_update_data(self) -> None: """Fetch all device data from the api.""" async with self.lock: @@ -119,6 +138,14 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if zone == top - 1: zone -= 1 + async def async_set_waveform_optional( + self, value: dict[str, Any], rapid: bool = False + ) -> None: + """Send a set_waveform_optional message to the device.""" + await async_execute_lifx( + partial(self.device.set_waveform_optional, value=value, rapid=rapid) + ) + async def async_get_color(self) -> None: """Send a get color message to the device.""" await async_execute_lifx(self.device.get_color) diff --git a/homeassistant/components/lifx/entity.py b/homeassistant/components/lifx/entity.py new file mode 100644 index 00000000000..0007ab998a9 --- /dev/null +++ b/homeassistant/components/lifx/entity.py @@ -0,0 +1,28 @@ +"""Support for LIFX lights.""" +from __future__ import annotations + +from aiolifx import products + +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import LIFXUpdateCoordinator + + +class LIFXEntity(CoordinatorEntity[LIFXUpdateCoordinator]): + """Representation of a LIFX entity with a coordinator.""" + + def __init__(self, coordinator: LIFXUpdateCoordinator) -> None: + """Initialise the light.""" + super().__init__(coordinator) + self.bulb = coordinator.device + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.serial_number)}, + connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)}, + manufacturer="LIFX", + name=coordinator.label, + model=products.product_map.get(self.bulb.product, "LIFX Bulb"), + sw_version=self.bulb.host_firmware_version, + ) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 28a678d5e8f..67bb3e91748 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -6,7 +6,6 @@ from datetime import datetime, timedelta import math from typing import Any -from aiolifx import products import aiolifx_effects as aiolifx_effects_module import voluptuous as vol @@ -20,20 +19,18 @@ from homeassistant.components.light import ( LightEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODEL, ATTR_SW_VERSION +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.color as color_util from .const import DATA_LIFX_MANAGER, DOMAIN from .coordinator import LIFXUpdateCoordinator +from .entity import LIFXEntity from .manager import ( SERVICE_EFFECT_COLORLOOP, SERVICE_EFFECT_PULSE, @@ -92,7 +89,7 @@ async def async_setup_entry( async_add_entities([entity]) -class LIFXLight(CoordinatorEntity[LIFXUpdateCoordinator], LightEntity): +class LIFXLight(LIFXEntity, LightEntity): """Representation of a LIFX light.""" _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT @@ -105,10 +102,9 @@ class LIFXLight(CoordinatorEntity[LIFXUpdateCoordinator], LightEntity): ) -> None: """Initialize the light.""" super().__init__(coordinator) - bulb = coordinator.device - self.mac_addr = bulb.mac_addr - self.bulb = bulb - bulb_features = lifx_features(bulb) + + self.mac_addr = self.bulb.mac_addr + bulb_features = lifx_features(self.bulb) self.manager = manager self.effects_conductor: aiolifx_effects_module.Conductor = ( manager.effects_conductor @@ -116,25 +112,13 @@ class LIFXLight(CoordinatorEntity[LIFXUpdateCoordinator], LightEntity): self.postponed_update: CALLBACK_TYPE | None = None self.entry = entry self._attr_unique_id = self.coordinator.serial_number - self._attr_name = bulb.label + self._attr_name = self.bulb.label self._attr_min_mireds = math.floor( color_util.color_temperature_kelvin_to_mired(bulb_features["max_kelvin"]) ) self._attr_max_mireds = math.ceil( color_util.color_temperature_kelvin_to_mired(bulb_features["min_kelvin"]) ) - info = DeviceInfo( - identifiers={(DOMAIN, coordinator.serial_number)}, - connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)}, - manufacturer="LIFX", - name=self.name, - ) - _map = products.product_map - if (model := (_map.get(bulb.product) or bulb.product)) is not None: - info[ATTR_MODEL] = str(model) - if (version := bulb.host_firmware_version) is not None: - info[ATTR_SW_VERSION] = version - self._attr_device_info = info if bulb_features["min_kelvin"] != bulb_features["max_kelvin"]: color_mode = ColorMode.COLOR_TEMP else: diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index fdea992c87d..8259314e77c 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from contextlib import contextmanager -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch from aiolifx.aiolifx import Light @@ -72,6 +72,8 @@ def _mocked_bulb() -> Light: bulb.label = LABEL bulb.color = [1, 2, 3, 4] bulb.power_level = 0 + bulb.fire_and_forget = AsyncMock() + bulb.set_reboot = Mock() bulb.try_sending = AsyncMock() bulb.set_infrared = MockLifxCommand(bulb) bulb.get_color = MockLifxCommand(bulb) @@ -79,6 +81,7 @@ def _mocked_bulb() -> Light: bulb.set_color = MockLifxCommand(bulb) bulb.get_hostfirmware = MockLifxCommand(bulb) bulb.get_version = MockLifxCommand(bulb) + bulb.set_waveform_optional = MockLifxCommand(bulb) bulb.product = 1 # LIFX Original 1000 return bulb diff --git a/tests/components/lifx/test_button.py b/tests/components/lifx/test_button.py new file mode 100644 index 00000000000..a485c882100 --- /dev/null +++ b/tests/components/lifx/test_button.py @@ -0,0 +1,132 @@ +"""Tests for button platform.""" +from homeassistant.components import lifx +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.lifx.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + SERIAL, + _mocked_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry + + +async def test_button_restart(hass: HomeAssistant) -> None: + """Test that a bulb can be restarted.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + unique_id = f"{SERIAL}_restart" + entity_id = "button.my_bulb_restart" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled + assert entity.unique_id == unique_id + + enabled_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) + assert not enabled_entity.disabled + + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + bulb.set_reboot.assert_called_once() + + +async def test_button_identify(hass: HomeAssistant) -> None: + """Test that a bulb can be identified.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + unique_id = f"{SERIAL}_identify" + entity_id = "button.my_bulb_identify" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled + assert entity.unique_id == unique_id + + enabled_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) + assert not enabled_entity.disabled + + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert len(bulb.set_power.calls) == 2 + + waveform_call_dict = bulb.set_waveform_optional.calls[0][1] + waveform_call_dict.pop("callb") + assert waveform_call_dict == { + "rapid": False, + "value": { + "transient": True, + "color": [0, 0, 1, 3500], + "skew_ratio": 0, + "period": 1000, + "cycles": 3, + "waveform": 1, + "set_hue": True, + "set_saturation": True, + "set_brightness": True, + "set_kelvin": True, + }, + } + + bulb.set_power.reset_mock() + bulb.set_waveform_optional.reset_mock() + bulb.power_level = 65535 + + await hass.services.async_call( + BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert len(bulb.set_waveform_optional.calls) == 1 + assert len(bulb.set_power.calls) == 0 From 34984a8af8efc5ef6d1d204404c517e7f7c2d1bb Mon Sep 17 00:00:00 2001 From: Leonardo Figueiro Date: Sun, 7 Aug 2022 06:07:01 -0300 Subject: [PATCH 200/903] Add switch to wilight (#62873) * Created switch.py and support * updated support.py * test for wilight switch * Update for Test * Updated test_switch.py * Trigger service with index * Updated support.py and switch.py * Updated support.py * Updated switch.py as PR#63614 * Updated switch.py * add type hints * Updated support.py * Updated switch.py * Updated switch.py and services.yaml * Updated pywilight * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update ci.yaml * Update ci.yaml * Updated as pywilight Renamed Device as PyWiLightDevice in pywilight. * Updated as pywilight Renamed Device as PyWiLightDevice in pywilight. * Updated as pywilight Renamed Device as PyWiLightDevice in pywilight. * Updated as pywilight Renamed Device as PyWiLightDevice in pywilight. * Update switch.py * Update homeassistant/components/wilight/support.py Co-authored-by: Martin Hjelmare * Update support.py * Update switch.py * Update support.py * Update support.py * Update switch.py * Update switch.py * Update services.yaml * Update switch.py * Update services.yaml * Update switch.py * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update switch.py * Update switch.py * Update switch.py * Update test_switch.py * Update test_switch.py * Update test_switch.py * Decrease exception scope * Clean up Co-authored-by: Martin Hjelmare --- homeassistant/components/wilight/__init__.py | 4 +- .../components/wilight/config_flow.py | 2 +- homeassistant/components/wilight/fan.py | 2 +- homeassistant/components/wilight/light.py | 2 +- .../components/wilight/manifest.json | 2 +- .../components/wilight/parent_device.py | 2 +- .../components/wilight/services.yaml | 24 ++ homeassistant/components/wilight/support.py | 87 +++++ homeassistant/components/wilight/switch.py | 322 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wilight/__init__.py | 1 + tests/components/wilight/test_switch.py | 264 ++++++++++++++ 13 files changed, 707 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/wilight/services.yaml create mode 100644 homeassistant/components/wilight/support.py create mode 100644 homeassistant/components/wilight/switch.py create mode 100644 tests/components/wilight/test_switch.py diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index fefde1644ad..326265b8b3f 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -2,7 +2,7 @@ from typing import Any -from pywilight.wilight_device import Device as PyWiLightDevice +from pywilight.wilight_device import PyWiLightDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -15,7 +15,7 @@ from .parent_device import WiLightParent DOMAIN = "wilight" # List the platforms that you want to support. -PLATFORMS = [Platform.COVER, Platform.FAN, Platform.LIGHT] +PLATFORMS = [Platform.COVER, Platform.FAN, Platform.LIGHT, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index dc8e5fc39cc..b7df1932cab 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -16,7 +16,7 @@ CONF_MODEL_NAME = "model_name" WILIGHT_MANUFACTURER = "All Automacao Ltda" # List the components supported by this integration. -ALLOWED_WILIGHT_COMPONENTS = ["cover", "fan", "light"] +ALLOWED_WILIGHT_COMPONENTS = ["cover", "fan", "light", "switch"] class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index c598e6db397..3d0c6d0ff39 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -13,7 +13,7 @@ from pywilight.const import ( WL_SPEED_LOW, WL_SPEED_MEDIUM, ) -from pywilight.wilight_device import Device as PyWiLightDevice +from pywilight.wilight_device import PyWiLightDevice from homeassistant.components.fan import DIRECTION_FORWARD, FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py index ea9e19dcb30..2509dc50737 100644 --- a/homeassistant/components/wilight/light.py +++ b/homeassistant/components/wilight/light.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from pywilight.const import ITEM_LIGHT, LIGHT_COLOR, LIGHT_DIMMER, LIGHT_ON_OFF -from pywilight.wilight_device import Device as PyWiLightDevice +from pywilight.wilight_device import PyWiLightDevice from homeassistant.components.light import ( ATTR_BRIGHTNESS, diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index 972de72a9c9..b1be0c80122 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.70"], + "requirements": ["pywilight==0.0.74"], "ssdp": [ { "manufacturer": "All Automacao Ltda" diff --git a/homeassistant/components/wilight/parent_device.py b/homeassistant/components/wilight/parent_device.py index 17a33fef633..8091e78cc76 100644 --- a/homeassistant/components/wilight/parent_device.py +++ b/homeassistant/components/wilight/parent_device.py @@ -5,7 +5,7 @@ import asyncio import logging import pywilight -from pywilight.wilight_device import Device as PyWiLightDevice +from pywilight.wilight_device import PyWiLightDevice import requests from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/wilight/services.yaml b/homeassistant/components/wilight/services.yaml new file mode 100644 index 00000000000..07a545bd5d7 --- /dev/null +++ b/homeassistant/components/wilight/services.yaml @@ -0,0 +1,24 @@ +set_watering_time: + description: Set watering time + target: + fields: + watering_time: + description: Duration for this irrigation to be turned on + example: 30 +set_pause_time: + description: Set pause time + target: + fields: + pause_time: + description: Duration for this irrigation to be paused + example: 24 +set_trigger: + description: Set trigger + target: + fields: + trigger_index: + description: Index of Trigger from 1 to 4 + example: "1" + trigger: + description: Configuration of trigger + example: "'12707001'" diff --git a/homeassistant/components/wilight/support.py b/homeassistant/components/wilight/support.py new file mode 100644 index 00000000000..6a03a854c70 --- /dev/null +++ b/homeassistant/components/wilight/support.py @@ -0,0 +1,87 @@ +"""Support for config validation using voluptuous and Translate Trigger.""" +from __future__ import annotations + +import calendar +import locale +import re +from typing import Any + +import voluptuous as vol + + +def wilight_trigger(value: Any) -> str | None: + """Check rules for WiLight Trigger.""" + step = 1 + err_desc = "Value is None" + result_128 = False + result_24 = False + result_60 = False + result_2 = False + + if value is not None: + step = 2 + err_desc = "Expected a string" + + if (step == 2) & isinstance(value, str): + step = 3 + err_desc = "String should only contain 8 decimals character" + if re.search(r"^([0-9]{8})$", value) is not None: + step = 4 + err_desc = "First 3 character should be less than 128" + result_128 = int(value[0:3]) < 128 + result_24 = int(value[3:5]) < 24 + result_60 = int(value[5:7]) < 60 + result_2 = int(value[7:8]) < 2 + + if (step == 4) & result_128: + step = 5 + err_desc = "Hour part should be less than 24" + + if (step == 5) & result_24: + step = 6 + err_desc = "Minute part should be less than 60" + + if (step == 6) & result_60: + step = 7 + err_desc = "Active part shoul be less than 2" + + if (step == 7) & result_2: + return value + + raise vol.Invalid(err_desc) + + +def wilight_to_hass_trigger(value: str | None) -> str | None: + """Convert wilight trigger to hass description. + + Ex: "12719001" -> "sun mon tue wed thu fri sat 19:00 On" + "00000000" -> "00:00 Off" + """ + if value is None: + return value + + locale.setlocale(locale.LC_ALL, "") + week_days = list(calendar.day_abbr) + days = bin(int(value[0:3]))[2:].zfill(8) + desc = "" + if int(days[7:8]) == 1: + desc += f"{week_days[6]} " + if int(days[6:7]) == 1: + desc += f"{week_days[0]} " + if int(days[5:6]) == 1: + desc += f"{week_days[1]} " + if int(days[4:5]) == 1: + desc += f"{week_days[2]} " + if int(days[3:4]) == 1: + desc += f"{week_days[3]} " + if int(days[2:3]) == 1: + desc += f"{week_days[4]} " + if int(days[1:2]) == 1: + desc += f"{week_days[5]} " + desc += f"{value[3:5]}:{value[5:7]} " + if int(value[7:8]) == 1: + desc += "On" + else: + desc += "Off" + + return desc diff --git a/homeassistant/components/wilight/switch.py b/homeassistant/components/wilight/switch.py new file mode 100644 index 00000000000..920fb66908c --- /dev/null +++ b/homeassistant/components/wilight/switch.py @@ -0,0 +1,322 @@ +"""Support for WiLight switches.""" +from __future__ import annotations + +from typing import Any + +from pywilight.const import ITEM_SWITCH, SWITCH_PAUSE_VALVE, SWITCH_VALVE +from pywilight.wilight_device import PyWiLightDevice +import voluptuous as vol + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN, WiLightDevice +from .parent_device import WiLightParent +from .support import wilight_to_hass_trigger, wilight_trigger as wl_trigger + +# Attr of features supported by the valve switch entities +ATTR_WATERING_TIME = "watering_time" +ATTR_PAUSE_TIME = "pause_time" +ATTR_TRIGGER_1 = "trigger_1" +ATTR_TRIGGER_2 = "trigger_2" +ATTR_TRIGGER_3 = "trigger_3" +ATTR_TRIGGER_4 = "trigger_4" +ATTR_TRIGGER_1_DESC = "trigger_1_description" +ATTR_TRIGGER_2_DESC = "trigger_2_description" +ATTR_TRIGGER_3_DESC = "trigger_3_description" +ATTR_TRIGGER_4_DESC = "trigger_4_description" + +# Attr of services data supported by the valve switch entities +ATTR_TRIGGER = "trigger" +ATTR_TRIGGER_INDEX = "trigger_index" + +# Service of features supported by the valve switch entities +SERVICE_SET_WATERING_TIME = "set_watering_time" +SERVICE_SET_PAUSE_TIME = "set_pause_time" +SERVICE_SET_TRIGGER = "set_trigger" + +# Range of features supported by the valve switch entities +RANGE_WATERING_TIME = 1800 +RANGE_PAUSE_TIME = 24 +RANGE_TRIGGER_INDEX = 4 + +# Service call validation schemas +VALID_WATERING_TIME = vol.All( + vol.Coerce(int), vol.Range(min=1, max=RANGE_WATERING_TIME) +) +VALID_PAUSE_TIME = vol.All(vol.Coerce(int), vol.Range(min=1, max=RANGE_PAUSE_TIME)) +VALID_TRIGGER_INDEX = vol.All( + vol.Coerce(int), vol.Range(min=1, max=RANGE_TRIGGER_INDEX) +) + +# Descriptions of the valve switch entities +DESC_WATERING = "watering" +DESC_PAUSE = "pause" + +# Icons of the valve switch entities +ICON_WATERING = "mdi:water" +ICON_PAUSE = "mdi:pause-circle-outline" + + +def entities_from_discovered_wilight(api_device: PyWiLightDevice) -> tuple[Any]: + """Parse configuration and add WiLight switch entities.""" + entities: Any = [] + for item in api_device.items: + if item["type"] == ITEM_SWITCH: + index = item["index"] + item_name = item["name"] + if item["sub_type"] == SWITCH_VALVE: + entities.append(WiLightValveSwitch(api_device, index, item_name)) + elif item["sub_type"] == SWITCH_PAUSE_VALVE: + entities.append(WiLightValvePauseSwitch(api_device, index, item_name)) + + return entities + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up WiLight switches from a config entry.""" + parent: WiLightParent = hass.data[DOMAIN][entry.entry_id] + + # Handle a discovered WiLight device. + assert parent.api + entities = entities_from_discovered_wilight(parent.api) + async_add_entities(entities) + + # Handle services for a discovered WiLight device. + async def set_watering_time(entity, service: Any) -> None: + if not isinstance(entity, WiLightValveSwitch): + raise ValueError("Entity is not a WiLight valve switch") + watering_time = service.data[ATTR_WATERING_TIME] + await entity.async_set_watering_time(watering_time=watering_time) + + async def set_trigger(entity, service: Any) -> None: + if not isinstance(entity, WiLightValveSwitch): + raise ValueError("Entity is not a WiLight valve switch") + trigger_index = service.data[ATTR_TRIGGER_INDEX] + trigger = service.data[ATTR_TRIGGER] + await entity.async_set_trigger(trigger_index=trigger_index, trigger=trigger) + + async def set_pause_time(entity, service: Any) -> None: + if not isinstance(entity, WiLightValvePauseSwitch): + raise ValueError("Entity is not a WiLight valve pause switch") + pause_time = service.data[ATTR_PAUSE_TIME] + await entity.async_set_pause_time(pause_time=pause_time) + + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_SET_WATERING_TIME, + { + vol.Required(ATTR_WATERING_TIME): VALID_WATERING_TIME, + }, + set_watering_time, + ) + + platform.async_register_entity_service( + SERVICE_SET_TRIGGER, + { + vol.Required(ATTR_TRIGGER_INDEX): VALID_TRIGGER_INDEX, + vol.Required(ATTR_TRIGGER): wl_trigger, + }, + set_trigger, + ) + + platform.async_register_entity_service( + SERVICE_SET_PAUSE_TIME, + { + vol.Required(ATTR_PAUSE_TIME): VALID_PAUSE_TIME, + }, + set_pause_time, + ) + + +def wilight_to_hass_pause_time(value: int) -> int: + """Convert wilight pause_time seconds to hass hour.""" + return round(value / 3600) + + +def hass_to_wilight_pause_time(value: int) -> int: + """Convert hass pause_time hours to wilight seconds.""" + return round(value * 3600) + + +class WiLightValveSwitch(WiLightDevice, SwitchEntity): + """Representation of a WiLights Valve switch.""" + + @property + def name(self) -> str: + """Return the name of the switch.""" + return f"{self._attr_name} {DESC_WATERING}" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._status.get("on", False) + + @property + def watering_time(self) -> int | None: + """Return watering time of valve switch. + + None is unknown, 1 is minimum, 1800 is maximum. + """ + return self._status.get("timer_target") + + @property + def trigger_1(self) -> str | None: + """Return trigger_1 of valve switch.""" + return self._status.get("trigger_1") + + @property + def trigger_2(self) -> str | None: + """Return trigger_2 of valve switch.""" + return self._status.get("trigger_2") + + @property + def trigger_3(self) -> str | None: + """Return trigger_3 of valve switch.""" + return self._status.get("trigger_3") + + @property + def trigger_4(self) -> str | None: + """Return trigger_4 of valve switch.""" + return self._status.get("trigger_4") + + @property + def trigger_1_description(self) -> str | None: + """Return trigger_1_description of valve switch.""" + return wilight_to_hass_trigger(self._status.get("trigger_1")) + + @property + def trigger_2_description(self) -> str | None: + """Return trigger_2_description of valve switch.""" + return wilight_to_hass_trigger(self._status.get("trigger_2")) + + @property + def trigger_3_description(self) -> str | None: + """Return trigger_3_description of valve switch.""" + return wilight_to_hass_trigger(self._status.get("trigger_3")) + + @property + def trigger_4_description(self) -> str | None: + """Return trigger_4_description of valve switch.""" + return wilight_to_hass_trigger(self._status.get("trigger_4")) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + attr: dict[str, Any] = {} + + if self.watering_time is not None: + attr[ATTR_WATERING_TIME] = self.watering_time + + if self.trigger_1 is not None: + attr[ATTR_TRIGGER_1] = self.trigger_1 + + if self.trigger_2 is not None: + attr[ATTR_TRIGGER_2] = self.trigger_2 + + if self.trigger_3 is not None: + attr[ATTR_TRIGGER_3] = self.trigger_3 + + if self.trigger_4 is not None: + attr[ATTR_TRIGGER_4] = self.trigger_4 + + if self.trigger_1_description is not None: + attr[ATTR_TRIGGER_1_DESC] = self.trigger_1_description + + if self.trigger_2_description is not None: + attr[ATTR_TRIGGER_2_DESC] = self.trigger_2_description + + if self.trigger_3_description is not None: + attr[ATTR_TRIGGER_3_DESC] = self.trigger_3_description + + if self.trigger_4_description is not None: + attr[ATTR_TRIGGER_4_DESC] = self.trigger_4_description + + return attr + + @property + def icon(self) -> str: + """Return the icon to use in the frontend.""" + return ICON_WATERING + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self._client.turn_on(self._index) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self._client.turn_off(self._index) + + async def async_set_watering_time(self, watering_time: int) -> None: + """Set the watering time.""" + await self._client.set_switch_time(self._index, watering_time) + + async def async_set_trigger(self, trigger_index: int, trigger: str) -> None: + """Set the trigger according to index.""" + if trigger_index == 1: + await self._client.set_switch_trigger_1(self._index, trigger) + if trigger_index == 2: + await self._client.set_switch_trigger_2(self._index, trigger) + if trigger_index == 3: + await self._client.set_switch_trigger_3(self._index, trigger) + if trigger_index == 4: + await self._client.set_switch_trigger_4(self._index, trigger) + + +class WiLightValvePauseSwitch(WiLightDevice, SwitchEntity): + """Representation of a WiLights Valve Pause switch.""" + + @property + def name(self) -> str: + """Return the name of the switch.""" + return f"{self._attr_name} {DESC_PAUSE}" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._status.get("on", False) + + @property + def pause_time(self) -> int | None: + """Return pause time of valve switch. + + None is unknown, 1 is minimum, 24 is maximum. + """ + pause_time = self._status.get("timer_target") + if pause_time is not None: + return wilight_to_hass_pause_time(pause_time) + return pause_time + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + attr: dict[str, Any] = {} + + if self.pause_time is not None: + attr[ATTR_PAUSE_TIME] = self.pause_time + + return attr + + @property + def icon(self) -> str: + """Return the icon to use in the frontend.""" + return ICON_PAUSE + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self._client.turn_on(self._index) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self._client.turn_off(self._index) + + async def async_set_pause_time(self, pause_time: int) -> None: + """Set the pause time.""" + target_time = hass_to_wilight_pause_time(pause_time) + await self._client.set_switch_time(self._index, target_time) diff --git a/requirements_all.txt b/requirements_all.txt index 8dbcab9d520..44075fdf14a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2039,7 +2039,7 @@ pywebpush==1.9.2 pywemo==0.9.1 # homeassistant.components.wilight -pywilight==0.0.70 +pywilight==0.0.74 # homeassistant.components.wiz pywizlight==0.5.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eba56137019..5470bf91d73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1378,7 +1378,7 @@ pywebpush==1.9.2 pywemo==0.9.1 # homeassistant.components.wilight -pywilight==0.0.70 +pywilight==0.0.74 # homeassistant.components.wiz pywizlight==0.5.14 diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py index dbcfbbaaa8c..acaf2aef2a8 100644 --- a/tests/components/wilight/__init__.py +++ b/tests/components/wilight/__init__.py @@ -27,6 +27,7 @@ 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_NAME_SWITCH = "WiLight 0105001900010011-00000000000010" UPNP_MODEL_NUMBER = "123456789012345678901234567890123456" UPNP_SERIAL = "000000000099" UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56" diff --git a/tests/components/wilight/test_switch.py b/tests/components/wilight/test_switch.py new file mode 100644 index 00000000000..035f5b37be5 --- /dev/null +++ b/tests/components/wilight/test_switch.py @@ -0,0 +1,264 @@ +"""Tests for the WiLight integration.""" +from unittest.mock import patch + +import pytest +import pywilight + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.wilight import DOMAIN as WILIGHT_DOMAIN +from homeassistant.components.wilight.switch import ( + ATTR_PAUSE_TIME, + ATTR_TRIGGER, + ATTR_TRIGGER_1, + ATTR_TRIGGER_2, + ATTR_TRIGGER_3, + ATTR_TRIGGER_4, + ATTR_TRIGGER_INDEX, + ATTR_WATERING_TIME, + SERVICE_SET_PAUSE_TIME, + SERVICE_SET_TRIGGER, + SERVICE_SET_WATERING_TIME, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import ( + HOST, + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_SWITCH, + UPNP_MODEL_NUMBER, + UPNP_SERIAL, + WILIGHT_ID, + setup_integration, +) + + +@pytest.fixture(name="dummy_device_from_host_switch") +def mock_dummy_device_from_host_switch(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_SWITCH, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", + return_value=device, + ): + yield device + + +async def test_loading_switch( + hass: HomeAssistant, + dummy_device_from_host_switch, +) -> None: + """Test the WiLight configuration entry loading.""" + + entry = await setup_integration(hass) + assert entry + assert entry.unique_id == WILIGHT_ID + + entity_registry = er.async_get(hass) + + # First segment of the strip + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wl000000000099_1_watering") + assert entry + assert entry.unique_id == "WL000000000099_0" + + # Seconnd segment of the strip + state = hass.states.get("switch.wl000000000099_2_pause") + assert state + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wl000000000099_2_pause") + assert entry + assert entry.unique_id == "WL000000000099_1" + + +async def test_on_off_switch_state( + hass: HomeAssistant, dummy_device_from_host_switch +) -> None: + """Test the change of state of the switch.""" + await setup_integration(hass) + + # On - watering + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wl000000000099_1_watering"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.state == STATE_ON + + # Off - watering + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wl000000000099_1_watering"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.state == STATE_OFF + + # On - pause + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wl000000000099_2_pause"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_2_pause") + assert state + assert state.state == STATE_ON + + # Off - pause + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wl000000000099_2_pause"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_2_pause") + assert state + assert state.state == STATE_OFF + + +async def test_switch_services( + hass: HomeAssistant, dummy_device_from_host_switch +) -> None: + """Test the services of the switch.""" + await setup_integration(hass) + + # Set watering time + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_WATERING_TIME, + {ATTR_WATERING_TIME: 30, ATTR_ENTITY_ID: "switch.wl000000000099_1_watering"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_WATERING_TIME) == 30 + + # Set pause time + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_PAUSE_TIME, + {ATTR_PAUSE_TIME: 18, ATTR_ENTITY_ID: "switch.wl000000000099_2_pause"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_2_pause") + assert state + assert state.attributes.get(ATTR_PAUSE_TIME) == 18 + + # Set trigger_1 + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_TRIGGER, + { + ATTR_TRIGGER_INDEX: "1", + ATTR_TRIGGER: "12715301", + ATTR_ENTITY_ID: "switch.wl000000000099_1_watering", + }, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_TRIGGER_1) == "12715301" + + # Set trigger_2 + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_TRIGGER, + { + ATTR_TRIGGER_INDEX: "2", + ATTR_TRIGGER: "12707301", + ATTR_ENTITY_ID: "switch.wl000000000099_1_watering", + }, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_TRIGGER_2) == "12707301" + + # Set trigger_3 + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_TRIGGER, + { + ATTR_TRIGGER_INDEX: "3", + ATTR_TRIGGER: "00015301", + ATTR_ENTITY_ID: "switch.wl000000000099_1_watering", + }, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_TRIGGER_3) == "00015301" + + # Set trigger_4 + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_TRIGGER, + { + ATTR_TRIGGER_INDEX: "4", + ATTR_TRIGGER: "00008300", + ATTR_ENTITY_ID: "switch.wl000000000099_1_watering", + }, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_TRIGGER_4) == "00008300" + + # Set watering time using WiLight Pause Switch to raise + with pytest.raises(ValueError) as exc_info: + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_WATERING_TIME, + {ATTR_WATERING_TIME: 30, ATTR_ENTITY_ID: "switch.wl000000000099_2_pause"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert str(exc_info.value) == "Entity is not a WiLight valve switch" From cd1227d8b9c4ae677f1218aeabc9dea497393db9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 7 Aug 2022 15:04:04 +0200 Subject: [PATCH 201/903] Fix default sensor names in NextDNS integration (#76264) --- homeassistant/components/nextdns/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 168c0be8cd0..422bc2a237b 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -135,7 +135,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="TCP Queries", + name="TCP queries", native_unit_of_measurement="queries", state_class=SensorStateClass.TOTAL, value=lambda data: data.tcp_queries, @@ -190,7 +190,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="TCP Queries Ratio", + name="TCP queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.tcp_queries_ratio, From 4aeaeeda0dce5bd1a839cdf92b747cf6e8e6a90e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 7 Aug 2022 15:42:47 +0200 Subject: [PATCH 202/903] Postpone broadlink platform switch until config entry is ready (#76371) --- homeassistant/components/broadlink/switch.py | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 6a015748bd0..d38898a513f 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -26,11 +26,13 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import BroadlinkDevice from .const import DOMAIN from .entity import BroadlinkEntity from .helpers import data_packet, import_device, mac_address @@ -80,8 +82,18 @@ async def async_setup_platform( host = config.get(CONF_HOST) if switches := config.get(CONF_SWITCHES): - platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) - platform_data.setdefault(mac_addr, []).extend(switches) + platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {}) + async_add_entities_config_entry: AddEntitiesCallback + device: BroadlinkDevice + async_add_entities_config_entry, device = platform_data.get( + mac_addr, (None, None) + ) + if not async_add_entities_config_entry: + raise PlatformNotReady + + async_add_entities_config_entry( + BroadlinkRMSwitch(device, config) for config in switches + ) else: _LOGGER.warning( @@ -104,12 +116,8 @@ async def async_setup_entry( switches: list[BroadlinkSwitch] = [] if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}: - platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {}) - user_defined_switches = platform_data.get(device.api.mac, {}) - switches.extend( - BroadlinkRMSwitch(device, config) for config in user_defined_switches - ) - + platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) + platform_data[device.api.mac] = async_add_entities, device elif device.api.type == "SP1": switches.append(BroadlinkSP1Switch(device)) From 1fe44d0997bc56dabd3b44b94b715032e53141b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 05:03:56 -1000 Subject: [PATCH 203/903] Ensure bluetooth recovers if Dbus gets restarted (#76249) --- .../components/bluetooth/__init__.py | 68 ++++++++++++++++--- tests/components/bluetooth/test_init.py | 56 +++++++++++++++ 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index ed04ca401ed..9492642e0e0 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging +import time from typing import TYPE_CHECKING, Final import async_timeout @@ -56,6 +57,10 @@ START_TIMEOUT = 9 SOURCE_LOCAL: Final = "local" +SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 +SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) +MONOTONIC_TIME = time.monotonic + @dataclass class BluetoothServiceInfoBleak(BluetoothServiceInfo): @@ -259,9 +264,10 @@ async def async_setup_entry( ) -> bool: """Set up the bluetooth integration from a config entry.""" manager: BluetoothManager = hass.data[DOMAIN] - await manager.async_start( - BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) - ) + async with manager.start_stop_lock: + await manager.async_start( + BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) + ) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True @@ -270,8 +276,6 @@ async def _async_update_listener( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> None: """Handle options update.""" - manager: BluetoothManager = hass.data[DOMAIN] - manager.async_start_reload() await hass.config_entries.async_reload(entry.entry_id) @@ -280,7 +284,9 @@ async def async_unload_entry( ) -> bool: """Unload a config entry.""" manager: BluetoothManager = hass.data[DOMAIN] - await manager.async_stop() + async with manager.start_stop_lock: + manager.async_start_reload() + await manager.async_stop() return True @@ -296,13 +302,19 @@ class BluetoothManager: self.hass = hass self._integration_matcher = integration_matcher self.scanner: HaBleakScanner | None = None + self.start_stop_lock = asyncio.Lock() self._cancel_device_detected: CALLBACK_TYPE | None = None self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None + self._cancel_stop: CALLBACK_TYPE | None = None + self._cancel_watchdog: CALLBACK_TYPE | None = None self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] + self._last_detection = 0.0 self._reloading = False + self._adapter: str | None = None + self._scanning_mode = BluetoothScanningMode.ACTIVE @hass_callback def async_setup(self) -> None: @@ -324,6 +336,8 @@ class BluetoothManager: ) -> None: """Set up BT Discovery.""" assert self.scanner is not None + self._adapter = adapter + self._scanning_mode = scanning_mode if self._reloading: # On reload, we need to reset the scanner instance # since the devices in its history may not be reachable @@ -388,7 +402,32 @@ class BluetoothManager: _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + self._async_setup_scanner_watchdog() + self._cancel_stop = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping + ) + + @hass_callback + def _async_setup_scanner_watchdog(self) -> None: + """If Dbus gets restarted or updated, we need to restart the scanner.""" + self._last_detection = MONOTONIC_TIME() + self._cancel_watchdog = async_track_time_interval( + self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL + ) + + async def _async_scanner_watchdog(self, now: datetime) -> None: + """Check if the scanner is running.""" + time_since_last_detection = MONOTONIC_TIME() - self._last_detection + if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: + return + _LOGGER.info( + "Bluetooth scanner has gone quiet for %s, restarting", + SCANNER_WATCHDOG_INTERVAL, + ) + async with self.start_stop_lock: + self.async_start_reload() + await self.async_stop() + await self.async_start(self._scanning_mode, self._adapter) @hass_callback def async_setup_unavailable_tracking(self) -> None: @@ -423,6 +462,7 @@ class BluetoothManager: self, device: BLEDevice, advertisement_data: AdvertisementData ) -> None: """Handle a detected device.""" + self._last_detection = MONOTONIC_TIME() matched_domains = self._integration_matcher.match_domains( device, advertisement_data ) @@ -535,14 +575,26 @@ class BluetoothManager: for device_adv in self.scanner.history.values() ] - async def async_stop(self, event: Event | None = None) -> None: + async def _async_hass_stopping(self, event: Event) -> None: + """Stop the Bluetooth integration at shutdown.""" + self._cancel_stop = None + await self.async_stop() + + async def async_stop(self) -> None: """Stop bluetooth discovery.""" + _LOGGER.debug("Stopping bluetooth discovery") + if self._cancel_watchdog: + self._cancel_watchdog() + self._cancel_watchdog = None if self._cancel_device_detected: self._cancel_device_detected() self._cancel_device_detected = None if self._cancel_unavailable_tracking: self._cancel_unavailable_tracking() self._cancel_unavailable_tracking = None + if self._cancel_stop: + self._cancel_stop() + self._cancel_stop = None if self.scanner: try: await self.scanner.stop() # type: ignore[no-untyped-call] diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 6cd22505dc4..45babd05748 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -10,6 +10,8 @@ import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, @@ -1562,3 +1564,57 @@ async def test_invalid_dbus_message(hass, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert "dbus" in caplog.text + + +async def test_recovery_from_dbus_restart( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test we can recover when DBus gets restarted out from under us.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) + await hass.async_block_till_done() + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + start_time_monotonic = 1000 + scanner = _get_underlying_scanner() + mock_discovered = [MagicMock()] + type(scanner).discovered_devices = mock_discovered + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + # Fire a callback to reset the timer + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic, + ): + scanner._callback( + BLEDevice("44:44:33:11:23:42", "any_name"), + AdvertisementData(local_name="any_name"), + ) + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + # We hit the timer, so we restart the scanner + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 2 From a6963e6a38cb53f8d4caa3d9d0e69976dbc348d2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 Aug 2022 17:06:03 +0200 Subject: [PATCH 204/903] Add zwave_js usb port selection (#76385) --- .../components/zwave_js/config_flow.py | 39 ++++++++++++-- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/zwave_js/test_config_flow.py | 52 +++++++++++++++++-- 5 files changed, 87 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 60807471e03..3da785cdcf2 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -8,6 +8,7 @@ from typing import Any import aiohttp from async_timeout import timeout +from serial.tools import list_ports import voluptuous as vol from zwave_js_server.version import VersionInfo, get_server_version @@ -119,6 +120,30 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio return version_info +def get_usb_ports() -> dict[str, str]: + """Return a dict of USB ports and their friendly names.""" + ports = list_ports.comports() + port_descriptions = {} + for port in ports: + usb_device = usb.usb_device_from_port(port) + dev_path = usb.get_serial_by_id(usb_device.device) + human_name = usb.human_readable_device_name( + dev_path, + usb_device.serial_number, + usb_device.manufacturer, + usb_device.description, + usb_device.vid, + usb_device.pid, + ) + port_descriptions[dev_path] = human_name + return port_descriptions + + +async def async_get_usb_ports(hass: HomeAssistant) -> dict[str, str]: + """Return a dict of USB ports and their friendly names.""" + return await hass.async_add_executor_job(get_usb_ports) + + class BaseZwaveJSFlow(FlowHandler): """Represent the base config flow for Z-Wave JS.""" @@ -402,7 +427,9 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): vid, pid, ) - self.context["title_placeholders"] = {CONF_NAME: self._title} + self.context["title_placeholders"] = { + CONF_NAME: self._title.split(" - ")[0].strip() + } return await self.async_step_usb_confirm() async def async_step_usb_confirm( @@ -579,7 +606,11 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): } if not self._usb_discovery: - schema = {vol.Required(CONF_USB_PATH, default=usb_path): str, **schema} + ports = await async_get_usb_ports(self.hass) + schema = { + vol.Required(CONF_USB_PATH, default=usb_path): vol.In(ports), + **schema, + } data_schema = vol.Schema(schema) @@ -801,9 +832,11 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info") emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False) + ports = await async_get_usb_ports(self.hass) + data_schema = vol.Schema( { - vol.Required(CONF_USB_PATH, default=usb_path): str, + vol.Required(CONF_USB_PATH, default=usb_path): vol.In(ports), vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str, vol.Optional( CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 29be66cd024..9ee08b0505d 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.40.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.40.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 44075fdf14a..7e07889d05f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1815,6 +1815,7 @@ pyserial-asyncio==0.6 # homeassistant.components.crownstone # homeassistant.components.usb # homeassistant.components.zha +# homeassistant.components.zwave_js pyserial==3.5 # homeassistant.components.sesame diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5470bf91d73..e04b0d7cea5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1250,6 +1250,7 @@ pyserial-asyncio==0.6 # homeassistant.components.crownstone # homeassistant.components.usb # homeassistant.components.zha +# homeassistant.components.zwave_js pyserial==3.5 # homeassistant.components.sia diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index a8a2c6c7191..6c4b18e8dc3 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1,9 +1,12 @@ """Test the Z-Wave JS config flow.""" import asyncio -from unittest.mock import DEFAULT, call, patch +from collections.abc import Generator +from copy import copy +from unittest.mock import DEFAULT, MagicMock, call, patch import aiohttp import pytest +from serial.tools.list_ports_common import ListPortInfo from zwave_js_server.version import VersionInfo from homeassistant import config_entries @@ -134,6 +137,45 @@ def mock_addon_setup_time(): yield addon_setup_time +@pytest.fixture(name="serial_port") +def serial_port_fixture() -> ListPortInfo: + """Return a mock serial port.""" + port = ListPortInfo("/test", skip_link_detection=True) + port.serial_number = "1234" + port.manufacturer = "Virtual serial port" + port.device = "/test" + port.description = "Some serial port" + port.pid = 9876 + port.vid = 5678 + + return port + + +@pytest.fixture(name="mock_list_ports", autouse=True) +def mock_list_ports_fixture(serial_port) -> Generator[MagicMock, None, None]: + """Mock list ports.""" + with patch( + "homeassistant.components.zwave_js.config_flow.list_ports.comports" + ) as mock_list_ports: + another_port = copy(serial_port) + another_port.device = "/new" + another_port.description = "New serial port" + another_port.serial_number = "5678" + another_port.pid = 8765 + mock_list_ports.return_value = [serial_port, another_port] + yield mock_list_ports + + +@pytest.fixture(name="mock_usb_serial_by_id", autouse=True) +def mock_usb_serial_by_id_fixture() -> Generator[MagicMock, None, None]: + """Mock usb serial by id.""" + with patch( + "homeassistant.components.zwave_js.config_flow.usb.get_serial_by_id" + ) as mock_usb_serial_by_id: + mock_usb_serial_by_id.side_effect = lambda x: x + yield mock_usb_serial_by_id + + async def test_manual(hass): """Test we create an entry with manual step.""" @@ -1397,7 +1439,7 @@ async def test_addon_installed_already_configured( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - "usb_path": "/test_new", + "usb_path": "/new", "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -1410,7 +1452,7 @@ async def test_addon_installed_already_configured( "core_zwave_js", { "options": { - "device": "/test_new", + "device": "/new", "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -1430,7 +1472,7 @@ async def test_addon_installed_already_configured( assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data["url"] == "ws://host1:3001" - assert entry.data["usb_path"] == "/test_new" + assert entry.data["usb_path"] == "/new" assert entry.data["s0_legacy_key"] == "new123" assert entry.data["s2_access_control_key"] == "new456" assert entry.data["s2_authenticated_key"] == "new789" @@ -2380,8 +2422,10 @@ async def test_import_addon_installed( set_addon_options, start_addon, get_addon_discovery_info, + serial_port, ): """Test import step while add-on already installed on Supervisor.""" + serial_port.device = "/test/imported" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, From ddf3d23c27739b64baee589e2f97ef6b00886d24 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 7 Aug 2022 18:10:32 +0200 Subject: [PATCH 205/903] Fix opentherm_gw startup failure handling (#76376) --- homeassistant/components/opentherm_gw/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index cdf360c8795..51071c9a0a1 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -117,6 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b timeout=CONNECTION_TIMEOUT, ) except (asyncio.TimeoutError, ConnectionError, SerialException) as ex: + await gateway.cleanup() raise ConfigEntryNotReady( f"Could not connect to gateway at {gateway.device_path}: {ex}" ) from ex From c7838c347f97d5da1c18b009ff6d226ceb04a3b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 06:11:11 -1000 Subject: [PATCH 206/903] Bump zeroconf to 0.39.0 (#76328) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 8061be2cf8a..4438a22040d 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.38.7"], + "requirements": ["zeroconf==0.39.0"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c0221647abf..f15daf093a3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.7.2 -zeroconf==0.38.7 +zeroconf==0.39.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 7e07889d05f..54606de114a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2515,7 +2515,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.7 +zeroconf==0.39.0 # homeassistant.components.zha zha-quirks==0.0.78 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e04b0d7cea5..c3fc6b31256 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1692,7 +1692,7 @@ yolink-api==0.0.9 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.7 +zeroconf==0.39.0 # homeassistant.components.zha zha-quirks==0.0.78 From 36808a0db42e0157e936cd806eadb9afe7c57699 Mon Sep 17 00:00:00 2001 From: rlippmann <70883373+rlippmann@users.noreply.github.com> Date: Sun, 7 Aug 2022 12:27:17 -0400 Subject: [PATCH 207/903] Add ecobee Smart Premium thermostat (#76365) Update const.py Add device model constant for ecobee Smart Premium thermostat --- homeassistant/components/ecobee/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 50dd606ad25..b4e0c485e45 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -36,6 +36,7 @@ ECOBEE_MODEL_TO_NAME = { "nikeEms": "ecobee3 lite EMS", "apolloSmart": "ecobee4 Smart", "vulcanSmart": "ecobee4 Smart", + "aresSmart": "ecobee Smart Premium", } PLATFORMS = [ From ea88f229a32dd2435dd661869f3d8e4ee02ec056 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 7 Aug 2022 12:41:12 -0500 Subject: [PATCH 208/903] Bump plexapi to 4.12.1 (#76393) --- homeassistant/components/plex/manifest.json | 2 +- homeassistant/components/plex/server.py | 34 ++++++++++----------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 912732efe98..1875b0e05dc 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.11.2", + "plexapi==4.12.1", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index b136bec73e9..058e8abbecd 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -462,24 +462,24 @@ class PlexServer: continue session_username = next(iter(session.usernames), None) - for player in session.players: - unique_id = f"{self.machine_identifier}:{player.machineIdentifier}" - if unique_id not in self.active_sessions: - _LOGGER.debug("Creating new Plex session: %s", session) - self.active_sessions[unique_id] = PlexSession(self, session) - if session_username and session_username not in monitored_users: - ignored_clients.add(player.machineIdentifier) - _LOGGER.debug( - "Ignoring %s client owned by '%s'", - player.product, - session_username, - ) - continue + player = session.player + unique_id = f"{self.machine_identifier}:{player.machineIdentifier}" + if unique_id not in self.active_sessions: + _LOGGER.debug("Creating new Plex session: %s", session) + self.active_sessions[unique_id] = PlexSession(self, session) + if session_username and session_username not in monitored_users: + ignored_clients.add(player.machineIdentifier) + _LOGGER.debug( + "Ignoring %s client owned by '%s'", + player.product, + session_username, + ) + continue - process_device("session", player) - available_clients[player.machineIdentifier][ - "session" - ] = self.active_sessions[unique_id] + process_device("session", player) + available_clients[player.machineIdentifier][ + "session" + ] = self.active_sessions[unique_id] for device in devices: process_device("PMS", device) diff --git a/requirements_all.txt b/requirements_all.txt index 54606de114a..48f802fa367 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1256,7 +1256,7 @@ pillow==9.2.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.11.2 +plexapi==4.12.1 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3fc6b31256..1ffcf08ea57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -874,7 +874,7 @@ pilight==0.1.1 pillow==9.2.0 # homeassistant.components.plex -plexapi==4.11.2 +plexapi==4.12.1 # homeassistant.components.plex plexauth==0.0.6 From 89e0db354859fa49d5c4489c6cf576f7033a0cb8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 Aug 2022 14:36:30 -0400 Subject: [PATCH 209/903] Remove Z-Wave JS trigger uart USB id (#76391) --- homeassistant/components/zwave_js/manifest.json | 9 --------- homeassistant/generated/usb.py | 5 ----- 2 files changed, 14 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 9ee08b0505d..5f9c8df7afc 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -18,15 +18,6 @@ "pid": "8A2A", "description": "*z-wave*", "known_devices": ["Nortek HUSBZB-1"] - }, - { - "vid": "10C4", - "pid": "EA60", - "known_devices": [ - "Aeotec Z-Stick 7", - "Silicon Labs UZB-7", - "Zooz ZST10 700" - ] } ], "zeroconf": ["_zwave-js-server._tcp.local."], diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index ef75fbef4d1..3583f96dc2c 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -105,10 +105,5 @@ USB = [ "vid": "10C4", "pid": "8A2A", "description": "*z-wave*" - }, - { - "domain": "zwave_js", - "vid": "10C4", - "pid": "EA60" } ] From d14b76e7fcf37048953398b54e552b44824a4063 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 8 Aug 2022 04:45:26 +1000 Subject: [PATCH 210/903] Enable the LIFX diagnostic buttons by default (#76389) --- homeassistant/components/lifx/button.py | 2 -- tests/components/lifx/test_button.py | 22 ++-------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/lifx/button.py b/homeassistant/components/lifx/button.py index 6d3f4fe51bf..3a4c73d2889 100644 --- a/homeassistant/components/lifx/button.py +++ b/homeassistant/components/lifx/button.py @@ -19,14 +19,12 @@ RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription( key=RESTART, name="Restart", device_class=ButtonDeviceClass.RESTART, - entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ) IDENTIFY_BUTTON_DESCRIPTION = ButtonEntityDescription( key=IDENTIFY, name="Identify", - entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ) diff --git a/tests/components/lifx/test_button.py b/tests/components/lifx/test_button.py index a485c882100..abc91128e25 100644 --- a/tests/components/lifx/test_button.py +++ b/tests/components/lifx/test_button.py @@ -43,18 +43,9 @@ async def test_button_restart(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) assert entity - assert entity.disabled + assert not entity.disabled assert entity.unique_id == unique_id - enabled_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) - assert not enabled_entity.disabled - - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=bulb): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - await hass.services.async_call( BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) @@ -84,18 +75,9 @@ async def test_button_identify(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) assert entity - assert entity.disabled + assert not entity.disabled assert entity.unique_id == unique_id - enabled_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) - assert not enabled_entity.disabled - - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=bulb): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - await hass.services.async_call( BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) From 7deeea02c2c0c6a8312293c713ca0edab78acab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sun, 7 Aug 2022 21:29:07 +0200 Subject: [PATCH 211/903] Update aioairzone to v0.4.8 (#76404) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index e4f94327708..bd8ed6b9920 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.6"], + "requirements": ["aioairzone==0.4.8"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 48f802fa367..50a4e5860bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -113,7 +113,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.6 +aioairzone==0.4.8 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ffcf08ea57..8207b8edfd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.6 +aioairzone==0.4.8 # homeassistant.components.ambient_station aioambient==2021.11.0 From e89459453b5414ed9f4e9e6434698ad5bf00f202 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 13:44:27 -0600 Subject: [PATCH 212/903] Add more controller-related RainMachine diagnostics (#76409) --- .../components/rainmachine/diagnostics.py | 35 +++- tests/components/rainmachine/conftest.py | 16 ++ .../fixtures/api_versions_data.json | 5 + .../fixtures/diagnostics_current_data.json | 21 ++ .../rainmachine/test_diagnostics.py | 198 ++++++++++-------- 5 files changed, 182 insertions(+), 93 deletions(-) create mode 100644 tests/components/rainmachine/fixtures/api_versions_data.json create mode 100644 tests/components/rainmachine/fixtures/diagnostics_current_data.json diff --git a/homeassistant/components/rainmachine/diagnostics.py b/homeassistant/components/rainmachine/diagnostics.py index 58b918c18ee..131f2e130e7 100644 --- a/homeassistant/components/rainmachine/diagnostics.py +++ b/homeassistant/components/rainmachine/diagnostics.py @@ -1,20 +1,38 @@ """Diagnostics support for RainMachine.""" from __future__ import annotations +import asyncio from typing import Any +from regenmaschine.errors import RequestError + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD +from homeassistant.const import ( + CONF_ELEVATION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_PASSWORD, +) from homeassistant.core import HomeAssistant from . import RainMachineData from .const import DOMAIN +CONF_STATION_ID = "stationID" +CONF_STATION_NAME = "stationName" +CONF_STATION_SOURCE = "stationSource" +CONF_TIMEZONE = "timezone" + TO_REDACT = { + CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD, + CONF_STATION_ID, + CONF_STATION_NAME, + CONF_STATION_SOURCE, + CONF_TIMEZONE, } @@ -24,6 +42,14 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + controller_tasks = { + "versions": data.controller.api.versions(), + "current_diagnostics": data.controller.diagnostics.current(), + } + controller_results = await asyncio.gather( + *controller_tasks.values(), return_exceptions=True + ) + return { "entry": { "title": entry.title, @@ -39,10 +65,9 @@ async def async_get_config_entry_diagnostics( TO_REDACT, ), "controller": { - "api_version": data.controller.api_version, - "hardware_version": data.controller.hardware_version, - "name": data.controller.name, - "software_version": data.controller.software_version, + category: result + for category, result in zip(controller_tasks, controller_results) + if not isinstance(result, RequestError) }, }, } diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py index 457df4a3ef2..72b4a5d4293 100644 --- a/tests/components/rainmachine/conftest.py +++ b/tests/components/rainmachine/conftest.py @@ -39,6 +39,8 @@ def config_entry_fixture(hass, config, controller_mac): @pytest.fixture(name="controller") def controller_fixture( controller_mac, + data_api_versions, + data_diagnostics_current, data_programs, data_provision_settings, data_restrictions_current, @@ -55,6 +57,8 @@ def controller_fixture( controller.mac = controller_mac controller.software_version = "4.0.925" + controller.api.versions.return_value = data_api_versions + controller.diagnostics.current.return_value = data_diagnostics_current controller.programs.all.return_value = data_programs controller.provisioning.settings.return_value = data_provision_settings controller.restrictions.current.return_value = data_restrictions_current @@ -70,6 +74,18 @@ def controller_mac_fixture(): return "aa:bb:cc:dd:ee:ff" +@pytest.fixture(name="data_api_versions", scope="session") +def data_api_versions_fixture(): + """Define API version data.""" + return json.loads(load_fixture("api_versions_data.json", "rainmachine")) + + +@pytest.fixture(name="data_diagnostics_current", scope="session") +def data_diagnostics_current_fixture(): + """Define current diagnostics data.""" + return json.loads(load_fixture("diagnostics_current_data.json", "rainmachine")) + + @pytest.fixture(name="data_programs", scope="session") def data_programs_fixture(): """Define program data.""" diff --git a/tests/components/rainmachine/fixtures/api_versions_data.json b/tests/components/rainmachine/fixtures/api_versions_data.json new file mode 100644 index 00000000000..d4ec1fb80e9 --- /dev/null +++ b/tests/components/rainmachine/fixtures/api_versions_data.json @@ -0,0 +1,5 @@ +{ + "apiVer": "4.6.1", + "hwVer": 3, + "swVer": "4.0.1144" +} diff --git a/tests/components/rainmachine/fixtures/diagnostics_current_data.json b/tests/components/rainmachine/fixtures/diagnostics_current_data.json new file mode 100644 index 00000000000..40afd504911 --- /dev/null +++ b/tests/components/rainmachine/fixtures/diagnostics_current_data.json @@ -0,0 +1,21 @@ +{ + "hasWifi": true, + "uptime": "3 days, 18:14:14", + "uptimeSeconds": 324854, + "memUsage": 16196, + "networkStatus": true, + "bootCompleted": true, + "lastCheckTimestamp": 1659895175, + "wizardHasRun": true, + "standaloneMode": false, + "cpuUsage": 1, + "lastCheck": "2022-08-07 11:59:35", + "softwareVersion": "4.0.1144", + "internetStatus": true, + "locationStatus": true, + "timeStatus": true, + "wifiMode": null, + "gatewayAddress": "172.16.20.1", + "cloudStatus": 0, + "weatherStatus": true +} diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index d770d01fd36..b0876a2f597 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -19,6 +19,90 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach }, "data": { "coordinator": { + "provision.settings": { + "system": { + "httpEnabled": True, + "rainSensorSnoozeDuration": 0, + "uiUnitsMetric": False, + "programZonesShowInactive": False, + "programSingleSchedule": False, + "standaloneMode": False, + "masterValveAfter": 0, + "touchSleepTimeout": 10, + "selfTest": False, + "useSoftwareRainSensor": False, + "defaultZoneWateringDuration": 300, + "maxLEDBrightness": 40, + "simulatorHistorySize": 0, + "vibration": False, + "masterValveBefore": 0, + "touchProgramToRun": None, + "useRainSensor": False, + "wizardHasRun": True, + "waterLogHistorySize": 365, + "netName": "Home", + "softwareRainSensorMinQPF": 5, + "touchAdvanced": False, + "useBonjourService": True, + "hardwareVersion": 3, + "touchLongPressTimeout": 3, + "showRestrictionsOnLed": False, + "parserDataSizeInDays": 6, + "programListShowInactive": True, + "parserHistorySize": 365, + "allowAlexaDiscovery": False, + "automaticUpdates": True, + "minLEDBrightness": 0, + "minWateringDurationThreshold": 0, + "localValveCount": 12, + "touchAuthAPSeconds": 60, + "useCommandLineArguments": False, + "databasePath": "/rainmachine-app/DB/Default", + "touchCyclePrograms": True, + "zoneListShowInactive": True, + "rainSensorRainStart": None, + "zoneDuration": [ + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + ], + "rainSensorIsNormallyClosed": True, + "useCorrectionForPast": True, + "useMasterValve": False, + "runParsersBeforePrograms": True, + "maxWateringCoef": 2, + "mixerHistorySize": 365, + }, + "location": { + "elevation": REDACTED, + "doyDownloaded": True, + "zip": None, + "windSensitivity": 0.5, + "krs": 0.16, + "stationID": REDACTED, + "stationSource": REDACTED, + "et0Average": 6.578, + "latitude": REDACTED, + "state": "Default", + "stationName": REDACTED, + "wsDays": 2, + "stationDownloaded": True, + "address": "Default", + "rainSensitivity": 0.8, + "timezone": REDACTED, + "longitude": REDACTED, + "name": "Home", + }, + }, "programs": [ { "uid": 1, @@ -297,90 +381,6 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach ], }, ], - "provision.settings": { - "system": { - "httpEnabled": True, - "rainSensorSnoozeDuration": 0, - "uiUnitsMetric": False, - "programZonesShowInactive": False, - "programSingleSchedule": False, - "standaloneMode": False, - "masterValveAfter": 0, - "touchSleepTimeout": 10, - "selfTest": False, - "useSoftwareRainSensor": False, - "defaultZoneWateringDuration": 300, - "maxLEDBrightness": 40, - "simulatorHistorySize": 0, - "vibration": False, - "masterValveBefore": 0, - "touchProgramToRun": None, - "useRainSensor": False, - "wizardHasRun": True, - "waterLogHistorySize": 365, - "netName": "Home", - "softwareRainSensorMinQPF": 5, - "touchAdvanced": False, - "useBonjourService": True, - "hardwareVersion": 3, - "touchLongPressTimeout": 3, - "showRestrictionsOnLed": False, - "parserDataSizeInDays": 6, - "programListShowInactive": True, - "parserHistorySize": 365, - "allowAlexaDiscovery": False, - "automaticUpdates": True, - "minLEDBrightness": 0, - "minWateringDurationThreshold": 0, - "localValveCount": 12, - "touchAuthAPSeconds": 60, - "useCommandLineArguments": False, - "databasePath": "/rainmachine-app/DB/Default", - "touchCyclePrograms": True, - "zoneListShowInactive": True, - "rainSensorRainStart": None, - "zoneDuration": [ - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - ], - "rainSensorIsNormallyClosed": True, - "useCorrectionForPast": True, - "useMasterValve": False, - "runParsersBeforePrograms": True, - "maxWateringCoef": 2, - "mixerHistorySize": 365, - }, - "location": { - "elevation": 1593.45141602, - "doyDownloaded": True, - "zip": None, - "windSensitivity": 0.5, - "krs": 0.16, - "stationID": 9172, - "stationSource": "station", - "et0Average": 6.578, - "latitude": REDACTED, - "state": "Default", - "stationName": "MY STATION", - "wsDays": 2, - "stationDownloaded": True, - "address": "Default", - "rainSensitivity": 0.8, - "timezone": "America/Los Angeles", - "longitude": REDACTED, - "name": "Home", - }, - }, "restrictions.current": { "hourly": False, "freeze": False, @@ -583,10 +583,32 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach ], }, "controller": { - "api_version": "4.5.0", - "hardware_version": 3, - "name": 12345, - "software_version": "4.0.925", + "versions": { + "apiVer": "4.6.1", + "hwVer": 3, + "swVer": "4.0.1144", + }, + "current_diagnostics": { + "hasWifi": True, + "uptime": "3 days, 18:14:14", + "uptimeSeconds": 324854, + "memUsage": 16196, + "networkStatus": True, + "bootCompleted": True, + "lastCheckTimestamp": 1659895175, + "wizardHasRun": True, + "standaloneMode": False, + "cpuUsage": 1, + "lastCheck": "2022-08-07 11:59:35", + "softwareVersion": "4.0.1144", + "internetStatus": True, + "locationStatus": True, + "timeStatus": True, + "wifiMode": None, + "gatewayAddress": "172.16.20.1", + "cloudStatus": 0, + "weatherStatus": True, + }, }, }, } From 9552250f3662e6b4d88081ca324fc3d18d0c51b0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 13:44:50 -0600 Subject: [PATCH 213/903] Fix bug where RainMachine entity states don't populate on startup (#76412) --- homeassistant/components/rainmachine/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index dccdaaba74c..f46b1aa0f4c 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -437,6 +437,11 @@ class RainMachineEntity(CoordinatorEntity): self.update_from_latest_data() self.async_write_ha_state() + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.update_from_latest_data() + @callback def update_from_latest_data(self) -> None: """Update the state.""" From 714d46b15333cf5b3b0e9bbf6c5d9b4beda73e14 Mon Sep 17 00:00:00 2001 From: Alex Yao <33379584+alexyao2015@users.noreply.github.com> Date: Sun, 7 Aug 2022 14:47:49 -0500 Subject: [PATCH 214/903] Silence Yeelight Discovery Log Errors (#76373) --- homeassistant/components/yeelight/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index b4afedd6c51..23a2a131913 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -269,7 +269,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await bulb.async_get_properties() await bulb.async_stop_listening() except (asyncio.TimeoutError, yeelight.BulbException) as err: - _LOGGER.error("Failed to get properties from %s: %s", host, err) + _LOGGER.debug("Failed to get properties from %s: %s", host, err) raise CannotConnect from err _LOGGER.debug("Get properties: %s", bulb.last_properties) return MODEL_UNKNOWN From 33b194e48d0c003f5a97f661b5d55f68eac9141f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 10:01:32 -1000 Subject: [PATCH 215/903] Switch a few recent merges to use FlowResultType (#76416) --- .../homekit_controller/test_config_flow.py | 18 ++++++--------- .../lacrosse_view/test_config_flow.py | 16 ++++++------- tests/components/lifx/test_config_flow.py | 16 ++++++------- .../components/simplisafe/test_config_flow.py | 23 ++++++++++--------- 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index e72d9452e52..ff9b89473d6 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -15,11 +15,7 @@ from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES from homeassistant.components.homekit_controller.storage import async_get_entity_storage -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_FORM, - FlowResultType, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo @@ -1017,7 +1013,7 @@ async def test_discovery_no_bluetooth_support(hass, controller): context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -1032,7 +1028,7 @@ async def test_bluetooth_not_homekit(hass, controller): context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_HK_BLUETOOTH_SERVICE_INFO, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -1047,7 +1043,7 @@ async def test_bluetooth_valid_device_no_discovery(hass, controller): context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "accessory_not_found_error" @@ -1065,7 +1061,7 @@ async def test_bluetooth_valid_device_discovery_paired(hass, controller): data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_paired" @@ -1084,7 +1080,7 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pair" assert storage.get_map("00:00:00:00:00:00") is None @@ -1095,7 +1091,7 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): } result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={"pairing_code": "111-22-333"} ) diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py index 82178f2801b..dc55f02bff8 100644 --- a/tests/components/lacrosse_view/test_config_flow.py +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -6,7 +6,7 @@ from lacrosse_view import Location, LoginError from homeassistant import config_entries from homeassistant.components.lacrosse_view.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -14,7 +14,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "location" assert result2["errors"] is None @@ -75,7 +75,7 @@ async def test_form_auth_false(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -94,7 +94,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -115,7 +115,7 @@ async def test_form_login_first(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -137,7 +137,7 @@ async def test_form_no_locations(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "no_locations"} @@ -159,5 +159,5 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index f007e9ee0e8..b346233874a 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.components.lifx import DOMAIN from homeassistant.components.lifx.const import CONF_SERIAL from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_TITLE, @@ -332,7 +332,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data={CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_config_flow_try_connect(): @@ -344,7 +344,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_config_flow_try_connect(): @@ -356,7 +356,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with _patch_discovery(no_device=True), _patch_config_flow_try_connect( @@ -370,7 +370,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -408,7 +408,7 @@ async def test_discovered_by_dhcp_or_discovery(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_config_flow_try_connect(), patch( @@ -462,7 +462,7 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -483,7 +483,7 @@ async def test_discovered_by_dhcp_updates_ip(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index cf92ed94d41..fe803ba187e 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant.components.simplisafe import DOMAIN from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResultType VALID_AUTH_CODE = "code12345123451234512345123451234512345123451" @@ -23,12 +24,12 @@ async def test_duplicate_error(config_entry, hass, setup_simplisafe): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -38,12 +39,12 @@ async def test_invalid_auth_code_length(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "too_short_code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth_code_length"} @@ -57,13 +58,13 @@ async def test_invalid_credentials(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth"} @@ -75,7 +76,7 @@ async def test_options_flow(config_entry, hass): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -101,7 +102,7 @@ async def test_step_reauth(config_entry, hass, setup_simplisafe): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -125,7 +126,7 @@ async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -177,10 +178,10 @@ async def test_unknown_error(hass, setup_simplisafe): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} From 56acb665148d09f17c18ecf69d3493d17cd86b90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 10:11:34 -1000 Subject: [PATCH 216/903] Fix Govee 5185 Meat Thermometers with older firmware not being discovered (#76414) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 624a38ebe9d..eb33df867e1 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -15,6 +15,10 @@ "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "manufacturer_id": 818, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" @@ -24,7 +28,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.6"], + "requirements": ["govee-ble==0.12.7"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index ef8193dad28..d704d00ab8a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -41,6 +41,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 818, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, { "domain": "govee_ble", "manufacturer_id": 14474, diff --git a/requirements_all.txt b/requirements_all.txt index 50a4e5860bd..3a755023ac7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.6 +govee-ble==0.12.7 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8207b8edfd5..0fbc84fc0a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.6 +govee-ble==0.12.7 # homeassistant.components.gree greeclimate==1.3.0 From 27f1955f2887716b36f3b45f46afaa9021d58a30 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 14:27:52 -0600 Subject: [PATCH 217/903] Automatically enable common RainMachine restriction entities (#76405) Automatically enable common delay-related RainMachine entities --- homeassistant/components/rainmachine/binary_sensor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index d9448d68f9d..3db64240788 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -80,7 +80,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Hourly restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="hourly", ), @@ -89,7 +88,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Month restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="month", ), @@ -98,7 +96,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Rain delay restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="rainDelay", ), @@ -116,7 +113,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Weekday restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="weekDay", ), From 8ea9f975fdae4188399210dcb4b11f5f789cea50 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 14:50:49 -0600 Subject: [PATCH 218/903] Fix bug potential in RainMachine switches by simplifying architecture (#76417) * Fix bug potential in RainMachine switches by simplifying architecture * Better typing (per code review) * Broader error catch --- .../components/rainmachine/switch.py | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index ee6ac670840..029c8c06771 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -2,12 +2,13 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine +from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass from datetime import datetime -from typing import Any +from typing import Any, TypeVar -from regenmaschine.errors import RequestError +from regenmaschine.errors import RainMachineError +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -104,6 +105,27 @@ VEGETATION_MAP = { } +_T = TypeVar("_T", bound="RainMachineBaseSwitch") +_P = ParamSpec("_P") + + +def raise_on_request_error( + func: Callable[Concatenate[_T, _P], Awaitable[None]] +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: + """Define a decorator to raise on a request error.""" + + async def decorator(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Decorate.""" + try: + await func(self, *args, **kwargs) + except RainMachineError as err: + raise HomeAssistantError( + f"Error while executing {func.__name__}: {err}", + ) from err + + return decorator + + @dataclass class RainMachineSwitchDescription( SwitchEntityDescription, @@ -197,22 +219,9 @@ class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity): self._attr_is_on = False self._entry = entry - async def _async_run_api_coroutine(self, api_coro: Coroutine) -> None: - """Await an API coroutine, handle any errors, and update as appropriate.""" - try: - resp = await api_coro - except RequestError as err: - raise HomeAssistantError( - f'Error while executing {api_coro.__name__} on "{self.name}": {err}', - ) from err - - if resp["statusCode"] != 0: - raise HomeAssistantError( - f'Error while executing {api_coro.__name__} on "{self.name}": {resp["message"]}', - ) - - # Because of how inextricably linked programs and zones are, anytime one is - # toggled, we make sure to update the data of both coordinators: + @callback + def _update_activities(self) -> None: + """Update all activity data.""" self.hass.async_create_task( async_update_programs_and_zones(self.hass, self._entry) ) @@ -250,6 +259,7 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch): await self.async_turn_off_when_active(**kwargs) + @raise_on_request_error async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" raise NotImplementedError @@ -265,6 +275,7 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch): await self.async_turn_on_when_active(**kwargs) + @raise_on_request_error async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" raise NotImplementedError @@ -290,17 +301,17 @@ class RainMachineProgram(RainMachineActivitySwitch): """Stop the program.""" await self.async_turn_off() + @raise_on_request_error async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" - await self._async_run_api_coroutine( - self._data.controller.programs.stop(self.entity_description.uid) - ) + await self._data.controller.programs.stop(self.entity_description.uid) + self._update_activities() + @raise_on_request_error async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" - await self._async_run_api_coroutine( - self._data.controller.programs.start(self.entity_description.uid) - ) + await self._data.controller.programs.start(self.entity_description.uid) + self._update_activities() @callback def update_from_latest_data(self) -> None: @@ -332,24 +343,21 @@ class RainMachineProgram(RainMachineActivitySwitch): class RainMachineProgramEnabled(RainMachineEnabledSwitch): """Define a switch to enable/disable a RainMachine program.""" + @raise_on_request_error async def async_turn_off(self, **kwargs: Any) -> None: """Disable the program.""" tasks = [ - self._async_run_api_coroutine( - self._data.controller.programs.stop(self.entity_description.uid) - ), - self._async_run_api_coroutine( - self._data.controller.programs.disable(self.entity_description.uid) - ), + self._data.controller.programs.stop(self.entity_description.uid), + self._data.controller.programs.disable(self.entity_description.uid), ] - await asyncio.gather(*tasks) + self._update_activities() + @raise_on_request_error async def async_turn_on(self, **kwargs: Any) -> None: """Enable the program.""" - await self._async_run_api_coroutine( - self._data.controller.programs.enable(self.entity_description.uid) - ) + await self._data.controller.programs.enable(self.entity_description.uid) + self._update_activities() class RainMachineZone(RainMachineActivitySwitch): @@ -363,20 +371,20 @@ class RainMachineZone(RainMachineActivitySwitch): """Stop a zone.""" await self.async_turn_off() + @raise_on_request_error async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" - await self._async_run_api_coroutine( - self._data.controller.zones.stop(self.entity_description.uid) - ) + await self._data.controller.zones.stop(self.entity_description.uid) + self._update_activities() + @raise_on_request_error async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" - await self._async_run_api_coroutine( - self._data.controller.zones.start( - self.entity_description.uid, - kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]), - ) + await self._data.controller.zones.start( + self.entity_description.uid, + kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]), ) + self._update_activities() @callback def update_from_latest_data(self) -> None: @@ -416,21 +424,18 @@ class RainMachineZone(RainMachineActivitySwitch): class RainMachineZoneEnabled(RainMachineEnabledSwitch): """Define a switch to enable/disable a RainMachine zone.""" + @raise_on_request_error async def async_turn_off(self, **kwargs: Any) -> None: """Disable the zone.""" tasks = [ - self._async_run_api_coroutine( - self._data.controller.zones.stop(self.entity_description.uid) - ), - self._async_run_api_coroutine( - self._data.controller.zones.disable(self.entity_description.uid) - ), + self._data.controller.zones.stop(self.entity_description.uid), + self._data.controller.zones.disable(self.entity_description.uid), ] - await asyncio.gather(*tasks) + self._update_activities() + @raise_on_request_error async def async_turn_on(self, **kwargs: Any) -> None: """Enable the zone.""" - await self._async_run_api_coroutine( - self._data.controller.zones.enable(self.entity_description.uid) - ) + await self._data.controller.zones.enable(self.entity_description.uid) + self._update_activities() From dc30d9793898fff130c6a78bc46294bc889b3b8f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 14:51:00 -0600 Subject: [PATCH 219/903] Add debug logging for unknown Notion errors (#76395) * Add debug logging for unknown Notion errors * Remove unused constant * Code review --- homeassistant/components/notion/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 2a73d12d946..eaa3f55e56c 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio from datetime import timedelta +import logging +import traceback from typing import Any from aionotion import async_get_client @@ -31,7 +33,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] ATTR_SYSTEM_MODE = "system_mode" ATTR_SYSTEM_NAME = "system_name" -DEFAULT_ATTRIBUTION = "Data provided by Notion" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -75,6 +76,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"There was a Notion error while updating {attr}: {result}" ) from result if isinstance(result, Exception): + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("".join(traceback.format_tb(result.__traceback__))) raise UpdateFailed( f"There was an unknown error while updating {attr}: {result}" ) from result From ceecab95596076e4fca41577dc91a5592e51da4b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 15:21:49 -0600 Subject: [PATCH 220/903] Add update entity to RainMachine (#76100) * Add update entity to RainMachine * Fix tests * Cleanup * Test missing controller diagnostics * Code review --- .coveragerc | 1 + .../components/rainmachine/__init__.py | 58 +- homeassistant/components/rainmachine/const.py | 2 + .../components/rainmachine/diagnostics.py | 23 +- .../components/rainmachine/update.py | 102 +++ tests/components/rainmachine/conftest.py | 12 + .../machine_firmware_update_status_data.json | 7 + .../rainmachine/test_diagnostics.py | 817 +++++++++++++++--- 8 files changed, 882 insertions(+), 140 deletions(-) create mode 100644 homeassistant/components/rainmachine/update.py create mode 100644 tests/components/rainmachine/fixtures/machine_firmware_update_status_data.json diff --git a/.coveragerc b/.coveragerc index 20c6fd2c60e..4c11fe46120 100644 --- a/.coveragerc +++ b/.coveragerc @@ -980,6 +980,7 @@ omit = homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py + homeassistant/components/rainmachine/update.py homeassistant/components/rainmachine/util.py homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/__init__.py diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index f46b1aa0f4c..1ad1fb734fa 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -36,6 +36,8 @@ from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller from .const import ( CONF_ZONE_RUN_TIME, + DATA_API_VERSIONS, + DATA_MACHINE_FIRMWARE_UPDATE_STATUS, DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, @@ -51,7 +53,13 @@ DEFAULT_SSL = True CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.SENSOR, + Platform.SWITCH, + Platform.UPDATE, +] CONF_CONDITION = "condition" CONF_DEWPOINT = "dewpoint" @@ -124,8 +132,10 @@ SERVICE_RESTRICT_WATERING_SCHEMA = SERVICE_SCHEMA.extend( ) COORDINATOR_UPDATE_INTERVAL_MAP = { - DATA_PROVISION_SETTINGS: timedelta(minutes=1), + DATA_API_VERSIONS: timedelta(minutes=1), + DATA_MACHINE_FIRMWARE_UPDATE_STATUS: timedelta(seconds=15), DATA_PROGRAMS: timedelta(seconds=30), + DATA_PROVISION_SETTINGS: timedelta(minutes=1), DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), DATA_ZONES: timedelta(seconds=15), @@ -215,7 +225,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data: dict = {} try: - if api_category == DATA_PROGRAMS: + if api_category == DATA_API_VERSIONS: + data = await controller.api.versions() + elif api_category == DATA_MACHINE_FIRMWARE_UPDATE_STATUS: + data = await controller.machine.get_firmware_update_status() + elif api_category == DATA_PROGRAMS: data = await controller.programs.all(include_inactive=True) elif api_category == DATA_PROVISION_SETTINGS: data = await controller.provisioning.settings() @@ -414,23 +428,32 @@ class RainMachineEntity(CoordinatorEntity): """Initialize.""" super().__init__(data.coordinators[description.api_category]) - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, data.controller.mac)}, - configuration_url=f"https://{entry.data[CONF_IP_ADDRESS]}:{entry.data[CONF_PORT]}", - connections={(dr.CONNECTION_NETWORK_MAC, data.controller.mac)}, - name=str(data.controller.name).capitalize(), - manufacturer="RainMachine", - model=( - f"Version {data.controller.hardware_version} " - f"(API: {data.controller.api_version})" - ), - sw_version=data.controller.software_version, - ) self._attr_extra_state_attributes = {} self._attr_unique_id = f"{data.controller.mac}_{description.key}" + self._entry = entry self._data = data + self._version_coordinator = data.coordinators[DATA_API_VERSIONS] self.entity_description = description + @property + def device_info(self) -> DeviceInfo: + """Return device information about this controller.""" + return DeviceInfo( + identifiers={(DOMAIN, self._data.controller.mac)}, + configuration_url=( + f"https://{self._entry.data[CONF_IP_ADDRESS]}:" + f"{self._entry.data[CONF_PORT]}" + ), + connections={(dr.CONNECTION_NETWORK_MAC, self._data.controller.mac)}, + name=str(self._data.controller.name).capitalize(), + manufacturer="RainMachine", + model=( + f"Version {self._version_coordinator.data['hwVer']} " + f"(API: {self._version_coordinator.data['apiVer']})" + ), + sw_version=self._version_coordinator.data["swVer"], + ) + @callback def _handle_coordinator_update(self) -> None: """Respond to a DataUpdateCoordinator update.""" @@ -440,6 +463,11 @@ class RainMachineEntity(CoordinatorEntity): async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() + self.async_on_remove( + self._version_coordinator.async_add_listener( + self._handle_coordinator_update, self.coordinator_context + ) + ) self.update_from_latest_data() @callback diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index f94e7011dce..d1b5bd9bd52 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -7,6 +7,8 @@ DOMAIN = "rainmachine" CONF_ZONE_RUN_TIME = "zone_run_time" +DATA_API_VERSIONS = "api.versions" +DATA_MACHINE_FIRMWARE_UPDATE_STATUS = "machine.firmware_update_status" DATA_PROGRAMS = "programs" DATA_PROVISION_SETTINGS = "provision.settings" DATA_RESTRICTIONS_CURRENT = "restrictions.current" diff --git a/homeassistant/components/rainmachine/diagnostics.py b/homeassistant/components/rainmachine/diagnostics.py index 131f2e130e7..47ded7990c6 100644 --- a/homeassistant/components/rainmachine/diagnostics.py +++ b/homeassistant/components/rainmachine/diagnostics.py @@ -1,10 +1,9 @@ """Diagnostics support for RainMachine.""" from __future__ import annotations -import asyncio from typing import Any -from regenmaschine.errors import RequestError +from regenmaschine.errors import RainMachineError from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -17,7 +16,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from . import RainMachineData -from .const import DOMAIN +from .const import DOMAIN, LOGGER CONF_STATION_ID = "stationID" CONF_STATION_NAME = "stationName" @@ -42,13 +41,11 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" data: RainMachineData = hass.data[DOMAIN][entry.entry_id] - controller_tasks = { - "versions": data.controller.api.versions(), - "current_diagnostics": data.controller.diagnostics.current(), - } - controller_results = await asyncio.gather( - *controller_tasks.values(), return_exceptions=True - ) + try: + controller_diagnostics = await data.controller.diagnostics.current() + except RainMachineError: + LOGGER.warning("Unable to download controller-specific diagnostics") + controller_diagnostics = None return { "entry": { @@ -64,10 +61,6 @@ async def async_get_config_entry_diagnostics( }, TO_REDACT, ), - "controller": { - category: result - for category, result in zip(controller_tasks, controller_results) - if not isinstance(result, RequestError) - }, + "controller_diagnostics": controller_diagnostics, }, } diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py new file mode 100644 index 00000000000..b191f2695a0 --- /dev/null +++ b/homeassistant/components/rainmachine/update.py @@ -0,0 +1,102 @@ +"""Support for RainMachine updates.""" +from __future__ import annotations + +from enum import Enum +from typing import Any + +from regenmaschine.errors import RequestError + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RainMachineData, RainMachineEntity +from .const import DATA_MACHINE_FIRMWARE_UPDATE_STATUS, DOMAIN +from .model import RainMachineEntityDescription + + +class UpdateStates(Enum): + """Define an enum for update states.""" + + IDLE = 1 + CHECKING = 2 + DOWNLOADING = 3 + UPGRADING = 4 + ERROR = 5 + REBOOT = 6 + + +UPDATE_STATE_MAP = { + 1: UpdateStates.IDLE, + 2: UpdateStates.CHECKING, + 3: UpdateStates.DOWNLOADING, + 4: UpdateStates.UPGRADING, + 5: UpdateStates.ERROR, + 6: UpdateStates.REBOOT, +} + + +UPDATE_DESCRIPTION = RainMachineEntityDescription( + key="update", + name="Firmware", + api_category=DATA_MACHINE_FIRMWARE_UPDATE_STATUS, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up WLED update based on a config entry.""" + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([RainMachineUpdateEntity(entry, data, UPDATE_DESCRIPTION)]) + + +class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity): + """Define a RainMachine update entity.""" + + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.PROGRESS + | UpdateEntityFeature.SPECIFIC_VERSION + ) + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + try: + await self._data.controller.machine.update_firmware() + except RequestError as err: + raise HomeAssistantError(f"Error while updating firmware: {err}") from err + + await self.coordinator.async_refresh() + + @callback + def update_from_latest_data(self) -> None: + """Update the state.""" + if version := self._version_coordinator.data["swVer"]: + self._attr_installed_version = version + else: + self._attr_installed_version = None + + data = self.coordinator.data + + if not data["update"]: + self._attr_in_progress = False + self._attr_latest_version = self._attr_installed_version + return + + self._attr_in_progress = UPDATE_STATE_MAP[data["updateStatus"]] in ( + UpdateStates.DOWNLOADING, + UpdateStates.UPGRADING, + UpdateStates.REBOOT, + ) + self._attr_latest_version = data["packageDetails"]["newVersion"] diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py index 72b4a5d4293..1dfef7e399c 100644 --- a/tests/components/rainmachine/conftest.py +++ b/tests/components/rainmachine/conftest.py @@ -41,6 +41,7 @@ def controller_fixture( controller_mac, data_api_versions, data_diagnostics_current, + data_machine_firmare_update_status, data_programs, data_provision_settings, data_restrictions_current, @@ -59,6 +60,9 @@ def controller_fixture( controller.api.versions.return_value = data_api_versions controller.diagnostics.current.return_value = data_diagnostics_current + controller.machine.get_firmware_update_status.return_value = ( + data_machine_firmare_update_status + ) controller.programs.all.return_value = data_programs controller.provisioning.settings.return_value = data_provision_settings controller.restrictions.current.return_value = data_restrictions_current @@ -86,6 +90,14 @@ def data_diagnostics_current_fixture(): return json.loads(load_fixture("diagnostics_current_data.json", "rainmachine")) +@pytest.fixture(name="data_machine_firmare_update_status", scope="session") +def data_machine_firmare_update_status_fixture(): + """Define machine firmware update status data.""" + return json.loads( + load_fixture("machine_firmware_update_status_data.json", "rainmachine") + ) + + @pytest.fixture(name="data_programs", scope="session") def data_programs_fixture(): """Define program data.""" diff --git a/tests/components/rainmachine/fixtures/machine_firmware_update_status_data.json b/tests/components/rainmachine/fixtures/machine_firmware_update_status_data.json new file mode 100644 index 00000000000..01dd95e6136 --- /dev/null +++ b/tests/components/rainmachine/fixtures/machine_firmware_update_status_data.json @@ -0,0 +1,7 @@ +{ + "lastUpdateCheckTimestamp": 1657825288, + "packageDetails": [], + "update": false, + "lastUpdateCheck": "2022-07-14 13:01:28", + "updateStatus": 1 +} diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index b0876a2f597..a600c5f7c34 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -1,4 +1,6 @@ """Test RainMachine diagnostics.""" +from regenmaschine.errors import RainMachineError + from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -19,89 +21,13 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach }, "data": { "coordinator": { - "provision.settings": { - "system": { - "httpEnabled": True, - "rainSensorSnoozeDuration": 0, - "uiUnitsMetric": False, - "programZonesShowInactive": False, - "programSingleSchedule": False, - "standaloneMode": False, - "masterValveAfter": 0, - "touchSleepTimeout": 10, - "selfTest": False, - "useSoftwareRainSensor": False, - "defaultZoneWateringDuration": 300, - "maxLEDBrightness": 40, - "simulatorHistorySize": 0, - "vibration": False, - "masterValveBefore": 0, - "touchProgramToRun": None, - "useRainSensor": False, - "wizardHasRun": True, - "waterLogHistorySize": 365, - "netName": "Home", - "softwareRainSensorMinQPF": 5, - "touchAdvanced": False, - "useBonjourService": True, - "hardwareVersion": 3, - "touchLongPressTimeout": 3, - "showRestrictionsOnLed": False, - "parserDataSizeInDays": 6, - "programListShowInactive": True, - "parserHistorySize": 365, - "allowAlexaDiscovery": False, - "automaticUpdates": True, - "minLEDBrightness": 0, - "minWateringDurationThreshold": 0, - "localValveCount": 12, - "touchAuthAPSeconds": 60, - "useCommandLineArguments": False, - "databasePath": "/rainmachine-app/DB/Default", - "touchCyclePrograms": True, - "zoneListShowInactive": True, - "rainSensorRainStart": None, - "zoneDuration": [ - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - ], - "rainSensorIsNormallyClosed": True, - "useCorrectionForPast": True, - "useMasterValve": False, - "runParsersBeforePrograms": True, - "maxWateringCoef": 2, - "mixerHistorySize": 365, - }, - "location": { - "elevation": REDACTED, - "doyDownloaded": True, - "zip": None, - "windSensitivity": 0.5, - "krs": 0.16, - "stationID": REDACTED, - "stationSource": REDACTED, - "et0Average": 6.578, - "latitude": REDACTED, - "state": "Default", - "stationName": REDACTED, - "wsDays": 2, - "stationDownloaded": True, - "address": "Default", - "rainSensitivity": 0.8, - "timezone": REDACTED, - "longitude": REDACTED, - "name": "Home", - }, + "api.versions": {"apiVer": "4.6.1", "hwVer": 3, "swVer": "4.0.1144"}, + "machine.firmware_update_status": { + "lastUpdateCheckTimestamp": 1657825288, + "packageDetails": [], + "update": False, + "lastUpdateCheck": "2022-07-14 13:01:28", + "updateStatus": 1, }, "programs": [ { @@ -381,6 +307,90 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach ], }, ], + "provision.settings": { + "system": { + "httpEnabled": True, + "rainSensorSnoozeDuration": 0, + "uiUnitsMetric": False, + "programZonesShowInactive": False, + "programSingleSchedule": False, + "standaloneMode": False, + "masterValveAfter": 0, + "touchSleepTimeout": 10, + "selfTest": False, + "useSoftwareRainSensor": False, + "defaultZoneWateringDuration": 300, + "maxLEDBrightness": 40, + "simulatorHistorySize": 0, + "vibration": False, + "masterValveBefore": 0, + "touchProgramToRun": None, + "useRainSensor": False, + "wizardHasRun": True, + "waterLogHistorySize": 365, + "netName": "Home", + "softwareRainSensorMinQPF": 5, + "touchAdvanced": False, + "useBonjourService": True, + "hardwareVersion": 3, + "touchLongPressTimeout": 3, + "showRestrictionsOnLed": False, + "parserDataSizeInDays": 6, + "programListShowInactive": True, + "parserHistorySize": 365, + "allowAlexaDiscovery": False, + "automaticUpdates": True, + "minLEDBrightness": 0, + "minWateringDurationThreshold": 0, + "localValveCount": 12, + "touchAuthAPSeconds": 60, + "useCommandLineArguments": False, + "databasePath": "/rainmachine-app/DB/Default", + "touchCyclePrograms": True, + "zoneListShowInactive": True, + "rainSensorRainStart": None, + "zoneDuration": [ + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + ], + "rainSensorIsNormallyClosed": True, + "useCorrectionForPast": True, + "useMasterValve": False, + "runParsersBeforePrograms": True, + "maxWateringCoef": 2, + "mixerHistorySize": 365, + }, + "location": { + "elevation": REDACTED, + "doyDownloaded": True, + "zip": None, + "windSensitivity": 0.5, + "krs": 0.16, + "stationID": REDACTED, + "stationSource": REDACTED, + "et0Average": 6.578, + "latitude": REDACTED, + "state": "Default", + "stationName": REDACTED, + "wsDays": 2, + "stationDownloaded": True, + "address": "Default", + "rainSensitivity": 0.8, + "timezone": REDACTED, + "longitude": REDACTED, + "name": "Home", + }, + }, "restrictions.current": { "hourly": False, "freeze": False, @@ -582,33 +592,620 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach }, ], }, - "controller": { - "versions": { - "apiVer": "4.6.1", - "hwVer": 3, - "swVer": "4.0.1144", - }, - "current_diagnostics": { - "hasWifi": True, - "uptime": "3 days, 18:14:14", - "uptimeSeconds": 324854, - "memUsage": 16196, - "networkStatus": True, - "bootCompleted": True, - "lastCheckTimestamp": 1659895175, - "wizardHasRun": True, - "standaloneMode": False, - "cpuUsage": 1, - "lastCheck": "2022-08-07 11:59:35", - "softwareVersion": "4.0.1144", - "internetStatus": True, - "locationStatus": True, - "timeStatus": True, - "wifiMode": None, - "gatewayAddress": "172.16.20.1", - "cloudStatus": 0, - "weatherStatus": True, - }, + "controller_diagnostics": { + "hasWifi": True, + "uptime": "3 days, 18:14:14", + "uptimeSeconds": 324854, + "memUsage": 16196, + "networkStatus": True, + "bootCompleted": True, + "lastCheckTimestamp": 1659895175, + "wizardHasRun": True, + "standaloneMode": False, + "cpuUsage": 1, + "lastCheck": "2022-08-07 11:59:35", + "softwareVersion": "4.0.1144", + "internetStatus": True, + "locationStatus": True, + "timeStatus": True, + "wifiMode": None, + "gatewayAddress": "172.16.20.1", + "cloudStatus": 0, + "weatherStatus": True, }, }, } + + +async def test_entry_diagnostics_failed_controller_diagnostics( + hass, config_entry, controller, hass_client, setup_rainmachine +): + """Test config entry diagnostics when the controller diagnostics API call fails.""" + controller.diagnostics.current.side_effect = RainMachineError + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "title": "Mock Title", + "data": { + "ip_address": "192.168.1.100", + "password": REDACTED, + "port": 8080, + "ssl": True, + }, + "options": {}, + }, + "data": { + "coordinator": { + "api.versions": {"apiVer": "4.6.1", "hwVer": 3, "swVer": "4.0.1144"}, + "machine.firmware_update_status": { + "lastUpdateCheckTimestamp": 1657825288, + "packageDetails": [], + "update": False, + "lastUpdateCheck": "2022-07-14 13:01:28", + "updateStatus": 1, + }, + "programs": [ + { + "uid": 1, + "name": "Morning", + "active": True, + "startTime": "06:00", + "cycles": 0, + "soak": 0, + "cs_on": False, + "delay": 0, + "delay_on": False, + "status": 0, + "startTimeParams": { + "offsetSign": 0, + "type": 0, + "offsetMinutes": 0, + }, + "frequency": {"type": 0, "param": "0"}, + "coef": 0, + "ignoreInternetWeather": False, + "futureField1": 0, + "freq_modified": 0, + "useWaterSense": False, + "nextRun": "2018-06-04", + "startDate": "2018-04-28", + "endDate": None, + "yearlyRecurring": True, + "simulationExpired": False, + "wateringTimes": [ + { + "id": 1, + "order": -1, + "name": "Landscaping", + "duration": 0, + "active": True, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 2, + "order": -1, + "name": "Flower Box", + "duration": 0, + "active": True, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 3, + "order": -1, + "name": "TEST", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 4, + "order": -1, + "name": "Zone 4", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 5, + "order": -1, + "name": "Zone 5", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 6, + "order": -1, + "name": "Zone 6", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 7, + "order": -1, + "name": "Zone 7", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 8, + "order": -1, + "name": "Zone 8", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 9, + "order": -1, + "name": "Zone 9", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 10, + "order": -1, + "name": "Zone 10", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 11, + "order": -1, + "name": "Zone 11", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 12, + "order": -1, + "name": "Zone 12", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + ], + }, + { + "uid": 2, + "name": "Evening", + "active": False, + "startTime": "06:00", + "cycles": 0, + "soak": 0, + "cs_on": False, + "delay": 0, + "delay_on": False, + "status": 0, + "startTimeParams": { + "offsetSign": 0, + "type": 0, + "offsetMinutes": 0, + }, + "frequency": {"type": 0, "param": "0"}, + "coef": 0, + "ignoreInternetWeather": False, + "futureField1": 0, + "freq_modified": 0, + "useWaterSense": False, + "nextRun": "2018-06-04", + "startDate": "2018-04-28", + "endDate": None, + "yearlyRecurring": True, + "simulationExpired": False, + "wateringTimes": [ + { + "id": 1, + "order": -1, + "name": "Landscaping", + "duration": 0, + "active": True, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 2, + "order": -1, + "name": "Flower Box", + "duration": 0, + "active": True, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 3, + "order": -1, + "name": "TEST", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 4, + "order": -1, + "name": "Zone 4", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 5, + "order": -1, + "name": "Zone 5", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 6, + "order": -1, + "name": "Zone 6", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 7, + "order": -1, + "name": "Zone 7", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 8, + "order": -1, + "name": "Zone 8", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 9, + "order": -1, + "name": "Zone 9", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 10, + "order": -1, + "name": "Zone 10", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 11, + "order": -1, + "name": "Zone 11", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 12, + "order": -1, + "name": "Zone 12", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + ], + }, + ], + "provision.settings": { + "system": { + "httpEnabled": True, + "rainSensorSnoozeDuration": 0, + "uiUnitsMetric": False, + "programZonesShowInactive": False, + "programSingleSchedule": False, + "standaloneMode": False, + "masterValveAfter": 0, + "touchSleepTimeout": 10, + "selfTest": False, + "useSoftwareRainSensor": False, + "defaultZoneWateringDuration": 300, + "maxLEDBrightness": 40, + "simulatorHistorySize": 0, + "vibration": False, + "masterValveBefore": 0, + "touchProgramToRun": None, + "useRainSensor": False, + "wizardHasRun": True, + "waterLogHistorySize": 365, + "netName": "Home", + "softwareRainSensorMinQPF": 5, + "touchAdvanced": False, + "useBonjourService": True, + "hardwareVersion": 3, + "touchLongPressTimeout": 3, + "showRestrictionsOnLed": False, + "parserDataSizeInDays": 6, + "programListShowInactive": True, + "parserHistorySize": 365, + "allowAlexaDiscovery": False, + "automaticUpdates": True, + "minLEDBrightness": 0, + "minWateringDurationThreshold": 0, + "localValveCount": 12, + "touchAuthAPSeconds": 60, + "useCommandLineArguments": False, + "databasePath": "/rainmachine-app/DB/Default", + "touchCyclePrograms": True, + "zoneListShowInactive": True, + "rainSensorRainStart": None, + "zoneDuration": [ + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + ], + "rainSensorIsNormallyClosed": True, + "useCorrectionForPast": True, + "useMasterValve": False, + "runParsersBeforePrograms": True, + "maxWateringCoef": 2, + "mixerHistorySize": 365, + }, + "location": { + "elevation": REDACTED, + "doyDownloaded": True, + "zip": None, + "windSensitivity": 0.5, + "krs": 0.16, + "stationID": REDACTED, + "stationSource": REDACTED, + "et0Average": 6.578, + "latitude": REDACTED, + "state": "Default", + "stationName": REDACTED, + "wsDays": 2, + "stationDownloaded": True, + "address": "Default", + "rainSensitivity": 0.8, + "timezone": REDACTED, + "longitude": REDACTED, + "name": "Home", + }, + }, + "restrictions.current": { + "hourly": False, + "freeze": False, + "month": False, + "weekDay": False, + "rainDelay": False, + "rainDelayCounter": -1, + "rainSensor": False, + }, + "restrictions.universal": { + "hotDaysExtraWatering": False, + "freezeProtectEnabled": True, + "freezeProtectTemp": 2, + "noWaterInWeekDays": "0000000", + "noWaterInMonths": "000000000000", + "rainDelayStartTime": 1524854551, + "rainDelayDuration": 0, + }, + "zones": [ + { + "uid": 1, + "name": "Landscaping", + "state": 0, + "active": True, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 4, + "master": False, + "waterSense": False, + }, + { + "uid": 2, + "name": "Flower Box", + "state": 0, + "active": True, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 5, + "master": False, + "waterSense": False, + }, + { + "uid": 3, + "name": "TEST", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 9, + "master": False, + "waterSense": False, + }, + { + "uid": 4, + "name": "Zone 4", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 5, + "name": "Zone 5", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 6, + "name": "Zone 6", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 7, + "name": "Zone 7", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 8, + "name": "Zone 8", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 9, + "name": "Zone 9", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 10, + "name": "Zone 10", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 11, + "name": "Zone 11", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 12, + "name": "Zone 12", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + ], + }, + "controller_diagnostics": None, + }, + } From d1ab93fbaf731e3966cd80392178f9e5684f560b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 Aug 2022 23:45:32 +0200 Subject: [PATCH 221/903] Add openexchangerates config flow (#76390) --- .coveragerc | 4 +- CODEOWNERS | 1 + .../components/openexchangerates/__init__.py | 62 +++- .../openexchangerates/config_flow.py | 132 +++++++++ .../components/openexchangerates/const.py | 2 + .../openexchangerates/coordinator.py | 20 +- .../openexchangerates/manifest.json | 4 +- .../components/openexchangerates/sensor.py | 140 +++++---- .../components/openexchangerates/strings.json | 33 +++ .../openexchangerates/translations/en.json | 33 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + .../components/openexchangerates/__init__.py | 1 + .../components/openexchangerates/conftest.py | 39 +++ .../openexchangerates/test_config_flow.py | 268 ++++++++++++++++++ 15 files changed, 661 insertions(+), 82 deletions(-) create mode 100644 homeassistant/components/openexchangerates/config_flow.py create mode 100644 homeassistant/components/openexchangerates/strings.json create mode 100644 homeassistant/components/openexchangerates/translations/en.json create mode 100644 tests/components/openexchangerates/__init__.py create mode 100644 tests/components/openexchangerates/conftest.py create mode 100644 tests/components/openexchangerates/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 4c11fe46120..5068574df78 100644 --- a/.coveragerc +++ b/.coveragerc @@ -851,7 +851,9 @@ omit = homeassistant/components/open_meteo/weather.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py - homeassistant/components/openexchangerates/* + homeassistant/components/openexchangerates/__init__.py + homeassistant/components/openexchangerates/coordinator.py + homeassistant/components/openexchangerates/sensor.py homeassistant/components/opengarage/__init__.py homeassistant/components/opengarage/binary_sensor.py homeassistant/components/opengarage/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index 1a322b09981..e10a8a0b26c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -767,6 +767,7 @@ build.json @home-assistant/supervisor /homeassistant/components/openerz/ @misialq /tests/components/openerz/ @misialq /homeassistant/components/openexchangerates/ @MartinHjelmare +/tests/components/openexchangerates/ @MartinHjelmare /homeassistant/components/opengarage/ @danielhiversen /tests/components/opengarage/ @danielhiversen /homeassistant/components/openhome/ @bazwilliams diff --git a/homeassistant/components/openexchangerates/__init__.py b/homeassistant/components/openexchangerates/__init__.py index 93d53614bdb..1b6ab4e65f1 100644 --- a/homeassistant/components/openexchangerates/__init__.py +++ b/homeassistant/components/openexchangerates/__init__.py @@ -1 +1,61 @@ -"""The openexchangerates component.""" +"""The Open Exchange Rates integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_BASE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import BASE_UPDATE_INTERVAL, DOMAIN, LOGGER +from .coordinator import OpenexchangeratesCoordinator + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Open Exchange Rates from a config entry.""" + api_key: str = entry.data[CONF_API_KEY] + base: str = entry.data[CONF_BASE] + + # Create one coordinator per base currency per API key. + existing_coordinators: dict[str, OpenexchangeratesCoordinator] = hass.data.get( + DOMAIN, {} + ) + existing_coordinator_for_api_key = { + existing_coordinator + for config_entry_id, existing_coordinator in existing_coordinators.items() + if (config_entry := hass.config_entries.async_get_entry(config_entry_id)) + and config_entry.data[CONF_API_KEY] == api_key + } + + # Adjust update interval by coordinators per API key. + update_interval = BASE_UPDATE_INTERVAL * (len(existing_coordinator_for_api_key) + 1) + coordinator = OpenexchangeratesCoordinator( + hass, + async_get_clientsession(hass), + api_key, + base, + update_interval, + ) + + LOGGER.debug("Coordinator update interval set to: %s", update_interval) + + # Set new interval on all coordinators for this API key. + for existing_coordinator in existing_coordinator_for_api_key: + existing_coordinator.update_interval = update_interval + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/openexchangerates/config_flow.py b/homeassistant/components/openexchangerates/config_flow.py new file mode 100644 index 00000000000..3c22f3e0fe0 --- /dev/null +++ b/homeassistant/components/openexchangerates/config_flow.py @@ -0,0 +1,132 @@ +"""Config flow for Open Exchange Rates integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from typing import Any + +from aioopenexchangerates import ( + Client, + OpenExchangeRatesAuthError, + OpenExchangeRatesClientError, +) +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_BASE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CLIENT_TIMEOUT, DEFAULT_BASE, DOMAIN, LOGGER + + +def get_data_schema( + currencies: dict[str, str], existing_data: Mapping[str, str] +) -> vol.Schema: + """Return a form schema.""" + return vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional( + CONF_BASE, default=existing_data.get(CONF_BASE) or DEFAULT_BASE + ): vol.In(currencies), + } + ) + + +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: + """Validate the user input allows us to connect.""" + client = Client(data[CONF_API_KEY], async_get_clientsession(hass)) + + async with async_timeout.timeout(CLIENT_TIMEOUT): + await client.get_latest(base=data[CONF_BASE]) + + return {"title": data[CONF_BASE]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Open Exchange Rates.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self.currencies: dict[str, str] = {} + self._reauth_entry: config_entries.ConfigEntry | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + currencies = await self.async_get_currencies() + + if user_input is None: + existing_data: Mapping[str, str] | dict[str, str] = ( + self._reauth_entry.data if self._reauth_entry else {} + ) + return self.async_show_form( + step_id="user", data_schema=get_data_schema(currencies, existing_data) + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except OpenExchangeRatesAuthError: + errors["base"] = "invalid_auth" + except OpenExchangeRatesClientError: + errors["base"] = "cannot_connect" + except asyncio.TimeoutError: + errors["base"] = "timeout_connect" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self._async_abort_entries_match( + { + CONF_API_KEY: user_input[CONF_API_KEY], + CONF_BASE: user_input[CONF_BASE], + } + ) + + if self._reauth_entry is not None: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=self._reauth_entry.data | user_input + ) + await self.hass.config_entries.async_reload(self._reauth_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=get_data_schema(currencies, user_input), + description_placeholders={"signup": "https://openexchangerates.org/signup"}, + errors=errors, + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle reauth.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() + + async def async_get_currencies(self) -> dict[str, str]: + """Get the available currencies.""" + if not self.currencies: + client = Client("dummy-api-key", async_get_clientsession(self.hass)) + try: + async with async_timeout.timeout(CLIENT_TIMEOUT): + self.currencies = await client.get_currencies() + except OpenExchangeRatesClientError as err: + raise AbortFlow("cannot_connect") from err + except asyncio.TimeoutError as err: + raise AbortFlow("timeout_connect") from err + return self.currencies + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Handle import from yaml/configuration.""" + return await self.async_step_user(import_config) diff --git a/homeassistant/components/openexchangerates/const.py b/homeassistant/components/openexchangerates/const.py index 2c037887489..146919cfe44 100644 --- a/homeassistant/components/openexchangerates/const.py +++ b/homeassistant/components/openexchangerates/const.py @@ -5,3 +5,5 @@ import logging DOMAIN = "openexchangerates" LOGGER = logging.getLogger(__package__) BASE_UPDATE_INTERVAL = timedelta(hours=2) +CLIENT_TIMEOUT = 10 +DEFAULT_BASE = "USD" diff --git a/homeassistant/components/openexchangerates/coordinator.py b/homeassistant/components/openexchangerates/coordinator.py index 0106edcd751..3795f33aec5 100644 --- a/homeassistant/components/openexchangerates/coordinator.py +++ b/homeassistant/components/openexchangerates/coordinator.py @@ -1,19 +1,22 @@ """Provide an OpenExchangeRates data coordinator.""" from __future__ import annotations -import asyncio from datetime import timedelta from aiohttp import ClientSession -from aioopenexchangerates import Client, Latest, OpenExchangeRatesClientError +from aioopenexchangerates import ( + Client, + Latest, + OpenExchangeRatesAuthError, + OpenExchangeRatesClientError, +) import async_timeout from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, LOGGER - -TIMEOUT = 10 +from .const import CLIENT_TIMEOUT, DOMAIN, LOGGER class OpenexchangeratesCoordinator(DataUpdateCoordinator[Latest]): @@ -33,14 +36,15 @@ class OpenexchangeratesCoordinator(DataUpdateCoordinator[Latest]): ) self.base = base self.client = Client(api_key, session) - self.setup_lock = asyncio.Lock() async def _async_update_data(self) -> Latest: """Update data from Open Exchange Rates.""" try: - async with async_timeout.timeout(TIMEOUT): + async with async_timeout.timeout(CLIENT_TIMEOUT): latest = await self.client.get_latest(base=self.base) - except (OpenExchangeRatesClientError) as err: + except OpenExchangeRatesAuthError as err: + raise ConfigEntryAuthFailed(err) from err + except OpenExchangeRatesClientError as err: raise UpdateFailed(err) from err LOGGER.debug("Result: %s", latest) diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index f2377478c5f..efa67ff39e9 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -3,6 +3,8 @@ "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", "requirements": ["aioopenexchangerates==0.4.0"], + "dependencies": ["repairs"], "codeowners": ["@MartinHjelmare"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "config_flow": true } diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 337cd3050ac..7f7681b6887 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -1,26 +1,26 @@ """Support for openexchangerates.org exchange rates service.""" from __future__ import annotations -from dataclasses import dataclass, field - import voluptuous as vol +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import BASE_UPDATE_INTERVAL, DOMAIN, LOGGER +from .const import DEFAULT_BASE, DOMAIN, LOGGER from .coordinator import OpenexchangeratesCoordinator ATTRIBUTION = "Data provided by openexchangerates.org" -DEFAULT_BASE = "USD" DEFAULT_NAME = "Exchange Rate Sensor" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -33,15 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -@dataclass -class DomainData: - """Data structure to hold data for this domain.""" - - coordinators: dict[tuple[str, str], OpenexchangeratesCoordinator] = field( - default_factory=dict, init=False - ) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -49,56 +40,48 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Open Exchange Rates sensor.""" - name: str = config[CONF_NAME] - api_key: str = config[CONF_API_KEY] - base: str = config[CONF_BASE] - quote: str = config[CONF_QUOTE] - - integration_data: DomainData = hass.data.setdefault(DOMAIN, DomainData()) - coordinators = integration_data.coordinators - - if (api_key, base) not in coordinators: - # Create one coordinator per base currency per API key. - update_interval = BASE_UPDATE_INTERVAL * ( - len( - { - coordinator_base - for coordinator_api_key, coordinator_base in coordinators - if coordinator_api_key == api_key - } - ) - + 1 + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - coordinator = coordinators[api_key, base] = OpenexchangeratesCoordinator( - hass, - async_get_clientsession(hass), - api_key, - base, - update_interval, + ) + + LOGGER.warning( + "Configuration of Open Exchange Rates integration in YAML is deprecated and " + "will be removed in Home Assistant 2022.11.; Your existing configuration " + "has been imported into the UI automatically and can be safely removed from" + " your configuration.yaml file" + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Open Exchange Rates sensor.""" + # Only YAML imported configs have name and quote in config entry data. + name: str | None = config_entry.data.get(CONF_NAME) + quote: str = config_entry.data.get(CONF_QUOTE, "EUR") + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + OpenexchangeratesSensor( + config_entry, coordinator, name, rate_quote, rate_quote == quote ) - - LOGGER.debug( - "Coordinator update interval set to: %s", coordinator.update_interval - ) - - # Set new interval on all coordinators for this API key. - for ( - coordinator_api_key, - _, - ), coordinator in coordinators.items(): - if coordinator_api_key == api_key: - coordinator.update_interval = update_interval - - coordinator = coordinators[api_key, base] - async with coordinator.setup_lock: - # We need to make sure that the coordinator data is ready. - if not coordinator.data: - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise PlatformNotReady - - async_add_entities([OpenexchangeratesSensor(coordinator, name, quote)]) + for rate_quote in coordinator.data.rates + ) class OpenexchangeratesSensor( @@ -109,20 +92,35 @@ class OpenexchangeratesSensor( _attr_attribution = ATTRIBUTION def __init__( - self, coordinator: OpenexchangeratesCoordinator, name: str, quote: str + self, + config_entry: ConfigEntry, + coordinator: OpenexchangeratesCoordinator, + name: str | None, + quote: str, + enabled: bool, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._attr_name = name - self._quote = quote + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer="Open Exchange Rates", + name=f"Open Exchange Rates {coordinator.base}", + ) + self._attr_entity_registry_enabled_default = enabled + if name and enabled: + # name is legacy imported from YAML config + # this block can be removed when removing import from YAML + self._attr_name = name + self._attr_has_entity_name = False + else: + self._attr_name = quote + self._attr_has_entity_name = True self._attr_native_unit_of_measurement = quote + self._attr_unique_id = f"{config_entry.entry_id}_{quote}" + self._quote = quote @property def native_value(self) -> float: """Return the state of the sensor.""" return round(self.coordinator.data.rates[self._quote], 4) - - @property - def extra_state_attributes(self) -> dict[str, float]: - """Return other attributes of the sensor.""" - return self.coordinator.data.rates diff --git a/homeassistant/components/openexchangerates/strings.json b/homeassistant/components/openexchangerates/strings.json new file mode 100644 index 00000000000..57180e367aa --- /dev/null +++ b/homeassistant/components/openexchangerates/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "base": "Base currency" + }, + "data_description": { + "base": "Using another base currency than USD requires a [paid plan]({signup})." + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Open Exchange Rates YAML configuration is being removed", + "description": "Configuring Open Exchange Rates using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/openexchangerates/translations/en.json b/homeassistant/components/openexchangerates/translations/en.json new file mode 100644 index 00000000000..011953904ff --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/en.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "cannot_connect": "Failed to connect", + "reauth_successful": "Re-authentication was successful", + "timeout_connect": "Timeout establishing connection" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "timeout_connect": "Timeout establishing connection", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "base": "Base currency" + }, + "data_description": { + "base": "Using another base currency than USD requires a [paid plan]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Open Exchange Rates using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Open Exchange Rates YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6d92f7cf7e7..327781c2562 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -258,6 +258,7 @@ FLOWS = { "onewire", "onvif", "open_meteo", + "openexchangerates", "opengarage", "opentherm_gw", "openuv", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fbc84fc0a8..7211bb3f08a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -191,6 +191,9 @@ aionotion==3.0.2 # homeassistant.components.oncue aiooncue==0.3.4 +# homeassistant.components.openexchangerates +aioopenexchangerates==0.4.0 + # homeassistant.components.acmeda aiopulse==0.4.3 diff --git a/tests/components/openexchangerates/__init__.py b/tests/components/openexchangerates/__init__.py new file mode 100644 index 00000000000..4547f25f4bc --- /dev/null +++ b/tests/components/openexchangerates/__init__.py @@ -0,0 +1 @@ +"""Tests for the Open Exchange Rates integration.""" diff --git a/tests/components/openexchangerates/conftest.py b/tests/components/openexchangerates/conftest.py new file mode 100644 index 00000000000..a1512442fd1 --- /dev/null +++ b/tests/components/openexchangerates/conftest.py @@ -0,0 +1,39 @@ +"""Provide common fixtures for tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.openexchangerates.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, data={"api_key": "test-api-key", "base": "USD"} + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.openexchangerates.async_setup_entry", + return_value=True, + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_latest_rates_config_flow( + request: pytest.FixtureRequest, +) -> Generator[AsyncMock, None, None]: + """Return a mocked WLED client.""" + with patch( + "homeassistant.components.openexchangerates.config_flow.Client.get_latest", + ) as mock_latest: + mock_latest.return_value = {"EUR": 1.0} + yield mock_latest diff --git a/tests/components/openexchangerates/test_config_flow.py b/tests/components/openexchangerates/test_config_flow.py new file mode 100644 index 00000000000..ee4ba57de2c --- /dev/null +++ b/tests/components/openexchangerates/test_config_flow.py @@ -0,0 +1,268 @@ +"""Test the Open Exchange Rates config flow.""" +import asyncio +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, patch + +from aioopenexchangerates import ( + OpenExchangeRatesAuthError, + OpenExchangeRatesClientError, +) +import pytest + +from homeassistant import config_entries +from homeassistant.components.openexchangerates.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="currencies", autouse=True) +def currencies_fixture(hass: HomeAssistant) -> Generator[AsyncMock, None, None]: + """Mock currencies.""" + with patch( + "homeassistant.components.openexchangerates.config_flow.Client.get_currencies", + return_value={"USD": "United States Dollar", "EUR": "Euro"}, + ) as mock_currencies: + yield mock_currencies + + +async def test_user_create_entry( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "test-api-key"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "USD" + assert result["data"] == { + "api_key": "test-api-key", + "base": "USD", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, +) -> None: + """Test we handle invalid auth.""" + mock_latest_rates_config_flow.side_effect = OpenExchangeRatesAuthError() + 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"], + {"api_key": "bad-api-key"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, +) -> None: + """Test we handle cannot connect error.""" + mock_latest_rates_config_flow.side_effect = OpenExchangeRatesClientError() + 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"], + {"api_key": "test-api-key"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, +) -> None: + """Test we handle unknown error.""" + mock_latest_rates_config_flow.side_effect = Exception() + 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"], + {"api_key": "test-api-key"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_already_configured_service( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we abort if the service is already configured.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "test-api-key"}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_no_currencies(hass: HomeAssistant, currencies: AsyncMock) -> None: + """Test we abort if the service fails to retrieve currencies.""" + currencies.side_effect = OpenExchangeRatesClientError() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_currencies_timeout(hass: HomeAssistant, currencies: AsyncMock) -> None: + """Test we abort if the service times out retrieving currencies.""" + + async def currencies_side_effect(): + await asyncio.sleep(1) + return {"USD": "United States Dollar", "EUR": "Euro"} + + currencies.side_effect = currencies_side_effect + + with patch( + "homeassistant.components.openexchangerates.config_flow.CLIENT_TIMEOUT", 0 + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "timeout_connect" + + +async def test_latest_rates_timeout( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, +) -> None: + """Test we abort if the service times out retrieving latest rates.""" + + async def latest_rates_side_effect(*args: Any, **kwargs: Any) -> dict[str, float]: + await asyncio.sleep(1) + return {"EUR": 1.0} + + mock_latest_rates_config_flow.side_effect = latest_rates_side_effect + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.openexchangerates.config_flow.CLIENT_TIMEOUT", 0 + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "test-api-key"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "timeout_connect"} + + +async def test_reauth( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we can reauthenticate the config entry.""" + mock_config_entry.add_to_hass(hass) + flow_context = { + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "title_placeholders": {"name": mock_config_entry.title}, + "unique_id": mock_config_entry.unique_id, + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, context=flow_context, data=mock_config_entry.data + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + mock_latest_rates_config_flow.side_effect = OpenExchangeRatesAuthError() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "invalid-test-api-key", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + mock_latest_rates_config_flow.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "new-test-api-key", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_create_entry( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test we can import data from configuration.yaml.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "api_key": "test-api-key", + "base": "USD", + "quote": "EUR", + "name": "test", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "USD" + assert result["data"] == { + "api_key": "test-api-key", + "base": "USD", + "quote": "EUR", + "name": "test", + } + assert len(mock_setup_entry.mock_calls) == 1 From f11fbf298919c306acd0ec5a4d6cb0485e9489b7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 8 Aug 2022 00:22:41 +0000 Subject: [PATCH 222/903] [ci skip] Translation update --- .../components/asuswrt/translations/sl.json | 11 ++++++++ .../automation/translations/sl.json | 2 +- .../binary_sensor/translations/sl.json | 2 +- .../components/calendar/translations/sl.json | 2 +- .../components/climate/translations/sl.json | 2 +- .../components/fan/translations/sl.json | 2 +- .../flunearyou/translations/ca.json | 12 +++++++++ .../components/group/translations/sl.json | 2 +- .../translations/sensor.ca.json | 1 + .../input_boolean/translations/sl.json | 2 +- .../components/light/translations/sl.json | 2 +- .../media_player/translations/sl.json | 2 +- .../components/mysensors/translations/ca.json | 8 ++++++ .../components/mysensors/translations/de.json | 9 +++++++ .../components/mysensors/translations/fr.json | 9 +++++++ .../components/mysensors/translations/id.json | 8 ++++++ .../mysensors/translations/pt-BR.json | 9 +++++++ .../components/mysensors/translations/ru.json | 9 +++++++ .../mysensors/translations/zh-Hant.json | 9 +++++++ .../openexchangerates/translations/fr.json | 27 +++++++++++++++++++ .../components/remote/translations/sl.json | 2 +- .../components/script/translations/sl.json | 2 +- .../components/sensor/translations/sl.json | 2 +- .../simplepush/translations/ca.json | 3 +++ .../components/switch/translations/sl.json | 2 +- .../tuya/translations/select.sl.json | 8 +++--- .../components/update/translations/sl.json | 7 +++++ .../components/vacuum/translations/sl.json | 2 +- .../wled/translations/select.sl.json | 2 +- 29 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/asuswrt/translations/sl.json create mode 100644 homeassistant/components/openexchangerates/translations/fr.json create mode 100644 homeassistant/components/update/translations/sl.json diff --git a/homeassistant/components/asuswrt/translations/sl.json b/homeassistant/components/asuswrt/translations/sl.json new file mode 100644 index 00000000000..fc65c3c714a --- /dev/null +++ b/homeassistant/components/asuswrt/translations/sl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Naziv" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automation/translations/sl.json b/homeassistant/components/automation/translations/sl.json index 9045a3f3d36..bb5966be95f 100644 --- a/homeassistant/components/automation/translations/sl.json +++ b/homeassistant/components/automation/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/binary_sensor/translations/sl.json b/homeassistant/components/binary_sensor/translations/sl.json index 6b47f4e9e7e..e787ff1eb25 100644 --- a/homeassistant/components/binary_sensor/translations/sl.json +++ b/homeassistant/components/binary_sensor/translations/sl.json @@ -106,7 +106,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" }, "battery": { diff --git a/homeassistant/components/calendar/translations/sl.json b/homeassistant/components/calendar/translations/sl.json index bd917673e78..b8583925c7d 100644 --- a/homeassistant/components/calendar/translations/sl.json +++ b/homeassistant/components/calendar/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/climate/translations/sl.json b/homeassistant/components/climate/translations/sl.json index 037f807b42d..30ce6018c2e 100644 --- a/homeassistant/components/climate/translations/sl.json +++ b/homeassistant/components/climate/translations/sl.json @@ -22,7 +22,7 @@ "fan_only": "Samo ventilator", "heat": "Toplo", "heat_cool": "Gretje/Hlajenje", - "off": "Izklju\u010den" + "off": "Izklopljen" } }, "title": "Klimat" diff --git a/homeassistant/components/fan/translations/sl.json b/homeassistant/components/fan/translations/sl.json index c987bd921c8..28fe83150a2 100644 --- a/homeassistant/components/fan/translations/sl.json +++ b/homeassistant/components/fan/translations/sl.json @@ -15,7 +15,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/flunearyou/translations/ca.json b/homeassistant/components/flunearyou/translations/ca.json index 9ae2ebd9a26..912f88d8b28 100644 --- a/homeassistant/components/flunearyou/translations/ca.json +++ b/homeassistant/components/flunearyou/translations/ca.json @@ -16,5 +16,17 @@ "title": "Configuraci\u00f3 Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "title": "Elimina Flu Near You" + } + } + }, + "title": "Flu Near You ja no est\u00e0 disponible" + } } } \ No newline at end of file diff --git a/homeassistant/components/group/translations/sl.json b/homeassistant/components/group/translations/sl.json index f810bbc6d2d..1295f420c92 100644 --- a/homeassistant/components/group/translations/sl.json +++ b/homeassistant/components/group/translations/sl.json @@ -5,7 +5,7 @@ "home": "Doma", "locked": "Zaklenjeno", "not_home": "Odsoten", - "off": "Izklju\u010den", + "off": "Izklopljen", "ok": "OK", "on": "Vklopljen", "open": "Odprto", diff --git a/homeassistant/components/homekit_controller/translations/sensor.ca.json b/homeassistant/components/homekit_controller/translations/sensor.ca.json index d8abac44cac..dde4926406c 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.ca.json +++ b/homeassistant/components/homekit_controller/translations/sensor.ca.json @@ -8,6 +8,7 @@ "sleepy": "Dispositiu final dorment" }, "homekit_controller__thread_status": { + "border_router": "Encaminador (router) frontera", "child": "Fill", "detached": "Desconnectat", "disabled": "Desactivat", diff --git a/homeassistant/components/input_boolean/translations/sl.json b/homeassistant/components/input_boolean/translations/sl.json index e0f7eb97344..535b2a2a3c6 100644 --- a/homeassistant/components/input_boolean/translations/sl.json +++ b/homeassistant/components/input_boolean/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/light/translations/sl.json b/homeassistant/components/light/translations/sl.json index 9bb6af3c7d5..c4662e582fe 100644 --- a/homeassistant/components/light/translations/sl.json +++ b/homeassistant/components/light/translations/sl.json @@ -19,7 +19,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/media_player/translations/sl.json b/homeassistant/components/media_player/translations/sl.json index 6d24f4e923f..3ea9be6cab7 100644 --- a/homeassistant/components/media_player/translations/sl.json +++ b/homeassistant/components/media_player/translations/sl.json @@ -11,7 +11,7 @@ "state": { "_": { "idle": "V pripravljenosti", - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen", "paused": "Na pavzi", "playing": "Predvajanje", diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 99cf16939ad..1527f2b3307 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -14,6 +14,7 @@ "invalid_serial": "Port s\u00e8rie inv\u00e0lid", "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid", "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida", + "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", "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", @@ -68,6 +69,13 @@ }, "description": "Configuraci\u00f3 de passarel\u00b7la Ethernet" }, + "select_gateway_type": { + "menu_options": { + "gw_mqtt": "Configura passarel\u00b7la (gateway) MQTT", + "gw_serial": "Configura passarel\u00b7la (gateway) s\u00e8rie", + "gw_tcp": "Configura passarel\u00b7la (gateway) TCP" + } + }, "user": { "data": { "gateway_type": "Tipus de passarel\u00b7la" diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index 0b90165fa5a..2ab3da62431 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -14,6 +14,7 @@ "invalid_serial": "Ung\u00fcltiger Serieller Port", "invalid_subscribe_topic": "Ung\u00fcltiges Abonnementthema", "invalid_version": "Ung\u00fcltige MySensors Version", + "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", "not_a_number": "Bitte eine Nummer eingeben", "port_out_of_range": "Die Portnummer muss mindestens 1 und darf h\u00f6chstens 65535 sein", "same_topic": "Themen zum Abonnieren und Ver\u00f6ffentlichen sind gleich", @@ -68,6 +69,14 @@ }, "description": "Einrichtung des Ethernet-Gateways" }, + "select_gateway_type": { + "description": "W\u00e4hle das zu konfigurierende Gateway aus.", + "menu_options": { + "gw_mqtt": "Konfiguriere ein MQTT-Gateway", + "gw_serial": "Konfiguriere ein serielles Gateway", + "gw_tcp": "Konfiguriere ein TCP-Gateway" + } + }, "user": { "data": { "gateway_type": "Gateway-Typ" diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json index 722bf639111..0c691e64f2f 100644 --- a/homeassistant/components/mysensors/translations/fr.json +++ b/homeassistant/components/mysensors/translations/fr.json @@ -14,6 +14,7 @@ "invalid_serial": "Port s\u00e9rie non valide", "invalid_subscribe_topic": "Sujet d'abonnement non valide", "invalid_version": "Version de MySensors non valide", + "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", "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", @@ -68,6 +69,14 @@ }, "description": "Configuration de la passerelle Ethernet" }, + "select_gateway_type": { + "description": "S\u00e9lectionnez la passerelle \u00e0 configurer.", + "menu_options": { + "gw_mqtt": "Configurer une passerelle MQTT", + "gw_serial": "Configurer une passerelle s\u00e9rie", + "gw_tcp": "Configurer une passerelle TCP" + } + }, "user": { "data": { "gateway_type": "Type de passerelle" diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index e6256bd8757..418da20676e 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -14,6 +14,7 @@ "invalid_serial": "Port serial tidak valid", "invalid_subscribe_topic": "Topik langganan tidak valid", "invalid_version": "Versi MySensors tidak valid", + "mqtt_required": "Integrasi MQTT belum disiapkan", "not_a_number": "Masukkan angka", "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535", "same_topic": "Topik subscribe dan publish sama", @@ -68,6 +69,13 @@ }, "description": "Pengaturan gateway Ethernet" }, + "select_gateway_type": { + "menu_options": { + "gw_mqtt": "Konfigurasikan gateway MQTT", + "gw_serial": "Konfigurasikan gateway serial", + "gw_tcp": "Konfigurasikan gateway TCP" + } + }, "user": { "data": { "gateway_type": "Jenis gateway" diff --git a/homeassistant/components/mysensors/translations/pt-BR.json b/homeassistant/components/mysensors/translations/pt-BR.json index 2c0f312da72..29b8a9e9372 100644 --- a/homeassistant/components/mysensors/translations/pt-BR.json +++ b/homeassistant/components/mysensors/translations/pt-BR.json @@ -14,6 +14,7 @@ "invalid_serial": "Porta serial inv\u00e1lida", "invalid_subscribe_topic": "T\u00f3pico de inscri\u00e7\u00e3o inv\u00e1lido", "invalid_version": "Vers\u00e3o MySensors inv\u00e1lida", + "mqtt_required": "A integra\u00e7\u00e3o do MQTT n\u00e3o est\u00e1 configurada", "not_a_number": "Por favor, digite um n\u00famero", "port_out_of_range": "O n\u00famero da porta deve ser no m\u00ednimo 1 e no m\u00e1ximo 65535", "same_topic": "Subscrever e publicar t\u00f3picos s\u00e3o os mesmos", @@ -68,6 +69,14 @@ }, "description": "Configura\u00e7\u00e3o do gateway Ethernet" }, + "select_gateway_type": { + "description": "Selecione qual gateway configurar.", + "menu_options": { + "gw_mqtt": "Configurar um gateway MQTT", + "gw_serial": "Configurar um gateway serial", + "gw_tcp": "Configurar um gateway TCP" + } + }, "user": { "data": { "gateway_type": "Tipo de gateway" diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index 2801b8e8c00..8d24debaf33 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -14,6 +14,7 @@ "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.", + "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", "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.", @@ -68,6 +69,14 @@ }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 Ethernet" }, + "select_gateway_type": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "menu_options": { + "gw_mqtt": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0448\u043b\u044e\u0437 MQTT", + "gw_serial": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0448\u043b\u044e\u0437", + "gw_tcp": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c TCP-\u0448\u043b\u044e\u0437\u0430" + } + }, "user": { "data": { "gateway_type": "\u0422\u0438\u043f \u0448\u043b\u044e\u0437\u0430" diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index a3ec7ba2dd7..378624f65b0 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -14,6 +14,7 @@ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", + "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", "not_a_number": "\u8acb\u8f38\u5165\u6578\u5b57", "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", @@ -68,6 +69,14 @@ }, "description": "\u9598\u9053\u5668\u4e59\u592a\u7db2\u8def\u8a2d\u5b9a" }, + "select_gateway_type": { + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u9598\u9053\u5668\u3002", + "menu_options": { + "gw_mqtt": "\u8a2d\u5b9a MQTT \u9598\u9053\u5668", + "gw_serial": "\u8a2d\u5b9a\u5e8f\u5217\u9598\u9053\u5668", + "gw_tcp": "\u8a2d\u5b9a TCP \u9598\u9053\u5668" + } + }, "user": { "data": { "gateway_type": "\u9598\u9053\u5668\u985e\u5225" diff --git a/homeassistant/components/openexchangerates/translations/fr.json b/homeassistant/components/openexchangerates/translations/fr.json new file mode 100644 index 00000000000..a6b5929245a --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "base": "Devise de r\u00e9f\u00e9rence" + }, + "data_description": { + "base": "L'utilisation d'une devise de r\u00e9f\u00e9rence autre que l'USD n\u00e9cessite un [forfait payant]({signup})." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/sl.json b/homeassistant/components/remote/translations/sl.json index 5d2864f5b89..115d50127b0 100644 --- a/homeassistant/components/remote/translations/sl.json +++ b/homeassistant/components/remote/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/script/translations/sl.json b/homeassistant/components/script/translations/sl.json index cd38e49dcb5..f57b7a5d6b7 100644 --- a/homeassistant/components/script/translations/sl.json +++ b/homeassistant/components/script/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/sensor/translations/sl.json b/homeassistant/components/sensor/translations/sl.json index a83b273946c..f544b44ecfd 100644 --- a/homeassistant/components/sensor/translations/sl.json +++ b/homeassistant/components/sensor/translations/sl.json @@ -23,7 +23,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/simplepush/translations/ca.json b/homeassistant/components/simplepush/translations/ca.json index d4e449d8a35..8755c00a076 100644 --- a/homeassistant/components/simplepush/translations/ca.json +++ b/homeassistant/components/simplepush/translations/ca.json @@ -22,6 +22,9 @@ "deprecated_yaml": { "description": "La configuraci\u00f3 de Simplepush mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Simplepush del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de Simplepush est\u00e0 sent eliminada" + }, + "removed_yaml": { + "title": "La configuraci\u00f3 YAML de Simplepush s'ha eliminat" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/translations/sl.json b/homeassistant/components/switch/translations/sl.json index 7995033d1c2..565d84b56c2 100644 --- a/homeassistant/components/switch/translations/sl.json +++ b/homeassistant/components/switch/translations/sl.json @@ -16,7 +16,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/tuya/translations/select.sl.json b/homeassistant/components/tuya/translations/select.sl.json index cad127241aa..4929c0e2e76 100644 --- a/homeassistant/components/tuya/translations/select.sl.json +++ b/homeassistant/components/tuya/translations/select.sl.json @@ -5,7 +5,7 @@ }, "tuya__basic_nightvision": { "0": "Samodejno", - "1": "Izklju\u010den", + "1": "Izklopljen", "2": "Vklopljen" }, "tuya__led_type": { @@ -14,7 +14,7 @@ "led": "LED" }, "tuya__light_mode": { - "none": "Izklju\u010den", + "none": "Izklopljen", "pos": "Navedite lokacijo stikala", "relay": "Navedite stanje vklopa/izklopa" }, @@ -25,9 +25,9 @@ "tuya__relay_status": { "last": "Zapomni si zadnje stanje", "memory": "Zapomni si zadnje stanje", - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen", - "power_off": "Izklju\u010den", + "power_off": "Izklopljen", "power_on": "Vklopljen" } } diff --git a/homeassistant/components/update/translations/sl.json b/homeassistant/components/update/translations/sl.json new file mode 100644 index 00000000000..30291553a42 --- /dev/null +++ b/homeassistant/components/update/translations/sl.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} je posodobljen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/sl.json b/homeassistant/components/vacuum/translations/sl.json index faa94ef07cf..defef95f6f8 100644 --- a/homeassistant/components/vacuum/translations/sl.json +++ b/homeassistant/components/vacuum/translations/sl.json @@ -19,7 +19,7 @@ "docked": "Priklju\u010den", "error": "Napaka", "idle": "V pripravljenosti", - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen", "paused": "Na pavzi", "returning": "Vra\u010danje na postajo" diff --git a/homeassistant/components/wled/translations/select.sl.json b/homeassistant/components/wled/translations/select.sl.json index b752ae28d89..339c39fde77 100644 --- a/homeassistant/components/wled/translations/select.sl.json +++ b/homeassistant/components/wled/translations/select.sl.json @@ -1,7 +1,7 @@ { "state": { "wled__live_override": { - "0": "Izklju\u010den", + "0": "Izklopljen", "1": "Vklopljen", "2": "Dokler se naprava znova ne za\u017eene" } From 42dd0cabb35920732bc1ceb52f37d5ae875df74c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 8 Aug 2022 07:52:39 +0300 Subject: [PATCH 223/903] Fix Shelly H&T sensors rounding (#76426) --- homeassistant/components/shelly/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 5a36b5e99bf..c37bbbd5ff8 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -362,6 +362,7 @@ RPC_SENSORS: Final = { sub_key="tC", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, + value=lambda status, _: round(status, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=True, @@ -392,6 +393,7 @@ RPC_SENSORS: Final = { sub_key="rh", name="Humidity", native_unit_of_measurement=PERCENTAGE, + value=lambda status, _: round(status, 1), device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=True, From bcc2be344a1c5405e12cc3de47ffbd9538b1ff8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 11:27:36 +0200 Subject: [PATCH 224/903] Bump actions/cache from 3.0.5 to 3.0.6 (#76432) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fd3909ca23a..ae7d8cfd48d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: cache: "pip" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -185,7 +185,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -211,7 +211,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -222,7 +222,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -260,7 +260,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -271,7 +271,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -312,7 +312,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -323,7 +323,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -353,7 +353,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -364,7 +364,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -480,7 +480,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -488,7 +488,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PIP_CACHE }} key: >- @@ -538,7 +538,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -570,7 +570,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -603,7 +603,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -647,7 +647,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -695,7 +695,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -749,7 +749,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From a1d5a4bc793cd85dbe6fd12380f7cef0b4b37d9e Mon Sep 17 00:00:00 2001 From: Laz <87186949+lazdavila@users.noreply.github.com> Date: Mon, 8 Aug 2022 19:18:42 +0930 Subject: [PATCH 225/903] Add Escea fireplace integration (#56039) Co-authored-by: Teemu R. Co-authored-by: J. Nick Koston Co-authored-by: Martin Hjelmare --- .coveragerc | 3 + CODEOWNERS | 2 + homeassistant/components/escea/__init__.py | 22 ++ homeassistant/components/escea/climate.py | 221 ++++++++++++++++++ homeassistant/components/escea/config_flow.py | 52 +++++ homeassistant/components/escea/const.py | 15 ++ homeassistant/components/escea/discovery.py | 75 ++++++ homeassistant/components/escea/manifest.json | 12 + homeassistant/components/escea/strings.json | 13 ++ .../components/escea/translations/en.json | 13 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/escea/__init__.py | 1 + tests/components/escea/test_config_flow.py | 117 ++++++++++ 16 files changed, 554 insertions(+) create mode 100644 homeassistant/components/escea/__init__.py create mode 100644 homeassistant/components/escea/climate.py create mode 100644 homeassistant/components/escea/config_flow.py create mode 100644 homeassistant/components/escea/const.py create mode 100644 homeassistant/components/escea/discovery.py create mode 100644 homeassistant/components/escea/manifest.json create mode 100644 homeassistant/components/escea/strings.json create mode 100644 homeassistant/components/escea/translations/en.json create mode 100644 tests/components/escea/__init__.py create mode 100644 tests/components/escea/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 5068574df78..9d839cdb5f6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -309,6 +309,9 @@ omit = homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/climate.py + homeassistant/components/escea/climate.py + homeassistant/components/escea/discovery.py + homeassistant/components/escea/__init__.py homeassistant/components/esphome/__init__.py homeassistant/components/esphome/binary_sensor.py homeassistant/components/esphome/button.py diff --git a/CODEOWNERS b/CODEOWNERS index e10a8a0b26c..906119e9006 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -309,6 +309,8 @@ build.json @home-assistant/supervisor /tests/components/epson/ @pszafer /homeassistant/components/epsonworkforce/ @ThaStealth /homeassistant/components/eq3btsmart/ @rytilahti +/homeassistant/components/escea/ @lazdavila +/tests/components/escea/ @lazdavila /homeassistant/components/esphome/ @OttoWinter @jesserockz /tests/components/esphome/ @OttoWinter @jesserockz /homeassistant/components/evil_genius_labs/ @balloob diff --git a/homeassistant/components/escea/__init__.py b/homeassistant/components/escea/__init__.py new file mode 100644 index 00000000000..95e6765fa95 --- /dev/null +++ b/homeassistant/components/escea/__init__.py @@ -0,0 +1,22 @@ +"""Platform for the Escea fireplace.""" + +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .discovery import async_start_discovery_service, async_stop_discovery_service + +PLATFORMS = [CLIMATE_DOMAIN] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + await async_start_discovery_service(hass) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload the config entry and stop discovery process.""" + await async_stop_discovery_service(hass) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py new file mode 100644 index 00000000000..7bd7d54353c --- /dev/null +++ b/homeassistant/components/escea/climate.py @@ -0,0 +1,221 @@ +"""Support for the Escea Fireplace.""" +from __future__ import annotations + +from collections.abc import Coroutine +import logging +from typing import Any + +from pescea import Controller + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + DATA_DISCOVERY_SERVICE, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, + DOMAIN, + ESCEA_FIREPLACE, + ESCEA_MANUFACTURER, + ICON, +) + +_LOGGER = logging.getLogger(__name__) + +_ESCEA_FAN_TO_HA = { + Controller.Fan.FLAME_EFFECT: FAN_LOW, + Controller.Fan.FAN_BOOST: FAN_HIGH, + Controller.Fan.AUTO: FAN_AUTO, +} +_HA_FAN_TO_ESCEA = {v: k for k, v in _ESCEA_FAN_TO_HA.items()} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize an Escea Controller.""" + discovery_service = hass.data[DATA_DISCOVERY_SERVICE] + + @callback + def init_controller(ctrl: Controller) -> None: + """Register the controller device.""" + + _LOGGER.debug("Controller UID=%s discovered", ctrl.device_uid) + + entity = ControllerEntity(ctrl) + async_add_entities([entity]) + + # create any components not yet created + for controller in discovery_service.controllers.values(): + init_controller(controller) + + # connect to register any further components + config_entry.async_on_unload( + async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller) + ) + + +class ControllerEntity(ClimateEntity): + """Representation of Escea Controller.""" + + _attr_fan_modes = list(_HA_FAN_TO_ESCEA) + _attr_has_entity_name = True + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] + _attr_icon = ICON + _attr_precision = PRECISION_WHOLE + _attr_should_poll = False + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + _attr_target_temperature_step = PRECISION_WHOLE + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, controller: Controller) -> None: + """Initialise ControllerDevice.""" + self._controller = controller + + self._attr_min_temp = controller.min_temp + self._attr_max_temp = controller.max_temp + + self._attr_unique_id = controller.device_uid + + # temporary assignment to get past mypy checker + unique_id: str = controller.device_uid + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ESCEA_MANUFACTURER, + name=ESCEA_FIREPLACE, + ) + + self._attr_available = True + + async def async_added_to_hass(self) -> None: + """Call on adding to hass. + + Registers for connect/disconnect/update events + """ + + @callback + def controller_disconnected(ctrl: Controller, ex: Exception) -> None: + """Disconnected from controller.""" + if ctrl is not self._controller: + return + self.set_available(False, ex) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_DISCONNECTED, controller_disconnected + ) + ) + + @callback + def controller_reconnected(ctrl: Controller) -> None: + """Reconnected to controller.""" + if ctrl is not self._controller: + return + self.set_available(True) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_RECONNECTED, controller_reconnected + ) + ) + + @callback + def controller_update(ctrl: Controller) -> None: + """Handle controller data updates.""" + if ctrl is not self._controller: + return + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_UPDATE, controller_update + ) + ) + + @callback + def set_available(self, available: bool, ex: Exception = None) -> None: + """Set availability for the controller.""" + if self._attr_available == available: + return + + if available: + _LOGGER.debug("Reconnected controller %s ", self._controller.device_uid) + else: + _LOGGER.debug( + "Controller %s disconnected due to exception: %s", + self._controller.device_uid, + ex, + ) + + self._attr_available = available + self.async_write_ha_state() + + @property + def hvac_mode(self) -> HVACMode: + """Return current operation ie. heat, cool, idle.""" + return HVACMode.HEAT if self._controller.is_on else HVACMode.OFF + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self._controller.current_temp + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + return self._controller.desired_temp + + @property + def fan_mode(self) -> str | None: + """Return the fan setting.""" + return _ESCEA_FAN_TO_HA[self._controller.fan] + + async def wrap_and_catch(self, coro: Coroutine) -> None: + """Catch any connection errors and set unavailable.""" + try: + await coro + except ConnectionError as ex: + self.set_available(False, ex) + else: + self.set_available(True) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + await self.wrap_and_catch(self._controller.set_desired_temp(temp)) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + await self.wrap_and_catch(self._controller.set_fan(_HA_FAN_TO_ESCEA[fan_mode])) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target operation mode.""" + await self.wrap_and_catch(self._controller.set_on(hvac_mode == HVACMode.HEAT)) + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self.wrap_and_catch(self._controller.set_on(True)) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + await self.wrap_and_catch(self._controller.set_on(False)) diff --git a/homeassistant/components/escea/config_flow.py b/homeassistant/components/escea/config_flow.py new file mode 100644 index 00000000000..5d0dfea1157 --- /dev/null +++ b/homeassistant/components/escea/config_flow.py @@ -0,0 +1,52 @@ +"""Config flow for escea.""" +import asyncio +from contextlib import suppress +import logging + +from async_timeout import timeout + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_entry_flow +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + DISPATCH_CONTROLLER_DISCOVERED, + DOMAIN, + ESCEA_FIREPLACE, + TIMEOUT_DISCOVERY, +) +from .discovery import async_start_discovery_service, async_stop_discovery_service + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass: HomeAssistant) -> bool: + + controller_ready = asyncio.Event() + + @callback + def dispatch_discovered(_): + controller_ready.set() + + remove_handler = async_dispatcher_connect( + hass, DISPATCH_CONTROLLER_DISCOVERED, dispatch_discovered + ) + + discovery_service = await async_start_discovery_service(hass) + + with suppress(asyncio.TimeoutError): + async with timeout(TIMEOUT_DISCOVERY): + await controller_ready.wait() + + remove_handler() + + if not discovery_service.controllers: + await async_stop_discovery_service(hass) + _LOGGER.debug("No controllers found") + return False + + _LOGGER.debug("Controllers %s", discovery_service.controllers) + return True + + +config_entry_flow.register_discovery_flow(DOMAIN, ESCEA_FIREPLACE, _async_has_devices) diff --git a/homeassistant/components/escea/const.py b/homeassistant/components/escea/const.py new file mode 100644 index 00000000000..c35e77e2719 --- /dev/null +++ b/homeassistant/components/escea/const.py @@ -0,0 +1,15 @@ +"""Constants used by the escea component.""" + +DOMAIN = "escea" +ESCEA_MANUFACTURER = "Escea" +ESCEA_FIREPLACE = "Escea Fireplace" +ICON = "mdi:fire" + +DATA_DISCOVERY_SERVICE = "escea_discovery" + +DISPATCH_CONTROLLER_DISCOVERED = "escea_controller_discovered" +DISPATCH_CONTROLLER_DISCONNECTED = "escea_controller_disconnected" +DISPATCH_CONTROLLER_RECONNECTED = "escea_controller_reconnected" +DISPATCH_CONTROLLER_UPDATE = "escea_controller_update" + +TIMEOUT_DISCOVERY = 20 diff --git a/homeassistant/components/escea/discovery.py b/homeassistant/components/escea/discovery.py new file mode 100644 index 00000000000..0d7f3024bfc --- /dev/null +++ b/homeassistant/components/escea/discovery.py @@ -0,0 +1,75 @@ +"""Internal discovery service for Escea Fireplace.""" +from __future__ import annotations + +from pescea import ( + AbstractDiscoveryService, + Controller, + Listener, + discovery_service as pescea_discovery_service, +) + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + DATA_DISCOVERY_SERVICE, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, +) + + +class DiscoveryServiceListener(Listener): + """Discovery data and interfacing with pescea library.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialise discovery service.""" + super().__init__() + self.hass = hass + + # Listener interface + def controller_discovered(self, ctrl: Controller) -> None: + """Handle new controller discoverery.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCOVERED, ctrl) + + def controller_disconnected(self, ctrl: Controller, ex: Exception) -> None: + """On disconnect from controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCONNECTED, ctrl, ex) + + def controller_reconnected(self, ctrl: Controller) -> None: + """On reconnect to controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl) + + def controller_update(self, ctrl: Controller) -> None: + """System update message is received from the controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl) + + +async def async_start_discovery_service( + hass: HomeAssistant, +) -> AbstractDiscoveryService: + """Set up the pescea internal discovery.""" + discovery_service = hass.data.get(DATA_DISCOVERY_SERVICE) + if discovery_service: + # Already started + return discovery_service + + # discovery local services + listener = DiscoveryServiceListener(hass) + discovery_service = pescea_discovery_service(listener) + hass.data[DATA_DISCOVERY_SERVICE] = discovery_service + + await discovery_service.start_discovery() + + return discovery_service + + +async def async_stop_discovery_service(hass: HomeAssistant) -> None: + """Stop the discovery service.""" + discovery_service = hass.data.get(DATA_DISCOVERY_SERVICE) + if not discovery_service: + return + + await discovery_service.close() + del hass.data[DATA_DISCOVERY_SERVICE] diff --git a/homeassistant/components/escea/manifest.json b/homeassistant/components/escea/manifest.json new file mode 100644 index 00000000000..4feb28e982c --- /dev/null +++ b/homeassistant/components/escea/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "escea", + "name": "Escea", + "documentation": "https://www.home-assistant.io/integrations/escea", + "codeowners": ["@lazdavila"], + "requirements": ["pescea==1.0.12"], + "config_flow": true, + "homekit": { + "models": ["Escea"] + }, + "iot_class": "local_push" +} diff --git a/homeassistant/components/escea/strings.json b/homeassistant/components/escea/strings.json new file mode 100644 index 00000000000..8744e74fd3f --- /dev/null +++ b/homeassistant/components/escea/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Do you want to set up an Escea fireplace?" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/escea/translations/en.json b/homeassistant/components/escea/translations/en.json new file mode 100644 index 00000000000..7d4e7b6fa8b --- /dev/null +++ b/homeassistant/components/escea/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Do you want to set up an Escea fireplace?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 327781c2562..597cb10fd7f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -99,6 +99,7 @@ FLOWS = { "enphase_envoy", "environment_canada", "epson", + "escea", "esphome", "evil_genius_labs", "ezviz", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 5284eef02a7..d59d37f4579 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -426,6 +426,7 @@ HOMEKIT = { "C105X": "roku", "C135X": "roku", "EB-*": "ecobee", + "Escea": "escea", "HHKBridge*": "hive", "Healty Home Coach": "netatmo", "Iota": "abode", diff --git a/requirements_all.txt b/requirements_all.txt index 3a755023ac7..ddf22e985c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1227,6 +1227,9 @@ peco==0.0.29 # homeassistant.components.pencom pencompy==0.0.3 +# homeassistant.components.escea +pescea==1.0.12 + # homeassistant.components.aruba # homeassistant.components.cisco_ios # homeassistant.components.pandora diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7211bb3f08a..a9c55603282 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -854,6 +854,9 @@ pdunehd==1.3.2 # homeassistant.components.peco peco==0.0.29 +# homeassistant.components.escea +pescea==1.0.12 + # homeassistant.components.aruba # homeassistant.components.cisco_ios # homeassistant.components.pandora diff --git a/tests/components/escea/__init__.py b/tests/components/escea/__init__.py new file mode 100644 index 00000000000..b31616f7e58 --- /dev/null +++ b/tests/components/escea/__init__.py @@ -0,0 +1 @@ +"""Escea tests.""" diff --git a/tests/components/escea/test_config_flow.py b/tests/components/escea/test_config_flow.py new file mode 100644 index 00000000000..c4a5b323d22 --- /dev/null +++ b/tests/components/escea/test_config_flow.py @@ -0,0 +1,117 @@ +"""Tests for Escea.""" + +from collections.abc import Callable, Coroutine +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.escea.const import DOMAIN, ESCEA_FIREPLACE +from homeassistant.components.escea.discovery import DiscoveryServiceListener +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_discovery_service") +def mock_discovery_service_fixture() -> AsyncMock: + """Mock discovery service.""" + discovery_service = AsyncMock() + discovery_service.controllers = {} + return discovery_service + + +@pytest.fixture(name="mock_controller") +def mock_controller_fixture() -> MagicMock: + """Mock controller.""" + controller = MagicMock() + return controller + + +def _mock_start_discovery( + discovery_service: MagicMock, controller: MagicMock +) -> Callable[[], Coroutine[None, None, None]]: + """Mock start discovery service.""" + + async def do_discovered() -> None: + """Call the listener callback.""" + listener: DiscoveryServiceListener = discovery_service.call_args[0][0] + listener.controller_discovered(controller) + + return do_discovered + + +async def test_not_found( + hass: HomeAssistant, mock_discovery_service: MagicMock +) -> None: + """Test not finding any Escea controllers.""" + + with patch( + "homeassistant.components.escea.discovery.pescea_discovery_service" + ) as discovery_service, patch( + "homeassistant.components.escea.config_flow.TIMEOUT_DISCOVERY", 0 + ): + discovery_service.return_value = mock_discovery_service + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_devices_found" + assert discovery_service.return_value.close.call_count == 1 + + +async def test_found( + hass: HomeAssistant, mock_controller: MagicMock, mock_discovery_service: AsyncMock +) -> None: + """Test finding an Escea controller.""" + mock_discovery_service.controllers["test-uid"] = mock_controller + + with patch( + "homeassistant.components.escea.async_setup_entry", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.escea.discovery.pescea_discovery_service" + ) as discovery_service: + discovery_service.return_value = mock_discovery_service + mock_discovery_service.start_discovery.side_effect = _mock_start_discovery( + discovery_service, mock_controller + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert mock_setup.call_count == 1 + + +async def test_single_instance_allowed(hass: HomeAssistant) -> None: + """Test single instance allowed.""" + config_entry = MockConfigEntry(domain=DOMAIN, title=ESCEA_FIREPLACE) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.escea.discovery.pescea_discovery_service" + ) as discovery_service: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + assert discovery_service.call_count == 0 From 7cd4be1310b3f76398b4404d3f4ecb26b9533cee Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Mon, 8 Aug 2022 13:47:05 +0200 Subject: [PATCH 226/903] Add tests for the HDMI-CEC integration (#75094) * Add basic tests to the HDMI-CEC component * Add tests for the HDMI-CEC switch component * Add test for watchdog code * Start adding tests for the HDMI-CEC media player platform Also some cleanup and code move. * Add more tests for media_player And cleanup some switch tests. * Improve xfail message for features * Align test pyCEC dependency with main dependency * Make fixtures snake_case * Cleanup call asserts * Cleanup service tests * fix issues with media player tests * Cleanup MockHDMIDevice class * Cleanup watchdog tests * Add myself as code owner for the HDMI-CEC integration * Fix async fire time changed time jump * Fix event api sync context * Delint tests * Parametrize watchdog test Co-authored-by: Martin Hjelmare --- .coveragerc | 1 - CODEOWNERS | 2 + homeassistant/components/hdmi_cec/__init__.py | 2 +- .../components/hdmi_cec/manifest.json | 2 +- requirements_test_all.txt | 3 + tests/components/hdmi_cec/__init__.py | 49 ++ tests/components/hdmi_cec/conftest.py | 59 +++ tests/components/hdmi_cec/test_init.py | 469 +++++++++++++++++ .../components/hdmi_cec/test_media_player.py | 493 ++++++++++++++++++ tests/components/hdmi_cec/test_switch.py | 252 +++++++++ 10 files changed, 1329 insertions(+), 3 deletions(-) create mode 100644 tests/components/hdmi_cec/__init__.py create mode 100644 tests/components/hdmi_cec/conftest.py create mode 100644 tests/components/hdmi_cec/test_init.py create mode 100644 tests/components/hdmi_cec/test_media_player.py create mode 100644 tests/components/hdmi_cec/test_switch.py diff --git a/.coveragerc b/.coveragerc index 9d839cdb5f6..0a1430cce91 100644 --- a/.coveragerc +++ b/.coveragerc @@ -473,7 +473,6 @@ omit = homeassistant/components/harmony/remote.py homeassistant/components/harmony/util.py homeassistant/components/haveibeenpwned/sensor.py - homeassistant/components/hdmi_cec/* homeassistant/components/heatmiser/climate.py homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvisioncam/switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 906119e9006..292879aabea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -436,6 +436,8 @@ build.json @home-assistant/supervisor /tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan /homeassistant/components/hassio/ @home-assistant/supervisor /tests/components/hassio/ @home-assistant/supervisor +/homeassistant/components/hdmi_cec/ @inytar +/tests/components/hdmi_cec/ @inytar /homeassistant/components/heatmiser/ @andylockran /homeassistant/components/heos/ @andrewsayre /tests/components/heos/ @andrewsayre diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 056eacb6a5b..fa64b3dac41 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -219,7 +219,7 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 def _adapter_watchdog(now=None): _LOGGER.debug("Reached _adapter_watchdog") - event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) + event.call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) if not adapter.initialized: _LOGGER.info("Adapter not initialized; Trying to restart") hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE) diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index 8ea56a51fa9..46e7f61719f 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -3,7 +3,7 @@ "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", "requirements": ["pyCEC==0.5.2"], - "codeowners": [], + "codeowners": ["@inytar"], "iot_class": "local_push", "loggers": ["pycec"] } diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9c55603282..0326765cf74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -936,6 +936,9 @@ py-synologydsm-api==1.0.8 # homeassistant.components.seventeentrack py17track==2021.12.2 +# homeassistant.components.hdmi_cec +pyCEC==0.5.2 + # homeassistant.components.control4 pyControl4==0.0.6 diff --git a/tests/components/hdmi_cec/__init__.py b/tests/components/hdmi_cec/__init__.py new file mode 100644 index 00000000000..c131bf96b41 --- /dev/null +++ b/tests/components/hdmi_cec/__init__.py @@ -0,0 +1,49 @@ +"""Tests for the HDMI-CEC component.""" +from unittest.mock import AsyncMock, Mock + +from homeassistant.components.hdmi_cec import KeyPressCommand, KeyReleaseCommand + + +class MockHDMIDevice: + """Mock of a HDMIDevice.""" + + def __init__(self, *, logical_address, **values): + """Mock of a HDMIDevice.""" + self.set_update_callback = Mock(side_effect=self._set_update_callback) + self.logical_address = logical_address + self.name = f"hdmi_{logical_address:x}" + if "power_status" not in values: + # Default to invalid state. + values["power_status"] = -1 + self._values = values + self.turn_on = Mock() + self.turn_off = Mock() + self.send_command = Mock() + self.async_send_command = AsyncMock() + + def __getattr__(self, name): + """Get attribute from `_values` if not explicitly set.""" + return self._values.get(name) + + def __setattr__(self, name, value): + """Set attributes in `_values` if not one of the known attributes.""" + if name in ("power_status", "status"): + self._values[name] = value + self._update() + else: + super().__setattr__(name, value) + + def _set_update_callback(self, update): + self._update = update + + +def assert_key_press_release(fnc, count=0, *, dst, key): + """Assert that correct KeyPressCommand & KeyReleaseCommand where sent.""" + assert fnc.call_count >= count * 2 + 1 + press_arg = fnc.call_args_list[count * 2].args[0] + release_arg = fnc.call_args_list[count * 2 + 1].args[0] + assert isinstance(press_arg, KeyPressCommand) + assert press_arg.key == key + assert press_arg.dst == dst + assert isinstance(release_arg, KeyReleaseCommand) + assert release_arg.dst == dst diff --git a/tests/components/hdmi_cec/conftest.py b/tests/components/hdmi_cec/conftest.py new file mode 100644 index 00000000000..c5f82c04e19 --- /dev/null +++ b/tests/components/hdmi_cec/conftest.py @@ -0,0 +1,59 @@ +"""Tests for the HDMI-CEC component.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.hdmi_cec import DOMAIN +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component + + +@pytest.fixture(name="mock_cec_adapter", autouse=True) +def mock_cec_adapter_fixture(): + """Mock CecAdapter. + + Always mocked as it imports the `cec` library which is part of `libcec`. + """ + with patch( + "homeassistant.components.hdmi_cec.CecAdapter", autospec=True + ) as mock_cec_adapter: + yield mock_cec_adapter + + +@pytest.fixture(name="mock_hdmi_network") +def mock_hdmi_network_fixture(): + """Mock HDMINetwork.""" + with patch( + "homeassistant.components.hdmi_cec.HDMINetwork", autospec=True + ) as mock_hdmi_network: + yield mock_hdmi_network + + +@pytest.fixture +def create_hdmi_network(hass, mock_hdmi_network): + """Create an initialized mock hdmi_network.""" + + async def hdmi_network(config=None): + if not config: + config = {} + await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + + mock_hdmi_network_instance = mock_hdmi_network.return_value + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + return mock_hdmi_network_instance + + return hdmi_network + + +@pytest.fixture +def create_cec_entity(hass): + """Create a CecEntity.""" + + async def cec_entity(hdmi_network, device): + new_device_callback = hdmi_network.set_new_device_callback.call_args.args[0] + new_device_callback(device) + await hass.async_block_till_done() + + return cec_entity diff --git a/tests/components/hdmi_cec/test_init.py b/tests/components/hdmi_cec/test_init.py new file mode 100644 index 00000000000..751c9b051f0 --- /dev/null +++ b/tests/components/hdmi_cec/test_init.py @@ -0,0 +1,469 @@ +"""Tests for the HDMI-CEC component.""" +from datetime import timedelta +from unittest.mock import ANY, PropertyMock, call, patch + +import pytest +import voluptuous as vol + +from homeassistant.components.hdmi_cec import ( + DOMAIN, + EVENT_HDMI_CEC_UNAVAILABLE, + SERVICE_POWER_ON, + SERVICE_SELECT_DEVICE, + SERVICE_SEND_COMMAND, + SERVICE_STANDBY, + SERVICE_UPDATE_DEVICES, + SERVICE_VOLUME, + WATCHDOG_INTERVAL, + CecCommand, + KeyPressCommand, + KeyReleaseCommand, + PhysicalAddress, + parse_mapping, +) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from . import assert_key_press_release + +from tests.common import ( + MockEntity, + MockEntityPlatform, + async_capture_events, + async_fire_time_changed, +) + + +@pytest.fixture(name="mock_tcp_adapter") +def mock_tcp_adapter_fixture(): + """Mock TcpAdapter.""" + with patch( + "homeassistant.components.hdmi_cec.TcpAdapter", autospec=True + ) as mock_tcp_adapter: + yield mock_tcp_adapter + + +@pytest.mark.parametrize( + "mapping,expected", + [ + ({}, []), + ( + { + "TV": "0.0.0.0", + "Pi Zero": "1.0.0.0", + "Fire TV Stick": "2.1.0.0", + "Chromecast": "2.2.0.0", + "Another Device": "2.3.0.0", + "BlueRay player": "3.0.0.0", + }, + [ + ("TV", "0.0.0.0"), + ("Pi Zero", "1.0.0.0"), + ("Fire TV Stick", "2.1.0.0"), + ("Chromecast", "2.2.0.0"), + ("Another Device", "2.3.0.0"), + ("BlueRay player", "3.0.0.0"), + ], + ), + ( + { + 1: "Pi Zero", + 2: { + 1: "Fire TV Stick", + 2: "Chromecast", + 3: "Another Device", + }, + 3: "BlueRay player", + }, + [ + ("Pi Zero", [1, 0, 0, 0]), + ("Fire TV Stick", [2, 1, 0, 0]), + ("Chromecast", [2, 2, 0, 0]), + ("Another Device", [2, 3, 0, 0]), + ("BlueRay player", [3, 0, 0, 0]), + ], + ), + ], +) +def test_parse_mapping_physical_address(mapping, expected): + """Test the device config mapping function.""" + result = parse_mapping(mapping) + result = [ + (r[0], str(r[1]) if isinstance(r[1], PhysicalAddress) else r[1]) for r in result + ] + assert result == expected + + +# Test Setup + + +async def test_setup_cec_adapter(hass, mock_cec_adapter, mock_hdmi_network): + """Test the general setup of this component.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + mock_cec_adapter.assert_called_once_with(name="HA", activate_source=False) + mock_hdmi_network.assert_called_once() + call_args = mock_hdmi_network.call_args + assert call_args == call(mock_cec_adapter.return_value, loop=ANY) + assert call_args.kwargs["loop"] in (None, hass.loop) + + mock_hdmi_network_instance = mock_hdmi_network.return_value + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + mock_hdmi_network_instance.start.assert_called_once_with() + mock_hdmi_network_instance.set_new_device_callback.assert_called_once() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + mock_hdmi_network_instance.stop.assert_called_once_with() + + +@pytest.mark.parametrize("osd_name", ["test", "test_a_long_name"]) +async def test_setup_set_osd_name(hass, osd_name, mock_cec_adapter): + """Test the setup of this component with the `osd_name` config setting.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {"osd_name": osd_name}}) + + mock_cec_adapter.assert_called_once_with(name=osd_name[:12], activate_source=False) + + +async def test_setup_tcp_adapter(hass, mock_tcp_adapter, mock_hdmi_network): + """Test the setup of this component with the TcpAdapter (`host` config setting).""" + host = "0.0.0.0" + + await async_setup_component(hass, DOMAIN, {DOMAIN: {"host": host}}) + + mock_tcp_adapter.assert_called_once_with(host, name="HA", activate_source=False) + mock_hdmi_network.assert_called_once() + call_args = mock_hdmi_network.call_args + assert call_args == call(mock_tcp_adapter.return_value, loop=ANY) + assert call_args.kwargs["loop"] in (None, hass.loop) + + mock_hdmi_network_instance = mock_hdmi_network.return_value + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + mock_hdmi_network_instance.start.assert_called_once_with() + mock_hdmi_network_instance.set_new_device_callback.assert_called_once() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + mock_hdmi_network_instance.stop.assert_called_once_with() + + +# Test services + + +async def test_service_power_on(hass, create_hdmi_network): + """Test the power on service call.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_POWER_ON, + {}, + blocking=True, + ) + + mock_hdmi_network_instance.power_on.assert_called_once_with() + + +async def test_service_standby(hass, create_hdmi_network): + """Test the standby service call.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_STANDBY, + {}, + blocking=True, + ) + + mock_hdmi_network_instance.standby.assert_called_once_with() + + +async def test_service_select_device_alias(hass, create_hdmi_network): + """Test the select device service call with a known alias.""" + mock_hdmi_network_instance = await create_hdmi_network( + {"devices": {"Chromecast": "1.0.0.0"}} + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_DEVICE, + {"device": "Chromecast"}, + blocking=True, + ) + + mock_hdmi_network_instance.active_source.assert_called_once() + physical_address = mock_hdmi_network_instance.active_source.call_args.args[0] + assert isinstance(physical_address, PhysicalAddress) + assert str(physical_address) == "1.0.0.0" + + +class MockCecEntity(MockEntity): + """Mock CEC entity.""" + + @property + def extra_state_attributes(self): + """Set the physical address in the attributes.""" + return {"physical_address": self._values["physical_address"]} + + +async def test_service_select_device_entity(hass, create_hdmi_network): + """Test the select device service call with an existing entity.""" + platform = MockEntityPlatform(hass) + await platform.async_add_entities( + [MockCecEntity(name="hdmi_3", physical_address="3.0.0.0")] + ) + + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_DEVICE, + {"device": "test_domain.hdmi_3"}, + blocking=True, + ) + + mock_hdmi_network_instance.active_source.assert_called_once() + physical_address = mock_hdmi_network_instance.active_source.call_args.args[0] + assert isinstance(physical_address, PhysicalAddress) + assert str(physical_address) == "3.0.0.0" + + +async def test_service_select_device_physical_address(hass, create_hdmi_network): + """Test the select device service call with a raw physical address.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_DEVICE, + {"device": "1.1.0.0"}, + blocking=True, + ) + + mock_hdmi_network_instance.active_source.assert_called_once() + physical_address = mock_hdmi_network_instance.active_source.call_args.args[0] + assert isinstance(physical_address, PhysicalAddress) + assert str(physical_address) == "1.1.0.0" + + +async def test_service_update_devices(hass, create_hdmi_network): + """Test the update devices service call.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_UPDATE_DEVICES, + {}, + blocking=True, + ) + + mock_hdmi_network_instance.scan.assert_called_once_with() + + +@pytest.mark.parametrize( + "count,calls", + [ + (3, 3), + (1, 1), + (0, 0), + pytest.param( + "", + 1, + marks=pytest.mark.xfail( + reason="While the code allows for an empty string the schema doesn't allow it", + raises=vol.MultipleInvalid, + ), + ), + ], +) +@pytest.mark.parametrize("direction,key", [("up", 65), ("down", 66)]) +async def test_service_volume_x_times( + hass, create_hdmi_network, count, calls, direction, key +): + """Test the volume service call with steps.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME, + {direction: count}, + blocking=True, + ) + + assert mock_hdmi_network_instance.send_command.call_count == calls * 2 + for i in range(calls): + assert_key_press_release( + mock_hdmi_network_instance.send_command, i, dst=5, key=key + ) + + +@pytest.mark.parametrize("direction,key", [("up", 65), ("down", 66)]) +async def test_service_volume_press(hass, create_hdmi_network, direction, key): + """Test the volume service call with press attribute.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME, + {direction: "press"}, + blocking=True, + ) + + mock_hdmi_network_instance.send_command.assert_called_once() + arg = mock_hdmi_network_instance.send_command.call_args.args[0] + assert isinstance(arg, KeyPressCommand) + assert arg.key == key + assert arg.dst == 5 + + +@pytest.mark.parametrize("direction,key", [("up", 65), ("down", 66)]) +async def test_service_volume_release(hass, create_hdmi_network, direction, key): + """Test the volume service call with release attribute.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME, + {direction: "release"}, + blocking=True, + ) + + mock_hdmi_network_instance.send_command.assert_called_once() + arg = mock_hdmi_network_instance.send_command.call_args.args[0] + assert isinstance(arg, KeyReleaseCommand) + assert arg.dst == 5 + + +@pytest.mark.parametrize( + "attr,key", + [ + ("toggle", 67), + ("on", 101), + ("off", 102), + pytest.param( + "", + 101, + marks=pytest.mark.xfail( + reason="The documentation mention it's allowed to pass an empty string, but the schema does not allow this", + raises=vol.MultipleInvalid, + ), + ), + ], +) +async def test_service_volume_mute(hass, create_hdmi_network, attr, key): + """Test the volume service call with mute.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME, + {"mute": attr}, + blocking=True, + ) + + assert mock_hdmi_network_instance.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_network_instance.send_command, key=key, dst=5) + + +@pytest.mark.parametrize( + "data,expected", + [ + ({"raw": "20:0D"}, "20:0d"), + pytest.param( + {"cmd": "36"}, + "ff:36", + marks=pytest.mark.xfail( + reason="String is converted in hex value, the final result looks like 'ff:24', not what you'd expect." + ), + ), + ({"cmd": 54}, "ff:36"), + pytest.param( + {"cmd": "36", "src": "1", "dst": "0"}, + "10:36", + marks=pytest.mark.xfail( + reason="String is converted in hex value, the final result looks like 'ff:24', not what you'd expect." + ), + ), + ({"cmd": 54, "src": "1", "dst": "0"}, "10:36"), + pytest.param( + {"cmd": "64", "src": "1", "dst": "0", "att": "4f:44"}, + "10:64:4f:44", + marks=pytest.mark.xfail( + reason="`att` only accepts a int or a HEX value, it seems good to allow for raw data here.", + raises=vol.MultipleInvalid, + ), + ), + pytest.param( + {"cmd": "0A", "src": "1", "dst": "0", "att": "1B"}, + "10:0a:1b", + marks=pytest.mark.xfail( + reason="The code tries to run `reduce` on this string and fails.", + raises=TypeError, + ), + ), + pytest.param( + {"cmd": "0A", "src": "1", "dst": "0", "att": "01"}, + "10:0a:1b", + marks=pytest.mark.xfail( + reason="The code tries to run `reduce` on this as an `int` and fails.", + raises=TypeError, + ), + ), + pytest.param( + {"cmd": "0A", "src": "1", "dst": "0", "att": ["1B", "44"]}, + "10:0a:1b:44", + marks=pytest.mark.xfail( + reason="While the code shows that it's possible to passthrough a list, the call schema does not allow it.", + raises=(vol.MultipleInvalid, TypeError), + ), + ), + ], +) +async def test_service_send_command(hass, create_hdmi_network, data, expected): + """Test the send command service call.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_COMMAND, + data, + blocking=True, + ) + + mock_hdmi_network_instance.send_command.assert_called_once() + command = mock_hdmi_network_instance.send_command.call_args.args[0] + assert isinstance(command, CecCommand) + assert str(command) == expected + + +@pytest.mark.parametrize( + "adapter_initialized_value, watchdog_actions", [(False, 1), (True, 0)] +) +async def test_watchdog( + hass, + create_hdmi_network, + mock_cec_adapter, + adapter_initialized_value, + watchdog_actions, +): + """Test the watchdog when adapter is down/up.""" + adapter_initialized = PropertyMock(return_value=adapter_initialized_value) + events = async_capture_events(hass, EVENT_HDMI_CEC_UNAVAILABLE) + + mock_cec_adapter_instance = mock_cec_adapter.return_value + type(mock_cec_adapter_instance).initialized = adapter_initialized + + mock_hdmi_network_instance = await create_hdmi_network() + + mock_hdmi_network_instance.set_initialized_callback.assert_called_once() + callback = mock_hdmi_network_instance.set_initialized_callback.call_args.args[0] + callback() + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=WATCHDOG_INTERVAL)) + await hass.async_block_till_done() + + adapter_initialized.assert_called_once_with() + assert len(events) == watchdog_actions + assert mock_cec_adapter_instance.init.call_count == watchdog_actions diff --git a/tests/components/hdmi_cec/test_media_player.py b/tests/components/hdmi_cec/test_media_player.py new file mode 100644 index 00000000000..861134e2715 --- /dev/null +++ b/tests/components/hdmi_cec/test_media_player.py @@ -0,0 +1,493 @@ +"""Tests for the HDMI-CEC media player platform.""" +from pycec.const import ( + DEVICE_TYPE_NAMES, + KEY_BACKWARD, + KEY_FORWARD, + KEY_MUTE_TOGGLE, + KEY_PAUSE, + KEY_PLAY, + KEY_STOP, + KEY_VOLUME_DOWN, + KEY_VOLUME_UP, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, + TYPE_AUDIO, + TYPE_PLAYBACK, + TYPE_RECORDER, + TYPE_TUNER, + TYPE_TV, + TYPE_UNKNOWN, +) +import pytest + +from homeassistant.components.hdmi_cec import EVENT_HDMI_CEC_UNAVAILABLE +from homeassistant.components.media_player import ( + DOMAIN as MEDIA_PLAYER_DOMAIN, + MediaPlayerEntityFeature as MPEF, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PLAY_PAUSE, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_STOP, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_UP, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) + +from . import MockHDMIDevice, assert_key_press_release + + +@pytest.fixture( + name="assert_state", + params=[ + False, + pytest.param( + True, + marks=pytest.mark.xfail( + reason="""State isn't updated because the function is missing the + `schedule_update_ha_state` for a correct push entity. Would still + update once the data comes back from the device.""" + ), + ), + ], + ids=["skip_assert_state", "run_assert_state"], +) +def assert_state_fixture(hass, request): + """Allow for skipping the assert state changes. + + This is broken in this entity, but we still want to test that + the rest of the code works as expected. + """ + + def test_state(state, expected): + if request.param: + assert state == expected + else: + assert True + + return test_state + + +async def test_load_platform(hass, create_hdmi_network, create_cec_entity): + """Test that media_player entity is loaded.""" + hdmi_network = await create_hdmi_network(config={"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_3") + assert state is not None + + state = hass.states.get("switch.hdmi_3") + assert state is None + + +@pytest.mark.parametrize("platform", [{}, {"platform": "switch"}]) +async def test_load_types(hass, create_hdmi_network, create_cec_entity, platform): + """Test that media_player entity is loaded when types is set.""" + config = platform | {"types": {"hdmi_cec.hdmi_4": "media_player"}} + hdmi_network = await create_hdmi_network(config=config) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_3") + assert state is None + + state = hass.states.get("switch.hdmi_3") + assert state is not None + + mock_hdmi_device = MockHDMIDevice(logical_address=4) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_4") + assert state is not None + + state = hass.states.get("switch.hdmi_4") + assert state is None + + +async def test_service_on(hass, create_hdmi_network, create_cec_entity, assert_state): + """Test that media_player triggers on `on` service.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("media_player.hdmi_3") + assert state.state != STATE_ON + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_hdmi_device.turn_on.assert_called_once_with() + + state = hass.states.get("media_player.hdmi_3") + assert_state(state.state, STATE_ON) + + +async def test_service_off(hass, create_hdmi_network, create_cec_entity, assert_state): + """Test that media_player triggers on `off` service.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("media_player.hdmi_3") + assert state.state != STATE_OFF + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + + mock_hdmi_device.turn_off.assert_called_once_with() + + state = hass.states.get("media_player.hdmi_3") + assert_state(state.state, STATE_OFF) + + +@pytest.mark.parametrize( + "type_id,expected_features", + [ + (TYPE_TV, (MPEF.TURN_ON, MPEF.TURN_OFF)), + ( + TYPE_RECORDER, + ( + MPEF.TURN_ON, + MPEF.TURN_OFF, + MPEF.PAUSE, + MPEF.STOP, + MPEF.PREVIOUS_TRACK, + MPEF.NEXT_TRACK, + ), + ), + pytest.param( + TYPE_RECORDER, + (MPEF.PLAY,), + marks=pytest.mark.xfail( + reason="The feature is wrongly set to PLAY_MEDIA, but should be PLAY." + ), + ), + (TYPE_UNKNOWN, (MPEF.TURN_ON, MPEF.TURN_OFF)), + pytest.param( + TYPE_TUNER, + ( + MPEF.TURN_ON, + MPEF.TURN_OFF, + MPEF.PAUSE, + MPEF.STOP, + ), + marks=pytest.mark.xfail( + reason="Checking for the wrong attribute, should be checking `type_id`, is checking `type`." + ), + ), + pytest.param( + TYPE_TUNER, + (MPEF.PLAY,), + marks=pytest.mark.xfail( + reason="The feature is wrongly set to PLAY_MEDIA, but should be PLAY." + ), + ), + pytest.param( + TYPE_PLAYBACK, + ( + MPEF.TURN_ON, + MPEF.TURN_OFF, + MPEF.PAUSE, + MPEF.STOP, + MPEF.PREVIOUS_TRACK, + MPEF.NEXT_TRACK, + ), + marks=pytest.mark.xfail( + reason="Checking for the wrong attribute, should be checking `type_id`, is checking `type`." + ), + ), + pytest.param( + TYPE_PLAYBACK, + (MPEF.PLAY,), + marks=pytest.mark.xfail( + reason="The feature is wrongly set to PLAY_MEDIA, but should be PLAY." + ), + ), + ( + TYPE_AUDIO, + ( + MPEF.TURN_ON, + MPEF.TURN_OFF, + MPEF.VOLUME_STEP, + MPEF.VOLUME_MUTE, + ), + ), + ], +) +async def test_supported_features( + hass, create_hdmi_network, create_cec_entity, type_id, expected_features +): + """Test that features load as expected.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice( + logical_address=3, type=type_id, type_name=DEVICE_TYPE_NAMES[type_id] + ) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + state = hass.states.get("media_player.hdmi_3") + supported_features = state.attributes["supported_features"] + for feature in expected_features: + assert supported_features & feature + + +@pytest.mark.parametrize( + "service,extra_data,key", + [ + (SERVICE_VOLUME_DOWN, None, KEY_VOLUME_DOWN), + (SERVICE_VOLUME_UP, None, KEY_VOLUME_UP), + (SERVICE_VOLUME_MUTE, {"is_volume_muted": True}, KEY_MUTE_TOGGLE), + (SERVICE_VOLUME_MUTE, {"is_volume_muted": False}, KEY_MUTE_TOGGLE), + ], +) +async def test_volume_services( + hass, create_hdmi_network, create_cec_entity, service, extra_data, key +): + """Test volume related commands.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=TYPE_AUDIO) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + data = {ATTR_ENTITY_ID: "media_player.hdmi_3"} + if extra_data: + data |= extra_data + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + service, + data, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=key) + + +@pytest.mark.parametrize( + "service,key", + [ + (SERVICE_MEDIA_NEXT_TRACK, KEY_FORWARD), + (SERVICE_MEDIA_PREVIOUS_TRACK, KEY_BACKWARD), + ], +) +async def test_track_change_services( + hass, create_hdmi_network, create_cec_entity, service, key +): + """Test track change related commands.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=TYPE_RECORDER) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + service, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=key) + + +@pytest.mark.parametrize( + "service,key,expected_state", + [ + pytest.param( + SERVICE_MEDIA_PLAY, + KEY_PLAY, + STATE_PLAYING, + marks=pytest.mark.xfail( + reason="The wrong feature is defined, should be PLAY, not PLAY_MEDIA" + ), + ), + (SERVICE_MEDIA_PAUSE, KEY_PAUSE, STATE_PAUSED), + (SERVICE_MEDIA_STOP, KEY_STOP, STATE_IDLE), + ], +) +async def test_playback_services( + hass, + create_hdmi_network, + create_cec_entity, + assert_state, + service, + key, + expected_state, +): + """Test playback related commands.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=TYPE_RECORDER) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + service, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=key) + + state = hass.states.get("media_player.hdmi_3") + assert_state(state.state, expected_state) + + +@pytest.mark.xfail(reason="PLAY feature isn't enabled") +async def test_play_pause_service( + hass, + create_hdmi_network, + create_cec_entity, + assert_state, +): + """Test play pause service.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice( + logical_address=3, type=TYPE_RECORDER, status=STATUS_PLAY + ) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PLAY_PAUSE, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=KEY_PAUSE) + + state = hass.states.get("media_player.hdmi_3") + assert_state(state.state, STATE_PAUSED) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PLAY_PAUSE, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 4 + assert_key_press_release(mock_hdmi_device.send_command, 1, dst=3, key=KEY_PLAY) + + +@pytest.mark.parametrize( + "type_id,update_data,expected_state", + [ + (TYPE_TV, {"power_status": POWER_OFF}, STATE_OFF), + (TYPE_TV, {"power_status": 3}, STATE_OFF), + (TYPE_TV, {"power_status": POWER_ON}, STATE_ON), + (TYPE_TV, {"power_status": 4}, STATE_ON), + (TYPE_TV, {"power_status": POWER_ON, "status": STATUS_PLAY}, STATE_ON), + (TYPE_RECORDER, {"power_status": POWER_OFF, "status": STATUS_PLAY}, STATE_OFF), + ( + TYPE_RECORDER, + {"power_status": POWER_ON, "status": STATUS_PLAY}, + STATE_PLAYING, + ), + (TYPE_RECORDER, {"power_status": POWER_ON, "status": STATUS_STOP}, STATE_IDLE), + ( + TYPE_RECORDER, + {"power_status": POWER_ON, "status": STATUS_STILL}, + STATE_PAUSED, + ), + (TYPE_RECORDER, {"power_status": POWER_ON, "status": None}, STATE_UNKNOWN), + ], +) +async def test_update_state( + hass, create_hdmi_network, create_cec_entity, type_id, update_data, expected_state +): + """Test state updates work as expected.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=type_id) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + for att, val in update_data.items(): + setattr(mock_hdmi_device, att, val) + await hass.async_block_till_done() + + state = hass.states.get("media_player.hdmi_3") + assert state.state == expected_state + + +@pytest.mark.parametrize( + "data,expected_state", + [ + ({"power_status": POWER_OFF}, STATE_OFF), + ({"power_status": 3}, STATE_OFF), + ({"power_status": POWER_ON, "type": TYPE_TV}, STATE_ON), + ({"power_status": 4, "type": TYPE_TV}, STATE_ON), + ({"power_status": POWER_ON, "type": TYPE_TV, "status": STATUS_PLAY}, STATE_ON), + ( + {"power_status": POWER_OFF, "type": TYPE_RECORDER, "status": STATUS_PLAY}, + STATE_OFF, + ), + ( + {"power_status": POWER_ON, "type": TYPE_RECORDER, "status": STATUS_PLAY}, + STATE_PLAYING, + ), + ( + {"power_status": POWER_ON, "type": TYPE_RECORDER, "status": STATUS_STOP}, + STATE_IDLE, + ), + ( + {"power_status": POWER_ON, "type": TYPE_RECORDER, "status": STATUS_STILL}, + STATE_PAUSED, + ), + ( + {"power_status": POWER_ON, "type": TYPE_RECORDER, "status": None}, + STATE_UNKNOWN, + ), + ], +) +async def test_starting_state( + hass, create_hdmi_network, create_cec_entity, data, expected_state +): + """Test starting states are set as expected.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, **data) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("media_player.hdmi_3") + assert state.state == expected_state + + +@pytest.mark.xfail( + reason="The code only sets the state to unavailable, doesn't set the `_attr_available` to false." +) +async def test_unavailable_status(hass, create_hdmi_network, create_cec_entity): + """Test entity goes into unavailable status when expected.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + hass.bus.async_fire(EVENT_HDMI_CEC_UNAVAILABLE) + + state = hass.states.get("media_player.hdmi_3") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/hdmi_cec/test_switch.py b/tests/components/hdmi_cec/test_switch.py new file mode 100644 index 00000000000..61e1e03e4a6 --- /dev/null +++ b/tests/components/hdmi_cec/test_switch.py @@ -0,0 +1,252 @@ +"""Tests for the HDMI-CEC switch platform.""" +import pytest + +from homeassistant.components.hdmi_cec import ( + EVENT_HDMI_CEC_UNAVAILABLE, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, + PhysicalAddress, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) + +from tests.components.hdmi_cec import MockHDMIDevice + + +@pytest.mark.parametrize("config", [{}, {"platform": "switch"}]) +async def test_load_platform(hass, create_hdmi_network, create_cec_entity, config): + """Test that switch entity is loaded.""" + hdmi_network = await create_hdmi_network(config=config) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_3") + assert state is None + + state = hass.states.get("switch.hdmi_3") + assert state is not None + + +async def test_load_types(hass, create_hdmi_network, create_cec_entity): + """Test that switch entity is loaded when types is set.""" + config = {"platform": "media_player", "types": {"hdmi_cec.hdmi_3": "switch"}} + hdmi_network = await create_hdmi_network(config=config) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_3") + assert state is None + + state = hass.states.get("switch.hdmi_3") + assert state is not None + + mock_hdmi_device = MockHDMIDevice(logical_address=4) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_4") + assert state is not None + + state = hass.states.get("switch.hdmi_4") + assert state is None + + +async def test_service_on(hass, create_hdmi_network, create_cec_entity): + """Test that switch triggers on `on` service.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, power_status=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("switch.hdmi_3") + assert state.state != STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "switch.hdmi_3"}, blocking=True + ) + + mock_hdmi_device.turn_on.assert_called_once_with() + + state = hass.states.get("switch.hdmi_3") + assert state.state == STATE_ON + + +async def test_service_off(hass, create_hdmi_network, create_cec_entity): + """Test that switch triggers on `off` service.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, power_status=4) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("switch.hdmi_3") + assert state.state != STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.hdmi_3"}, + blocking=True, + ) + + mock_hdmi_device.turn_off.assert_called_once_with() + + state = hass.states.get("switch.hdmi_3") + assert state.state == STATE_OFF + + +@pytest.mark.parametrize( + "power_status,expected_state", + [(3, STATE_OFF), (POWER_OFF, STATE_OFF), (4, STATE_ON), (POWER_ON, STATE_ON)], +) +@pytest.mark.parametrize( + "status", + [ + None, + STATUS_PLAY, + STATUS_STOP, + STATUS_STILL, + ], +) +async def test_device_status_change( + hass, create_hdmi_network, create_cec_entity, power_status, expected_state, status +): + """Test state change on device status change.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, status=status) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + mock_hdmi_device.power_status = power_status + await hass.async_block_till_done() + + state = hass.states.get("switch.hdmi_3") + if power_status in (POWER_ON, 4) and status is not None: + pytest.xfail( + reason="`CecSwitchEntity.is_on` returns `False` here instead of `true` as expected." + ) + assert state.state == expected_state + + +@pytest.mark.parametrize( + "device_values, expected", + [ + ({"osd_name": "Switch", "vendor": "Nintendo"}, "Nintendo Switch"), + ({"type_name": "TV"}, "TV 3"), + ({"type_name": "Playback", "osd_name": "Switch"}, "Playback 3 (Switch)"), + ({"type_name": "TV", "vendor": "Samsung"}, "TV 3"), + ( + {"type_name": "Playback", "osd_name": "Super PC", "vendor": "Unknown"}, + "Playback 3 (Super PC)", + ), + ], +) +async def test_friendly_name( + hass, create_hdmi_network, create_cec_entity, device_values, expected +): + """Test friendly name setup.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, **device_values) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + state = hass.states.get("switch.hdmi_3") + assert state.attributes["friendly_name"] == expected + + +@pytest.mark.parametrize( + "device_values,expected_attributes", + [ + ( + {"physical_address": PhysicalAddress("3.0.0.0")}, + {"physical_address": "3.0.0.0"}, + ), + pytest.param( + {}, + {}, + marks=pytest.mark.xfail( + reason="physical address logic returns a string 'None' instead of not being set." + ), + ), + ( + {"physical_address": PhysicalAddress("3.0.0.0"), "vendor_id": 5}, + {"physical_address": "3.0.0.0", "vendor_id": 5, "vendor_name": None}, + ), + ( + { + "physical_address": PhysicalAddress("3.0.0.0"), + "vendor_id": 5, + "vendor": "Samsung", + }, + {"physical_address": "3.0.0.0", "vendor_id": 5, "vendor_name": "Samsung"}, + ), + ( + {"physical_address": PhysicalAddress("3.0.0.0"), "type": 1}, + {"physical_address": "3.0.0.0", "type_id": 1, "type": None}, + ), + ( + { + "physical_address": PhysicalAddress("3.0.0.0"), + "type": 1, + "type_name": "TV", + }, + {"physical_address": "3.0.0.0", "type_id": 1, "type": "TV"}, + ), + ], +) +async def test_extra_state_attributes( + hass, create_hdmi_network, create_cec_entity, device_values, expected_attributes +): + """Test extra state attributes.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, **device_values) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + state = hass.states.get("switch.hdmi_3") + attributes = state.attributes + # We don't care about these attributes, so just copy them to the expected attributes + for att in ("friendly_name", "icon"): + expected_attributes[att] = attributes[att] + assert attributes == expected_attributes + + +@pytest.mark.parametrize( + "device_type,expected_icon", + [ + (None, "mdi:help"), + (0, "mdi:television"), + (1, "mdi:microphone"), + (2, "mdi:help"), + (3, "mdi:radio"), + (4, "mdi:play"), + (5, "mdi:speaker"), + ], +) +async def test_icon( + hass, create_hdmi_network, create_cec_entity, device_type, expected_icon +): + """Test icon selection.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=device_type) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + state = hass.states.get("switch.hdmi_3") + assert state.attributes["icon"] == expected_icon + + +@pytest.mark.xfail( + reason="The code only sets the state to unavailable, doesn't set the `_attr_available` to false." +) +async def test_unavailable_status(hass, create_hdmi_network, create_cec_entity): + """Test entity goes into unavailable status when expected.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + hass.bus.async_fire(EVENT_HDMI_CEC_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("switch.hdmi_3") + assert state.state == STATE_UNAVAILABLE From 3026a70f961438546705c35756b0c1771a967e49 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:55:23 +0200 Subject: [PATCH 227/903] Improve type hints in zwave_js select entity (#76449) --- homeassistant/components/zwave_js/select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index f61149e5de7..81e60582764 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -82,7 +82,7 @@ class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): ) ) - async def async_select_option(self, option: str | int) -> None: + async def async_select_option(self, option: str) -> None: """Change the selected option.""" key = next( key @@ -133,7 +133,7 @@ class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): ) ) - async def async_select_option(self, option: str | int) -> None: + async def async_select_option(self, option: str) -> None: """Change the selected option.""" # We know we can assert because this value is part of the discovery schema assert self._tones_value From fe9c101817b392137814d578a5372c6932d7093a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Aug 2022 15:22:22 +0200 Subject: [PATCH 228/903] Improve select type hints (#76446) --- homeassistant/components/advantage_air/select.py | 4 ++-- homeassistant/components/xiaomi_miio/select.py | 2 +- homeassistant/components/yamaha_musiccast/select.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index 9cfece25b24..97ec0f9705c 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -46,11 +46,11 @@ class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): self._attr_options.append(zone["name"]) @property - def current_option(self): + def current_option(self) -> str: """Return the current MyZone.""" return self._number_to_name[self._ac["myZone"]] - async def async_select_option(self, option): + async def async_select_option(self, option: str) -> None: """Set the MyZone.""" await self.async_change( {self.ac_key: {"info": {"myZone": self._name_to_number[option]}}} diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index b7e6a65775e..7f6f8ce1cb1 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -164,7 +164,7 @@ class XiaomiAirHumidifierSelector(XiaomiSelector): self.async_write_ha_state() @property - def current_option(self): + def current_option(self) -> str: """Return the current option.""" return self.led_brightness.lower() diff --git a/homeassistant/components/yamaha_musiccast/select.py b/homeassistant/components/yamaha_musiccast/select.py index d57d1c07f68..475d32311ed 100644 --- a/homeassistant/components/yamaha_musiccast/select.py +++ b/homeassistant/components/yamaha_musiccast/select.py @@ -52,11 +52,11 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): return DEVICE_CLASS_MAPPING.get(self.capability.id) @property - def options(self): + def options(self) -> list[str]: """Return the list possible options.""" return list(self.capability.options.values()) @property - def current_option(self): + def current_option(self) -> str | None: """Return the currently selected option.""" return self.capability.options.get(self.capability.current) From 3fcdad74fc4616f13d4b6254823e9cb1cbd1ed66 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Aug 2022 15:35:45 +0200 Subject: [PATCH 229/903] Fix iCloud listeners (#76437) --- homeassistant/components/icloud/account.py | 4 +++- homeassistant/components/icloud/device_tracker.py | 2 +- homeassistant/components/icloud/sensor.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 4dc3c07aba7..d0e3b8059a4 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -17,7 +17,7 @@ from pyicloud.services.findmyiphone import AppleDevice from homeassistant.components.zone import async_active_zone from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME -from homeassistant.core import HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_point_in_utc_time @@ -104,6 +104,8 @@ class IcloudAccount: self._retried_fetch = False self._config_entry = config_entry + self.listeners: list[CALLBACK_TYPE] = [] + def setup(self) -> None: """Set up an iCloud account.""" try: diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index eaebb0b7717..35a411ff11c 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -34,7 +34,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for iCloud component.""" - account = hass.data[DOMAIN][entry.unique_id] + account: IcloudAccount = hass.data[DOMAIN][entry.unique_id] tracked = set[str]() @callback diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 6e415aa3350..3feb30f078f 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -20,7 +20,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for iCloud component.""" - account = hass.data[DOMAIN][entry.unique_id] + account: IcloudAccount = hass.data[DOMAIN][entry.unique_id] tracked = set[str]() @callback From f67a45f643f504fd22f490dad39b4b0b71c7fdab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 8 Aug 2022 16:16:40 +0200 Subject: [PATCH 230/903] Update coverage to 6.4.3 (#76443) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2331e971711..6685d06c3b0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.4.2 +coverage==6.4.3 freezegun==1.2.1 mock-open==1.4.0 mypy==0.971 From 9f240d5bab97b144f4e467684dced5c4f04433c8 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 8 Aug 2022 16:52:36 +0200 Subject: [PATCH 231/903] Bump NextDNS backend library (#76300) * Bump NextDNS backend library * Update tests * Update diagnostics tests * Use fixtures --- .../components/nextdns/diagnostics.py | 3 + .../components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/__init__.py | 42 ++++++++++++ .../components/nextdns/fixtures/settings.json | 67 +++++++++++++++++++ tests/components/nextdns/test_diagnostics.py | 5 ++ 7 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tests/components/nextdns/fixtures/settings.json diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py index 4be22684395..077f2ca2988 100644 --- a/homeassistant/components/nextdns/diagnostics.py +++ b/homeassistant/components/nextdns/diagnostics.py @@ -13,6 +13,7 @@ from .const import ( ATTR_ENCRYPTION, ATTR_IP_VERSIONS, ATTR_PROTOCOLS, + ATTR_SETTINGS, ATTR_STATUS, CONF_PROFILE_ID, DOMAIN, @@ -31,6 +32,7 @@ async def async_get_config_entry_diagnostics( encryption_coordinator = coordinators[ATTR_ENCRYPTION] ip_versions_coordinator = coordinators[ATTR_IP_VERSIONS] protocols_coordinator = coordinators[ATTR_PROTOCOLS] + settings_coordinator = coordinators[ATTR_SETTINGS] status_coordinator = coordinators[ATTR_STATUS] diagnostics_data = { @@ -39,6 +41,7 @@ async def async_get_config_entry_diagnostics( "encryption_coordinator_data": asdict(encryption_coordinator.data), "ip_versions_coordinator_data": asdict(ip_versions_coordinator.data), "protocols_coordinator_data": asdict(protocols_coordinator.data), + "settings_coordinator_data": asdict(settings_coordinator.data), "status_coordinator_data": asdict(status_coordinator.data), } diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 3e2d3ebb3d0..eb5d71173bf 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -3,7 +3,7 @@ "name": "NextDNS", "documentation": "https://www.home-assistant.io/integrations/nextdns", "codeowners": ["@bieniu"], - "requirements": ["nextdns==1.0.2"], + "requirements": ["nextdns==1.1.0"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["nextdns"] diff --git a/requirements_all.txt b/requirements_all.txt index ddf22e985c7..6564512a494 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1106,7 +1106,7 @@ nextcloudmonitor==1.1.0 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.2 +nextdns==1.1.0 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0326765cf74..d28ad4ac97f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -787,7 +787,7 @@ nexia==2.0.2 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.2 +nextdns==1.1.0 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 3b513b811b8..32b8bed76fa 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -54,6 +54,48 @@ SETTINGS = Settings( typosquatting_protection=True, web3=True, youtube_restricted_mode=False, + block_9gag=True, + block_amazon=True, + block_blizzard=True, + block_dailymotion=True, + block_discord=True, + block_disneyplus=True, + block_ebay=True, + block_facebook=True, + block_fortnite=True, + block_hulu=True, + block_imgur=True, + block_instagram=True, + block_leagueoflegends=True, + block_messenger=True, + block_minecraft=True, + block_netflix=True, + block_pinterest=True, + block_primevideo=True, + block_reddit=True, + block_roblox=True, + block_signal=True, + block_skype=True, + block_snapchat=True, + block_spotify=True, + block_steam=True, + block_telegram=True, + block_tiktok=True, + block_tinder=True, + block_tumblr=True, + block_twitch=True, + block_twitter=True, + block_vimeo=True, + block_vk=True, + block_whatsapp=True, + block_xboxlive=True, + block_youtube=True, + block_zoom=True, + block_dating=True, + block_gambling=True, + block_piracy=True, + block_porn=True, + block_social_networks=True, ) diff --git a/tests/components/nextdns/fixtures/settings.json b/tests/components/nextdns/fixtures/settings.json new file mode 100644 index 00000000000..9593ca6325e --- /dev/null +++ b/tests/components/nextdns/fixtures/settings.json @@ -0,0 +1,67 @@ +{ + "block_page": false, + "cache_boost": true, + "cname_flattening": true, + "anonymized_ecs": true, + "logs": true, + "web3": true, + "allow_affiliate": true, + "block_disguised_trackers": true, + "ai_threat_detection": true, + "block_csam": true, + "block_ddns": true, + "block_nrd": true, + "block_parked_domains": true, + "cryptojacking_protection": true, + "dga_protection": true, + "dns_rebinding_protection": true, + "google_safe_browsing": false, + "idn_homograph_attacks_protection": true, + "threat_intelligence_feeds": true, + "typosquatting_protection": true, + "block_bypass_methods": true, + "safesearch": false, + "youtube_restricted_mode": false, + "block_9gag": true, + "block_amazon": true, + "block_blizzard": true, + "block_dailymotion": true, + "block_discord": true, + "block_disneyplus": true, + "block_ebay": true, + "block_facebook": true, + "block_fortnite": true, + "block_hulu": true, + "block_imgur": true, + "block_instagram": true, + "block_leagueoflegends": true, + "block_messenger": true, + "block_minecraft": true, + "block_netflix": true, + "block_pinterest": true, + "block_primevideo": true, + "block_reddit": true, + "block_roblox": true, + "block_signal": true, + "block_skype": true, + "block_snapchat": true, + "block_spotify": true, + "block_steam": true, + "block_telegram": true, + "block_tiktok": true, + "block_tinder": true, + "block_tumblr": true, + "block_twitch": true, + "block_twitter": true, + "block_vimeo": true, + "block_vk": true, + "block_whatsapp": true, + "block_xboxlive": true, + "block_youtube": true, + "block_zoom": true, + "block_dating": true, + "block_gambling": true, + "block_piracy": true, + "block_porn": true, + "block_social_networks": true +} diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 0702a2fa1d8..ec4ffa10aaf 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -1,11 +1,13 @@ """Test NextDNS diagnostics.""" from collections.abc import Awaitable, Callable +import json from aiohttp import ClientSession from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant +from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.nextdns import init_integration @@ -14,6 +16,8 @@ async def test_entry_diagnostics( hass: HomeAssistant, hass_client: Callable[..., Awaitable[ClientSession]] ) -> None: """Test config entry diagnostics.""" + settings = json.loads(load_fixture("settings.json", "nextdns")) + entry = await init_integration(hass) result = await get_diagnostics_for_config_entry(hass, hass_client, entry) @@ -60,6 +64,7 @@ async def test_entry_diagnostics( "tcp_queries_ratio": 0.0, "udp_queries_ratio": 40.0, } + assert result["settings_coordinator_data"] == settings assert result["status_coordinator_data"] == { "all_queries": 100, "allowed_queries": 30, From 6540bed59d433f812af712642a2793bff21eaaba Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 8 Aug 2022 23:15:31 +0800 Subject: [PATCH 232/903] Defer preload stream start on startup (#75801) --- homeassistant/components/camera/__init__.py | 4 ++-- tests/components/camera/test_init.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 77bd0b57f1c..5aa348c9fb8 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -39,7 +39,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_FILENAME, CONTENT_TYPE_MULTIPART, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) @@ -374,7 +374,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: stream.add_provider("hls") await stream.start() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, preload_stream) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, preload_stream) @callback def update_tokens(time: datetime) -> None: diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index f800a4ac2bd..cea9c527946 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ENTITY_ID, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, STATE_UNAVAILABLE, ) from homeassistant.exceptions import HomeAssistantError @@ -374,7 +374,7 @@ async def test_no_preload_stream(hass, mock_stream): ) as mock_stream_source: mock_stream_source.return_value = io.BytesIO() await async_setup_component(hass, "camera", {DOMAIN: {"platform": "demo"}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert not mock_request_stream.called @@ -396,7 +396,7 @@ async def test_preload_stream(hass, mock_stream): hass, "camera", {DOMAIN: {"platform": "demo"}} ) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert mock_create_stream.called From ccf7b8fbb97569298607e84185d3fc391fe13e8f Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 8 Aug 2022 13:50:41 -0400 Subject: [PATCH 233/903] Bump version of pyunifiprotect to 4.0.12 (#76465) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 815b5250e1d..ad18be3dba9 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.11", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.0.12", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 6564512a494..7d67df172ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2010,7 +2010,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.11 +pyunifiprotect==4.0.12 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d28ad4ac97f..c9c6ec04051 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.11 +pyunifiprotect==4.0.12 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From e199b243747209db9c75a80c9a46d2507e5b58a5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 8 Aug 2022 20:00:05 +0200 Subject: [PATCH 234/903] Update sentry-sdk to 1.9.2 (#76444) --- 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 0400424a7fc..0cbe9eb636f 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.9.0"], + "requirements": ["sentry-sdk==1.9.2"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7d67df172ed..46884ba53bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2170,7 +2170,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.9.0 +sentry-sdk==1.9.2 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9c6ec04051..6acb6d88822 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1464,7 +1464,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.9.0 +sentry-sdk==1.9.2 # homeassistant.components.sharkiq sharkiq==0.0.1 From 8456a25f86a9849cd737a780d064c59f88b44091 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 8 Aug 2022 20:03:21 +0200 Subject: [PATCH 235/903] Update apprise to 1.0.0 (#76441) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 450e0a964df..a1b2efcad89 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==0.9.9"], + "requirements": ["apprise==1.0.0"], "codeowners": ["@caronc"], "iot_class": "cloud_push", "loggers": ["apprise"] diff --git a/requirements_all.txt b/requirements_all.txt index 46884ba53bd..1cd3875344c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -319,7 +319,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==0.9.9 +apprise==1.0.0 # homeassistant.components.aprs aprslib==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6acb6d88822..d387fd49e1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ androidtv[async]==0.0.67 anthemav==1.4.1 # homeassistant.components.apprise -apprise==0.9.9 +apprise==1.0.0 # homeassistant.components.aprs aprslib==0.7.0 From d139d1e175558e4422a5f8b72be0ddfc3868e95b Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 8 Aug 2022 16:00:50 -0400 Subject: [PATCH 236/903] Add UniFi Protect media source (#73244) --- .../components/unifiprotect/config_flow.py | 9 + .../components/unifiprotect/const.py | 2 + homeassistant/components/unifiprotect/data.py | 7 + .../components/unifiprotect/media_source.py | 862 ++++++++++++++++++ .../components/unifiprotect/strings.json | 3 +- .../unifiprotect/translations/en.json | 1 + .../components/unifiprotect/views.py | 4 +- .../unifiprotect/test_config_flow.py | 2 + .../unifiprotect/test_media_source.py | 793 ++++++++++++++++ tests/components/unifiprotect/test_views.py | 2 +- 10 files changed, 1681 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/unifiprotect/media_source.py create mode 100644 tests/components/unifiprotect/test_media_source.py diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 330a5e530a1..f07ca923a53 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -35,7 +35,9 @@ from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, + CONF_MAX_MEDIA, CONF_OVERRIDE_CHOST, + DEFAULT_MAX_MEDIA, DEFAULT_PORT, DEFAULT_VERIFY_SSL, DOMAIN, @@ -221,6 +223,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_DISABLE_RTSP: False, CONF_ALL_UPDATES: False, CONF_OVERRIDE_CHOST: False, + CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA, }, ) @@ -383,6 +386,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_OVERRIDE_CHOST, False ), ): bool, + vol.Optional( + CONF_MAX_MEDIA, + default=self.config_entry.options.get( + CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA + ), + ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), } ), ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 3c29d0c9972..9710279a7c4 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -19,6 +19,7 @@ ATTR_ANONYMIZE = "anonymize" CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" +CONF_MAX_MEDIA = "max_media" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, @@ -31,6 +32,7 @@ DEFAULT_ATTRIBUTION = "Powered by UniFi Protect Server" DEFAULT_BRAND = "Ubiquiti" DEFAULT_SCAN_INTERVAL = 5 DEFAULT_VERIFY_SSL = False +DEFAULT_MAX_MEDIA = 1000 DEVICES_THAT_ADOPT = { ModelType.CAMERA, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index d4140759a7b..74ef6ab37f8 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -26,6 +26,8 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_DISABLE_RTSP, + CONF_MAX_MEDIA, + DEFAULT_MAX_MEDIA, DEVICES_THAT_ADOPT, DISPATCH_ADOPT, DISPATCH_CHANNELS, @@ -82,6 +84,11 @@ class ProtectData: """Check if RTSP is disabled.""" return self._entry.options.get(CONF_DISABLE_RTSP, False) + @property + def max_events(self) -> int: + """Max number of events to load at once.""" + return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) + def get_by_types( self, device_types: Iterable[ModelType] ) -> Generator[ProtectAdoptableDeviceModel, None, None]: diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py new file mode 100644 index 00000000000..17db4db5c3c --- /dev/null +++ b/homeassistant/components/unifiprotect/media_source.py @@ -0,0 +1,862 @@ +"""UniFi Protect media sources.""" + +from __future__ import annotations + +import asyncio +from datetime import date, datetime, timedelta +from enum import Enum +from typing import Any, cast + +from pyunifiprotect.data import Camera, Event, EventType, SmartDetectObjectType +from pyunifiprotect.exceptions import NvrError +from pyunifiprotect.utils import from_js_time +from yarl import URL + +from homeassistant.components.camera import CameraImageView +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_IMAGE, + MEDIA_CLASS_VIDEO, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from .const import DOMAIN +from .data import ProtectData +from .views import async_generate_event_video_url, async_generate_thumbnail_url + +VIDEO_FORMAT = "video/mp4" +THUMBNAIL_WIDTH = 185 +THUMBNAIL_HEIGHT = 185 + + +class SimpleEventType(str, Enum): + """Enum to Camera Video events.""" + + ALL = "all" + RING = "ring" + MOTION = "motion" + SMART = "smart" + + +class IdentifierType(str, Enum): + """UniFi Protect identifier type.""" + + EVENT = "event" + EVENT_THUMB = "eventthumb" + BROWSE = "browse" + + +class IdentifierTimeType(str, Enum): + """UniFi Protect identifier subtype.""" + + RECENT = "recent" + RANGE = "range" + + +EVENT_MAP = { + SimpleEventType.ALL: None, + SimpleEventType.RING: EventType.RING, + SimpleEventType.MOTION: EventType.MOTION, + SimpleEventType.SMART: EventType.SMART_DETECT, +} +EVENT_NAME_MAP = { + SimpleEventType.ALL: "All Events", + SimpleEventType.RING: "Ring Events", + SimpleEventType.MOTION: "Motion Events", + SimpleEventType.SMART: "Smart Detections", +} + + +def get_ufp_event(event_type: SimpleEventType) -> EventType | None: + """Get UniFi Protect event type from SimpleEventType.""" + + return EVENT_MAP[event_type] + + +async def async_get_media_source(hass: HomeAssistant) -> MediaSource: + """Set up UniFi Protect media source.""" + + data_sources: dict[str, ProtectData] = {} + for data in hass.data.get(DOMAIN, {}).values(): + if isinstance(data, ProtectData): + data_sources[data.api.bootstrap.nvr.id] = data + + return ProtectMediaSource(hass, data_sources) + + +@callback +def _get_start_end(hass: HomeAssistant, start: datetime) -> tuple[datetime, datetime]: + start = dt_util.as_local(start) + end = dt_util.now() + + start = start.replace(day=1, hour=1, minute=0, second=0, microsecond=0) + end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + + return start, end + + +@callback +def _bad_identifier(identifier: str, err: Exception | None = None) -> BrowseMediaSource: + msg = f"Unexpected identifier: {identifier}" + if err is None: + raise BrowseError(msg) + raise BrowseError(msg) from err + + +@callback +def _bad_identifier_media(identifier: str, err: Exception | None = None) -> PlayMedia: + return cast(PlayMedia, _bad_identifier(identifier, err)) + + +@callback +def _format_duration(duration: timedelta) -> str: + formatted = "" + seconds = int(duration.total_seconds()) + if seconds > 3600: + hours = seconds // 3600 + formatted += f"{hours}h " + seconds -= hours * 3600 + if seconds > 60: + minutes = seconds // 60 + formatted += f"{minutes}m " + seconds -= minutes * 60 + if seconds > 0: + formatted += f"{seconds}s " + + return formatted.strip() + + +class ProtectMediaSource(MediaSource): + """Represents all UniFi Protect NVRs.""" + + name: str = "UniFi Protect" + _registry: er.EntityRegistry | None + + def __init__( + self, hass: HomeAssistant, data_sources: dict[str, ProtectData] + ) -> None: + """Initialize the UniFi Protect media source.""" + + super().__init__(DOMAIN) + self.hass = hass + self.data_sources = data_sources + self._registry = None + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Return a streamable URL and associated mime type for a UniFi Protect event. + + Accepted identifier format are + + * {nvr_id}:event:{event_id} - MP4 video clip for specific event + * {nvr_id}:eventthumb:{event_id} - Thumbnail JPEG for specific event + """ + + parts = item.identifier.split(":") + if len(parts) != 3 or parts[1] not in ("event", "eventthumb"): + return _bad_identifier_media(item.identifier) + + thumbnail_only = parts[1] == "eventthumb" + try: + data = self.data_sources[parts[0]] + except (KeyError, IndexError) as err: + return _bad_identifier_media(item.identifier, err) + + event = data.api.bootstrap.events.get(parts[2]) + if event is None: + try: + event = await data.api.get_event(parts[2]) + except NvrError as err: + return _bad_identifier_media(item.identifier, err) + else: + # cache the event for later + data.api.bootstrap.events[event.id] = event + + nvr = data.api.bootstrap.nvr + if thumbnail_only: + return PlayMedia( + async_generate_thumbnail_url(event.id, nvr.id), "image/jpeg" + ) + return PlayMedia(async_generate_event_video_url(event), "video/mp4") + + async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: + """Return a browsable UniFi Protect media source. + + Identifier formatters for UniFi Protect media sources are all in IDs from + the UniFi Protect instance since events may not always map 1:1 to a Home + Assistant device or entity. It also drasically speeds up resolution. + + The UniFi Protect Media source is timebased for the events recorded by the NVR. + So its structure is a bit different then many other media players. All browsable + media is a video clip. The media source could be greatly cleaned up if/when the + frontend has filtering supporting. + + * ... Each NVR Console (hidden if there is only one) + * All Cameras + * ... Camera X + * All Events + * ... Event Type X + * Last 24 Hours -> Events + * Last 7 Days -> Events + * Last 30 Days -> Events + * ... This Month - X + * Whole Month -> Events + * ... Day X -> Events + + Accepted identifier formats: + + * {nvr_id}:event:{event_id} + Specific Event for NVR + * {nvr_id}:eventthumb:{event_id} + Specific Event Thumbnail for NVR + * {nvr_id}:browse + Root NVR browse source + * {nvr_id}:browse:all|{camera_id} + Root Camera(s) browse source + * {nvr_id}:browse:all|{camera_id}:all|{event_type} + Root Camera(s) Event Type(s) browse source + * {nvr_id}:browse:all|{camera_id}:all|{event_type}:recent:{day_count} + Listing of all events in last {day_count}, sorted in reverse chronological order + * {nvr_id}:browse:all|{camera_id}:all|{event_type}:range:{year}:{month} + List of folders for each day in month + all events for month + * {nvr_id}:browse:all|{camera_id}:all|{event_type}:range:{year}:{month}:all|{day} + Listing of all events for give {day} + {month} + {year} combination in chronological order + """ + + if not item.identifier: + return await self._build_sources() + + parts = item.identifier.split(":") + + try: + data = self.data_sources[parts[0]] + except (KeyError, IndexError) as err: + return _bad_identifier(item.identifier, err) + + if len(parts) < 2: + return _bad_identifier(item.identifier) + + try: + identifier_type = IdentifierType(parts[1]) + except ValueError as err: + return _bad_identifier(item.identifier, err) + + if identifier_type in (IdentifierType.EVENT, IdentifierType.EVENT_THUMB): + thumbnail_only = identifier_type == IdentifierType.EVENT_THUMB + return await self._resolve_event(data, parts[2], thumbnail_only) + + # rest are params for browse + parts = parts[2:] + + # {nvr_id}:browse + if len(parts) == 0: + return await self._build_console(data) + + # {nvr_id}:browse:all|{camera_id} + camera_id = parts.pop(0) + if len(parts) == 0: + return await self._build_camera(data, camera_id, build_children=True) + + # {nvr_id}:browse:all|{camera_id}:all|{event_type} + try: + event_type = SimpleEventType(parts.pop(0).lower()) + except (IndexError, ValueError) as err: + return _bad_identifier(item.identifier, err) + + if len(parts) == 0: + return await self._build_events_type( + data, camera_id, event_type, build_children=True + ) + + try: + time_type = IdentifierTimeType(parts.pop(0)) + except ValueError as err: + return _bad_identifier(item.identifier, err) + + if len(parts) == 0: + return _bad_identifier(item.identifier) + + # {nvr_id}:browse:all|{camera_id}:all|{event_type}:recent:{day_count} + if time_type == IdentifierTimeType.RECENT: + try: + days = int(parts.pop(0)) + except (IndexError, ValueError) as err: + return _bad_identifier(item.identifier, err) + + return await self._build_recent( + data, camera_id, event_type, days, build_children=True + ) + + # {nvr_id}:all|{camera_id}:all|{event_type}:range:{year}:{month} + # {nvr_id}:all|{camera_id}:all|{event_type}:range:{year}:{month}:all|{day} + try: + start, is_month, is_all = self._parse_range(parts) + except (IndexError, ValueError) as err: + return _bad_identifier(item.identifier, err) + + if is_month: + return await self._build_month( + data, camera_id, event_type, start, build_children=True + ) + return await self._build_days( + data, camera_id, event_type, start, build_children=True, is_all=is_all + ) + + def _parse_range(self, parts: list[str]) -> tuple[date, bool, bool]: + day = 1 + is_month = True + is_all = True + year = int(parts[0]) + month = int(parts[1]) + if len(parts) == 3: + is_month = False + if parts[2] != "all": + is_all = False + day = int(parts[2]) + + start = date(year=year, month=month, day=day) + return start, is_month, is_all + + async def _resolve_event( + self, data: ProtectData, event_id: str, thumbnail_only: bool = False + ) -> BrowseMediaSource: + """Resolve a specific event.""" + + subtype = "eventthumb" if thumbnail_only else "event" + try: + event = await data.api.get_event(event_id) + except NvrError as err: + return _bad_identifier( + f"{data.api.bootstrap.nvr.id}:{subtype}:{event_id}", err + ) + + if event.start is None or event.end is None: + raise BrowseError("Event is still ongoing") + + return await self._build_event(data, event, thumbnail_only) + + async def get_registry(self) -> er.EntityRegistry: + """Get or return Entity Registry.""" + + if self._registry is None: + self._registry = await er.async_get_registry(self.hass) + return self._registry + + def _breadcrumb( + self, + data: ProtectData, + base_title: str, + camera: Camera | None = None, + event_type: SimpleEventType | None = None, + count: int | None = None, + ) -> str: + title = base_title + if count is not None: + if count == data.max_events: + title = f"{title} ({count} TRUNCATED)" + else: + title = f"{title} ({count})" + + if event_type is not None: + title = f"{EVENT_NAME_MAP[event_type].title()} > {title}" + + if camera is not None: + title = f"{camera.display_name} > {title}" + title = f"{data.api.bootstrap.nvr.display_name} > {title}" + + return title + + async def _build_event( + self, + data: ProtectData, + event: dict[str, Any] | Event, + thumbnail_only: bool = False, + ) -> BrowseMediaSource: + """Build media source for an individual event.""" + + if isinstance(event, Event): + event_id = event.id + event_type = event.type + start = event.start + end = event.end + else: + event_id = event["id"] + event_type = event["type"] + start = from_js_time(event["start"]) + end = from_js_time(event["end"]) + + assert end is not None + + title = dt_util.as_local(start).strftime("%x %X") + duration = end - start + title += f" {_format_duration(duration)}" + if event_type == EventType.RING.value: + event_text = "Ring Event" + elif event_type == EventType.MOTION.value: + event_text = "Motion Event" + elif event_type == EventType.SMART_DETECT.value: + if isinstance(event, Event): + smart_type = event.smart_detect_types[0] + else: + smart_type = SmartDetectObjectType(event["smartDetectTypes"][0]) + event_text = f"Smart Detection - {smart_type.name.title()}" + title += f" {event_text}" + + nvr = data.api.bootstrap.nvr + if thumbnail_only: + return BrowseMediaSource( + domain=DOMAIN, + identifier=f"{nvr.id}:eventthumb:{event_id}", + media_class=MEDIA_CLASS_IMAGE, + media_content_type="image/jpeg", + title=title, + can_play=True, + can_expand=False, + thumbnail=async_generate_thumbnail_url( + event_id, nvr.id, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT + ), + ) + + return BrowseMediaSource( + domain=DOMAIN, + identifier=f"{nvr.id}:event:{event_id}", + media_class=MEDIA_CLASS_VIDEO, + media_content_type="video/mp4", + title=title, + can_play=True, + can_expand=False, + thumbnail=async_generate_thumbnail_url( + event_id, nvr.id, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT + ), + ) + + async def _build_events( + self, + data: ProtectData, + start: datetime, + end: datetime, + camera_id: str | None = None, + event_type: EventType | None = None, + reserve: bool = False, + ) -> list[BrowseMediaSource]: + """Build media source for a given range of time and event type.""" + + if event_type is None: + types = [ + EventType.RING, + EventType.MOTION, + EventType.SMART_DETECT, + ] + else: + types = [event_type] + + sources: list[BrowseMediaSource] = [] + events = await data.api.get_events_raw( + start=start, end=end, types=types, limit=data.max_events + ) + events = sorted(events, key=lambda e: cast(int, e["start"]), reverse=reserve) + for event in events: + # do not process ongoing events + if event.get("start") is None or event.get("end") is None: + continue + + if camera_id is not None and event.get("camera") != camera_id: + continue + + # smart detect events have a paired motion event + if ( + event.get("type") == EventType.MOTION.value + and len(event.get("smartDetectEvents", [])) > 0 + ): + continue + + sources.append(await self._build_event(data, event)) + + return sources + + async def _build_recent( + self, + data: ProtectData, + camera_id: str, + event_type: SimpleEventType, + days: int, + build_children: bool = False, + ) -> BrowseMediaSource: + """Build media source for events in relative days.""" + + base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}" + title = f"Last {days} Days" + if days == 1: + title = "Last 24 Hours" + + source = BrowseMediaSource( + domain=DOMAIN, + identifier=f"{base_id}:recent:{days}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="video/mp4", + title=title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children: + return source + + now = dt_util.now() + + args = { + "data": data, + "start": now - timedelta(days=days), + "end": now, + "reserve": True, + } + if event_type != SimpleEventType.ALL: + args["event_type"] = get_ufp_event(event_type) + + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + args["camera_id"] = camera_id + + events = await self._build_events(**args) # type: ignore[arg-type] + source.children = events # type: ignore[assignment] + source.title = self._breadcrumb( + data, + title, + camera=camera, + event_type=event_type, + count=len(events), + ) + return source + + async def _build_month( + self, + data: ProtectData, + camera_id: str, + event_type: SimpleEventType, + start: date, + build_children: bool = False, + ) -> BrowseMediaSource: + """Build media source for selectors for a given month.""" + + base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}" + + title = f"{start.strftime('%B %Y')}" + source = BrowseMediaSource( + domain=DOMAIN, + identifier=f"{base_id}:range:{start.year}:{start.month}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children: + return source + + month = start.month + children = [self._build_days(data, camera_id, event_type, start, is_all=True)] + while start.month == month: + children.append( + self._build_days(data, camera_id, event_type, start, is_all=False) + ) + start = start + timedelta(hours=24) + + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + + source.children = await asyncio.gather(*children) + source.title = self._breadcrumb( + data, + title, + camera=camera, + event_type=event_type, + ) + + return source + + async def _build_days( + self, + data: ProtectData, + camera_id: str, + event_type: SimpleEventType, + start: date, + is_all: bool = True, + build_children: bool = False, + ) -> BrowseMediaSource: + """Build media source for events for a given day or whole month.""" + + base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}" + + if is_all: + title = "Whole Month" + identifier = f"{base_id}:range:{start.year}:{start.month}:all" + else: + title = f"{start.strftime('%x')}" + identifier = f"{base_id}:range:{start.year}:{start.month}:{start.day}" + source = BrowseMediaSource( + domain=DOMAIN, + identifier=identifier, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children: + return source + + start_dt = datetime( + year=start.year, + month=start.month, + day=start.day, + hour=0, + minute=0, + second=0, + tzinfo=dt_util.DEFAULT_TIME_ZONE, + ) + if is_all: + if start_dt.month < 12: + end_dt = start_dt.replace(month=start_dt.month + 1) + else: + end_dt = start_dt.replace(year=start_dt.year + 1, month=1) + else: + end_dt = start_dt + timedelta(hours=24) + + args = { + "data": data, + "start": start_dt, + "end": end_dt, + "reserve": False, + } + if event_type != SimpleEventType.ALL: + args["event_type"] = get_ufp_event(event_type) + + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + args["camera_id"] = camera_id + + title = f"{start.strftime('%B %Y')} > {title}" + events = await self._build_events(**args) # type: ignore[arg-type] + source.children = events # type: ignore[assignment] + source.title = self._breadcrumb( + data, + title, + camera=camera, + event_type=event_type, + count=len(events), + ) + + return source + + async def _build_events_type( + self, + data: ProtectData, + camera_id: str, + event_type: SimpleEventType, + build_children: bool = False, + ) -> BrowseMediaSource: + """Build folder media source for a selectors for a given event type.""" + + base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}" + + title = EVENT_NAME_MAP[event_type].title() + source = BrowseMediaSource( + domain=DOMAIN, + identifier=base_id, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children or data.api.bootstrap.recording_start is None: + return source + + children = [ + self._build_recent(data, camera_id, event_type, 1), + self._build_recent(data, camera_id, event_type, 7), + self._build_recent(data, camera_id, event_type, 30), + ] + + start, end = _get_start_end(self.hass, data.api.bootstrap.recording_start) + while end > start: + children.append(self._build_month(data, camera_id, event_type, end.date())) + end = (end - timedelta(days=1)).replace(day=1) + + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + source.children = await asyncio.gather(*children) + source.title = self._breadcrumb(data, title, camera=camera) + + return source + + async def _get_camera_thumbnail_url(self, camera: Camera) -> str | None: + """Get camera thumbnail URL using the first available camera entity.""" + + if not camera.is_connected or camera.is_privacy_on: + return None + + entity_id: str | None = None + entity_registry = await self.get_registry() + for channel in camera.channels: + # do not use the package camera + if channel.id == 3: + continue + + base_id = f"{camera.mac}_{channel.id}" + entity_id = entity_registry.async_get_entity_id( + Platform.CAMERA, DOMAIN, base_id + ) + if entity_id is None: + entity_id = entity_registry.async_get_entity_id( + Platform.CAMERA, DOMAIN, f"{base_id}_insecure" + ) + + if entity_id: + # verify entity is available + entry = entity_registry.async_get(entity_id) + if entry and not entry.disabled: + break + entity_id = None + + if entity_id is not None: + url = URL(CameraImageView.url.format(entity_id=entity_id)) + return str( + url.update_query({"width": THUMBNAIL_WIDTH, "height": THUMBNAIL_HEIGHT}) + ) + return None + + async def _build_camera( + self, data: ProtectData, camera_id: str, build_children: bool = False + ) -> BrowseMediaSource: + """Build media source for selectors for a UniFi Protect camera.""" + + name = "All Cameras" + is_doorbell = data.api.bootstrap.has_doorbell + has_smart = data.api.bootstrap.has_smart_detections + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + if camera is None: + raise BrowseError(f"Unknown Camera ID: {camera_id}") + name = camera.name or camera.market_name or camera.type + is_doorbell = camera.feature_flags.has_chime + has_smart = camera.feature_flags.has_smart_detect + + thumbnail_url: str | None = None + if camera is not None: + thumbnail_url = await self._get_camera_thumbnail_url(camera) + source = BrowseMediaSource( + domain=DOMAIN, + identifier=f"{data.api.bootstrap.nvr.id}:browse:{camera_id}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=name, + can_play=False, + can_expand=True, + thumbnail=thumbnail_url, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children: + return source + + source.children = [ + await self._build_events_type(data, camera_id, SimpleEventType.MOTION), + ] + + if is_doorbell: + source.children.insert( + 0, + await self._build_events_type(data, camera_id, SimpleEventType.RING), + ) + + if has_smart: + source.children.append( + await self._build_events_type(data, camera_id, SimpleEventType.SMART) + ) + + if is_doorbell or has_smart: + source.children.insert( + 0, + await self._build_events_type(data, camera_id, SimpleEventType.ALL), + ) + + source.title = self._breadcrumb(data, name) + + return source + + async def _build_cameras(self, data: ProtectData) -> list[BrowseMediaSource]: + """Build media source for a single UniFi Protect NVR.""" + + cameras: list[BrowseMediaSource] = [await self._build_camera(data, "all")] + + for camera in data.api.bootstrap.cameras.values(): + if not camera.can_read_media(data.api.bootstrap.auth_user): + continue + cameras.append(await self._build_camera(data, camera.id)) + + return cameras + + async def _build_console(self, data: ProtectData) -> BrowseMediaSource: + """Build media source for a single UniFi Protect NVR.""" + + base = BrowseMediaSource( + domain=DOMAIN, + identifier=f"{data.api.bootstrap.nvr.id}:browse", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=data.api.bootstrap.nvr.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + children=await self._build_cameras(data), + ) + + return base + + async def _build_sources(self) -> BrowseMediaSource: + """Return all media source for all UniFi Protect NVRs.""" + + consoles: list[BrowseMediaSource] = [] + print(len(self.data_sources.values())) + for data_source in self.data_sources.values(): + if not data_source.api.bootstrap.has_media: + continue + console_source = await self._build_console(data_source) + consoles.append(console_source) + + if len(consoles) == 1: + return consoles[0] + + return BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=self.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + children=consoles, + ) diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d789459960b..d3cfe24abd2 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -49,7 +49,8 @@ "data": { "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", - "override_connection_host": "Override Connection Host" + "override_connection_host": "Override Connection Host", + "max_media": "Max number of event to load for Media Browser (increases RAM usage)" } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index b9d787b382e..5d690e3fd3e 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "disable_rtsp": "Disable the RTSP stream", + "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If not enabled, they will only update once every 15 minutes.", diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py index ea523d36dd2..a8a767c8879 100644 --- a/homeassistant/components/unifiprotect/views.py +++ b/homeassistant/components/unifiprotect/views.py @@ -53,8 +53,8 @@ def async_generate_event_video_url(event: Event) -> str: url = url_format.format( nvr_id=event.api.bootstrap.nvr.id, camera_id=event.camera_id, - start=event.start.isoformat(), - end=event.end.isoformat(), + start=event.start.replace(microsecond=0).isoformat(), + end=event.end.replace(microsecond=0).isoformat(), ) return url diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 5304e05fe13..d0fb0dba9f2 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -238,6 +238,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "id": "UnifiProtect", "port": 443, "verify_ssl": False, + "max_media": 1000, }, version=2, unique_id=dr.format_mac(MAC_ADDR), @@ -268,6 +269,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "all_updates": True, "disable_rtsp": True, "override_connection_host": True, + "max_media": 1000, } diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py new file mode 100644 index 00000000000..e1e7f3cacde --- /dev/null +++ b/tests/components/unifiprotect/test_media_source.py @@ -0,0 +1,793 @@ +"""Tests for unifiprotect.media_source.""" + +from datetime import datetime, timedelta +from ipaddress import IPv4Address +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from pyunifiprotect.data import ( + Bootstrap, + Camera, + Event, + EventType, + Permission, + SmartDetectObjectType, +) +from pyunifiprotect.exceptions import NvrError + +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_IMAGE, + MEDIA_CLASS_VIDEO, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source import MediaSourceItem +from homeassistant.components.unifiprotect.const import DOMAIN +from homeassistant.components.unifiprotect.media_source import ( + ProtectMediaSource, + async_get_media_source, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import MockUFPFixture +from .utils import init_entry + +from tests.common import MockConfigEntry + + +async def test_get_media_source(hass: HomeAssistant) -> None: + """Test the async_get_media_source function and ProtectMediaSource constructor.""" + source = await async_get_media_source(hass) + assert isinstance(source, ProtectMediaSource) + assert source.domain == DOMAIN + + +@pytest.mark.parametrize( + "identifier", + [ + "test_id:bad_type:test_id", + "bad_id:event:test_id", + "test_id:event:bad_id", + "test_id", + ], +) +async def test_resolve_media_bad_identifier( + hass: HomeAssistant, ufp: MockUFPFixture, identifier: str +): + """Test resolving bad identifiers.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + ufp.api.get_event = AsyncMock(side_effect=NvrError) + await init_entry(hass, ufp, [], regenerate_ids=False) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, identifier, None) + with pytest.raises(BrowseError): + await source.async_resolve_media(media_item) + + +async def test_resolve_media_thumbnail( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test resolving event thumbnails.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.bootstrap.events = {"test_event_id": event} + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:eventthumb:test_event_id", None) + play_media = await source.async_resolve_media(media_item) + + assert play_media.mime_type == "image/jpeg" + assert play_media.url.startswith( + "/api/unifiprotect/thumbnail/test_id/test_event_id" + ) + + +async def test_resolve_media_event( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test resolving event clips.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_event = AsyncMock(return_value=event) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:event:test_event_id", None) + play_media = await source.async_resolve_media(media_item) + + start = event.start.replace(microsecond=0).isoformat() + end = event.end.replace(microsecond=0).isoformat() + + assert play_media.mime_type == "video/mp4" + assert play_media.url.startswith( + f"/api/unifiprotect/video/test_id/{event.camera_id}/{start}/{end}" + ) + + +@pytest.mark.parametrize( + "identifier", + [ + "bad_id:event:test_id", + "test_id", + "test_id:bad_type", + "test_id:browse:all:all:bad_type", + "test_id:browse:all:bad_event", + "test_id:browse:all:all:recent", + "test_id:browse:all:all:recent:not_a_num", + "test_id:browse:all:all:range", + "test_id:browse:all:all:range:not_a_num", + "test_id:browse:all:all:range:2022:not_a_num", + "test_id:browse:all:all:range:2022:1:not_a_num", + "test_id:browse:all:all:range:2022:1:50", + "test_id:browse:all:all:invalid", + "test_id:event:bad_event_id", + "test_id:browse:bad_camera_id", + ], +) +async def test_browse_media_bad_identifier( + hass: HomeAssistant, ufp: MockUFPFixture, identifier: str +): + """Test browsing media with bad identifiers.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + ufp.api.get_event = AsyncMock(side_effect=NvrError) + await init_entry(hass, ufp, [], regenerate_ids=False) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, identifier, None) + with pytest.raises(BrowseError): + await source.async_browse_media(media_item) + + +async def test_browse_media_event_ongoing( + hass: HomeAssistant, ufp: MockUFPFixture, fixed_now: datetime, doorbell: Camera +): + """Test browsing event that is still ongoing.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_event = AsyncMock(return_value=event) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, f"test_id:event:{event.id}", None) + with pytest.raises(BrowseError): + await source.async_browse_media(media_item) + + +async def test_browse_media_root_multiple_consoles( + hass: HomeAssistant, ufp: MockUFPFixture, bootstrap: Bootstrap +): + """Test browsing root level media with multiple consoles.""" + + ufp.api.bootstrap._has_media = True + + await hass.config_entries.async_setup(ufp.entry.entry_id) + await hass.async_block_till_done() + + bootstrap2 = bootstrap.copy() + bootstrap2._has_media = True + bootstrap2.nvr = bootstrap.nvr.copy() + bootstrap2.nvr.id = "test_id2" + bootstrap2.nvr.mac = "A2E00C826924" + bootstrap2.nvr.name = "UnifiProtect2" + + api2 = Mock() + bootstrap2.nvr._api = api2 + bootstrap2._api = api2 + + api2.bootstrap = bootstrap2 + api2._bootstrap = bootstrap2 + api2.api_path = "/api" + api2.base_url = "https://127.0.0.2" + api2.connection_host = IPv4Address("127.0.0.2") + api2.get_nvr = AsyncMock(return_value=bootstrap2.nvr) + api2.update = AsyncMock(return_value=bootstrap2) + api2.async_disconnect_ws = AsyncMock() + + with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.2", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect2", + "port": 443, + "verify_ssl": False, + }, + version=2, + ) + mock_config.add_to_hass(hass) + + mock_api.return_value = api2 + + await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, None, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UniFi Protect" + assert len(browse.children) == 2 + assert browse.children[0].title.startswith("UnifiProtect") + assert browse.children[0].identifier.startswith("test_id") + assert browse.children[1].title.startswith("UnifiProtect") + assert browse.children[0].identifier.startswith("test_id") + + +async def test_browse_media_root_multiple_consoles_only_one_media( + hass: HomeAssistant, ufp: MockUFPFixture, bootstrap: Bootstrap +): + """Test browsing root level media with multiple consoles.""" + + ufp.api.bootstrap._has_media = True + + await hass.config_entries.async_setup(ufp.entry.entry_id) + await hass.async_block_till_done() + + bootstrap2 = bootstrap.copy() + bootstrap2._has_media = False + bootstrap2.nvr = bootstrap.nvr.copy() + bootstrap2.nvr.id = "test_id2" + bootstrap2.nvr.mac = "A2E00C826924" + bootstrap2.nvr.name = "UnifiProtect2" + + api2 = Mock() + bootstrap2.nvr._api = api2 + bootstrap2._api = api2 + + api2.bootstrap = bootstrap2 + api2._bootstrap = bootstrap2 + api2.api_path = "/api" + api2.base_url = "https://127.0.0.2" + api2.connection_host = IPv4Address("127.0.0.2") + api2.get_nvr = AsyncMock(return_value=bootstrap2.nvr) + api2.update = AsyncMock(return_value=bootstrap2) + api2.async_disconnect_ws = AsyncMock() + + with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.2", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect2", + "port": 443, + "verify_ssl": False, + }, + version=2, + ) + mock_config.add_to_hass(hass) + + mock_api.return_value = api2 + + await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, None, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect" + assert browse.identifier == "test_id:browse" + assert len(browse.children) == 1 + assert browse.children[0].title == "All Cameras" + assert browse.children[0].identifier == "test_id:browse:all" + + +async def test_browse_media_root_single_console( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): + """Test browsing root level media with a single console.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, None, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect" + assert browse.identifier == "test_id:browse" + assert len(browse.children) == 2 + assert browse.children[0].title == "All Cameras" + assert browse.children[0].identifier == "test_id:browse:all" + assert browse.children[1].title == doorbell.name + assert browse.children[1].identifier == f"test_id:browse:{doorbell.id}" + assert browse.children[1].thumbnail is not None + + +async def test_browse_media_camera( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, camera: Camera +): + """Test browsing camera selector level media.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell, camera]) + + ufp.api.bootstrap.auth_user.all_permissions = [ + Permission.unifi_dict_to_dict( + {"rawPermission": "camera:create,read,write,delete,deletemedia:*"} + ), + Permission.unifi_dict_to_dict( + {"rawPermission": f"camera:readmedia:{doorbell.id}"} + ), + ] + + entity_registry = er.async_get(hass) + entity_registry.async_update_entity( + "camera.test_camera_high", disabled_by=er.RegistryEntryDisabler("user") + ) + await hass.async_block_till_done() + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:browse", None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect" + assert browse.identifier == "test_id:browse" + assert len(browse.children) == 2 + assert browse.children[0].title == "All Cameras" + assert browse.children[0].identifier == "test_id:browse:all" + assert browse.children[1].title == doorbell.name + assert browse.children[1].identifier == f"test_id:browse:{doorbell.id}" + assert browse.children[1].thumbnail is None + + +async def test_browse_media_camera_offline( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): + """Test browsing camera selector level media when camera is offline.""" + + doorbell.is_connected = False + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell]) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:browse", None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect" + assert browse.identifier == "test_id:browse" + assert len(browse.children) == 2 + assert browse.children[0].title == "All Cameras" + assert browse.children[0].identifier == "test_id:browse:all" + assert browse.children[1].title == doorbell.name + assert browse.children[1].identifier == f"test_id:browse:{doorbell.id}" + assert browse.children[1].thumbnail is None + + +async def test_browse_media_event_type( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): + """Test browsing event type selector level media.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:browse:all", None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect > All Cameras" + assert browse.identifier == "test_id:browse:all" + assert len(browse.children) == 4 + assert browse.children[0].title == "All Events" + assert browse.children[0].identifier == "test_id:browse:all:all" + assert browse.children[1].title == "Ring Events" + assert browse.children[1].identifier == "test_id:browse:all:ring" + assert browse.children[2].title == "Motion Events" + assert browse.children[2].identifier == "test_id:browse:all:motion" + assert browse.children[3].title == "Smart Detections" + assert browse.children[3].identifier == "test_id:browse:all:smart" + + +async def test_browse_media_time( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing time selector level media.""" + + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + base_id = f"test_id:browse:{doorbell.id}:all" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == f"UnifiProtect > {doorbell.name} > All Events" + assert browse.identifier == base_id + assert len(browse.children) == 4 + assert browse.children[0].title == "Last 24 Hours" + assert browse.children[0].identifier == f"{base_id}:recent:1" + assert browse.children[1].title == "Last 7 Days" + assert browse.children[1].identifier == f"{base_id}:recent:7" + assert browse.children[2].title == "Last 30 Days" + assert browse.children[2].identifier == f"{base_id}:recent:30" + assert browse.children[3].title == f"{fixed_now.strftime('%B %Y')}" + assert ( + browse.children[3].identifier + == f"{base_id}:range:{fixed_now.year}:{fixed_now.month}" + ) + + +async def test_browse_media_recent( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing event selector level media for recent days.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_events_raw = AsyncMock(return_value=[event.unifi_dict()]) + + base_id = f"test_id:browse:{doorbell.id}:motion:recent:1" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > Motion Events > Last 24 Hours (1)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" + + +async def test_browse_media_recent_truncated( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing event selector level media for recent days.""" + + ufp.entry.options = {"max_media": 1} + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_events_raw = AsyncMock(return_value=[event.unifi_dict()]) + + base_id = f"test_id:browse:{doorbell.id}:motion:recent:1" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > Motion Events > Last 24 Hours (1 TRUNCATED)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" + + +async def test_browse_media_event( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing specific event.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.RING, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_event = AsyncMock(return_value=event) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:event:test_event_id", None) + + browse = await source.async_browse_media(media_item) + + assert browse.identifier == "test_id:event:test_event_id" + assert browse.children is None + assert browse.media_class == MEDIA_CLASS_VIDEO + + +async def test_browse_media_eventthumb( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing specific event.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[SmartDetectObjectType.PERSON], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_event = AsyncMock(return_value=event) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:eventthumb:test_event_id", None) + + browse = await source.async_browse_media(media_item) + + assert browse.identifier == "test_id:eventthumb:test_event_id" + assert browse.children is None + assert browse.media_class == MEDIA_CLASS_IMAGE + + +async def test_browse_media_day( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing day selector level media.""" + + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + base_id = ( + f"test_id:browse:{doorbell.id}:all:range:{fixed_now.year}:{fixed_now.month}" + ) + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')}" + ) + assert browse.identifier == base_id + assert len(browse.children) in (29, 30, 31, 32) + assert browse.children[0].title == "Whole Month" + assert browse.children[0].identifier == f"{base_id}:all" + + +async def test_browse_media_browse_day( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test events for a specific day.""" + + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_events_raw = AsyncMock(return_value=[event.unifi_dict()]) + + base_id = f"test_id:browse:{doorbell.id}:motion:range:{fixed_now.year}:{fixed_now.month}:1" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + start = fixed_now.replace(day=1) + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > Motion Events > {fixed_now.strftime('%B %Y')} > {start.strftime('%x')} (1)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" + + +async def test_browse_media_browse_whole_month( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test events for a specific day.""" + + fixed_now = fixed_now.replace(month=11) + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_events_raw = AsyncMock(return_value=[event.unifi_dict()]) + + base_id = ( + f"test_id:browse:{doorbell.id}:all:range:{fixed_now.year}:{fixed_now.month}:all" + ) + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')} > Whole Month (1)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" + + +async def test_browse_media_browse_whole_month_december( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test events for a specific day.""" + + fixed_now = fixed_now.replace(month=12) + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event1 = Event( + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=3663), + end=fixed_now, + score=100, + smart_detect_types=[SmartDetectObjectType.PERSON], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event1._api = ufp.api + event2 = Event( + id="test_event_id2", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=["test_event_id"], + camera_id=doorbell.id, + ) + event2._api = ufp.api + event3 = Event( + id="test_event_id3", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id="other_camera", + ) + event3._api = ufp.api + event4 = Event( + id="test_event_id4", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event4._api = ufp.api + + ufp.api.get_events_raw = AsyncMock( + return_value=[ + event1.unifi_dict(), + event2.unifi_dict(), + event3.unifi_dict(), + event4.unifi_dict(), + ] + ) + + base_id = ( + f"test_id:browse:{doorbell.id}:all:range:{fixed_now.year}:{fixed_now.month}:all" + ) + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')} > Whole Month (1)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index e64a0a87377..0768252f6c9 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -341,7 +341,7 @@ async def test_video_bad_params( url = async_generate_event_video_url(event) from_value = event_start if start is not None else fixed_now to_value = start if start is not None else end - url = url.replace(from_value.isoformat(), to_value) + url = url.replace(from_value.replace(microsecond=0).isoformat(), to_value) http_client = await hass_client() response = cast(ClientResponse, await http_client.get(url)) From 36d6ef6228d2b0b5ce2af175d8679965ff3d8888 Mon Sep 17 00:00:00 2001 From: rlippmann <70883373+rlippmann@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:34:38 -0400 Subject: [PATCH 237/903] Add ecobee CO2, VOC, and AQI sensors (#76366) * Add support for CO2, VOC, and AQI sensors * Update sensor.py * Formatting changes * Use class thermostat member in async_update * Remove thermostat attribute, add mixin class * Add docstrings * Add blank line * Sort imports Co-authored-by: Martin Hjelmare --- homeassistant/components/ecobee/sensor.py | 68 ++++++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 250aac68a04..38671189132 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -1,6 +1,8 @@ """Support for Ecobee sensors.""" from __future__ import annotations +from dataclasses import dataclass + from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN from homeassistant.components.sensor import ( @@ -10,27 +12,73 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_FAHRENHEIT +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + +@dataclass +class EcobeeSensorEntityDescriptionMixin: + """Represent the required ecobee entity description attributes.""" + + runtime_key: str + + +@dataclass +class EcobeeSensorEntityDescription( + SensorEntityDescription, EcobeeSensorEntityDescriptionMixin +): + """Represent the ecobee sensor entity description.""" + + +SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = ( + EcobeeSensorEntityDescription( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualTemperature", ), - SensorEntityDescription( + EcobeeSensorEntityDescription( key="humidity", name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualHumidity", + ), + EcobeeSensorEntityDescription( + key="co2PPM", + name="CO2", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualCO2", + ), + EcobeeSensorEntityDescription( + key="vocPPM", + name="VOC", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualVOC", + ), + EcobeeSensorEntityDescription( + key="airQuality", + name="Air Quality Index", + device_class=SensorDeviceClass.AQI, + native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualAQScore", ), ) @@ -40,7 +88,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up ecobee (temperature and humidity) sensors.""" + """Set up ecobee sensors.""" data = hass.data[DOMAIN] entities = [ EcobeeSensor(data, sensor["name"], index, description) @@ -58,7 +106,11 @@ class EcobeeSensor(SensorEntity): """Representation of an Ecobee sensor.""" def __init__( - self, data, sensor_name, sensor_index, description: SensorEntityDescription + self, + data, + sensor_name, + sensor_index, + description: EcobeeSensorEntityDescription, ): """Initialize the sensor.""" self.entity_description = description @@ -66,7 +118,6 @@ class EcobeeSensor(SensorEntity): self.sensor_name = sensor_name self.index = sensor_index self._state = None - self._attr_name = f"{sensor_name} {description.name}" @property @@ -141,5 +192,6 @@ class EcobeeSensor(SensorEntity): for item in sensor["capability"]: if item["type"] != self.entity_description.key: continue - self._state = item["value"] + thermostat = self.data.ecobee.get_thermostat(self.index) + self._state = thermostat["runtime"][self.entity_description.runtime_key] break From acbeb8c881cdbe0c67119cf924df955e765bd9f5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 8 Aug 2022 14:53:27 -0600 Subject: [PATCH 238/903] Bump `regenmaschine` to 2022.08.0 (#76483) --- homeassistant/components/rainmachine/__init__.py | 16 ++++++++++++++-- .../components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 1ad1fb734fa..a5426552ae2 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -31,6 +31,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity, UpdateFailed +from homeassistant.util.dt import as_timestamp, utcnow from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller @@ -299,7 +300,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_restrict_watering(call: ServiceCall) -> None: """Restrict watering for a time period.""" controller = async_get_controller_for_service_call(hass, call) - await controller.restrictions.restrict(call.data[CONF_DURATION]) + duration = call.data[CONF_DURATION] + await controller.restrictions.set_universal( + { + "rainDelayStartTime": round(as_timestamp(utcnow())), + "rainDelayDuration": duration.total_seconds(), + }, + ) await async_update_programs_and_zones(hass, entry) async def async_stop_all(call: ServiceCall) -> None: @@ -317,7 +324,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unrestrict_watering(call: ServiceCall) -> None: """Unrestrict watering.""" controller = async_get_controller_for_service_call(hass, call) - await controller.restrictions.unrestrict() + await controller.restrictions.set_universal( + { + "rainDelayStartTime": round(as_timestamp(utcnow())), + "rainDelayDuration": 0, + }, + ) await async_update_programs_and_zones(hass, entry) for service_name, schema, method in ( diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 4d60730ba6c..b183fc1b24f 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.07.3"], + "requirements": ["regenmaschine==2022.08.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 1cd3875344c..d2777a873f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2082,7 +2082,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.07.3 +regenmaschine==2022.08.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d387fd49e1d..75a0c06acc9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1409,7 +1409,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.07.3 +regenmaschine==2022.08.0 # homeassistant.components.renault renault-api==0.1.11 From cefc535edba7ae15aaf10606e756b0222f26b85f Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen Date: Mon, 8 Aug 2022 23:35:05 +0200 Subject: [PATCH 239/903] Add JustNimbus integration (#75718) Co-authored-by: Franck Nijhof --- .coveragerc | 4 + CODEOWNERS | 2 + .../components/justnimbus/__init__.py | 25 +++ .../components/justnimbus/config_flow.py | 60 ++++++ homeassistant/components/justnimbus/const.py | 13 ++ .../components/justnimbus/coordinator.py | 34 +++ homeassistant/components/justnimbus/entity.py | 37 ++++ .../components/justnimbus/manifest.json | 9 + homeassistant/components/justnimbus/sensor.py | 193 ++++++++++++++++++ .../components/justnimbus/strings.json | 19 ++ .../justnimbus/translations/en.json | 19 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/justnimbus/__init__.py | 1 + .../components/justnimbus/test_config_flow.py | 84 ++++++++ 16 files changed, 507 insertions(+) create mode 100644 homeassistant/components/justnimbus/__init__.py create mode 100644 homeassistant/components/justnimbus/config_flow.py create mode 100644 homeassistant/components/justnimbus/const.py create mode 100644 homeassistant/components/justnimbus/coordinator.py create mode 100644 homeassistant/components/justnimbus/entity.py create mode 100644 homeassistant/components/justnimbus/manifest.json create mode 100644 homeassistant/components/justnimbus/sensor.py create mode 100644 homeassistant/components/justnimbus/strings.json create mode 100644 homeassistant/components/justnimbus/translations/en.json create mode 100644 tests/components/justnimbus/__init__.py create mode 100644 tests/components/justnimbus/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0a1430cce91..8a83b98873e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -602,6 +602,10 @@ omit = homeassistant/components/juicenet/number.py homeassistant/components/juicenet/sensor.py homeassistant/components/juicenet/switch.py + homeassistant/components/justnimbus/const.py + homeassistant/components/justnimbus/coordinator.py + homeassistant/components/justnimbus/entity.py + homeassistant/components/justnimbus/sensor.py homeassistant/components/kaiterra/* homeassistant/components/kankun/switch.py homeassistant/components/keba/* diff --git a/CODEOWNERS b/CODEOWNERS index 292879aabea..c92ad3b5ba9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -555,6 +555,8 @@ build.json @home-assistant/supervisor /tests/components/jewish_calendar/ @tsvi /homeassistant/components/juicenet/ @jesserockz /tests/components/juicenet/ @jesserockz +/homeassistant/components/justnimbus/ @kvanzuijlen +/tests/components/justnimbus/ @kvanzuijlen /homeassistant/components/kaiterra/ @Michsior14 /homeassistant/components/kaleidescape/ @SteveEasley /tests/components/kaleidescape/ @SteveEasley diff --git a/homeassistant/components/justnimbus/__init__.py b/homeassistant/components/justnimbus/__init__.py new file mode 100644 index 00000000000..695faa4f529 --- /dev/null +++ b/homeassistant/components/justnimbus/__init__.py @@ -0,0 +1,25 @@ +"""The JustNimbus integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, PLATFORMS +from .coordinator import JustNimbusCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up JustNimbus from a config entry.""" + coordinator = JustNimbusCoordinator(hass=hass, entry=entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/justnimbus/config_flow.py b/homeassistant/components/justnimbus/config_flow.py new file mode 100644 index 00000000000..bb55b1852b8 --- /dev/null +++ b/homeassistant/components/justnimbus/config_flow.py @@ -0,0 +1,60 @@ +"""Config flow for JustNimbus integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import justnimbus +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_CLIENT_ID +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + }, +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for JustNimbus.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + await self.async_set_unique_id(user_input[CONF_CLIENT_ID]) + self._abort_if_unique_id_configured() + + client = justnimbus.JustNimbusClient(client_id=user_input[CONF_CLIENT_ID]) + try: + await self.hass.async_add_executor_job(client.get_data) + except justnimbus.InvalidClientID: + errors["base"] = "invalid_auth" + except justnimbus.JustNimbusError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title="JustNimbus", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/justnimbus/const.py b/homeassistant/components/justnimbus/const.py new file mode 100644 index 00000000000..cf3d4ef825f --- /dev/null +++ b/homeassistant/components/justnimbus/const.py @@ -0,0 +1,13 @@ +"""Constants for the JustNimbus integration.""" + +from typing import Final + +from homeassistant.const import Platform + +DOMAIN = "justnimbus" + +VOLUME_FLOW_RATE_LITERS_PER_MINUTE: Final = "L/min" + +PLATFORMS = [ + Platform.SENSOR, +] diff --git a/homeassistant/components/justnimbus/coordinator.py b/homeassistant/components/justnimbus/coordinator.py new file mode 100644 index 00000000000..606cea0e922 --- /dev/null +++ b/homeassistant/components/justnimbus/coordinator.py @@ -0,0 +1,34 @@ +"""JustNimbus coordinator.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import justnimbus + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class JustNimbusCoordinator(DataUpdateCoordinator[justnimbus.JustNimbusModel]): + """Data update coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(minutes=1), + ) + self._client = justnimbus.JustNimbusClient(client_id=entry.data[CONF_CLIENT_ID]) + + async def _async_update_data(self) -> justnimbus.JustNimbusModel: + """Fetch the latest data from the source.""" + return await self.hass.async_add_executor_job(self._client.get_data) diff --git a/homeassistant/components/justnimbus/entity.py b/homeassistant/components/justnimbus/entity.py new file mode 100644 index 00000000000..f9ea5ba1151 --- /dev/null +++ b/homeassistant/components/justnimbus/entity.py @@ -0,0 +1,37 @@ +"""Base Entity for JustNimbus sensors.""" +from __future__ import annotations + +import justnimbus + +from homeassistant.components.sensor import SensorEntity +from homeassistant.helpers import update_coordinator +from homeassistant.helpers.entity import DeviceInfo + +from . import JustNimbusCoordinator +from .const import DOMAIN + + +class JustNimbusEntity( + update_coordinator.CoordinatorEntity[justnimbus.JustNimbusModel], + SensorEntity, +): + """Defines a base JustNimbus entity.""" + + def __init__( + self, + *, + device_id: str, + coordinator: JustNimbusCoordinator, + ) -> None: + """Initialize the JustNimbus entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + name="JustNimbus Sensor", + manufacturer="JustNimbus", + ) + + @property + def available(self) -> bool: + """Return device availability.""" + return super().available and getattr(self.coordinator.data, "error_code") == 0 diff --git a/homeassistant/components/justnimbus/manifest.json b/homeassistant/components/justnimbus/manifest.json new file mode 100644 index 00000000000..ca25832df00 --- /dev/null +++ b/homeassistant/components/justnimbus/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "justnimbus", + "name": "JustNimbus", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/justnimbus", + "requirements": ["justnimbus==0.6.0"], + "codeowners": ["@kvanzuijlen"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py new file mode 100644 index 00000000000..6041f84e25a --- /dev/null +++ b/homeassistant/components/justnimbus/sensor.py @@ -0,0 +1,193 @@ +"""Support for the JustNimbus platform.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_CLIENT_ID, + PRESSURE_BAR, + TEMP_CELSIUS, + VOLUME_LITERS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import JustNimbusCoordinator +from .const import DOMAIN, VOLUME_FLOW_RATE_LITERS_PER_MINUTE +from .entity import JustNimbusEntity + + +@dataclass +class JustNimbusEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[JustNimbusCoordinator], Any] + + +@dataclass +class JustNimbusEntityDescription( + SensorEntityDescription, JustNimbusEntityDescriptionMixin +): + """Describes JustNimbus sensor entity.""" + + +SENSOR_TYPES = ( + JustNimbusEntityDescription( + key="pump_flow", + name="Pump flow", + icon="mdi:pump", + native_unit_of_measurement=VOLUME_FLOW_RATE_LITERS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.pump_flow, + ), + JustNimbusEntityDescription( + key="drink_flow", + name="Drink flow", + icon="mdi:water-pump", + native_unit_of_measurement=VOLUME_FLOW_RATE_LITERS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.drink_flow, + ), + JustNimbusEntityDescription( + key="pump_pressure", + name="Pump pressure", + native_unit_of_measurement=PRESSURE_BAR, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.pump_pressure, + ), + JustNimbusEntityDescription( + key="pump_starts", + name="Pump starts", + icon="mdi:restart", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.pump_starts, + ), + JustNimbusEntityDescription( + key="pump_hours", + name="Pump hours", + icon="mdi:clock", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.pump_hours, + ), + JustNimbusEntityDescription( + key="reservoir_temp", + name="Reservoir Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.reservoir_temp, + ), + JustNimbusEntityDescription( + key="reservoir_content", + name="Reservoir content", + icon="mdi:car-coolant-level", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.reservoir_content, + ), + JustNimbusEntityDescription( + key="total_saved", + name="Total saved", + icon="mdi:water-opacity", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.total_saved, + ), + JustNimbusEntityDescription( + key="total_replenished", + name="Total replenished", + icon="mdi:water", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.total_replenished, + ), + JustNimbusEntityDescription( + key="error_code", + name="Error code", + icon="mdi:bug", + entity_registry_enabled_default=False, + native_unit_of_measurement="", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.error_code, + ), + JustNimbusEntityDescription( + key="totver", + name="Total use", + icon="mdi:chart-donut", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.totver, + ), + JustNimbusEntityDescription( + key="reservoir_content_max", + name="Max reservoir content", + icon="mdi:waves", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.TOTAL, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.reservoir_content_max, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the JustNimbus sensor.""" + coordinator: JustNimbusCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + JustNimbusSensor( + device_id=entry.data[CONF_CLIENT_ID], + description=description, + coordinator=coordinator, + ) + for description in SENSOR_TYPES + ) + + +class JustNimbusSensor( + JustNimbusEntity, +): + """Implementation of the JustNimbus sensor.""" + + def __init__( + self, + *, + device_id: str, + description: JustNimbusEntityDescription, + coordinator: JustNimbusCoordinator, + ) -> None: + """Initialize the sensor.""" + self.entity_description: JustNimbusEntityDescription = description + super().__init__( + device_id=device_id, + coordinator=coordinator, + ) + self._attr_unique_id = f"{device_id}_{description.key}" + + @property + def native_value(self) -> StateType: + """Return sensor state.""" + return self.entity_description.value_fn(self.coordinator) diff --git a/homeassistant/components/justnimbus/strings.json b/homeassistant/components/justnimbus/strings.json new file mode 100644 index 00000000000..609b1425e93 --- /dev/null +++ b/homeassistant/components/justnimbus/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "client_id": "Client ID" + } + } + }, + "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/justnimbus/translations/en.json b/homeassistant/components/justnimbus/translations/en.json new file mode 100644 index 00000000000..31443841e8a --- /dev/null +++ b/homeassistant/components/justnimbus/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "client_id": "Client ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 597cb10fd7f..d9da1ff2057 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -183,6 +183,7 @@ FLOWS = { "izone", "jellyfin", "juicenet", + "justnimbus", "kaleidescape", "keenetic_ndms2", "kmtronic", diff --git a/requirements_all.txt b/requirements_all.txt index d2777a873f3..6b67daab6fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -925,6 +925,9 @@ jellyfin-apiclient-python==1.8.1 # homeassistant.components.rest jsonpath==0.82 +# homeassistant.components.justnimbus +justnimbus==0.6.0 + # homeassistant.components.kaiterra kaiterra-async-client==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75a0c06acc9..0c80e40f5e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -675,6 +675,9 @@ jellyfin-apiclient-python==1.8.1 # homeassistant.components.rest jsonpath==0.82 +# homeassistant.components.justnimbus +justnimbus==0.6.0 + # homeassistant.components.konnected konnected==1.2.0 diff --git a/tests/components/justnimbus/__init__.py b/tests/components/justnimbus/__init__.py new file mode 100644 index 00000000000..46d872e08c6 --- /dev/null +++ b/tests/components/justnimbus/__init__.py @@ -0,0 +1 @@ +"""Tests for the JustNimbus integration.""" diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py new file mode 100644 index 00000000000..d2cfb64d4c7 --- /dev/null +++ b/tests/components/justnimbus/test_config_flow.py @@ -0,0 +1,84 @@ +"""Test the JustNimbus config flow.""" +from unittest.mock import patch + +from justnimbus.exceptions import InvalidClientID, JustNimbusError +import pytest + +from homeassistant import config_entries +from homeassistant.components.justnimbus.const import DOMAIN +from homeassistant.const import CONF_CLIENT_ID +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) + + +@pytest.mark.parametrize( + "side_effect,errors", + ( + ( + InvalidClientID(client_id="test_id"), + {"base": "invalid_auth"}, + ), + ( + JustNimbusError(), + {"base": "cannot_connect"}, + ), + ), +) +async def test_form_errors( + hass: HomeAssistant, + side_effect: JustNimbusError, + errors: dict, +) -> None: + """Test we handle errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "justnimbus.JustNimbusClient.get_data", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={ + CONF_CLIENT_ID: "test_id", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == errors + + await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) + + +async def _set_up_justnimbus(hass: HomeAssistant, flow_id: str) -> None: + """Reusable successful setup of JustNimbus sensor.""" + with patch("justnimbus.JustNimbusClient.get_data"), patch( + "homeassistant.components.justnimbus.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id=flow_id, + user_input={ + CONF_CLIENT_ID: "test_id", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JustNimbus" + assert result2["data"] == { + CONF_CLIENT_ID: "test_id", + } + assert len(mock_setup_entry.mock_calls) == 1 From 01de1c6304d2b496e3117ee4126609a47557a880 Mon Sep 17 00:00:00 2001 From: Sarabveer Singh <4297171+sarabveer@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:49:07 -0400 Subject: [PATCH 240/903] Update HomeKit PM2.5 mappings to US AQI (#76358) --- .../components/homekit/type_sensors.py | 3 +-- homeassistant/components/homekit/util.py | 25 +++++-------------- tests/components/homekit/test_type_sensors.py | 8 +++--- tests/components/homekit/test_util.py | 15 ++++++----- 4 files changed, 20 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 7e9b4cbbb31..e877ffff07a 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -56,7 +56,6 @@ from .util import ( convert_to_float, density_to_air_quality, density_to_air_quality_pm10, - density_to_air_quality_pm25, temperature_to_homekit, ) @@ -239,7 +238,7 @@ class PM25Sensor(AirQualitySensor): if self.char_density.value != density: self.char_density.set_value(density) _LOGGER.debug("%s: Set density to %d", self.entity_id, density) - air_quality = density_to_air_quality_pm25(density) + air_quality = density_to_air_quality(density) if self.char_quality.value != air_quality: self.char_quality.set_value(air_quality) _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 34df1008e76..b7af7d516dd 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -400,16 +400,16 @@ def temperature_to_states(temperature: float | int, unit: str) -> float: def density_to_air_quality(density: float) -> int: - """Map PM2.5 density to HomeKit AirQuality level.""" - if density <= 35: + """Map PM2.5 µg/m3 density to HomeKit AirQuality level.""" + if density <= 12: # US AQI 0-50 (HomeKit: Excellent) return 1 - if density <= 75: + if density <= 35.4: # US AQI 51-100 (HomeKit: Good) return 2 - if density <= 115: + if density <= 55.4: # US AQI 101-150 (HomeKit: Fair) return 3 - if density <= 150: + if density <= 150.4: # US AQI 151-200 (HomeKit: Inferior) return 4 - return 5 + return 5 # US AQI 201+ (HomeKit: Poor) def density_to_air_quality_pm10(density: float) -> int: @@ -425,19 +425,6 @@ def density_to_air_quality_pm10(density: float) -> int: return 5 -def density_to_air_quality_pm25(density: float) -> int: - """Map PM2.5 density to HomeKit AirQuality level.""" - if density <= 25: - return 1 - if density <= 50: - return 2 - if density <= 100: - return 3 - if density <= 300: - return 4 - return 5 - - def get_persist_filename_for_entry_id(entry_id: str) -> str: """Determine the filename of the homekit state file.""" return f"{DOMAIN}.{entry_id}.state" diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 75069ce9467..b916d447d12 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -126,7 +126,7 @@ async def test_air_quality(hass, hk_driver): hass.states.async_set(entity_id, "34") await hass.async_block_till_done() assert acc.char_density.value == 34 - assert acc.char_quality.value == 1 + assert acc.char_quality.value == 2 hass.states.async_set(entity_id, "200") await hass.async_block_till_done() @@ -205,7 +205,7 @@ async def test_pm25(hass, hk_driver): hass.states.async_set(entity_id, "23") await hass.async_block_till_done() assert acc.char_density.value == 23 - assert acc.char_quality.value == 1 + assert acc.char_quality.value == 2 hass.states.async_set(entity_id, "34") await hass.async_block_till_done() @@ -215,12 +215,12 @@ async def test_pm25(hass, hk_driver): hass.states.async_set(entity_id, "90") await hass.async_block_till_done() assert acc.char_density.value == 90 - assert acc.char_quality.value == 3 + assert acc.char_quality.value == 4 hass.states.async_set(entity_id, "200") await hass.async_block_till_done() assert acc.char_density.value == 200 - assert acc.char_quality.value == 4 + assert acc.char_quality.value == 5 hass.states.async_set(entity_id, "400") await hass.async_block_till_done() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 3dd30af2056..f3811ce34c5 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -224,12 +224,15 @@ def test_temperature_to_states(): def test_density_to_air_quality(): """Test map PM2.5 density to HomeKit AirQuality level.""" assert density_to_air_quality(0) == 1 - assert density_to_air_quality(35) == 1 - assert density_to_air_quality(35.1) == 2 - assert density_to_air_quality(75) == 2 - assert density_to_air_quality(115) == 3 - assert density_to_air_quality(150) == 4 - assert density_to_air_quality(300) == 5 + assert density_to_air_quality(12) == 1 + assert density_to_air_quality(12.1) == 2 + assert density_to_air_quality(35.4) == 2 + assert density_to_air_quality(35.5) == 3 + assert density_to_air_quality(55.4) == 3 + assert density_to_air_quality(55.5) == 4 + assert density_to_air_quality(150.4) == 4 + assert density_to_air_quality(150.5) == 5 + assert density_to_air_quality(200) == 5 async def test_async_show_setup_msg(hass, hk_driver, mock_get_source_ip): From 759503f8633adb6a6f601ee2d6964a7e6923eb75 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 8 Aug 2022 16:46:48 -0600 Subject: [PATCH 241/903] Ensure ConfirmRepairFlow can make use of translation placeholders (#76336) * Ensure ConfirmRepairFlow can make use of translation placeholders * Automatically determine the issue * Fix tests * Update homeassistant/components/repairs/issue_handler.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/repairs/issue_handler.py | 12 ++++++++++-- tests/components/repairs/test_websocket_api.py | 15 ++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 5695e99998b..b37d4c10e06 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -27,7 +27,6 @@ class ConfirmRepairFlow(RepairsFlow): self, user_input: dict[str, str] | None = None ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" - return await (self.async_step_confirm()) async def async_step_confirm( @@ -37,7 +36,16 @@ class ConfirmRepairFlow(RepairsFlow): if user_input is not None: return self.async_create_entry(title="", data={}) - return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + issue_registry = async_get_issue_registry(self.hass) + description_placeholders = None + if issue := issue_registry.async_get_issue(self.handler, self.issue_id): + description_placeholders = issue.translation_placeholders + + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders=description_placeholders, + ) class RepairsFlowManager(data_entry_flow.FlowManager): diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 1cb83d81b06..2a506c7a248 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -253,14 +253,19 @@ async def test_fix_non_existing_issue( @pytest.mark.parametrize( - "domain, step", + "domain, step, description_placeholders", ( - ("fake_integration", "custom_step"), - ("fake_integration_default_handler", "confirm"), + ("fake_integration", "custom_step", None), + ("fake_integration_default_handler", "confirm", {"abc": "123"}), ), ) async def test_fix_issue( - hass: HomeAssistant, hass_client, hass_ws_client, domain, step + hass: HomeAssistant, + hass_client, + hass_ws_client, + domain, + step, + description_placeholders, ) -> None: """Test we can fix an issue.""" assert await async_setup_component(hass, "http", {}) @@ -288,7 +293,7 @@ async def test_fix_issue( flow_id = data["flow_id"] assert data == { "data_schema": [], - "description_placeholders": None, + "description_placeholders": description_placeholders, "errors": None, "flow_id": ANY, "handler": domain, From 2d431453033d209f5f56455824a3b584c3c989b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Aug 2022 13:06:49 -1000 Subject: [PATCH 242/903] Bump aiohomekit to 1.2.6 (#76488) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 5f6b3f92220..fa7bf39d385 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.5"], + "requirements": ["aiohomekit==1.2.6"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 6b67daab6fd..aed97e37a0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.5 +aiohomekit==1.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c80e40f5e9..7f612cf9526 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.5 +aiohomekit==1.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http From 12721da063c5edeadb42118559dbb0f3ba274b28 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 9 Aug 2022 00:28:50 +0000 Subject: [PATCH 243/903] [ci skip] Translation update --- .../anthemav/translations/zh-Hant.json | 2 +- .../components/demo/translations/ja.json | 9 +++++ .../deutsche_bahn/translations/ja.json | 7 ++++ .../components/escea/translations/ca.json | 8 +++++ .../components/escea/translations/de.json | 13 ++++++++ .../components/escea/translations/fr.json | 13 ++++++++ .../components/escea/translations/id.json | 13 ++++++++ .../components/escea/translations/ja.json | 8 +++++ .../escea/translations/zh-Hant.json | 13 ++++++++ .../justnimbus/translations/fr.json | 19 +++++++++++ .../components/mysensors/translations/id.json | 1 + .../components/mysensors/translations/ja.json | 7 ++++ .../components/mysensors/translations/no.json | 9 +++++ .../openexchangerates/translations/ca.json | 23 +++++++++++++ .../openexchangerates/translations/de.json | 33 +++++++++++++++++++ .../openexchangerates/translations/id.json | 33 +++++++++++++++++++ .../openexchangerates/translations/ja.json | 25 ++++++++++++++ .../openexchangerates/translations/no.json | 33 +++++++++++++++++++ .../openexchangerates/translations/pt-BR.json | 33 +++++++++++++++++++ .../translations/zh-Hant.json | 33 +++++++++++++++++++ .../simplepush/translations/zh-Hant.json | 2 +- .../soundtouch/translations/zh-Hant.json | 2 +- .../unifiprotect/translations/fr.json | 1 + 23 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/deutsche_bahn/translations/ja.json create mode 100644 homeassistant/components/escea/translations/ca.json create mode 100644 homeassistant/components/escea/translations/de.json create mode 100644 homeassistant/components/escea/translations/fr.json create mode 100644 homeassistant/components/escea/translations/id.json create mode 100644 homeassistant/components/escea/translations/ja.json create mode 100644 homeassistant/components/escea/translations/zh-Hant.json create mode 100644 homeassistant/components/justnimbus/translations/fr.json create mode 100644 homeassistant/components/openexchangerates/translations/ca.json create mode 100644 homeassistant/components/openexchangerates/translations/de.json create mode 100644 homeassistant/components/openexchangerates/translations/id.json create mode 100644 homeassistant/components/openexchangerates/translations/ja.json create mode 100644 homeassistant/components/openexchangerates/translations/no.json create mode 100644 homeassistant/components/openexchangerates/translations/pt-BR.json create mode 100644 homeassistant/components/openexchangerates/translations/zh-Hant.json diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json index acba04f49e6..d68bd690c48 100644 --- a/homeassistant/components/anthemav/translations/zh-Hant.json +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index 30467e3df5b..97eb4866bfa 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -1,5 +1,14 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "SUBMIT(\u9001\u4fe1)\u3092\u62bc\u3057\u3066\u3001\u96fb\u6e90\u304c\u4ea4\u63db\u3055\u308c\u305f\u3053\u3068\u3092\u78ba\u8a8d\u3057\u307e\u3059" + } + } + } + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/deutsche_bahn/translations/ja.json b/homeassistant/components/deutsche_bahn/translations/ja.json new file mode 100644 index 00000000000..191ec874abf --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "\u30c9\u30a4\u30c4\u9244\u9053(Deutsche Bahn)\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/ca.json b/homeassistant/components/escea/translations/ca.json new file mode 100644 index 00000000000..d28b9050682 --- /dev/null +++ b/homeassistant/components/escea/translations/ca.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/de.json b/homeassistant/components/escea/translations/de.json new file mode 100644 index 00000000000..9ff884c6073 --- /dev/null +++ b/homeassistant/components/escea/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 einen Escea-Kamin einrichten?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/fr.json b/homeassistant/components/escea/translations/fr.json new file mode 100644 index 00000000000..68cd0cce6dc --- /dev/null +++ b/homeassistant/components/escea/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer une chemin\u00e9e Escea\u00a0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/id.json b/homeassistant/components/escea/translations/id.json new file mode 100644 index 00000000000..66ad19d2a15 --- /dev/null +++ b/homeassistant/components/escea/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin menyiapkan tungku Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/ja.json b/homeassistant/components/escea/translations/ja.json new file mode 100644 index 00000000000..a64c00ee212 --- /dev/null +++ b/homeassistant/components/escea/translations/ja.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/zh-Hant.json b/homeassistant/components/escea/translations/zh-Hant.json new file mode 100644 index 00000000000..403184e5a23 --- /dev/null +++ b/homeassistant/components/escea/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Escea fireplace\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/fr.json b/homeassistant/components/justnimbus/translations/fr.json new file mode 100644 index 00000000000..a8c26f118e3 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "client_id": "ID du client" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index 418da20676e..67a7469b48a 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -70,6 +70,7 @@ "description": "Pengaturan gateway Ethernet" }, "select_gateway_type": { + "description": "Pilih gateway mana yang akan dikonfigurasi.", "menu_options": { "gw_mqtt": "Konfigurasikan gateway MQTT", "gw_serial": "Konfigurasikan gateway serial", diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index 4a73363bf16..2732a622511 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -68,6 +68,13 @@ }, "description": "\u30a4\u30fc\u30b5\u30cd\u30c3\u30c8 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, + "select_gateway_type": { + "description": "\u8a2d\u5b9a\u3059\u308b\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "menu_options": { + "gw_mqtt": "MQTT\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a", + "gw_tcp": "TCP\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a" + } + }, "user": { "data": { "gateway_type": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u7a2e\u985e" diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index 14fb39910fe..1e45899d981 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -14,6 +14,7 @@ "invalid_serial": "Ugyldig serieport", "invalid_subscribe_topic": "Ugyldig abonnementsemne", "invalid_version": "Ugyldig MySensors-versjon", + "mqtt_required": "MQTT-integrasjonen er ikke satt opp", "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", @@ -68,6 +69,14 @@ }, "description": "Ethernet gateway-oppsett" }, + "select_gateway_type": { + "description": "Velg hvilken gateway som skal konfigureres.", + "menu_options": { + "gw_mqtt": "Konfigurer en MQTT-gateway", + "gw_serial": "Konfigurer en seriell gateway", + "gw_tcp": "Konfigurer en TCP-gateway" + } + }, "user": { "data": { "gateway_type": "" diff --git a/homeassistant/components/openexchangerates/translations/ca.json b/homeassistant/components/openexchangerates/translations/ca.json new file mode 100644 index 00000000000..f5a93caa71d --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/de.json b/homeassistant/components/openexchangerates/translations/de.json new file mode 100644 index 00000000000..51a0294c816 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/de.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "base": "Basisw\u00e4hrung" + }, + "data_description": { + "base": "Die Verwendung einer anderen Basisw\u00e4hrung als USD erfordert einen [kostenpflichtigen Plan]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Open Exchange Rates mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die YAML-Konfiguration f\u00fcr Open Exchange Rates aus deiner configuration.yaml und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Open Exchange Rates YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/id.json b/homeassistant/components/openexchangerates/translations/id.json new file mode 100644 index 00000000000..0b5f0183e2b --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/id.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "reauth_successful": "Autentikasi ulang berhasil", + "timeout_connect": "Tenggang waktu membuat koneksi habis" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "timeout_connect": "Tenggang waktu membuat koneksi habis", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "base": "Mata uang dasar" + }, + "data_description": { + "base": "Menggunakan mata uang dasar selain USD memerlukan [paket berbayar]({daftar})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Open Exchange Rates lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Open Exchange Rates dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Open Exchange Rates dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ja.json b/homeassistant/components/openexchangerates/translations/ja.json new file mode 100644 index 00000000000..35a212e6d82 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + }, + "data_description": { + "base": "\u7c73\u30c9\u30eb\u4ee5\u5916\u306e\u57fa\u672c\u901a\u8ca8\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001[\u6709\u6599\u30d7\u30e9\u30f3] ({signup}) \u304c\u5fc5\u8981\u3067\u3059\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/no.json b/homeassistant/components/openexchangerates/translations/no.json new file mode 100644 index 00000000000..93a85a767c8 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "timeout_connect": "Tidsavbrudd oppretter forbindelse" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "timeout_connect": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "base": "Grunnvaluta" + }, + "data_description": { + "base": "Bruk av en annen basisvaluta enn USD krever en [betalt plan]( {signup} )." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av \u00e5pne valutakurser ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Open Exchange Rates YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Open Exchange Rates YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/pt-BR.json b/homeassistant/components/openexchangerates/translations/pt-BR.json new file mode 100644 index 00000000000..b7a8bc33614 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/pt-BR.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falhou ao conectar", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave API", + "base": "Moeda base" + }, + "data_description": { + "base": "Usar outra moeda base que n\u00e3o seja USD requer um [plano pago]( {signup} )." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o de Open Exchange Rates usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Open Exchange Rates do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o de YAML de Open Exchange Rates est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/zh-Hant.json b/homeassistant/components/openexchangerates/translations/zh-Hant.json new file mode 100644 index 00000000000..c9f8df654e4 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "base": "\u57fa\u6e96\u8ca8\u5e63" + }, + "data_description": { + "base": "\u4f7f\u7528\u5176\u4ed6\u8ca8\u5e63\u53d6\u4ee3\u7f8e\u91d1\u505a\u70ba\u57fa\u6e96\u8ca8\u5e63\u70ba [\u4ed8\u8cbb]({signup}) \u670d\u52d9\u3002" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Open Exchange Rates \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Open Exchange Rates YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Open Exchange Rates YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/zh-Hant.json b/homeassistant/components/simplepush/translations/zh-Hant.json index 2e5b3576135..ea51e58e648 100644 --- a/homeassistant/components/simplepush/translations/zh-Hant.json +++ b/homeassistant/components/simplepush/translations/zh-Hant.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Simplepush \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Simplepush \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Simplepush YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" }, "removed_yaml": { diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json index 80d78ba9d20..4387df76e05 100644 --- a/homeassistant/components/soundtouch/translations/zh-Hant.json +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Bose SoundTouch \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Bose SoundTouch YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } diff --git a/homeassistant/components/unifiprotect/translations/fr.json b/homeassistant/components/unifiprotect/translations/fr.json index e1f189a55d5..c6527d39a39 100644 --- a/homeassistant/components/unifiprotect/translations/fr.json +++ b/homeassistant/components/unifiprotect/translations/fr.json @@ -47,6 +47,7 @@ "data": { "all_updates": "M\u00e9triques en temps r\u00e9el (AVERTISSEMENT\u00a0: augmente consid\u00e9rablement l'utilisation du processeur)", "disable_rtsp": "D\u00e9sactiver le flux RTSP", + "max_media": "Nombre maximal d'\u00e9v\u00e9nements \u00e0 charger pour le navigateur multim\u00e9dia (augmente l'utilisation de la RAM)", "override_connection_host": "Ignorer l'h\u00f4te de connexion" }, "description": "L'option Mesures en temps r\u00e9el ne doit \u00eatre activ\u00e9e que si vous avez activ\u00e9 les capteurs de diagnostic et souhaitez qu'ils soient mis \u00e0 jour en temps r\u00e9el. S'ils ne sont pas activ\u00e9s, ils ne seront mis \u00e0 jour qu'une fois toutes les 15 minutes.", From 7d427ddbd4b5eca09b0dfa6f7523e2efd2af5afc Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 9 Aug 2022 06:36:39 +0100 Subject: [PATCH 244/903] Allow parsing to happen in PassiveBluetoothProcessorCoordinator (#76384) --- .../bluetooth/passive_update_processor.py | 49 +++-- .../components/govee_ble/__init__.py | 4 + homeassistant/components/govee_ble/sensor.py | 16 +- homeassistant/components/inkbird/__init__.py | 9 +- homeassistant/components/inkbird/sensor.py | 16 +- homeassistant/components/moat/__init__.py | 9 +- homeassistant/components/moat/sensor.py | 16 +- .../components/sensorpush/__init__.py | 9 +- homeassistant/components/sensorpush/sensor.py | 16 +- .../components/xiaomi_ble/__init__.py | 44 +++- homeassistant/components/xiaomi_ble/sensor.py | 40 +--- .../test_passive_update_processor.py | 190 ++++++++++++++++-- 12 files changed, 288 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 78966d9b7ab..bb9e82a7dbe 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -52,11 +52,17 @@ class PassiveBluetoothDataUpdate(Generic[_T]): ) -class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): +class PassiveBluetoothProcessorCoordinator( + Generic[_T], BasePassiveBluetoothCoordinator +): """Passive bluetooth processor coordinator for bluetooth advertisements. The coordinator is responsible for dispatching the bluetooth data, to each processor, and tracking devices. + + The update_method should return the data that is dispatched to each processor. + This is normally a parsed form of the data, but you can just forward the + BluetoothServiceInfoBleak if needed. """ def __init__( @@ -65,10 +71,18 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): logger: logging.Logger, address: str, mode: BluetoothScanningMode, + update_method: Callable[[BluetoothServiceInfoBleak], _T], ) -> None: """Initialize the coordinator.""" super().__init__(hass, logger, address, mode) self._processors: list[PassiveBluetoothDataProcessor] = [] + self._update_method = update_method + self.last_update_success = True + + @property + def available(self) -> bool: + """Return if the device is available.""" + return super().available and self.last_update_success @callback def async_register_processor( @@ -102,8 +116,22 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): super()._async_handle_bluetooth_event(service_info, change) if self.hass.is_stopping: return + + try: + update = self._update_method(service_info) + except Exception as err: # pylint: disable=broad-except + self.last_update_success = False + self.logger.exception( + "Unexpected error updating %s data: %s", self.name, err + ) + return + + if not self.last_update_success: + self.last_update_success = True + self.logger.info("Coordinator %s recovered", self.name) + for processor in self._processors: - processor.async_handle_bluetooth_event(service_info, change) + processor.async_handle_update(update) _PassiveBluetoothDataProcessorT = TypeVar( @@ -123,9 +151,8 @@ class PassiveBluetoothDataProcessor(Generic[_T]): the appropriate format. The processor will call the update_method every time the bluetooth device - receives a new advertisement data from the coordinator with the following signature: - - update_method(service_info: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate + receives a new advertisement data from the coordinator with the data + returned by he update_method of the coordinator. As the size of each advertisement is limited, the update_method should return a PassiveBluetoothDataUpdate object that contains only data that @@ -138,9 +165,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): def __init__( self, - update_method: Callable[ - [BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T] - ], + update_method: Callable[[_T], PassiveBluetoothDataUpdate[_T]], ) -> None: """Initialize the coordinator.""" self.coordinator: PassiveBluetoothProcessorCoordinator @@ -244,14 +269,10 @@ class PassiveBluetoothDataProcessor(Generic[_T]): update_callback(data) @callback - def async_handle_bluetooth_event( - self, - service_info: BluetoothServiceInfoBleak, - change: BluetoothChange, - ) -> None: + def async_handle_update(self, update: _T) -> None: """Handle a Bluetooth event.""" try: - new_data = self.update_method(service_info) + new_data = self.update_method(update) except Exception as err: # pylint: disable=broad-except self.last_update_success = False self.coordinator.logger.exception( diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index 7a134e43ace..a2dbecf85a2 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import logging +from govee_ble import GoveeBluetoothDeviceData + from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, @@ -22,6 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Govee BLE device from a config entry.""" address = entry.unique_id assert address is not None + data = GoveeBluetoothDeviceData() coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( @@ -29,6 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index d0b9447d9e3..4faa6befa06 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -3,14 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from govee_ble import ( - DeviceClass, - DeviceKey, - GoveeBluetoothDeviceData, - SensorDeviceInfo, - SensorUpdate, - Units, -) +from govee_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -129,12 +122,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = GoveeBluetoothDeviceData() - processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( GoveeBluetoothSensorEntity, async_add_entities diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 0272114b83c..5ed0d6fb367 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import logging +from inkbird_ble import INKBIRDBluetoothDeviceData + from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, @@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up INKBIRD BLE device from a config entry.""" address = entry.unique_id assert address is not None + data = INKBIRDBluetoothDeviceData() coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 0648ca80383..71d6f00ea40 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -3,14 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from inkbird_ble import ( - DeviceClass, - DeviceKey, - INKBIRDBluetoothDeviceData, - SensorDeviceInfo, - SensorUpdate, - Units, -) +from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -129,12 +122,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = INKBIRDBluetoothDeviceData() - processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( INKBIRDBluetoothSensorEntity, async_add_entities diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py index 237948a8ff6..ed360f53b65 100644 --- a/homeassistant/components/moat/__init__.py +++ b/homeassistant/components/moat/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import logging +from moat_ble import MoatBluetoothDeviceData + from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, @@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Moat BLE device from a config entry.""" address = entry.unique_id assert address is not None + data = MoatBluetoothDeviceData() coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py index 295e2877aed..c5e02a38dcd 100644 --- a/homeassistant/components/moat/sensor.py +++ b/homeassistant/components/moat/sensor.py @@ -3,14 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from moat_ble import ( - DeviceClass, - DeviceKey, - MoatBluetoothDeviceData, - SensorDeviceInfo, - SensorUpdate, - Units, -) +from moat_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -136,12 +129,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = MoatBluetoothDeviceData() - processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( MoatBluetoothSensorEntity, async_add_entities diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index d4a0872ba3f..7828a581d07 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import logging +from sensorpush_ble import SensorPushBluetoothDeviceData + from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, @@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SensorPush BLE device from a config entry.""" address = entry.unique_id assert address is not None + data = SensorPushBluetoothDeviceData() coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 9bfa59e3876..8a4db7aff14 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -3,14 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from sensorpush_ble import ( - DeviceClass, - DeviceKey, - SensorDeviceInfo, - SensorPushBluetoothDeviceData, - SensorUpdate, - Units, -) +from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -130,12 +123,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = SensorPushBluetoothDeviceData() - processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( SensorPushBluetoothSensorEntity, async_add_entities diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 791ac1447ad..e3e30e0c79e 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,7 +3,14 @@ from __future__ import annotations import logging -from homeassistant.components.bluetooth import BluetoothScanningMode +from xiaomi_ble import SensorUpdate, XiaomiBluetoothDeviceData +from xiaomi_ble.parser import EncryptionScheme + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, +) from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -18,14 +25,47 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) +def process_service_info( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + data: XiaomiBluetoothDeviceData, + service_info: BluetoothServiceInfoBleak, +) -> SensorUpdate: + """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + update = data.update(service_info) + + # If device isn't pending we know it has seen at least one broadcast with a payload + # If that payload was encrypted and the bindkey was not verified then we need to reauth + if ( + not data.pending + and data.encryption_scheme != EncryptionScheme.NONE + and not data.bindkey_verified + ): + entry.async_start_reauth(hass, data={"device": data}) + + return update + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Xiaomi BLE device from a config entry.""" address = entry.unique_id assert address is not None + + kwargs = {} + if bindkey := entry.data.get("bindkey"): + kwargs["bindkey"] = bytes.fromhex(bindkey) + data = XiaomiBluetoothDeviceData(**kwargs) + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=lambda service_info: process_service_info( + hass, entry, data, service_info + ), ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index dcb95422609..d22ed46dd83 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -3,18 +3,9 @@ from __future__ import annotations from typing import Optional, Union -from xiaomi_ble import ( - DeviceClass, - DeviceKey, - SensorDeviceInfo, - SensorUpdate, - Units, - XiaomiBluetoothDeviceData, -) -from xiaomi_ble.parser import EncryptionScheme +from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries -from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, @@ -165,27 +156,6 @@ def sensor_update_to_bluetooth_data_update( ) -def process_service_info( - hass: HomeAssistant, - entry: config_entries.ConfigEntry, - data: XiaomiBluetoothDeviceData, - service_info: BluetoothServiceInfoBleak, -) -> PassiveBluetoothDataUpdate: - """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" - update = data.update(service_info) - - # If device isn't pending we know it has seen at least one broadcast with a payload - # If that payload was encrypted and the bindkey was not verified then we need to reauth - if ( - not data.pending - and data.encryption_scheme != EncryptionScheme.NONE - and not data.bindkey_verified - ): - entry.async_start_reauth(hass, data={"device": data}) - - return sensor_update_to_bluetooth_data_update(update) - - async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, @@ -195,13 +165,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - kwargs = {} - if bindkey := entry.data.get("bindkey"): - kwargs["bindkey"] = bytes.fromhex(bindkey) - data = XiaomiBluetoothDeviceData(**kwargs) - processor = PassiveBluetoothDataProcessor( - lambda service_info: process_service_info(hass, entry, data, service_info) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( XiaomiBluetoothSensorEntity, async_add_entities diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 6a092746a68..5653b938ada 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -84,14 +84,25 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" + assert data == {"test": "data"} return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -186,14 +197,24 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): await hass.async_block_till_done() @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -271,14 +292,24 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -326,8 +357,14 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta run_count = 0 @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" nonlocal run_count @@ -337,7 +374,11 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -379,8 +420,14 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): run_count = 0 @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" nonlocal run_count @@ -390,7 +437,11 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -721,8 +772,14 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): update_count = 0 @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" nonlocal update_count @@ -732,7 +789,11 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -835,14 +896,24 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -899,14 +970,24 @@ async def test_passive_bluetooth_entity_with_entity_platform( entity_platform = MockEntityPlatform(hass) @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -992,8 +1073,18 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + @callback + def _mock_update_method( + service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -1075,3 +1166,68 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st key="motion", device_id=None ) cancel_coordinator() + + +async def test_exception_from_coordinator_update_method( + hass, caplog, mock_bleak_scanner_start +): + """Test we handle exceptions from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + run_count = 0 + + @callback + def _mock_update_method( + service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + nonlocal run_count + run_count += 1 + if run_count == 2: + raise Exception("Test exception") + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + processor.async_add_listener(MagicMock()) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + + # We should go unavailable once we get an exception + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert "Test exception" in caplog.text + assert processor.available is False + + # We should go available again once we get data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + unregister_processor() + cancel_coordinator() From f90d007e73b52cd06b2a450b2f9a215b4b0b384d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 9 Aug 2022 15:08:46 +0300 Subject: [PATCH 245/903] Add config flow to `android_ip_webcam` (#76222) Co-authored-by: Martin Hjelmare --- .coveragerc | 4 +- CODEOWNERS | 2 + .../components/android_ip_webcam/__init__.py | 394 ++++-------------- .../android_ip_webcam/binary_sensor.py | 61 +-- .../components/android_ip_webcam/camera.py | 60 ++- .../android_ip_webcam/config_flow.py | 84 ++++ .../components/android_ip_webcam/const.py | 41 ++ .../android_ip_webcam/coordinator.py | 42 ++ .../components/android_ip_webcam/entity.py | 32 ++ .../android_ip_webcam/manifest.json | 4 +- .../components/android_ip_webcam/sensor.py | 211 +++++++--- .../components/android_ip_webcam/strings.json | 26 ++ .../components/android_ip_webcam/switch.py | 214 +++++++--- .../android_ip_webcam/translations/en.json | 26 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + .../components/android_ip_webcam/__init__.py | 1 + .../components/android_ip_webcam/conftest.py | 25 ++ .../fixtures/sensor_data.json | 57 +++ .../fixtures/status_data.json | 62 +++ .../android_ip_webcam/test_config_flow.py | 122 ++++++ .../components/android_ip_webcam/test_init.py | 82 ++++ 22 files changed, 1076 insertions(+), 478 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/config_flow.py create mode 100644 homeassistant/components/android_ip_webcam/const.py create mode 100644 homeassistant/components/android_ip_webcam/coordinator.py create mode 100644 homeassistant/components/android_ip_webcam/entity.py create mode 100644 homeassistant/components/android_ip_webcam/strings.json create mode 100644 homeassistant/components/android_ip_webcam/translations/en.json create mode 100644 tests/components/android_ip_webcam/__init__.py create mode 100644 tests/components/android_ip_webcam/conftest.py create mode 100644 tests/components/android_ip_webcam/fixtures/sensor_data.json create mode 100644 tests/components/android_ip_webcam/fixtures/status_data.json create mode 100644 tests/components/android_ip_webcam/test_config_flow.py create mode 100644 tests/components/android_ip_webcam/test_init.py diff --git a/.coveragerc b/.coveragerc index 8a83b98873e..a94b2a8babc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -56,7 +56,9 @@ omit = homeassistant/components/ambient_station/sensor.py homeassistant/components/amcrest/* homeassistant/components/ampio/* - homeassistant/components/android_ip_webcam/* + homeassistant/components/android_ip_webcam/binary_sensor.py + homeassistant/components/android_ip_webcam/sensor.py + homeassistant/components/android_ip_webcam/switch.py homeassistant/components/androidtv/diagnostics.py homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index c92ad3b5ba9..59835aed315 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -72,6 +72,8 @@ build.json @home-assistant/supervisor /homeassistant/components/amcrest/ @flacjacket /homeassistant/components/analytics/ @home-assistant/core @ludeeus /tests/components/analytics/ @home-assistant/core @ludeeus +/homeassistant/components/android_ip_webcam/ @engrbm87 +/tests/components/android_ip_webcam/ @engrbm87 /homeassistant/components/androidtv/ @JeffLIrion @ollo69 /tests/components/androidtv/ @JeffLIrion @ollo69 /homeassistant/components/anthemav/ @hyralex diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index cc24e6f4182..12885db6375 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -1,13 +1,12 @@ -"""Support for Android IP Webcam.""" +"""The Android IP Webcam integration.""" from __future__ import annotations -import asyncio -from datetime import timedelta - from pydroid_ipcam import PyDroidIPCam import voluptuous as vol -from homeassistant.components.mjpeg import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -20,168 +19,64 @@ from homeassistant.const import ( CONF_USERNAME, Platform, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType -from homeassistant.util.dt import utcnow -ATTR_AUD_CONNS = "Audio Connections" -ATTR_HOST = "host" -ATTR_VID_CONNS = "Video Connections" +from .const import ( + CONF_MOTION_SENSOR, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_TIMEOUT, + DOMAIN, + SCAN_INTERVAL, + SENSORS, + SWITCHES, +) +from .coordinator import AndroidIPCamDataUpdateCoordinator -CONF_MOTION_SENSOR = "motion_sensor" - -DATA_IP_WEBCAM = "android_ip_webcam" -DEFAULT_NAME = "IP Webcam" -DEFAULT_PORT = 8080 -DEFAULT_TIMEOUT = 10 -DOMAIN = "android_ip_webcam" - -SCAN_INTERVAL = timedelta(seconds=10) -SIGNAL_UPDATE_DATA = "android_ip_webcam_update" - -KEY_MAP = { - "audio_connections": "Audio Connections", - "adet_limit": "Audio Trigger Limit", - "antibanding": "Anti-banding", - "audio_only": "Audio Only", - "battery_level": "Battery Level", - "battery_temp": "Battery Temperature", - "battery_voltage": "Battery Voltage", - "coloreffect": "Color Effect", - "exposure": "Exposure Level", - "exposure_lock": "Exposure Lock", - "ffc": "Front-facing Camera", - "flashmode": "Flash Mode", - "focus": "Focus", - "focus_homing": "Focus Homing", - "focus_region": "Focus Region", - "focusmode": "Focus Mode", - "gps_active": "GPS Active", - "idle": "Idle", - "ip_address": "IPv4 Address", - "ipv6_address": "IPv6 Address", - "ivideon_streaming": "Ivideon Streaming", - "light": "Light Level", - "mirror_flip": "Mirror Flip", - "motion": "Motion", - "motion_active": "Motion Active", - "motion_detect": "Motion Detection", - "motion_event": "Motion Event", - "motion_limit": "Motion Limit", - "night_vision": "Night Vision", - "night_vision_average": "Night Vision Average", - "night_vision_gain": "Night Vision Gain", - "orientation": "Orientation", - "overlay": "Overlay", - "photo_size": "Photo Size", - "pressure": "Pressure", - "proximity": "Proximity", - "quality": "Quality", - "scenemode": "Scene Mode", - "sound": "Sound", - "sound_event": "Sound Event", - "sound_timeout": "Sound Timeout", - "torch": "Torch", - "video_connections": "Video Connections", - "video_chunk_len": "Video Chunk Length", - "video_recording": "Video Recording", - "video_size": "Video Size", - "whitebalance": "White Balance", - "whitebalance_lock": "White Balance Lock", - "zoom": "Zoom", -} - -ICON_MAP = { - "audio_connections": "mdi:speaker", - "battery_level": "mdi:battery", - "battery_temp": "mdi:thermometer", - "battery_voltage": "mdi:battery-charging-100", - "exposure_lock": "mdi:camera", - "ffc": "mdi:camera-front-variant", - "focus": "mdi:image-filter-center-focus", - "gps_active": "mdi:crosshairs-gps", - "light": "mdi:flashlight", - "motion": "mdi:run", - "night_vision": "mdi:weather-night", - "overlay": "mdi:monitor", - "pressure": "mdi:gauge", - "proximity": "mdi:map-marker-radius", - "quality": "mdi:quality-high", - "sound": "mdi:speaker", - "sound_event": "mdi:speaker", - "sound_timeout": "mdi:speaker", - "torch": "mdi:white-balance-sunny", - "video_chunk_len": "mdi:video", - "video_connections": "mdi:eye", - "video_recording": "mdi:record-rec", - "whitebalance_lock": "mdi:white-balance-auto", -} - -SWITCHES = [ - "exposure_lock", - "ffc", - "focus", - "gps_active", - "motion_detect", - "night_vision", - "overlay", - "torch", - "whitebalance_lock", - "video_recording", +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.CAMERA, + Platform.SENSOR, + Platform.SWITCH, ] -SENSORS = [ - "audio_connections", - "battery_level", - "battery_temp", - "battery_voltage", - "light", - "motion", - "pressure", - "proximity", - "sound", - "video_connections", -] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional( - CONF_TIMEOUT, default=DEFAULT_TIMEOUT - ): cv.positive_int, - vol.Optional( - CONF_SCAN_INTERVAL, default=SCAN_INTERVAL - ): cv.time_period, - vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, - vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, - vol.Optional(CONF_SWITCHES): vol.All( - cv.ensure_list, [vol.In(SWITCHES)] - ), - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSORS)] - ), - vol.Optional(CONF_MOTION_SENSOR): cv.boolean, - } - ) - ], - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional( + CONF_TIMEOUT, default=DEFAULT_TIMEOUT + ): cv.positive_int, + vol.Optional( + CONF_SCAN_INTERVAL, default=SCAN_INTERVAL + ): cv.time_period, + vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, + vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [vol.In(SWITCHES)] + ), + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSORS)] + ), + vol.Optional(CONF_MOTION_SENSOR): cv.boolean, + } + ) + ], + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -189,165 +84,52 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the IP Webcam component.""" - webcams = hass.data[DATA_IP_WEBCAM] = {} - websession = async_get_clientsession(hass) - - async def async_setup_ipcamera(cam_config): - """Set up an IP camera.""" - host = cam_config[CONF_HOST] - username: str | None = cam_config.get(CONF_USERNAME) - password: str | None = cam_config.get(CONF_PASSWORD) - name: str = cam_config[CONF_NAME] - interval = cam_config[CONF_SCAN_INTERVAL] - switches = cam_config.get(CONF_SWITCHES) - sensors = cam_config.get(CONF_SENSORS) - motion = cam_config.get(CONF_MOTION_SENSOR) - - # Init ip webcam - cam = PyDroidIPCam( - websession, - host, - cam_config[CONF_PORT], - username=username, - password=password, - timeout=cam_config[CONF_TIMEOUT], - ssl=False, - ) - - if switches is None: - switches = [ - setting for setting in cam.enabled_settings if setting in SWITCHES - ] - - if sensors is None: - sensors = [sensor for sensor in cam.enabled_sensors if sensor in SENSORS] - sensors.extend(["audio_connections", "video_connections"]) - - if motion is None: - motion = "motion_active" in cam.enabled_sensors - - async def async_update_data(now): - """Update data from IP camera in SCAN_INTERVAL.""" - await cam.update() - async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) - - async_track_point_in_utc_time(hass, async_update_data, utcnow() + interval) - - await async_update_data(None) - - # Load platforms - webcams[host] = cam - - mjpeg_camera = { - CONF_MJPEG_URL: cam.mjpeg_url, - CONF_STILL_IMAGE_URL: cam.image_url, - } - if username and password: - mjpeg_camera.update({CONF_USERNAME: username, CONF_PASSWORD: password}) - - # Remove incorrect config entry setup via mjpeg platform discovery. - mjpeg_config_entry = next( - ( - config_entry - for config_entry in hass.config_entries.async_entries("mjpeg") - if all( - config_entry.options.get(key) == val - for key, val in mjpeg_camera.items() - ) - ), - None, - ) - if mjpeg_config_entry: - await hass.config_entries.async_remove(mjpeg_config_entry.entry_id) - - mjpeg_camera[CONF_NAME] = name + if DOMAIN not in config: + return True + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + for entry in config[DOMAIN]: hass.async_create_task( - discovery.async_load_platform( - hass, Platform.CAMERA, DOMAIN, mjpeg_camera, config + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry ) ) - if sensors: - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.SENSOR, - DOMAIN, - {CONF_NAME: name, CONF_HOST: host, CONF_SENSORS: sensors}, - config, - ) - ) - - if switches: - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.SWITCH, - DOMAIN, - {CONF_NAME: name, CONF_HOST: host, CONF_SWITCHES: switches}, - config, - ) - ) - - if motion: - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.BINARY_SENSOR, - DOMAIN, - {CONF_HOST: host, CONF_NAME: name}, - config, - ) - ) - - tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] - if tasks: - await asyncio.wait(tasks) - return True -class AndroidIPCamEntity(Entity): - """The Android device running IP Webcam.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Android IP Webcam from a config entry.""" + websession = async_get_clientsession(hass) + cam = PyDroidIPCam( + websession, + entry.data[CONF_HOST], + entry.data[CONF_PORT], + username=entry.data.get(CONF_USERNAME), + password=entry.data.get(CONF_PASSWORD), + ssl=False, + ) + coordinator = AndroidIPCamDataUpdateCoordinator(hass, entry, cam) + await coordinator.async_config_entry_first_refresh() - def __init__(self, host, ipcam): - """Initialize the data object.""" - self._host = host - self._ipcam = ipcam + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - async def async_added_to_hass(self): - """Register update dispatcher.""" + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - @callback - def async_ipcam_update(host): - """Update callback.""" - if self._host != host: - return - self.async_schedule_update_ha_state(True) + return True - self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) - ) - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) - @property - def available(self): - """Return True if entity is available.""" - return self._ipcam.available - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - state_attr = {ATTR_HOST: self._host} - if self._ipcam.status_data is None: - return state_attr - - state_attr[ATTR_VID_CONNS] = self._ipcam.status_data.get("video_connections") - state_attr[ATTR_AUD_CONNS] = self._ipcam.status_data.get("audio_connections") - - return state_attr + return unload_ok diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index c5a2bb25cfe..a2dc25d825b 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -4,46 +4,57 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import CONF_HOST, CONF_NAME, DATA_IP_WEBCAM, KEY_MAP, AndroidIPCamEntity +from .const import DOMAIN, MOTION_ACTIVE +from .coordinator import AndroidIPCamDataUpdateCoordinator +from .entity import AndroidIPCamBaseEntity + +BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription( + key="motion_active", + name="Motion active", + device_class=BinarySensorDeviceClass.MOTION, +) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the IP Webcam binary sensors.""" - if discovery_info is None: - return + """Set up the IP Webcam sensors from config entry.""" - host = discovery_info[CONF_HOST] - name = discovery_info[CONF_NAME] - ipcam = hass.data[DATA_IP_WEBCAM][host] + coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] - async_add_entities([IPWebcamBinarySensor(name, host, ipcam, "motion_active")], True) + async_add_entities([IPWebcamBinarySensor(coordinator)]) -class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorEntity): +class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity): """Representation of an IP Webcam binary sensor.""" - _attr_device_class = BinarySensorDeviceClass.MOTION - - def __init__(self, name, host, ipcam, sensor): + def __init__( + self, + coordinator: AndroidIPCamDataUpdateCoordinator, + ) -> None: """Initialize the binary sensor.""" - super().__init__(host, ipcam) + self.entity_description = BINARY_SENSOR_DESCRIPTION + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}-{BINARY_SENSOR_DESCRIPTION.key}" + ) + super().__init__(coordinator) - self._sensor = sensor - self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._attr_name = f"{name} {self._mapped_name}" - self._attr_is_on = None + @property + def available(self) -> bool: + """Return avaibility if setting is enabled.""" + return MOTION_ACTIVE in self.cam.enabled_sensors and super().available - async def async_update(self): - """Retrieve latest state.""" - state, _ = self._ipcam.export_sensor(self._sensor) - self._attr_is_on = state == 1.0 + @property + def is_on(self) -> bool: + """Return if motion is detected.""" + return self.cam.export_sensor(MOTION_ACTIVE)[0] == 1.0 diff --git a/homeassistant/components/android_ip_webcam/camera.py b/homeassistant/components/android_ip_webcam/camera.py index de1223c7f5f..db6548411a9 100644 --- a/homeassistant/components/android_ip_webcam/camera.py +++ b/homeassistant/components/android_ip_webcam/camera.py @@ -2,43 +2,59 @@ from __future__ import annotations from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging -from homeassistant.const import HTTP_BASIC_AUTHENTICATION +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + HTTP_BASIC_AUTHENTICATION, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import DOMAIN +from .coordinator import AndroidIPCamDataUpdateCoordinator -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the IP Webcam camera.""" - if discovery_info is None: - return - + """Set up the IP Webcam camera from config entry.""" filter_urllib3_logging() - async_add_entities([IPWebcamCamera(**discovery_info)]) + coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities([IPWebcamCamera(coordinator)]) class IPWebcamCamera(MjpegCamera): """Representation of a IP Webcam camera.""" - def __init__( - self, - name: str, - mjpeg_url: str, - still_image_url: str, - username: str | None = None, - password: str = "", - ) -> None: + _attr_has_entity_name = True + + def __init__(self, coordinator: AndroidIPCamDataUpdateCoordinator) -> None: """Initialize the camera.""" + name = None + # keep imported name until YAML is removed + if CONF_NAME in coordinator.config_entry.data: + name = coordinator.config_entry.data[CONF_NAME] + self._attr_has_entity_name = False + super().__init__( name=name, - mjpeg_url=mjpeg_url, - still_image_url=still_image_url, + mjpeg_url=coordinator.cam.mjpeg_url, + still_image_url=coordinator.cam.image_url, authentication=HTTP_BASIC_AUTHENTICATION, - username=username, - password=password, + username=coordinator.config_entry.data.get(CONF_USERNAME), + password=coordinator.config_entry.data.get(CONF_PASSWORD, ""), + ) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-camera" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + name=name or coordinator.config_entry.data[CONF_HOST], ) diff --git a/homeassistant/components/android_ip_webcam/config_flow.py b/homeassistant/components/android_ip_webcam/config_flow.py new file mode 100644 index 00000000000..09f0fdaa3a2 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for Android IP Webcam integration.""" +from __future__ import annotations + +from typing import Any + +from pydroid_ipcam import PyDroidIPCam +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_TIMEOUT, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_PORT, DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Inclusive(CONF_USERNAME, "authentication"): str, + vol.Inclusive(CONF_PASSWORD, "authentication"): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: + """Validate the user input allows us to connect.""" + + websession = async_get_clientsession(hass) + cam = PyDroidIPCam( + websession, + data[CONF_HOST], + data[CONF_PORT], + username=data.get(CONF_USERNAME), + password=data.get(CONF_PASSWORD), + ssl=False, + ) + await cam.update() + return cam.available + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Android IP Webcam.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + # to be removed when YAML import is removed + title = user_input.get(CONF_NAME) or user_input[CONF_HOST] + if await validate_input(self.hass, user_input): + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors={"base": "cannot_connect"}, + ) + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + import_config.pop(CONF_SCAN_INTERVAL) + import_config.pop(CONF_TIMEOUT) + return await self.async_step_user(import_config) diff --git a/homeassistant/components/android_ip_webcam/const.py b/homeassistant/components/android_ip_webcam/const.py new file mode 100644 index 00000000000..6672628d977 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/const.py @@ -0,0 +1,41 @@ +"""Constants for the Android IP Webcam integration.""" + +from datetime import timedelta +from typing import Final + +DOMAIN: Final = "android_ip_webcam" +DEFAULT_NAME: Final = "IP Webcam" +DEFAULT_PORT: Final = 8080 +DEFAULT_TIMEOUT: Final = 10 + +CONF_MOTION_SENSOR: Final = "motion_sensor" + +MOTION_ACTIVE: Final = "motion_active" +SCAN_INTERVAL: Final = timedelta(seconds=10) + + +SWITCHES = [ + "exposure_lock", + "ffc", + "focus", + "gps_active", + "motion_detect", + "night_vision", + "overlay", + "torch", + "whitebalance_lock", + "video_recording", +] + +SENSORS = [ + "audio_connections", + "battery_level", + "battery_temp", + "battery_voltage", + "light", + "motion", + "pressure", + "proximity", + "sound", + "video_connections", +] diff --git a/homeassistant/components/android_ip_webcam/coordinator.py b/homeassistant/components/android_ip_webcam/coordinator.py new file mode 100644 index 00000000000..3940c6df7e4 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/coordinator.py @@ -0,0 +1,42 @@ +"""Coordinator object for the Android IP Webcam integration.""" + +from datetime import timedelta +import logging + +from pydroid_ipcam import PyDroidIPCam + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]): + """Coordinator class for the Android IP Webcam.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + cam: PyDroidIPCam, + ) -> None: + """Initialize the Android IP Webcam.""" + self.hass = hass + self.config_entry: ConfigEntry = config_entry + self.cam = cam + super().__init__( + self.hass, + _LOGGER, + name=f"{DOMAIN} {config_entry.data[CONF_HOST]}", + update_interval=timedelta(seconds=10), + ) + + async def _async_update_data(self) -> None: + """Update Android IP Webcam entities.""" + await self.cam.update() + if not self.cam.available: + raise UpdateFailed diff --git a/homeassistant/components/android_ip_webcam/entity.py b/homeassistant/components/android_ip_webcam/entity.py new file mode 100644 index 00000000000..025132e4bfb --- /dev/null +++ b/homeassistant/components/android_ip_webcam/entity.py @@ -0,0 +1,32 @@ +"""Base class for Android IP Webcam entities.""" + +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import AndroidIPCamDataUpdateCoordinator + + +class AndroidIPCamBaseEntity(CoordinatorEntity[AndroidIPCamDataUpdateCoordinator]): + """Base class for Android IP Webcam entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AndroidIPCamDataUpdateCoordinator, + ) -> None: + """Initialize the base entity.""" + super().__init__(coordinator) + if CONF_NAME in coordinator.config_entry.data: + # name is legacy imported from YAML config + # this block can be removed when removing import from YAML + self._attr_name = f"{coordinator.config_entry.data[CONF_NAME]} {self.entity_description.name}" + self._attr_has_entity_name = False + self.cam = coordinator.cam + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + name=coordinator.config_entry.data.get(CONF_NAME) + or coordinator.config_entry.data[CONF_HOST], + ) diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 39223e6636d..0023454728a 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -1,8 +1,10 @@ { "domain": "android_ip_webcam", "name": "Android IP Webcam", + "config_flow": true, + "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "requirements": ["pydroid-ipcam==1.3.1"], - "codeowners": [], + "codeowners": ["@engrbm87"], "iot_class": "local_polling" } diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index b3d43a51217..d699121d6c9 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -1,75 +1,172 @@ """Support for Android IP Webcam sensors.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from collections.abc import Callable +from dataclasses import dataclass -from . import ( - CONF_HOST, - CONF_NAME, - CONF_SENSORS, - DATA_IP_WEBCAM, - ICON_MAP, - KEY_MAP, - AndroidIPCamEntity, +from pydroid_ipcam import PyDroidIPCam + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .coordinator import AndroidIPCamDataUpdateCoordinator +from .entity import AndroidIPCamBaseEntity + + +@dataclass +class AndroidIPWebcamSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[PyDroidIPCam], StateType] + + +@dataclass +class AndroidIPWebcamSensorEntityDescription( + SensorEntityDescription, AndroidIPWebcamSensorEntityDescriptionMixin +): + """Entity description class for Android IP Webcam sensors.""" + + unit_fn: Callable[[PyDroidIPCam], str | None] = lambda _: None + + +SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = ( + AndroidIPWebcamSensorEntityDescription( + key="audio_connections", + name="Audio connections", + icon="mdi:speaker", + state_class=SensorStateClass.TOTAL, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.status_data.get("audio_connections"), + ), + AndroidIPWebcamSensorEntityDescription( + key="battery_level", + name="Battery level", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.export_sensor("battery_level")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("battery_level")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="battery_temp", + name="Battery temperature", + icon="mdi:thermometer", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="battery_voltage", + name="Battery voltage", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="light", + name="Light level", + icon="mdi:flashlight", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("light")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("light")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="motion", + name="Motion", + icon="mdi:run", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("motion")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("motion")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="pressure", + name="Pressure", + icon="mdi:gauge", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("pressure")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("pressure")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="proximity", + name="Proximity", + icon="mdi:map-marker-radius", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("proximity")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("proximity")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="sound", + name="Sound", + icon="mdi:speaker", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("sound")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("sound")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="video_connections", + name="Video connections", + icon="mdi:eye", + state_class=SensorStateClass.TOTAL, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.status_data.get("video_connections"), + ), ) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the IP Webcam Sensor.""" - if discovery_info is None: - return + """Set up the IP Webcam sensors from config entry.""" - host = discovery_info[CONF_HOST] - name = discovery_info[CONF_NAME] - sensors = discovery_info[CONF_SENSORS] - ipcam = hass.data[DATA_IP_WEBCAM][host] - - all_sensors = [] - - for sensor in sensors: - all_sensors.append(IPWebcamSensor(name, host, ipcam, sensor)) - - async_add_entities(all_sensors, True) + coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + sensor_types = [ + sensor + for sensor in SENSOR_TYPES + if sensor.key + in coordinator.cam.enabled_sensors + ["audio_connections", "video_connections"] + ] + async_add_entities( + IPWebcamSensor(coordinator, description) for description in sensor_types + ) -class IPWebcamSensor(AndroidIPCamEntity, SensorEntity): +class IPWebcamSensor(AndroidIPCamBaseEntity, SensorEntity): """Representation of a IP Webcam sensor.""" - def __init__(self, name, host, ipcam, sensor): + entity_description: AndroidIPWebcamSensorEntityDescription + + def __init__( + self, + coordinator: AndroidIPCamDataUpdateCoordinator, + description: AndroidIPWebcamSensorEntityDescription, + ) -> None: """Initialize the sensor.""" - super().__init__(host, ipcam) - - self._sensor = sensor - self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._attr_name = f"{name} {self._mapped_name}" - self._attr_native_value = None - self._attr_native_unit_of_measurement = None - - async def async_update(self): - """Retrieve latest state.""" - if self._sensor in ("audio_connections", "video_connections"): - if not self._ipcam.status_data: - return - self._attr_native_value = self._ipcam.status_data.get(self._sensor) - self._attr_native_unit_of_measurement = "Connections" - else: - ( - self._attr_native_value, - self._attr_native_unit_of_measurement, - ) = self._ipcam.export_sensor(self._sensor) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}" + self.entity_description = description + super().__init__(coordinator) @property - def icon(self): - """Return the icon for the sensor.""" - if self._sensor == "battery_level" and self._attr_native_value is not None: - return icon_for_battery_level(int(self._attr_native_value)) - return ICON_MAP.get(self._sensor, "mdi:eye") + def native_value(self) -> StateType: + """Return native value of sensor.""" + return self.entity_description.value_fn(self.cam) + + @property + def native_unit_of_measurement(self) -> str | None: + """Return native unit of measurement of sensor.""" + return self.entity_description.unit_fn(self.cam) diff --git a/homeassistant/components/android_ip_webcam/strings.json b/homeassistant/components/android_ip_webcam/strings.json new file mode 100644 index 00000000000..a9ade78a413 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Android IP Webcam YAML configuration is being removed", + "description": "Configuring Android IP Webcam using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Android IP Webcam YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index 0e49990236a..57f78b20e3d 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -1,88 +1,170 @@ """Support for Android IP Webcam settings.""" from __future__ import annotations -from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from collections.abc import Callable +from dataclasses import dataclass -from . import ( - CONF_HOST, - CONF_NAME, - CONF_SWITCHES, - DATA_IP_WEBCAM, - ICON_MAP, - KEY_MAP, - AndroidIPCamEntity, +from pydroid_ipcam import PyDroidIPCam + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AndroidIPCamDataUpdateCoordinator +from .entity import AndroidIPCamBaseEntity + + +@dataclass +class AndroidIPWebcamSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + on_func: Callable[[PyDroidIPCam], None] + off_func: Callable[[PyDroidIPCam], None] + + +@dataclass +class AndroidIPWebcamSwitchEntityDescription( + SwitchEntityDescription, AndroidIPWebcamSwitchEntityDescriptionMixin +): + """Entity description class for Android IP Webcam switches.""" + + +SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = ( + AndroidIPWebcamSwitchEntityDescription( + key="exposure_lock", + name="Exposure lock", + icon="mdi:camera", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("exposure_lock", True), + off_func=lambda ipcam: ipcam.change_setting("exposure_lock", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="ffc", + name="Front-facing camera", + icon="mdi:camera-front-variant", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("ffc", True), + off_func=lambda ipcam: ipcam.change_setting("ffc", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="focus", + name="Focus", + icon="mdi:image-filter-center-focus", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.torch(activate=True), + off_func=lambda ipcam: ipcam.torch(activate=False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="gps_active", + name="GPS active", + icon="mdi:crosshairs-gps", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("gps_active", True), + off_func=lambda ipcam: ipcam.change_setting("gps_active", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="motion_detect", + name="Motion detection", + icon="mdi:flash", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("motion_detect", True), + off_func=lambda ipcam: ipcam.change_setting("motion_detect", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="night_vision", + name="Night vision", + icon="mdi:weather-night", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("night_vision", True), + off_func=lambda ipcam: ipcam.change_setting("night_vision", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="overlay", + name="Overlay", + icon="mdi:monitor", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("overlay", True), + off_func=lambda ipcam: ipcam.change_setting("overlay", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="torch", + name="Torch", + icon="mdi:white-balance-sunny", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.torch(activate=True), + off_func=lambda ipcam: ipcam.torch(activate=False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="whitebalance_lock", + name="White balance lock", + icon="mdi:white-balance-auto", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("whitebalance_lock", True), + off_func=lambda ipcam: ipcam.change_setting("whitebalance_lock", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="video_recording", + name="Video recording", + icon="mdi:record-rec", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.record(activate=True), + off_func=lambda ipcam: ipcam.record(activate=False), + ), ) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the IP Webcam switch platform.""" - if discovery_info is None: - return + """Set up the IP Webcam switches from config entry.""" - host = discovery_info[CONF_HOST] - name = discovery_info[CONF_NAME] - switches = discovery_info[CONF_SWITCHES] - ipcam = hass.data[DATA_IP_WEBCAM][host] - - all_switches = [] - - for setting in switches: - all_switches.append(IPWebcamSettingsSwitch(name, host, ipcam, setting)) - - async_add_entities(all_switches, True) + coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + switch_types = [ + switch + for switch in SWITCH_TYPES + if switch.key in coordinator.cam.enabled_settings + ] + async_add_entities( + [ + IPWebcamSettingSwitch(coordinator, description) + for description in switch_types + ] + ) -class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchEntity): - """An abstract class for an IP Webcam setting.""" +class IPWebcamSettingSwitch(AndroidIPCamBaseEntity, SwitchEntity): + """Representation of a IP Webcam setting.""" - def __init__(self, name, host, ipcam, setting): - """Initialize the settings switch.""" - super().__init__(host, ipcam) + entity_description: AndroidIPWebcamSwitchEntityDescription - self._setting = setting - self._mapped_name = KEY_MAP.get(self._setting, self._setting) - self._attr_name = f"{name} {self._mapped_name}" - self._attr_is_on = False + def __init__( + self, + coordinator: AndroidIPCamDataUpdateCoordinator, + description: AndroidIPWebcamSwitchEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}" - async def async_update(self): - """Get the updated status of the switch.""" - self._attr_is_on = bool(self._ipcam.current_settings.get(self._setting)) + @property + def is_on(self) -> bool: + """Return if settings is on or off.""" + return bool(self.cam.current_settings.get(self.entity_description.key)) async def async_turn_on(self, **kwargs): """Turn device on.""" - if self._setting == "torch": - await self._ipcam.torch(activate=True) - elif self._setting == "focus": - await self._ipcam.focus(activate=True) - elif self._setting == "video_recording": - await self._ipcam.record(record=True) - else: - await self._ipcam.change_setting(self._setting, True) - self._attr_is_on = True - self.async_write_ha_state() + await self.entity_description.on_func(self.cam) + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): """Turn device off.""" - if self._setting == "torch": - await self._ipcam.torch(activate=False) - elif self._setting == "focus": - await self._ipcam.focus(activate=False) - elif self._setting == "video_recording": - await self._ipcam.record(record=False) - else: - await self._ipcam.change_setting(self._setting, False) - self._attr_is_on = False - self.async_write_ha_state() - - @property - def icon(self): - """Return the icon for the switch.""" - return ICON_MAP.get(self._setting, "mdi:flash") + await self.entity_description.off_func(self.cam) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/android_ip_webcam/translations/en.json b/homeassistant/components/android_ip_webcam/translations/en.json new file mode 100644 index 00000000000..43cd63356b4 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Android IP Webcam using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Android IP Webcam YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Android IP Webcamepush YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d9da1ff2057..a582f2b719c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,6 +28,7 @@ FLOWS = { "amberelectric", "ambiclimate", "ambient_station", + "android_ip_webcam", "androidtv", "anthemav", "apple_tv", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f612cf9526..c1c224f4812 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1018,6 +1018,9 @@ pydeconz==103 # homeassistant.components.dexcom pydexcom==0.2.3 +# homeassistant.components.android_ip_webcam +pydroid-ipcam==1.3.1 + # homeassistant.components.econet pyeconet==0.1.15 diff --git a/tests/components/android_ip_webcam/__init__.py b/tests/components/android_ip_webcam/__init__.py new file mode 100644 index 00000000000..331547929ea --- /dev/null +++ b/tests/components/android_ip_webcam/__init__.py @@ -0,0 +1 @@ +"""Tests for the Android IP Webcam integration.""" diff --git a/tests/components/android_ip_webcam/conftest.py b/tests/components/android_ip_webcam/conftest.py new file mode 100644 index 00000000000..83a040222f8 --- /dev/null +++ b/tests/components/android_ip_webcam/conftest.py @@ -0,0 +1,25 @@ +"""Fixtures for tests.""" +from http import HTTPStatus + +import pytest + +from homeassistant.const import CONTENT_TYPE_JSON + +from tests.common import load_fixture + + +@pytest.fixture +def aioclient_mock_fixture(aioclient_mock) -> None: + """Fixture to provide a aioclient mocker.""" + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + text=load_fixture("android_ip_webcam/status_data.json"), + status=HTTPStatus.OK, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.get( + "http://1.1.1.1:8080/sensors.json", + text=load_fixture("android_ip_webcam/sensor_data.json"), + status=HTTPStatus.OK, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) diff --git a/tests/components/android_ip_webcam/fixtures/sensor_data.json b/tests/components/android_ip_webcam/fixtures/sensor_data.json new file mode 100644 index 00000000000..341e8e13931 --- /dev/null +++ b/tests/components/android_ip_webcam/fixtures/sensor_data.json @@ -0,0 +1,57 @@ +{ + "light": { + "unit": "lx", + "data": [ + [1659602753873, [708.0]], + [1659602773708, [991.0]] + ] + }, + "proximity": { "unit": "cm", "data": [[1659602773709, [5.0]]] }, + "motion": { + "unit": "", + "data": [ + [1659602752922, [8.0]], + [1659602773709, [12.0]] + ] + }, + "motion_active": { "unit": "", "data": [[1659602773710, [0.0]]] }, + "battery_voltage": { + "unit": "V", + "data": [ + [1659602769351, [4.077]], + [1659602771352, [4.077]], + [1659602773353, [4.077]] + ] + }, + "battery_level": { + "unit": "%", + "data": [ + [1659602769351, [87.0]], + [1659602771352, [87.0]], + [1659602773353, [87.0]] + ] + }, + "battery_temp": { + "unit": "℃", + "data": [ + [1659602769351, [33.9]], + [1659602771352, [33.9]], + [1659602773353, [33.9]] + ] + }, + "sound": { + "unit": "dB", + "data": [ + [1659602768710, [181.0]], + [1659602769200, [191.0]], + [1659602769690, [186.0]], + [1659602770181, [186.0]], + [1659602770670, [188.0]], + [1659602771145, [192.0]], + [1659602771635, [192.0]], + [1659602772125, [179.0]], + [1659602772615, [186.0]], + [1659602773261, [178.0]] + ] + } +} diff --git a/tests/components/android_ip_webcam/fixtures/status_data.json b/tests/components/android_ip_webcam/fixtures/status_data.json new file mode 100644 index 00000000000..7d30b67c4ca --- /dev/null +++ b/tests/components/android_ip_webcam/fixtures/status_data.json @@ -0,0 +1,62 @@ +{ + "video_connections": 0, + "audio_connections": 0, + "video_status": { "result": "status", "enabled": "True", "mode": "none" }, + "curvals": { + "orientation": "landscape", + "idle": "off", + "audio_only": "off", + "overlay": "off", + "quality": "49", + "focus_homing": "off", + "ip_address": "192.168.3.88", + "motion_limit": "250", + "adet_limit": "200", + "night_vision": "off", + "night_vision_average": "2", + "night_vision_gain": "1.0", + " ": "off", + "motion_detect": "on", + "motion_display": "off", + "video_chunk_len": "60", + "gps_active": "off", + "video_size": "1920x1080", + "mirror_flip": "none", + "ffc": "off", + "rtsp_video_formats": "", + "rtsp_audio_formats": "", + "video_connections": "0", + "audio_connections": "0", + "ivideon_streaming": "off", + "zoom": "100", + "crop_x": "50", + "crop_y": "50", + "coloreffect": "none", + "scenemode": "auto", + "focusmode": "continuous-video", + "whitebalance": "auto", + "flashmode": "off", + "antibanding": "off", + "torch": "off", + "focus_distance": "0.0", + "focal_length": "4.25", + "aperture": "1.7", + "filter_density": "0.0", + "exposure_ns": "9384", + "frame_duration": "33333333", + "iso": "100", + "manual_sensor": "off", + "photo_size": "1920x1080", + "photo_rotation": "-1" + }, + "idle": ["on", "off"], + "audio_only": ["on", "off"], + "overlay": ["on", "off"], + "focus_homing": ["on", "off"], + "night_vision": ["on", "off"], + "motion_detect": ["on", "off"], + "motion_display": ["on", "off"], + "gps_active": ["on", "off"], + "ffc": ["on", "off"], + "torch": ["on", "off"] +} diff --git a/tests/components/android_ip_webcam/test_config_flow.py b/tests/components/android_ip_webcam/test_config_flow.py new file mode 100644 index 00000000000..1ede523ecd2 --- /dev/null +++ b/tests/components/android_ip_webcam/test_config_flow.py @@ -0,0 +1,122 @@ +"""Test the Android IP Webcam config flow.""" +from datetime import timedelta +from unittest.mock import patch + +import aiohttp + +from homeassistant import config_entries +from homeassistant.components.android_ip_webcam.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .test_init import MOCK_CONFIG_DATA + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.android_ip_webcam.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, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "port": 8080, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_success(hass: HomeAssistant, aioclient_mock_fixture) -> None: + """Test a successful import of yaml.""" + with patch( + "homeassistant.components.android_ip_webcam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "name": "IP Webcam", + "host": "1.1.1.1", + "port": 8080, + "timeout": 10, + "scan_interval": timedelta(seconds=30), + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IP Webcam" + assert result2["data"] == { + "name": "IP Webcam", + "host": "1.1.1.1", + "port": 8080, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_device_already_configured( + hass: HomeAssistant, aioclient_mock_fixture +) -> None: + """Test aborting if the device is already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_cannot_connect( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientError, + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/android_ip_webcam/test_init.py b/tests/components/android_ip_webcam/test_init.py new file mode 100644 index 00000000000..e0c21445d71 --- /dev/null +++ b/tests/components/android_ip_webcam/test_init.py @@ -0,0 +1,82 @@ +"""Tests for the Android IP Webcam integration.""" + + +from collections.abc import Awaitable +from typing import Callable + +import aiohttp + +from homeassistant.components.android_ip_webcam.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.repairs import get_repairs +from tests.test_util.aiohttp import AiohttpClientMocker + +MOCK_CONFIG_DATA = { + "name": "IP Webcam", + "host": "1.1.1.1", + "port": 8080, +} + + +async def test_setup( + hass: HomeAssistant, + aioclient_mock_fixture, + hass_ws_client: Callable[ + [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] + ], +) -> None: + """Test integration failed due to an error.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: [MOCK_CONFIG_DATA]}) + assert hass.config_entries.async_entries(DOMAIN) + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "deprecated_yaml" + + +async def test_successful_config_entry( + hass: HomeAssistant, aioclient_mock_fixture +) -> None: + """Test settings up integration from config entry.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.LOADED + + +async def test_setup_failed( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test integration failed due to an error.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientError, + ) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass: HomeAssistant, aioclient_mock_fixture) -> None: + """Test removing integration.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert entry.entry_id not in hass.data[DOMAIN] From 46a8f191970cc40cb144038c184a07a16e7fa899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 9 Aug 2022 15:24:53 +0200 Subject: [PATCH 246/903] Update aioqsw to v0.2.0 (#76509) --- .../components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/qnap_qsw/test_coordinator.py | 12 + tests/components/qnap_qsw/util.py | 335 ++++++++++++++++++ 5 files changed, 350 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 83c9423f0f4..690297d69bc 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -3,7 +3,7 @@ "name": "QNAP QSW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", - "requirements": ["aioqsw==0.1.1"], + "requirements": ["aioqsw==0.2.0"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioqsw"], diff --git a/requirements_all.txt b/requirements_all.txt index aed97e37a0e..f47af097bcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -232,7 +232,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.1 +aioqsw==0.2.0 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1c224f4812..56019b553a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -207,7 +207,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.1 +aioqsw==0.2.0 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index 107cfa580b7..125b333c8d6 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -18,6 +18,8 @@ from .util import ( FIRMWARE_CONDITION_MOCK, FIRMWARE_INFO_MOCK, FIRMWARE_UPDATE_CHECK_MOCK, + PORTS_STATISTICS_MOCK, + PORTS_STATUS_MOCK, SYSTEM_BOARD_MOCK, SYSTEM_SENSOR_MOCK, SYSTEM_TIME_MOCK, @@ -44,6 +46,12 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", return_value=FIRMWARE_UPDATE_CHECK_MOCK, ) as mock_firmware_update_check, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", + return_value=PORTS_STATISTICS_MOCK, + ) as mock_ports_statistics, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_status", + return_value=PORTS_STATUS_MOCK, + ) as mock_ports_status, patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, ) as mock_system_board, patch( @@ -65,6 +73,8 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.assert_called_once() mock_firmware_info.assert_called_once() mock_firmware_update_check.assert_called_once() + mock_ports_statistics.assert_called_once() + mock_ports_status.assert_called_once() mock_system_board.assert_called_once() mock_system_sensor.assert_called_once() mock_system_time.assert_called_once() @@ -74,6 +84,8 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.reset_mock() mock_firmware_info.reset_mock() mock_firmware_update_check.reset_mock() + mock_ports_statistics.reset_mock() + mock_ports_status.reset_mock() mock_system_board.reset_mock() mock_system_sensor.reset_mock() mock_system_time.reset_mock() diff --git a/tests/components/qnap_qsw/util.py b/tests/components/qnap_qsw/util.py index a057dfbe3ac..d3a62d413fa 100644 --- a/tests/components/qnap_qsw/util.py +++ b/tests/components/qnap_qsw/util.py @@ -18,6 +18,10 @@ from aioqsw.const import ( API_ERROR_MESSAGE, API_FAN1_SPEED, API_FAN2_SPEED, + API_FCS_ERRORS, + API_FULL_DUPLEX, + API_KEY, + API_LINK, API_MAC_ADDR, API_MAX_SWITCH_TEMP, API_MESSAGE, @@ -28,10 +32,15 @@ from aioqsw.const import ( API_PRODUCT, API_PUB_DATE, API_RESULT, + API_RX_ERRORS, + API_RX_OCTETS, API_SERIAL, + API_SPEED, API_SWITCH_TEMP, API_TRUNK_NUM, + API_TX_OCTETS, API_UPTIME, + API_VAL, API_VERSION, ) @@ -111,6 +120,326 @@ FIRMWARE_UPDATE_CHECK_MOCK = { }, } +PORTS_STATISTICS_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: [ + { + API_KEY: "1", + API_VAL: { + API_RX_OCTETS: 20000, + API_RX_ERRORS: 20, + API_TX_OCTETS: 10000, + API_FCS_ERRORS: 10, + }, + }, + { + API_KEY: "2", + API_VAL: { + API_RX_OCTETS: 2000, + API_RX_ERRORS: 2, + API_TX_OCTETS: 1000, + API_FCS_ERRORS: 1, + }, + }, + { + API_KEY: "3", + API_VAL: { + API_RX_OCTETS: 200, + API_RX_ERRORS: 0, + API_TX_OCTETS: 100, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "4", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "5", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "6", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "7", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "8", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "9", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "10", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "11", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "12", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "29", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "30", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "31", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "32", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "33", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "34", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + ], +} + +PORTS_STATUS_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: [ + { + API_KEY: "1", + API_VAL: { + API_LINK: True, + API_FULL_DUPLEX: True, + API_SPEED: "10000", + }, + }, + { + API_KEY: "2", + API_VAL: { + API_LINK: True, + API_FULL_DUPLEX: True, + API_SPEED: "1000", + }, + }, + { + API_KEY: "3", + API_VAL: { + API_LINK: True, + API_FULL_DUPLEX: False, + API_SPEED: "100", + }, + }, + { + API_KEY: "4", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "5", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "6", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "7", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "8", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "9", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "10", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "11", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "12", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "29", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "30", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "31", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "32", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "33", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "34", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + ], +} + SYSTEM_COMMAND_MOCK = { API_ERROR_CODE: 200, API_ERROR_MESSAGE: "OK", @@ -170,6 +499,12 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", return_value=FIRMWARE_UPDATE_CHECK_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", + return_value=PORTS_STATISTICS_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_status", + return_value=PORTS_STATUS_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, From a0ceb38f5f38b2bbe471e15e2ffcf8f9270da09d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 03:35:08 -1000 Subject: [PATCH 247/903] Bump govee-ble to 0.14.0 to fix H5052 sensors (#76497) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index eb33df867e1..2ba97b95d08 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -28,7 +28,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.7"], + "requirements": ["govee-ble==0.14.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index f47af097bcc..cde7071920d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.7 +govee-ble==0.14.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56019b553a3..e5cef1cf7f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.7 +govee-ble==0.14.0 # homeassistant.components.gree greeclimate==1.3.0 From 4e3db5bb5ce8cb5e1e814a06c13907b84942e187 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 9 Aug 2022 15:35:22 +0200 Subject: [PATCH 248/903] Update sqlalchemy to 1.4.40 (#76505) --- 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 f0c1f81689a..9486d2eaf1e 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.4.38", "fnvhash==0.1.0"], + "requirements": ["sqlalchemy==1.4.40", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 0e6c68071cc..3e3faaa7372 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.38"], + "requirements": ["sqlalchemy==1.4.40"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f15daf093a3..b7c2fd8cbb5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 scapy==2.4.5 -sqlalchemy==1.4.38 +sqlalchemy==1.4.40 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index cde7071920d..93120e76d30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2243,7 +2243,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.38 +sqlalchemy==1.4.40 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5cef1cf7f8..c15cb4fc568 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1522,7 +1522,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.38 +sqlalchemy==1.4.40 # homeassistant.components.srp_energy srpenergy==1.3.6 From 2b2ea3dd73bfeb49bf50cf5cd16645221606e057 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 9 Aug 2022 15:35:38 +0200 Subject: [PATCH 249/903] Update flake8-noqa to 1.2.8 (#76506) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4867f712b8e..cb9a5ed04dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - flake8-comprehensions==3.10.0 - - flake8-noqa==1.2.5 + - flake8-noqa==1.2.8 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a6cb45b22b2..115e826537a 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -5,7 +5,7 @@ black==22.6.0 codespell==2.1.0 flake8-comprehensions==3.10.0 flake8-docstrings==1.6.0 -flake8-noqa==1.2.5 +flake8-noqa==1.2.8 flake8==4.0.1 isort==5.10.1 mccabe==0.6.1 From 891158f332cccca2942f55037df983bb22641d65 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 9 Aug 2022 22:41:19 +0800 Subject: [PATCH 250/903] Use stream to generate fallback image for onvif (#75584) --- homeassistant/components/onvif/camera.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 5aa49f68aa6..528b9605bbc 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -136,13 +136,16 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - image = None + + if self.stream and self.stream.keepalive: + return await self.stream.async_get_image(width, height) if self.device.capabilities.snapshot: try: image = await self.device.device.get_snapshot( self.profile.token, self._basic_auth ) + return image except ONVIFError as err: LOGGER.error( "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", @@ -150,17 +153,14 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): err, ) - if image is None: - assert self._stream_uri - return await ffmpeg.async_get_image( - self.hass, - self._stream_uri, - extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS), - width=width, - height=height, - ) - - return image + assert self._stream_uri + return await ffmpeg.async_get_image( + self.hass, + self._stream_uri, + extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS), + width=width, + height=height, + ) async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" From 6bae03c14b17b929d43fc682fdf7856e7da14432 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 04:49:17 -1000 Subject: [PATCH 251/903] Fix inkbird ibbq2s that identify with xbbq (#76492) --- homeassistant/components/inkbird/manifest.json | 3 ++- homeassistant/generated/bluetooth.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index 686c9bada2d..b0ef08143c2 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -7,9 +7,10 @@ { "local_name": "sps" }, { "local_name": "Inkbird*" }, { "local_name": "iBBQ*" }, + { "local_name": "xBBQ*" }, { "local_name": "tps" } ], - "requirements": ["inkbird-ble==0.5.1"], + "requirements": ["inkbird-ble==0.5.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index d704d00ab8a..15a822599c6 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -75,6 +75,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "inkbird", "local_name": "iBBQ*" }, + { + "domain": "inkbird", + "local_name": "xBBQ*" + }, { "domain": "inkbird", "local_name": "tps" diff --git a/requirements_all.txt b/requirements_all.txt index 93120e76d30..5eca3255533 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -902,7 +902,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.1 +inkbird-ble==0.5.2 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c15cb4fc568..4d9e3611c4d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.1 +inkbird-ble==0.5.2 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 From 929eeac1e4cfc4d6d7d7d9ff6a13a63212f9660e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 04:53:21 -1000 Subject: [PATCH 252/903] Add support for Govee 5184 BBQ sensors (#76490) --- homeassistant/components/govee_ble/manifest.json | 4 ++++ homeassistant/generated/bluetooth.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 2ba97b95d08..e8df2af3abb 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -7,6 +7,10 @@ { "local_name": "Govee*" }, { "local_name": "GVH5*" }, { "local_name": "B5178*" }, + { + "manufacturer_id": 6966, + "service_uuid": "00008451-0000-1000-8000-00805f9b34fb" + }, { "manufacturer_id": 26589, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 15a822599c6..d7af6e6ee11 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -31,6 +31,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "govee_ble", "local_name": "B5178*" }, + { + "domain": "govee_ble", + "manufacturer_id": 6966, + "service_uuid": "00008451-0000-1000-8000-00805f9b34fb" + }, { "domain": "govee_ble", "manufacturer_id": 26589, From b19ace912469b076a2527f25cefab404edfcc6e9 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 9 Aug 2022 16:54:07 +0200 Subject: [PATCH 253/903] Use constructor instead of factory method for sensors in here_travel_time (#76471) --- .../components/here_travel_time/sensor.py | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index c4be60d5569..e7bc88f2210 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -153,38 +153,6 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] ) -def create_origin_sensor( - config_entry: ConfigEntry, hass: HomeAssistant -) -> OriginSensor: - """Create a origin sensor.""" - return OriginSensor( - config_entry.entry_id, - config_entry.data[CONF_NAME], - SensorEntityDescription( - name="Origin", - icon="mdi:store-marker", - key=ATTR_ORIGIN_NAME, - ), - hass.data[DOMAIN][config_entry.entry_id], - ) - - -def create_destination_sensor( - config_entry: ConfigEntry, hass: HomeAssistant -) -> DestinationSensor: - """Create a destination sensor.""" - return DestinationSensor( - config_entry.entry_id, - config_entry.data[CONF_NAME], - SensorEntityDescription( - name="Destination", - icon="mdi:store-marker", - key=ATTR_DESTINATION_NAME, - ), - hass.data[DOMAIN][config_entry.entry_id], - ) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -214,18 +182,22 @@ async def async_setup_entry( ) -> None: """Add HERE travel time entities from a config_entry.""" + entry_id = config_entry.entry_id + name = config_entry.data[CONF_NAME] + coordinator = hass.data[DOMAIN][entry_id] + sensors: list[HERETravelTimeSensor] = [] for sensor_description in sensor_descriptions(config_entry.data[CONF_MODE]): sensors.append( HERETravelTimeSensor( - config_entry.entry_id, - config_entry.data[CONF_NAME], + entry_id, + name, sensor_description, - hass.data[DOMAIN][config_entry.entry_id], + coordinator, ) ) - sensors.append(create_origin_sensor(config_entry, hass)) - sensors.append(create_destination_sensor(config_entry, hass)) + sensors.append(OriginSensor(entry_id, name, coordinator)) + sensors.append(DestinationSensor(entry_id, name, coordinator)) async_add_entities(sensors) @@ -278,6 +250,20 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): class OriginSensor(HERETravelTimeSensor): """Sensor holding information about the route origin.""" + def __init__( + self, + unique_id_prefix: str, + name: str, + coordinator: HereTravelTimeDataUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + sensor_description = SensorEntityDescription( + name="Origin", + icon="mdi:store-marker", + key=ATTR_ORIGIN_NAME, + ) + super().__init__(unique_id_prefix, name, sensor_description, coordinator) + @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """GPS coordinates.""" @@ -292,6 +278,20 @@ class OriginSensor(HERETravelTimeSensor): class DestinationSensor(HERETravelTimeSensor): """Sensor holding information about the route destination.""" + def __init__( + self, + unique_id_prefix: str, + name: str, + coordinator: HereTravelTimeDataUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + sensor_description = SensorEntityDescription( + name="Destination", + icon="mdi:store-marker", + key=ATTR_DESTINATION_NAME, + ) + super().__init__(unique_id_prefix, name, sensor_description, coordinator) + @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """GPS coordinates.""" From bcdf880364faa93cdd94c10f08b84f8887cc87a3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Aug 2022 16:55:52 +0200 Subject: [PATCH 254/903] Add siren checks to pylint plugin (#76460) --- pylint/plugins/hass_enforce_type_hints.py | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 527c9358971..f7a109507d8 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1477,6 +1477,58 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "select": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="SelectEntity", + matches=[ + TypeHintMatch( + function_name="capability_attributes", + return_type="dict[str, Any]", + ), + TypeHintMatch( + function_name="options", + return_type="list[str]", + ), + TypeHintMatch( + function_name="current_option", + return_type=["str", None], + ), + TypeHintMatch( + function_name="select_option", + return_type=None, + ), + TypeHintMatch( + function_name="select_option", + arg_types={1: "str"}, + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], + "siren": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="SirenEntity", + matches=[ + TypeHintMatch( + function_name="available_tones", + return_type=["dict[int, str]", "list[int | str]", None], + ), + ], + ), + ], } From bd795be0e985d0a6e907a61ea6f920cda42c5010 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Aug 2022 16:56:15 +0200 Subject: [PATCH 255/903] Cleanup device_class checks in pylint plugin (#76458) --- pylint/plugins/hass_enforce_type_hints.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index f7a109507d8..257f4fb3613 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -611,10 +611,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ClassTypeHintMatch( base_class="AlarmControlPanelEntity", matches=[ - TypeHintMatch( - function_name="device_class", - return_type=["str", None], - ), TypeHintMatch( function_name="code_format", return_type=["CodeFormat", None], @@ -1220,10 +1216,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ClassTypeHintMatch( base_class="FanEntity", matches=[ - TypeHintMatch( - function_name="device_class", - return_type=["str", None], - ), TypeHintMatch( function_name="percentage", return_type=["int", None], @@ -1428,10 +1420,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ClassTypeHintMatch( base_class="LockEntity", matches=[ - TypeHintMatch( - function_name="device_class", - return_type=["str", None], - ), TypeHintMatch( function_name="changed_by", return_type=["str", None], From 753a3c0921d50d7778d9bb184fe4e957e3dbf412 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 9 Aug 2022 17:45:48 +0200 Subject: [PATCH 256/903] Add new sensors to NextDNS integration (#76262) * Add DNS-over-HTTP/3 sensors * Update tests --- homeassistant/components/nextdns/sensor.py | 22 ++++++++++ tests/components/nextdns/__init__.py | 1 + tests/components/nextdns/test_diagnostics.py | 12 +++--- tests/components/nextdns/test_sensor.py | 42 ++++++++++++++++++-- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 422bc2a237b..62cd671835b 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -107,6 +107,17 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL, value=lambda data: data.doh_queries, ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="doh3_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="DNS-over-HTTP/3 queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.doh3_queries, + ), NextDnsSensorEntityDescription[AnalyticsProtocols]( key="dot_queries", coordinator_type=ATTR_PROTOCOLS, @@ -162,6 +173,17 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries_ratio, ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="doh3_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_registry_enabled_default=False, + icon="mdi:dns", + entity_category=EntityCategory.DIAGNOSTIC, + name="DNS-over-HTTP/3 queries ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doh3_queries_ratio, + ), NextDnsSensorEntityDescription[AnalyticsProtocols]( key="dot_queries_ratio", coordinator_type=ATTR_PROTOCOLS, diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 32b8bed76fa..04d838f8f58 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -25,6 +25,7 @@ ENCRYPTION = AnalyticsEncryption(encrypted_queries=60, unencrypted_queries=40) IP_VERSIONS = AnalyticsIpVersions(ipv4_queries=90, ipv6_queries=10) PROTOCOLS = AnalyticsProtocols( doh_queries=20, + doh3_queries=15, doq_queries=10, dot_queries=30, tcp_queries=0, diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index ec4ffa10aaf..21c030274cc 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -52,17 +52,17 @@ async def test_entry_diagnostics( } assert result["protocols_coordinator_data"] == { "doh_queries": 20, - "doh3_queries": 0, + "doh3_queries": 15, "doq_queries": 10, "dot_queries": 30, "tcp_queries": 0, "udp_queries": 40, - "doh_queries_ratio": 20.0, - "doh3_queries_ratio": 0.0, - "doq_queries_ratio": 10.0, - "dot_queries_ratio": 30.0, + "doh_queries_ratio": 17.4, + "doh3_queries_ratio": 13.0, + "doq_queries_ratio": 8.7, + "dot_queries_ratio": 26.1, "tcp_queries_ratio": 0.0, - "udp_queries_ratio": 40.0, + "udp_queries_ratio": 34.8, } assert result["settings_coordinator_data"] == settings assert result["status_coordinator_data"] == { diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index a90999a592b..c3c7577bd83 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -31,6 +31,13 @@ async def test_sensor(hass: HomeAssistant) -> None: suggested_object_id="fake_profile_dns_over_https_queries", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh3_queries", + suggested_object_id="fake_profile_dns_over_http_3_queries", + disabled_by=None, + ) registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, @@ -38,6 +45,13 @@ async def test_sensor(hass: HomeAssistant) -> None: suggested_object_id="fake_profile_dns_over_https_queries_ratio", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh3_queries_ratio", + suggested_object_id="fake_profile_dns_over_http_3_queries_ratio", + disabled_by=None, + ) registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, @@ -212,7 +226,7 @@ async def test_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.fake_profile_dns_over_https_queries_ratio") assert state - assert state.state == "20.0" + assert state.state == "17.4" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -220,6 +234,26 @@ async def test_sensor(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "xyz12_doh_queries_ratio" + state = hass.states.get("sensor.fake_profile_dns_over_http_3_queries") + assert state + assert state.state == "15" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_http_3_queries") + assert entry + assert entry.unique_id == "xyz12_doh3_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_http_3_queries_ratio") + assert state + assert state.state == "13.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_http_3_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_doh3_queries_ratio" + state = hass.states.get("sensor.fake_profile_dns_over_quic_queries") assert state assert state.state == "10" @@ -232,7 +266,7 @@ async def test_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.fake_profile_dns_over_quic_queries_ratio") assert state - assert state.state == "10.0" + assert state.state == "8.7" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -252,7 +286,7 @@ async def test_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.fake_profile_dns_over_tls_queries_ratio") assert state - assert state.state == "30.0" + assert state.state == "26.1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -382,7 +416,7 @@ async def test_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.fake_profile_udp_queries_ratio") assert state - assert state.state == "40.0" + assert state.state == "34.8" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE From 6eb1dbdb74a578a2872bc91450d141c8c1fc3e6d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 9 Aug 2022 17:51:04 +0200 Subject: [PATCH 257/903] Add NextDNS binary sensor platform (#75266) * Add binary_sensor platform * Add tests * Add quality scale * Sort coordinators * Remove quality scale * Fix docstring --- homeassistant/components/nextdns/__init__.py | 14 ++- .../components/nextdns/binary_sensor.py | 103 ++++++++++++++++++ homeassistant/components/nextdns/const.py | 2 + tests/components/nextdns/__init__.py | 5 + .../components/nextdns/test_binary_sensor.py | 86 +++++++++++++++ 5 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nextdns/binary_sensor.py create mode 100644 tests/components/nextdns/test_binary_sensor.py diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 2f68abee847..a92186f6f14 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -15,6 +15,7 @@ from nextdns import ( AnalyticsProtocols, AnalyticsStatus, ApiError, + ConnectionStatus, InvalidApiKeyError, NextDns, Settings, @@ -31,6 +32,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + ATTR_CONNECTION, ATTR_DNSSEC, ATTR_ENCRYPTION, ATTR_IP_VERSIONS, @@ -40,6 +42,7 @@ from .const import ( CONF_PROFILE_ID, DOMAIN, UPDATE_INTERVAL_ANALYTICS, + UPDATE_INTERVAL_CONNECTION, UPDATE_INTERVAL_SETTINGS, ) @@ -131,10 +134,19 @@ class NextDnsSettingsUpdateCoordinator(NextDnsUpdateCoordinator[Settings]): return await self.nextdns.get_settings(self.profile_id) +class NextDnsConnectionUpdateCoordinator(NextDnsUpdateCoordinator[ConnectionStatus]): + """Class to manage fetching NextDNS connection data from API.""" + + async def _async_update_data_internal(self) -> ConnectionStatus: + """Update data via library.""" + return await self.nextdns.connection_status(self.profile_id) + + _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] COORDINATORS = [ + (ATTR_CONNECTION, NextDnsConnectionUpdateCoordinator, UPDATE_INTERVAL_CONNECTION), (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_IP_VERSIONS, NextDnsIpVersionsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), diff --git a/homeassistant/components/nextdns/binary_sensor.py b/homeassistant/components/nextdns/binary_sensor.py new file mode 100644 index 00000000000..af80d14a89b --- /dev/null +++ b/homeassistant/components/nextdns/binary_sensor.py @@ -0,0 +1,103 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic + +from nextdns import ConnectionStatus + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsConnectionUpdateCoordinator, TCoordinatorData +from .const import ATTR_CONNECTION, DOMAIN + +PARALLEL_UPDATES = 1 + + +@dataclass +class NextDnsBinarySensorRequiredKeysMixin(Generic[TCoordinatorData]): + """Mixin for required keys.""" + + state: Callable[[TCoordinatorData, str], bool] + + +@dataclass +class NextDnsBinarySensorEntityDescription( + BinarySensorEntityDescription, + NextDnsBinarySensorRequiredKeysMixin[TCoordinatorData], +): + """NextDNS binary sensor entity description.""" + + +SENSORS = ( + NextDnsBinarySensorEntityDescription[ConnectionStatus]( + key="this_device_nextdns_connection_status", + entity_category=EntityCategory.DIAGNOSTIC, + name="This device NextDNS connection status", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + state=lambda data, _: data.connected, + ), + NextDnsBinarySensorEntityDescription[ConnectionStatus]( + key="this_device_profile_connection_status", + entity_category=EntityCategory.DIAGNOSTIC, + name="This device profile connection status", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + state=lambda data, profile_id: profile_id == data.profile_id, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add NextDNS entities from a config_entry.""" + coordinator: NextDnsConnectionUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + ATTR_CONNECTION + ] + + sensors: list[NextDnsBinarySensor] = [] + for description in SENSORS: + sensors.append(NextDnsBinarySensor(coordinator, description)) + + async_add_entities(sensors) + + +class NextDnsBinarySensor( + CoordinatorEntity[NextDnsConnectionUpdateCoordinator], BinarySensorEntity +): + """Define an NextDNS binary sensor.""" + + _attr_has_entity_name = True + entity_description: NextDnsBinarySensorEntityDescription + + def __init__( + self, + coordinator: NextDnsConnectionUpdateCoordinator, + description: NextDnsBinarySensorEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_is_on = description.state(coordinator.data, coordinator.profile_id) + self.entity_description = description + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = self.entity_description.state( + self.coordinator.data, self.coordinator.profile_id + ) + self.async_write_ha_state() diff --git a/homeassistant/components/nextdns/const.py b/homeassistant/components/nextdns/const.py index d455dd79635..8cac556c87c 100644 --- a/homeassistant/components/nextdns/const.py +++ b/homeassistant/components/nextdns/const.py @@ -1,6 +1,7 @@ """Constants for NextDNS integration.""" from datetime import timedelta +ATTR_CONNECTION = "connection" ATTR_DNSSEC = "dnssec" ATTR_ENCRYPTION = "encryption" ATTR_IP_VERSIONS = "ip_versions" @@ -11,6 +12,7 @@ ATTR_STATUS = "status" CONF_PROFILE_ID = "profile_id" CONF_PROFILE_NAME = "profile_name" +UPDATE_INTERVAL_CONNECTION = timedelta(minutes=5) UPDATE_INTERVAL_ANALYTICS = timedelta(minutes=10) UPDATE_INTERVAL_SETTINGS = timedelta(minutes=1) diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 04d838f8f58..8c80db1fdff 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -7,6 +7,7 @@ from nextdns import ( AnalyticsIpVersions, AnalyticsProtocols, AnalyticsStatus, + ConnectionStatus, Settings, ) @@ -16,6 +17,7 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +CONNECTION_STATUS = ConnectionStatus(connected=True, profile_id="abcdef") PROFILES = [{"id": "xyz12", "fingerprint": "aabbccdd123", "name": "Fake Profile"}] STATUS = AnalyticsStatus( default_queries=40, allowed_queries=30, blocked_queries=20, relayed_queries=10 @@ -129,6 +131,9 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: ), patch( "homeassistant.components.nextdns.NextDns.get_settings", return_value=SETTINGS, + ), patch( + "homeassistant.components.nextdns.NextDns.connection_status", + return_value=CONNECTION_STATUS, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nextdns/test_binary_sensor.py b/tests/components/nextdns/test_binary_sensor.py new file mode 100644 index 00000000000..eb1478a5809 --- /dev/null +++ b/tests/components/nextdns/test_binary_sensor.py @@ -0,0 +1,86 @@ +"""Test binary sensor of NextDNS integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import CONNECTION_STATUS, init_integration + +from tests.common import async_fire_time_changed + + +async def test_binary_Sensor(hass: HomeAssistant) -> None: + """Test states of the binary sensors.""" + registry = er.async_get(hass) + + await init_integration(hass) + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert state + assert state.state == STATE_ON + + entry = registry.async_get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert entry + assert entry.unique_id == "xyz12_this_device_nextdns_connection_status" + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_profile_connection_status" + ) + assert state + assert state.state == STATE_OFF + + entry = registry.async_get( + "binary_sensor.fake_profile_this_device_profile_connection_status" + ) + assert entry + assert entry.unique_id == "xyz12_this_device_profile_connection_status" + + +async def test_availability(hass: HomeAssistant) -> None: + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + await init_integration(hass) + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON + + future = utcnow() + timedelta(minutes=10) + with patch( + "homeassistant.components.nextdns.NextDns.connection_status", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=20) + with patch( + "homeassistant.components.nextdns.NextDns.connection_status", + return_value=CONNECTION_STATUS, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON From 7d0a4ee00a5af7bbb1ecc22a031285557e9be76b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Aug 2022 17:54:33 +0200 Subject: [PATCH 258/903] Improve type hints in rfxtrx siren entity (#76459) --- homeassistant/components/rfxtrx/siren.py | 45 ++++++++++++++++-------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py index 282933f7f85..acf06518959 100644 --- a/homeassistant/components/rfxtrx/siren.py +++ b/homeassistant/components/rfxtrx/siren.py @@ -1,6 +1,7 @@ """Support for RFXtrx sirens.""" from __future__ import annotations +from datetime import datetime from typing import Any import RFXtrx as rfxtrxmod @@ -26,7 +27,7 @@ SECURITY_PANIC_OFF = "End Panic" SECURITY_PANIC_ALL = {SECURITY_PANIC_ON, SECURITY_PANIC_OFF} -def supported(event: rfxtrxmod.RFXtrxEvent): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports sirens.""" device = event.device @@ -104,16 +105,16 @@ class RfxtrxOffDelayMixin(Entity): _timeout: CALLBACK_TYPE | None = None _off_delay: float | None = None - def _setup_timeout(self): + def _setup_timeout(self) -> None: @callback - def _done(_): + def _done(_: datetime) -> None: self._timeout = None self.async_write_ha_state() if self._off_delay: self._timeout = async_call_later(self.hass, self._off_delay, _done) - def _cancel_timeout(self): + def _cancel_timeout(self) -> None: if self._timeout: self._timeout() self._timeout = None @@ -125,7 +126,13 @@ class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES _device: rfxtrxmod.ChimeDevice - def __init__(self, device, device_id, off_delay=None, event=None): + def __init__( + self, + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + off_delay: float | None = None, + event: rfxtrxmod.RFXtrxEvent | None = None, + ) -> None: """Initialize the entity.""" super().__init__(device, device_id, event) self._attr_available_tones = list(self._device.COMMANDS.values()) @@ -133,11 +140,11 @@ class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): self._off_delay = off_delay @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._timeout is not None - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._cancel_timeout() @@ -152,7 +159,7 @@ class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): self.async_write_ha_state() - def _apply_event(self, event: rfxtrxmod.ControlEvent): + def _apply_event(self, event: rfxtrxmod.ControlEvent) -> None: """Apply a received event.""" super()._apply_event(event) @@ -162,7 +169,9 @@ class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): self._setup_timeout() @callback - def _handle_event(self, event, device_id): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if self._event_applies(event, device_id): self._apply_event(event) @@ -176,7 +185,13 @@ class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin) _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF _device: rfxtrxmod.SecurityDevice - def __init__(self, device, device_id, off_delay=None, event=None): + def __init__( + self, + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + off_delay: float | None = None, + event: rfxtrxmod.RFXtrxEvent | None = None, + ) -> None: """Initialize the entity.""" super().__init__(device, device_id, event) self._on_value = get_first_key(self._device.STATUS, SECURITY_PANIC_ON) @@ -184,11 +199,11 @@ class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin) self._off_delay = off_delay @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._timeout is not None - async def async_turn_on(self, **kwargs: Any): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._cancel_timeout() @@ -206,7 +221,7 @@ class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin) self.async_write_ha_state() - def _apply_event(self, event: rfxtrxmod.SensorEvent): + def _apply_event(self, event: rfxtrxmod.SensorEvent) -> None: """Apply a received event.""" super()._apply_event(event) @@ -219,7 +234,9 @@ class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin) self._cancel_timeout() @callback - def _handle_event(self, event, device_id): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if self._event_applies(event, device_id): self._apply_event(event) From 38c57944fa1353e6bb1f0e3ba3187b86010f1e9d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Aug 2022 18:32:36 +0200 Subject: [PATCH 259/903] Improve type hints in zha number entity (#76468) --- homeassistant/components/zha/number.py | 30 +++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 4252bf0e14c..14967c1b91c 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -278,12 +278,18 @@ async def async_setup_entry( class ZhaNumber(ZhaEntity, NumberEntity): """Representation of a ZHA Number entity.""" - def __init__(self, unique_id, zha_device, channels, **kwargs): + def __init__( + self, + unique_id: str, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs: Any, + ) -> None: """Init this entity.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._analog_output_channel = self.cluster_channels.get(CHANNEL_ANALOG_OUTPUT) + self._analog_output_channel = self.cluster_channels[CHANNEL_ANALOG_OUTPUT] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -291,12 +297,12 @@ class ZhaNumber(ZhaEntity, NumberEntity): ) @property - def native_value(self): + def native_value(self) -> float | None: """Return the current value.""" return self._analog_output_channel.present_value @property - def native_min_value(self): + def native_min_value(self) -> float: """Return the minimum value.""" min_present_value = self._analog_output_channel.min_present_value if min_present_value is not None: @@ -304,7 +310,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return 0 @property - def native_max_value(self): + def native_max_value(self) -> float: """Return the maximum value.""" max_present_value = self._analog_output_channel.max_present_value if max_present_value is not None: @@ -312,7 +318,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return 1023 @property - def native_step(self): + def native_step(self) -> float | None: """Return the value step.""" resolution = self._analog_output_channel.resolution if resolution is not None: @@ -320,7 +326,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return super().native_step @property - def name(self): + def name(self) -> str: """Return the name of the number entity.""" description = self._analog_output_channel.description if description is not None and len(description) > 0: @@ -328,7 +334,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return super().name @property - def icon(self): + def icon(self) -> str | None: """Return the icon to be used for this entity.""" application_type = self._analog_output_channel.application_type if application_type is not None: @@ -336,7 +342,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return super().icon @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" engineering_units = self._analog_output_channel.engineering_units return UNITS.get(engineering_units) @@ -346,13 +352,13 @@ class ZhaNumber(ZhaEntity, NumberEntity): """Handle value update from channel.""" self.async_write_ha_state() - async def async_set_native_value(self, value): + async def async_set_native_value(self, value: float) -> None: """Update the current value from HA.""" num_value = float(value) if await self._analog_output_channel.async_set_present_value(num_value): self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Attempt to retrieve the state of the entity.""" await super().async_update() _LOGGER.debug("polling current state") From 23fc15115076dbf592f75454e77cd051a9e6dcf0 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 9 Aug 2022 14:41:02 -0400 Subject: [PATCH 260/903] Bump version of pyunifiprotect to 4.0.13 (#76523) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ad18be3dba9..246a4643412 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.12", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.0.13", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 5eca3255533..a779d2ada82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2013,7 +2013,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.12 +pyunifiprotect==4.0.13 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d9e3611c4d..142247940a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1370,7 +1370,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.12 +pyunifiprotect==4.0.13 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From ad361b8fc227f5636030ae40ab7c8453d3449f4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 09:48:48 -1000 Subject: [PATCH 261/903] Fix pairing with HK accessories that do not provide format for vendor chars (#76502) --- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/schlage_sense.json | 356 ++++++++++++++++++ .../specific_devices/test_schlage_sense.py | 40 ++ 5 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/schlage_sense.json create mode 100644 tests/components/homekit_controller/specific_devices/test_schlage_sense.py diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index fa7bf39d385..3107f5efbb2 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.6"], + "requirements": ["aiohomekit==1.2.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index a779d2ada82..1949dd01ed2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.6 +aiohomekit==1.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 142247940a8..7483bdc31d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.6 +aiohomekit==1.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/fixtures/schlage_sense.json b/tests/components/homekit_controller/fixtures/schlage_sense.json new file mode 100644 index 00000000000..04e1923da55 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/schlage_sense.json @@ -0,0 +1,356 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "Schlage ", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "BE479CAM619", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "SENSE ", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "AAAAAAA000", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "004.027.000", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 51, + "perms": ["pr"], + "format": "string", + "value": "1.3.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "00000054-0000-1000-8000-0026BB765291", + "iid": 50, + "perms": ["pr"], + "format": "string", + "value": "002.001.000", + "maxLen": 64 + } + ] + }, + { + "iid": 10, + "type": "7F0DEE73-4A3F-4103-98E6-A46CD301BDFB", + "characteristics": [ + { + "type": "44FF6853-58DB-4956-B298-5F6650DD61F6", + "iid": 25, + "perms": ["pw"], + "format": "data" + }, + { + "type": "CF68C40F-DC6F-4F7E-918C-4C536B643A2B", + "iid": 26, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "4058C2B8-4545-4E77-B6B7-157C38F9718B", + "iid": 27, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 0, + "minValue": 1, + "maxValue": 5, + "minStep": 1 + }, + { + "type": "B498F4B5-6364-4F79-B5CC-1563ADE070DF", + "iid": 28, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 1 + }, + { + "type": "AFAE7AD2-8DD3-4B20-BAE0-C0B18B79EDB5", + "iid": 29, + "perms": ["pw"], + "format": "data" + }, + { + "type": "87D91EC6-C508-4CAD-89F1-A21B0BF179A0", + "iid": 30, + "perms": ["pr"], + "format": "data", + "value": "000a00000000000000000000" + }, + { + "type": "4C3E2641-F57F-11E3-A3AC-0800200C9A66", + "iid": 31, + "perms": ["pr"], + "format": "uint64", + "value": 3468600224 + }, + { + "type": "EEC26990-F628-11E3-A3AC-0800200C9A66", + "iid": 32, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 4, + "minValue": 4, + "maxValue": 8, + "minStep": 1 + }, + { + "type": "BCDE3B9E-3963-4123-B24D-42ECCBB3A9C4", + "iid": 33, + "perms": ["pr"], + "format": "data", + "value": "4e6f6e65" + }, + { + "type": "A9464D14-6806-4375-BA53-E14F7E0A6BEE", + "iid": 34, + "perms": ["pr", "pw"], + "format": null, + "value": "ff" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 35, + "perms": ["pr"], + "format": "string", + "value": "Additional Settings", + "description": "Name", + "maxLen": 64 + }, + { + "type": "63D23C2F-2FBB-45E8-8540-47CC26C517D0", + "iid": 36, + "perms": ["pr"], + "format": "uint8", + "value": 100 + } + ] + }, + { + "iid": 23, + "type": "00000044-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000019-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": ["pw"], + "format": "data", + "description": "Lock Control Point" + }, + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr"], + "format": "string", + "value": "02.00.00", + "description": "Version", + "maxLen": 64 + }, + { + "type": "0000001F-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr"], + "format": "data", + "value": "012431443133423434392d423941312d334135392d463042412d3245393030304233453430450208000000000000000003010404030001ff", + "description": "Logs" + }, + { + "type": "00000005-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr", "pw"], + "format": "bool", + "value": true, + "description": "Audio Feedback" + }, + { + "type": "0000001A-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw"], + "format": "uint32", + "value": 0, + "description": "Lock Management Auto Security Timeout", + "unit": "seconds" + }, + { + "type": "00000001-0000-1000-8000-0026BB765291", + "iid": 21, + "perms": ["pr", "pw"], + "format": "bool", + "value": false, + "description": "Administrator Only Access" + } + ] + }, + { + "iid": 30, + "type": "00000045-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000001D-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 3, + "description": "Lock Current State", + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "0000001E-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Lock Target State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr"], + "format": "string", + "value": "Lock Mechanism", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 34, + "type": "1F6B43AA-94DE-4BA9-981C-DA38823117BD", + "characteristics": [ + { + "type": "048D8799-695B-4A7F-A7F7-A4A1301587FE", + "iid": 39, + "perms": ["pw"], + "format": "data" + }, + { + "type": "66B7C7FD-95A7-4F89-B0AD-38073A67C46C", + "iid": 40, + "perms": ["pw"], + "format": "data" + }, + { + "type": "507EFC3F-9231-438C-976A-FA04427F1F8F", + "iid": 41, + "perms": ["pw"], + "format": "data" + }, + { + "type": "1DC15719-0882-4BAD-AB0F-9AEAB0600C90", + "iid": 42, + "perms": ["pr"], + "format": "data", + "value": "03" + } + ] + }, + { + "iid": 39, + "type": "00000055-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000004C-0000-1000-8000-0026BB765291", + "iid": 45, + "perms": [], + "format": "data", + "description": "Pair Setup" + }, + { + "type": "0000004E-0000-1000-8000-0026BB765291", + "iid": 46, + "perms": [], + "format": "data", + "description": "Pair Verify" + }, + { + "type": "0000004F-0000-1000-8000-0026BB765291", + "iid": 47, + "perms": [], + "format": "uint8", + "description": "Pairing Features" + }, + { + "type": "00000050-0000-1000-8000-0026BB765291", + "iid": 48, + "perms": ["pr", "pw"], + "format": "data", + "value": null, + "description": "Pairing Pairings" + } + ] + }, + { + "iid": 44, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 62, + "perms": ["pr"], + "format": "string", + "value": "02.00.00", + "description": "Version", + "maxLen": 64 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_schlage_sense.py b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py new file mode 100644 index 00000000000..d572989e345 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py @@ -0,0 +1,40 @@ +"""Make sure that Schlage Sense is enumerated properly.""" + + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_schlage_sense_setup(hass): + """Test that the accessory can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "schlage_sense.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="SENSE ", + model="BE479CAM619", + manufacturer="Schlage ", + sw_version="004.027.000", + hw_version="1.3.0", + serial_number="AAAAAAA000", + devices=[], + entities=[ + EntityTestInfo( + entity_id="lock.sense_lock_mechanism", + friendly_name="SENSE Lock Mechanism", + unique_id="homekit-AAAAAAA000-30", + supported_features=0, + state="unknown", + ), + ], + ), + ) From 70aeaa3c76a7e67dfbae3deb01105db9050bbdd3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 9 Aug 2022 22:10:26 +0200 Subject: [PATCH 262/903] Use Callback protocol for AutomationActionType (#76054) --- .../components/automation/__init__.py | 25 +++++++++++++++---- homeassistant/components/calendar/trigger.py | 3 ++- .../components/homekit/type_triggers.py | 2 +- .../components/philips_js/__init__.py | 6 +++-- homeassistant/components/webostv/__init__.py | 7 ++++-- 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c743e1f83fd..c3a669511bc 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,9 +1,9 @@ """Allow to set up simple automation rules via the config file.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable import logging -from typing import Any, TypedDict, cast +from typing import Any, Protocol, TypedDict, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -110,7 +110,17 @@ ATTR_VARIABLES = "variables" SERVICE_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) -AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] + + +class AutomationActionType(Protocol): + """Protocol type for automation action callback.""" + + async def __call__( + self, + run_variables: dict[str, Any], + context: Context | None = None, + ) -> None: + """Define action callback type.""" class AutomationTriggerData(TypedDict): @@ -437,7 +447,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity): else: await self.async_disable() - async def async_trigger(self, run_variables, context=None, skip_condition=False): + async def async_trigger( + self, + run_variables: dict[str, Any], + context: Context | None = None, + skip_condition: bool = False, + ) -> None: """Trigger automation. This method is a coroutine. @@ -462,7 +477,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): this = None if state := self.hass.states.get(self.entity_id): this = state.as_dict() - variables = {"this": this, **(run_variables or {})} + variables: dict[str, Any] = {"this": this, **(run_variables or {})} if self._variables: try: variables = self._variables.async_render(self.hass, variables) diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index bb6e874b47f..7845037f896 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -1,6 +1,7 @@ """Offer calendar automation rules.""" from __future__ import annotations +from collections.abc import Coroutine import datetime import logging from typing import Any @@ -47,7 +48,7 @@ class CalendarEventListener: def __init__( self, hass: HomeAssistant, - job: HassJob, + job: HassJob[..., Coroutine[Any, Any, None]], trigger_data: dict[str, Any], entity: CalendarEntity, event_type: str, diff --git a/homeassistant/components/homekit/type_triggers.py b/homeassistant/components/homekit/type_triggers.py index 4b3a7e73cac..776fe6f3110 100644 --- a/homeassistant/components/homekit/type_triggers.py +++ b/homeassistant/components/homekit/type_triggers.py @@ -66,7 +66,7 @@ class DeviceTriggerAccessory(HomeAccessory): async def async_trigger( self, - run_variables: dict, + run_variables: dict[str, Any], context: Context | None = None, skip_condition: bool = False, ) -> None: diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 24b3f9a91e0..154df3ed214 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Mapping +from collections.abc import Callable, Coroutine, Mapping from datetime import timedelta import logging from typing import Any @@ -84,7 +84,9 @@ class PluggableAction: def __init__(self, update: Callable[[], None]) -> None: """Initialize.""" self._update = update - self._actions: dict[Any, tuple[HassJob, dict[str, Any]]] = {} + self._actions: dict[ + Any, tuple[HassJob[..., Coroutine[Any, Any, None]], dict[str, Any]] + ] = {} def __bool__(self): """Return if we have something attached.""" diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index d9b2acb1836..32161e6bad6 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1,7 +1,7 @@ """Support for LG webOS Smart TV.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from contextlib import suppress import logging from typing import Any @@ -170,7 +170,10 @@ class PluggableAction: def __init__(self) -> None: """Initialize.""" - self._actions: dict[Callable[[], None], tuple[HassJob, dict[str, Any]]] = {} + self._actions: dict[ + Callable[[], None], + tuple[HassJob[..., Coroutine[Any, Any, None]], dict[str, Any]], + ] = {} def __bool__(self) -> bool: """Return if we have something attached.""" From dc47121f2cb9cb5146048df39977093e6bd7690a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 9 Aug 2022 22:12:33 +0200 Subject: [PATCH 263/903] Better type hass_job method calls (#76053) --- homeassistant/core.py | 20 +++++++++++-------- homeassistant/helpers/discovery.py | 6 ++++-- homeassistant/helpers/event.py | 32 ++++++++++++++++-------------- homeassistant/helpers/start.py | 6 ++++-- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 7b41fe476aa..fcd41ddc856 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -816,7 +816,7 @@ class Event: class _FilterableJob(NamedTuple): """Event listener job to be executed with optional filter.""" - job: HassJob[[Event], None | Awaitable[None]] + job: HassJob[[Event], Coroutine[Any, Any, None] | None] event_filter: Callable[[Event], bool] | None run_immediately: bool @@ -907,7 +907,7 @@ class EventBus: def listen( self, event_type: str, - listener: Callable[[Event], None | Awaitable[None]], + listener: Callable[[Event], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -928,7 +928,7 @@ class EventBus: def async_listen( self, event_type: str, - listener: Callable[[Event], None | Awaitable[None]], + listener: Callable[[Event], Coroutine[Any, Any, None] | None], event_filter: Callable[[Event], bool] | None = None, run_immediately: bool = False, ) -> CALLBACK_TYPE: @@ -968,7 +968,9 @@ class EventBus: return remove_listener def listen_once( - self, event_type: str, listener: Callable[[Event], None | Awaitable[None]] + self, + event_type: str, + listener: Callable[[Event], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -989,7 +991,9 @@ class EventBus: @callback def async_listen_once( - self, event_type: str, listener: Callable[[Event], None | Awaitable[None]] + self, + event_type: str, + listener: Callable[[Event], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -1463,7 +1467,7 @@ class Service: def __init__( self, - func: Callable[[ServiceCall], None | Awaitable[None]], + func: Callable[[ServiceCall], Coroutine[Any, Any, None] | None], schema: vol.Schema | None, context: Context | None = None, ) -> None: @@ -1533,7 +1537,7 @@ class ServiceRegistry: self, domain: str, service: str, - service_func: Callable[[ServiceCall], Awaitable[None] | None], + service_func: Callable[[ServiceCall], Coroutine[Any, Any, None] | None], schema: vol.Schema | None = None, ) -> None: """ @@ -1550,7 +1554,7 @@ class ServiceRegistry: self, domain: str, service: str, - service_func: Callable[[ServiceCall], Awaitable[None] | None], + service_func: Callable[[ServiceCall], Coroutine[Any, Any, None] | None], schema: vol.Schema | None = None, ) -> None: """ diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 61117fb7d04..375c3b09c2e 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -7,7 +7,7 @@ There are two different types of discoveries that can be fired/listened for. """ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from typing import Any, TypedDict from homeassistant import core, setup @@ -36,7 +36,9 @@ class DiscoveryDict(TypedDict): def async_listen( hass: core.HomeAssistant, service: str, - callback: Callable[[str, DiscoveryInfoType | None], Awaitable[None] | None], + callback: Callable[ + [str, DiscoveryInfoType | None], Coroutine[Any, Any, None] | None + ], ) -> None: """Set up listener for discovery of specific service. diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d18af953ec6..2a34773a413 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Iterable, Sequence +from collections.abc import Callable, Coroutine, Iterable, Sequence import copy from dataclasses import dataclass from datetime import datetime, timedelta @@ -141,7 +141,7 @@ def threaded_listener_factory( def async_track_state_change( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[str, State | None, State], Awaitable[None] | None], + action: Callable[[str, State | None, State], Coroutine[Any, Any, None] | None], from_state: None | str | Iterable[str] = None, to_state: None | str | Iterable[str] = None, ) -> CALLBACK_TYPE: @@ -714,7 +714,9 @@ def async_track_state_change_filtered( def async_track_template( hass: HomeAssistant, template: Template, - action: Callable[[str, State | None, State | None], Awaitable[None] | None], + action: Callable[ + [str, State | None, State | None], Coroutine[Any, Any, None] | None + ], variables: TemplateVarsType | None = None, ) -> CALLBACK_TYPE: """Add a listener that fires when a a template evaluates to 'true'. @@ -1188,7 +1190,7 @@ def async_track_template_result( def async_track_same_state( hass: HomeAssistant, period: timedelta, - action: Callable[[], Awaitable[None] | None], + action: Callable[[], Coroutine[Any, Any, None] | None], async_check_same_func: Callable[[str, State | None, State | None], bool], entity_ids: str | Iterable[str] = MATCH_ALL, ) -> CALLBACK_TYPE: @@ -1257,8 +1259,8 @@ track_same_state = threaded_listener_factory(async_track_same_state) @bind_hass def async_track_point_in_time( hass: HomeAssistant, - action: HassJob[[datetime], Awaitable[None] | None] - | Callable[[datetime], Awaitable[None] | None], + action: HassJob[[datetime], Coroutine[Any, Any, None] | None] + | Callable[[datetime], Coroutine[Any, Any, None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in time.""" @@ -1279,8 +1281,8 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time) @bind_hass def async_track_point_in_utc_time( hass: HomeAssistant, - action: HassJob[[datetime], Awaitable[None] | None] - | Callable[[datetime], Awaitable[None] | None], + action: HassJob[[datetime], Coroutine[Any, Any, None] | None] + | Callable[[datetime], Coroutine[Any, Any, None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" @@ -1293,7 +1295,7 @@ def async_track_point_in_utc_time( cancel_callback: asyncio.TimerHandle | None = None @callback - def run_action(job: HassJob[[datetime], Awaitable[None] | None]) -> None: + def run_action(job: HassJob[[datetime], Coroutine[Any, Any, None] | None]) -> None: """Call the action.""" nonlocal cancel_callback # Depending on the available clock support (including timer hardware @@ -1330,8 +1332,8 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim def async_call_later( hass: HomeAssistant, delay: float | timedelta, - action: HassJob[[datetime], Awaitable[None] | None] - | Callable[[datetime], Awaitable[None] | None], + action: HassJob[[datetime], Coroutine[Any, Any, None] | None] + | Callable[[datetime], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Add a listener that is called in .""" if not isinstance(delay, timedelta): @@ -1346,7 +1348,7 @@ call_later = threaded_listener_factory(async_call_later) @bind_hass def async_track_time_interval( hass: HomeAssistant, - action: Callable[[datetime], Awaitable[None] | None], + action: Callable[[datetime], Coroutine[Any, Any, None] | None], interval: timedelta, ) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" @@ -1388,7 +1390,7 @@ class SunListener: """Helper class to help listen to sun events.""" hass: HomeAssistant = attr.ib() - job: HassJob[[], Awaitable[None] | None] = attr.ib() + job: HassJob[[], Coroutine[Any, Any, None] | None] = attr.ib() event: str = attr.ib() offset: timedelta | None = attr.ib() _unsub_sun: CALLBACK_TYPE | None = attr.ib(default=None) @@ -1479,7 +1481,7 @@ time_tracker_timestamp = time.time @bind_hass def async_track_utc_time_change( hass: HomeAssistant, - action: Callable[[datetime], Awaitable[None] | None], + action: Callable[[datetime], Coroutine[Any, Any, None] | None], hour: Any | None = None, minute: Any | None = None, second: Any | None = None, @@ -1544,7 +1546,7 @@ track_utc_time_change = threaded_listener_factory(async_track_utc_time_change) @bind_hass def async_track_time_change( hass: HomeAssistant, - action: Callable[[datetime], Awaitable[None] | None], + action: Callable[[datetime], Coroutine[Any, Any, None] | None], hour: Any | None = None, minute: Any | None = None, second: Any | None = None, diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 6c17ae5be3a..f6c9a536a23 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -1,7 +1,8 @@ """Helpers to help during startup.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine +from typing import Any from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback @@ -9,7 +10,8 @@ from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, cal @callback def async_at_start( - hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Awaitable[None] | None] + hass: HomeAssistant, + at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Execute something when Home Assistant is started. From 596b7fed34cc4e64bc99167dc36672e052f157d8 Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Tue, 9 Aug 2022 14:46:22 -0600 Subject: [PATCH 264/903] Fix #76283 (#76531) --- homeassistant/components/sms/notify.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index d076f3625ba..21b48946f55 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -20,31 +20,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the SMS notification service.""" - if SMS_GATEWAY not in hass.data[DOMAIN]: - _LOGGER.error("SMS gateway not found, cannot initialize service") - return - - gateway = hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] - if discovery_info is None: number = config[CONF_RECIPIENT] else: number = discovery_info[CONF_RECIPIENT] - return SMSNotificationService(gateway, number) + return SMSNotificationService(hass, number) class SMSNotificationService(BaseNotificationService): """Implement the notification service for SMS.""" - def __init__(self, gateway, number): + def __init__(self, hass, number): """Initialize the service.""" - self.gateway = gateway + + self.hass = hass self.number = number async def async_send_message(self, message="", **kwargs): """Send SMS message.""" + if SMS_GATEWAY not in self.hass.data[DOMAIN]: + _LOGGER.error("SMS gateway not found, cannot send message") + return + + gateway = self.hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] + targets = kwargs.get(CONF_TARGET, [self.number]) smsinfo = { "Class": -1, @@ -67,6 +68,6 @@ class SMSNotificationService(BaseNotificationService): encoded_message["Number"] = target try: # Actually send the message - await self.gateway.send_sms_async(encoded_message) + await gateway.send_sms_async(encoded_message) except gammu.GSMError as exc: _LOGGER.error("Sending to %s failed: %s", target, exc) From b04352e7456a271dfc42e8aff1c597df4549eb0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 11:08:38 -1000 Subject: [PATCH 265/903] Bump aiohomekit to 1.2.8 (#76532) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3107f5efbb2..cf3069e3b0d 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.7"], + "requirements": ["aiohomekit==1.2.8"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 1949dd01ed2..dcc87172176 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.7 +aiohomekit==1.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7483bdc31d1..1df383e4ea9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.7 +aiohomekit==1.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http From 4a938ec33ee8df17e619fc64a519ef7c2ff856d8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 10 Aug 2022 00:23:36 +0000 Subject: [PATCH 266/903] [ci skip] Translation update --- .../android_ip_webcam/translations/en.json | 2 +- .../android_ip_webcam/translations/fr.json | 20 +++++++++++ .../android_ip_webcam/translations/hu.json | 26 +++++++++++++++ .../android_ip_webcam/translations/it.json | 20 +++++++++++ .../translations/zh-Hant.json | 26 +++++++++++++++ .../components/anthemav/translations/ru.json | 2 +- .../components/demo/translations/it.json | 13 +++++++- .../components/demo/translations/ru.json | 2 +- .../deutsche_bahn/translations/it.json | 7 ++++ .../deutsche_bahn/translations/ru.json | 2 +- .../components/escea/translations/hu.json | 13 ++++++++ .../components/escea/translations/it.json | 8 +++++ .../components/escea/translations/no.json | 13 ++++++++ .../components/escea/translations/ru.json | 13 ++++++++ .../flunearyou/translations/it.json | 12 +++++++ .../components/google/translations/ru.json | 2 +- .../justnimbus/translations/hu.json | 19 +++++++++++ .../justnimbus/translations/it.json | 19 +++++++++++ .../justnimbus/translations/no.json | 19 +++++++++++ .../justnimbus/translations/ru.json | 19 +++++++++++ .../justnimbus/translations/zh-Hant.json | 19 +++++++++++ .../lg_soundbar/translations/ru.json | 2 +- .../components/lyric/translations/ru.json | 2 +- .../components/miflora/translations/ru.json | 2 +- .../components/mitemp_bt/translations/ru.json | 2 +- .../components/mysensors/translations/hu.json | 9 +++++ .../components/mysensors/translations/it.json | 9 +++++ .../components/nest/translations/ru.json | 2 +- .../openalpr_local/translations/ru.json | 2 +- .../openexchangerates/translations/hu.json | 33 +++++++++++++++++++ .../openexchangerates/translations/it.json | 29 ++++++++++++++++ .../openexchangerates/translations/ru.json | 33 +++++++++++++++++++ .../radiotherm/translations/ru.json | 2 +- .../components/recorder/translations/es.json | 2 +- .../components/senz/translations/ru.json | 2 +- .../simplepush/translations/it.json | 4 +++ .../simplepush/translations/ru.json | 4 +-- .../simplisafe/translations/it.json | 1 + .../simplisafe/translations/ru.json | 2 +- .../components/solaredge/translations/it.json | 2 +- .../soundtouch/translations/ru.json | 2 +- .../steam_online/translations/ru.json | 2 +- .../unifiprotect/translations/hu.json | 1 + .../unifiprotect/translations/it.json | 1 + .../unifiprotect/translations/no.json | 1 + .../unifiprotect/translations/ru.json | 1 + .../unifiprotect/translations/zh-Hant.json | 1 + .../components/uscis/translations/ru.json | 2 +- .../components/xbox/translations/ru.json | 2 +- 49 files changed, 410 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/translations/fr.json create mode 100644 homeassistant/components/android_ip_webcam/translations/hu.json create mode 100644 homeassistant/components/android_ip_webcam/translations/it.json create mode 100644 homeassistant/components/android_ip_webcam/translations/zh-Hant.json create mode 100644 homeassistant/components/deutsche_bahn/translations/it.json create mode 100644 homeassistant/components/escea/translations/hu.json create mode 100644 homeassistant/components/escea/translations/it.json create mode 100644 homeassistant/components/escea/translations/no.json create mode 100644 homeassistant/components/escea/translations/ru.json create mode 100644 homeassistant/components/justnimbus/translations/hu.json create mode 100644 homeassistant/components/justnimbus/translations/it.json create mode 100644 homeassistant/components/justnimbus/translations/no.json create mode 100644 homeassistant/components/justnimbus/translations/ru.json create mode 100644 homeassistant/components/justnimbus/translations/zh-Hant.json create mode 100644 homeassistant/components/openexchangerates/translations/hu.json create mode 100644 homeassistant/components/openexchangerates/translations/it.json create mode 100644 homeassistant/components/openexchangerates/translations/ru.json diff --git a/homeassistant/components/android_ip_webcam/translations/en.json b/homeassistant/components/android_ip_webcam/translations/en.json index 43cd63356b4..775263225ea 100644 --- a/homeassistant/components/android_ip_webcam/translations/en.json +++ b/homeassistant/components/android_ip_webcam/translations/en.json @@ -20,7 +20,7 @@ "issues": { "deprecated_yaml": { "description": "Configuring Android IP Webcam using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Android IP Webcam YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Android IP Webcamepush YAML configuration is being removed" + "title": "The Android IP Webcam YAML configuration is being removed" } } } \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/fr.json b/homeassistant/components/android_ip_webcam/translations/fr.json new file mode 100644 index 00000000000..0e83b0feaf7 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/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" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/hu.json b/homeassistant/components/android_ip_webcam/translations/hu.json new file mode 100644 index 00000000000..e728b9eee54 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Az Android IP webkamera konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el az Android IP Webkamera YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az Android IP webkamera YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/it.json b/homeassistant/components/android_ip_webcam/translations/it.json new file mode 100644 index 00000000000..7c04ebfdaef --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/zh-Hant.json b/homeassistant/components/android_ip_webcam/translations/zh-Hant.json new file mode 100644 index 00000000000..523c5a8b0b3 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Android IP Webcam \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Android IP Webcam YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Android IP Webcam YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ru.json b/homeassistant/components/anthemav/translations/ru.json index e55ad7100e7..f56475d331d 100644 --- a/homeassistant/components/anthemav/translations/ru.json +++ b/homeassistant/components/anthemav/translations/ru.json @@ -18,7 +18,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 80281a62703..e8265baebf7 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Premere INVIA per confermare che l'alimentatore \u00e8 stato sostituito", + "title": "L'alimentatore deve essere sostituito" + } + } + }, + "title": "L'alimentazione non \u00e8 stabile" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Premere OK quando il liquido delle frecce \u00e8 stato riempito", + "description": "Premere INVIA quando il liquido delle frecce \u00e8 stato riempito", "title": "Il liquido delle frecce deve essere rabboccato" } } diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index 29b4229dacb..b7f3ddb20a7 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -15,7 +15,7 @@ "fix_flow": { "step": { "confirm": { - "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 OK, \u043a\u043e\u0433\u0434\u0430 \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430.", + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\", \u043a\u043e\u0433\u0434\u0430 \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430.", "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u043b\u0438\u0442\u044c \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432" } } diff --git a/homeassistant/components/deutsche_bahn/translations/it.json b/homeassistant/components/deutsche_bahn/translations/it.json new file mode 100644 index 00000000000..d390f84ff0a --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/it.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "L'integrazione Deutsche Bahn sar\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/ru.json b/homeassistant/components/deutsche_bahn/translations/ru.json index 2cfbb695fc3..b5fe6857a73 100644 --- a/homeassistant/components/deutsche_bahn/translations/ru.json +++ b/homeassistant/components/deutsche_bahn/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.11. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.11. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/escea/translations/hu.json b/homeassistant/components/escea/translations/hu.json new file mode 100644 index 00000000000..015057fd92d --- /dev/null +++ b/homeassistant/components/escea/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "Szeretne Escea kandall\u00f3t be\u00e1ll\u00edtani?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/it.json b/homeassistant/components/escea/translations/it.json new file mode 100644 index 00000000000..047fc1d0ff1 --- /dev/null +++ b/homeassistant/components/escea/translations/it.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/no.json b/homeassistant/components/escea/translations/no.json new file mode 100644 index 00000000000..1058194709f --- /dev/null +++ b/homeassistant/components/escea/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 sette opp en Escea peis?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/ru.json b/homeassistant/components/escea/translations/ru.json new file mode 100644 index 00000000000..eda098cfc2b --- /dev/null +++ b/homeassistant/components/escea/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \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 \u043a\u0430\u043c\u0438\u043d Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/it.json b/homeassistant/components/flunearyou/translations/it.json index af43ada9596..4b3bd589325 100644 --- a/homeassistant/components/flunearyou/translations/it.json +++ b/homeassistant/components/flunearyou/translations/it.json @@ -16,5 +16,17 @@ "title": "Configurare Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "title": "Rimuovi Flu Near You" + } + } + }, + "title": "Flu Near You non \u00e8 pi\u00f9 disponibile" + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 57a6791cedf..be7b92a707c 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -35,7 +35,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" }, "removed_track_new_yaml": { diff --git a/homeassistant/components/justnimbus/translations/hu.json b/homeassistant/components/justnimbus/translations/hu.json new file mode 100644 index 00000000000..8b141e6a2f5 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "client_id": "\u00dcgyf\u00e9lazonos\u00edt\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/it.json b/homeassistant/components/justnimbus/translations/it.json new file mode 100644 index 00000000000..0824bb1be49 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore inaspettato" + }, + "step": { + "user": { + "data": { + "client_id": "ID Cliente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/no.json b/homeassistant/components/justnimbus/translations/no.json new file mode 100644 index 00000000000..e8a0a650d99 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "client_id": "Klient ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/ru.json b/homeassistant/components/justnimbus/translations/ru.json new file mode 100644 index 00000000000..719ec98c326 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/ru.json @@ -0,0 +1,19 @@ +{ + "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": { + "client_id": "ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/zh-Hant.json b/homeassistant/components/justnimbus/translations/zh-Hant.json new file mode 100644 index 00000000000..27986db1a84 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\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": { + "client_id": "\u5ba2\u6236\u7aef ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ru.json b/homeassistant/components/lg_soundbar/translations/ru.json index f7961eb2e7e..38f8ad9f92a 100644 --- a/homeassistant/components/lg_soundbar/translations/ru.json +++ b/homeassistant/components/lg_soundbar/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "already_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.", "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." }, "error": { diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json index 536f1a9c0cc..98fd17ed407 100644 --- a/homeassistant/components/lyric/translations/ru.json +++ b/homeassistant/components/lyric/translations/ru.json @@ -20,7 +20,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Honeywell Lyric\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Honeywell Lyric\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Honeywell Lyric \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } diff --git a/homeassistant/components/miflora/translations/ru.json b/homeassistant/components/miflora/translations/ru.json index b5bf7bfd3c1..8c3a3fcbdb5 100644 --- a/homeassistant/components/miflora/translations/ru.json +++ b/homeassistant/components/miflora/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0431\u044b\u043b\u0430 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Mi Flora \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0431\u044b\u043b\u0430 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Mi Flora \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/mitemp_bt/translations/ru.json b/homeassistant/components/mitemp_bt/translations/ru.json index e25532777b8..34620d5d689 100644 --- a/homeassistant/components/mitemp_bt/translations/ru.json +++ b/homeassistant/components/mitemp_bt/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Mijia BLE \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Mijia BLE \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index e1d9c10b053..5253dd7b427 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -14,6 +14,7 @@ "invalid_serial": "\u00c9rv\u00e9nytelen soros port", "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si (subscribe) topik", "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3", + "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", "not_a_number": "Adj meg egy sz\u00e1mot.", "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie", "same_topic": "A feliratkoz\u00e1s \u00e9s a k\u00f6zz\u00e9t\u00e9tel t\u00e9m\u00e1i ugyanazok", @@ -68,6 +69,14 @@ }, "description": "Ethernet \u00e1tj\u00e1r\u00f3 be\u00e1ll\u00edt\u00e1sa" }, + "select_gateway_type": { + "description": "V\u00e1lassza ki a konfigur\u00e1land\u00f3 \u00e1tj\u00e1r\u00f3t.", + "menu_options": { + "gw_mqtt": "MQTT \u00e1tj\u00e1r\u00f3 konfigur\u00e1l\u00e1sa", + "gw_serial": "Soros-port \u00e1tj\u00e1r\u00f3 konfigur\u00e1l\u00e1sa", + "gw_tcp": "TCP-\u00e1tj\u00e1r\u00f3 konfigur\u00e1l\u00e1sa" + } + }, "user": { "data": { "gateway_type": "\u00c1tj\u00e1r\u00f3 t\u00edpusa" diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index 0a16a4f045c..0f4a2746790 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -14,6 +14,7 @@ "invalid_serial": "Porta seriale non valida", "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", + "mqtt_required": "L'integrazione MQTT non \u00e8 configurata", "not_a_number": "Digita 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", @@ -68,6 +69,14 @@ }, "description": "Configurazione del gateway Ethernet" }, + "select_gateway_type": { + "description": "Seleziona quale gateway configurare.", + "menu_options": { + "gw_mqtt": "Configura un gateway MQTT", + "gw_serial": "Configura un gateway seriale", + "gw_tcp": "Configura un gateway TCP" + } + }, "user": { "data": { "gateway_type": "Tipo di gateway" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 71721cda936..44295514840 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -99,7 +99,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" }, "removed_app_auth": { diff --git a/homeassistant/components/openalpr_local/translations/ru.json b/homeassistant/components/openalpr_local/translations/ru.json index 180151e5fd5..171aaa8a5c9 100644 --- a/homeassistant/components/openalpr_local/translations/ru.json +++ b/homeassistant/components/openalpr_local/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/openexchangerates/translations/hu.json b/homeassistant/components/openexchangerates/translations/hu.json new file mode 100644 index 00000000000..83f3ebae2b3 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "base": "Alapdeviza" + }, + "data_description": { + "base": "Az USD-n k\u00edv\u00fcli m\u00e1sik alapdeviza haszn\u00e1lat\u00e1hoz [fizet\u0151s csomag]({signup}) sz\u00fcks\u00e9ges." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Az Open Exchange Rates konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az Open Exchange Rates YAML-konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/it.json b/homeassistant/components/openexchangerates/translations/it.json new file mode 100644 index 00000000000..d42f7122593 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "timeout_connect": "Tempo scaduto per stabile la connessione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "timeout_connect": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "base": "Valuta di base" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Open Exchange Rates tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Open Exchange Rates dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ru.json b/homeassistant/components/openexchangerates/translations/ru.json new file mode 100644 index 00000000000..1707c8e8646 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "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.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\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", + "base": "\u0411\u0430\u0437\u043e\u0432\u0430\u044f \u0432\u0430\u043b\u044e\u0442\u0430" + }, + "data_description": { + "base": "\u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0431\u0430\u0437\u043e\u0432\u043e\u0439 \u0432\u0430\u043b\u044e\u0442\u044b, \u043e\u0442\u043b\u0438\u0447\u043d\u043e\u0439 \u043e\u0442 USD, \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f [\u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0430]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Open Exchange Rates \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Open Exchange Rates \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ru.json b/homeassistant/components/radiotherm/translations/ru.json index 6d4615f9820..e052a1653ea 100644 --- a/homeassistant/components/radiotherm/translations/ru.json +++ b/homeassistant/components/radiotherm/translations/ru.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } }, diff --git a/homeassistant/components/recorder/translations/es.json b/homeassistant/components/recorder/translations/es.json index 86bdd0abec8..e15c071240e 100644 --- a/homeassistant/components/recorder/translations/es.json +++ b/homeassistant/components/recorder/translations/es.json @@ -4,7 +4,7 @@ "current_recorder_run": "Hora de inicio de la ejecuci\u00f3n actual", "database_engine": "Motor de la base de datos", "database_version": "Versi\u00f3n de la base de datos", - "estimated_db_size": "Mida estimada de la base de datos (MiB)", + "estimated_db_size": "Tama\u00f1o estimado de la base de datos (MiB)", "oldest_recorder_run": "Hora de inicio de ejecuci\u00f3n m\u00e1s antigua" } } diff --git a/homeassistant/components/senz/translations/ru.json b/homeassistant/components/senz/translations/ru.json index 21c6b830841..0c8f912d2fe 100644 --- a/homeassistant/components/senz/translations/ru.json +++ b/homeassistant/components/senz/translations/ru.json @@ -19,7 +19,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"nVent RAYCHEM SENZ\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"nVent RAYCHEM SENZ\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 nVent RAYCHEM SENZ \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } diff --git a/homeassistant/components/simplepush/translations/it.json b/homeassistant/components/simplepush/translations/it.json index b3a9b44f938..be311f7e0c3 100644 --- a/homeassistant/components/simplepush/translations/it.json +++ b/homeassistant/components/simplepush/translations/it.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "La configurazione di Simplepush tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Simplepush dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Simplepush sar\u00e0 rimossa" + }, + "removed_yaml": { + "description": "La configurazione di Simplepush tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML di Simplepush dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Simplepush \u00e8 stata rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json index e615a4c4c72..1e7a61fb1cb 100644 --- a/homeassistant/components/simplepush/translations/ru.json +++ b/homeassistant/components/simplepush/translations/ru.json @@ -20,11 +20,11 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" }, "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Simplepush \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Simplepush \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index ba75954ce71..6f2a61e2df4 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Account gi\u00e0 registrato", "invalid_auth": "Autenticazione non valida", + "invalid_auth_code_length": "I codici di autorizzazione SimpliSafe sono lunghi 45 caratteri", "unknown": "Errore imprevisto" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 863a67c3b89..0d09bcd0faf 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -35,7 +35,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "SimpliSafe \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u0441\u0432\u043e\u0435 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\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](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 [\u0441\u044e\u0434\u0430]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SimpliSafe." + "description": "SimpliSafe \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u0441\u0432\u043e\u0435 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\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](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 [\u0441\u044e\u0434\u0430]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u043e\u0448\u043b\u0438 \u0432 SimpliSafe \u0432 \u0441\u0432\u043e\u0435\u043c \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0432\u043a\u043b\u0430\u0434\u043a\u0443, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0432\u044b\u0448\u0435\u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.\n\n\u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/solaredge/translations/it.json b/homeassistant/components/solaredge/translations/it.json index f89c85eeaee..8bf3efce908 100644 --- a/homeassistant/components/solaredge/translations/it.json +++ b/homeassistant/components/solaredge/translations/it.json @@ -14,7 +14,7 @@ "data": { "api_key": "Chiave API", "name": "Il nome di questa installazione", - "site_id": "Il sito-id di SolarEdge" + "site_id": "Il site-id di SolarEdge" }, "title": "Definisci i parametri API per questa installazione" } diff --git a/homeassistant/components/soundtouch/translations/ru.json b/homeassistant/components/soundtouch/translations/ru.json index 318fe8abef6..62fa8df84ad 100644 --- a/homeassistant/components/soundtouch/translations/ru.json +++ b/homeassistant/components/soundtouch/translations/ru.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index 828b66f1dc8..48ec5e0c944 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Steam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } }, diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index 645ec1f8c90..d9162f74a91 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Val\u00f3s idej\u0171 m\u00e9r\u0151sz\u00e1mok (FIGYELEM: nagym\u00e9rt\u00e9kben n\u00f6veli a CPU terhel\u00e9st)", "disable_rtsp": "Az RTSP adatfolyam letilt\u00e1sa", + "max_media": "A m\u00e9diab\u00f6ng\u00e9sz\u0151be bet\u00f6ltend\u0151 esem\u00e9nyek maxim\u00e1lis sz\u00e1ma (n\u00f6veli a RAM-haszn\u00e1latot)", "override_connection_host": "Kapcsolat c\u00edm\u00e9nek fel\u00fclb\u00edr\u00e1l\u00e1sa" }, "description": "A Val\u00f3s idej\u0171 m\u00e9r\u0151sz\u00e1mokat csak akkor javasolt haszn\u00e1lni, ha enged\u00e9lyezte a diagnosztikai \u00e9rz\u00e9kel\u0151ket, \u00e9s szeretn\u00e9, hogy azok val\u00f3s id\u0151ben friss\u00fcljenek. Ha nincs enged\u00e9lyezve, akkor csak 15 percenk\u00e9nt friss\u00fclnek.", diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index 8ea65342e78..00592e72ea1 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Metriche in tempo reale (ATTENZIONE: aumenta notevolmente l'utilizzo della CPU)", "disable_rtsp": "Disabilita il flusso RTSP", + "max_media": "Numero massimo di eventi da caricare per Media Browser (aumenta l'utilizzo della RAM)", "override_connection_host": "Sostituisci host di connessione" }, "description": "L'opzione delle metriche in tempo reale dovrebbe essere abilitata solo se hai abilitato i sensori di diagnostica e desideri che vengano aggiornati in tempo reale. Se non sono abilitati, si aggiorneranno solo una volta ogni 15 minuti.", diff --git a/homeassistant/components/unifiprotect/translations/no.json b/homeassistant/components/unifiprotect/translations/no.json index e11ed432313..947d5c76887 100644 --- a/homeassistant/components/unifiprotect/translations/no.json +++ b/homeassistant/components/unifiprotect/translations/no.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Sanntidsm\u00e5linger (ADVARSEL: \u00d8ker CPU-bruken betraktelig)", "disable_rtsp": "Deaktiver RTSP-str\u00f8mmen", + "max_media": "Maks antall hendelser som skal lastes for medienettleseren (\u00f8ker RAM-bruken)", "override_connection_host": "Overstyr tilkoblingsvert" }, "description": "Alternativet sanntidsm\u00e5linger b\u00f8r bare aktiveres hvis du har aktivert diagnostikksensorene og vil ha dem oppdatert i sanntid. Hvis den ikke er aktivert, vil de bare oppdatere en gang hvert 15. minutt.", diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index 2e06e887cf4..e81404f2a95 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -47,6 +47,7 @@ "data": { "all_updates": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438 (\u0412\u041d\u0418\u041c\u0410\u041d\u0418\u0415: \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043d\u0430 \u0426\u041f)", "disable_rtsp": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0442\u043e\u043a RTSP", + "max_media": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0434\u043b\u044f \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u043c\u0443\u043b\u044c\u0442\u0438\u043c\u0435\u0434\u0438\u0430 (\u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0439 \u043f\u0430\u043c\u044f\u0442\u0438)", "override_connection_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0443\u0437\u0435\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438' \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u043b\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438 \u0438 \u0445\u043e\u0442\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u043e\u043d\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u043b\u0438\u0441\u044c \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d, \u043e\u043d\u0438 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u0440\u0430\u0437 \u0432 15 \u043c\u0438\u043d\u0443\u0442.", diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json index d0c23849e12..ba996c5e123 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hant.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -47,6 +47,7 @@ "data": { "all_updates": "\u5373\u6642\u6307\u6a19\uff08\u8b66\u544a\uff1a\u5927\u91cf\u63d0\u5347 CPU \u4f7f\u7528\u7387\uff09", "disable_rtsp": "\u95dc\u9589 RTSP \u4e32\u6d41", + "max_media": "\u5a92\u9ad4\u700f\u89bd\u5668\u6700\u9ad8\u8f09\u5165\u4e8b\u4ef6\u6578\uff08\u589e\u52a0\u8a18\u61b6\u9ad4\u4f7f\u7528\uff09", "override_connection_host": "\u7f6e\u63db\u9023\u7dda\u4e3b\u6a5f\u7aef" }, "description": "\u50c5\u6709\u7576\u958b\u555f\u8a3a\u65b7\u611f\u6e2c\u5668\u3001\u4e26\u9700\u8981\u5373\u6642\u66f4\u65b0\u6642\uff0c\u624d\u5efa\u8b70\u958b\u555f\u5373\u6642\u6307\u6a19\u9078\u9805\u3002\u672a\u555f\u7528\u72c0\u6cc1\u4e0b\u70ba\u6bcf 15 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\u3002", diff --git a/homeassistant/components/uscis/translations/ru.json b/homeassistant/components/uscis/translations/ru.json index f3b70020245..d6cd9da954e 100644 --- a/homeassistant/components/uscis/translations/ru.json +++ b/homeassistant/components/uscis/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0421\u043b\u0443\u0436\u0431\u044b \u0433\u0440\u0430\u0436\u0434\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u0438\u043c\u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0421\u0428\u0410 (USCIS) \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0421\u043b\u0443\u0436\u0431\u044b \u0433\u0440\u0430\u0436\u0434\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u0438\u043c\u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0421\u0428\u0410 (USCIS) \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f USCIS \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/xbox/translations/ru.json b/homeassistant/components/xbox/translations/ru.json index ca47fd08c8c..1fddc4e3ade 100644 --- a/homeassistant/components/xbox/translations/ru.json +++ b/homeassistant/components/xbox/translations/ru.json @@ -16,7 +16,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } From 19295d33ba60bad0a9dfbc4d512e156a09a010fc Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 10 Aug 2022 14:11:49 +0300 Subject: [PATCH 267/903] Migrate BraviaTV to new async backend (#75727) --- .coveragerc | 1 + homeassistant/components/braviatv/__init__.py | 251 +---------------- .../components/braviatv/config_flow.py | 123 ++++----- homeassistant/components/braviatv/const.py | 1 - .../components/braviatv/coordinator.py | 258 ++++++++++++++++++ homeassistant/components/braviatv/entity.py | 2 +- .../components/braviatv/manifest.json | 4 +- .../components/braviatv/media_player.py | 13 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/braviatv/test_config_flow.py | 83 +++--- 11 files changed, 385 insertions(+), 363 deletions(-) create mode 100644 homeassistant/components/braviatv/coordinator.py diff --git a/.coveragerc b/.coveragerc index a94b2a8babc..7e60c9ae891 100644 --- a/.coveragerc +++ b/.coveragerc @@ -139,6 +139,7 @@ omit = homeassistant/components/bosch_shc/switch.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py + homeassistant/components/braviatv/coordinator.py homeassistant/components/braviatv/entity.py homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/remote.py diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index e1d90681d2a..539dd980ffc 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -1,27 +1,20 @@ -"""The Bravia TV component.""" +"""The Bravia TV integration.""" from __future__ import annotations -import asyncio -from collections.abc import Iterable -from datetime import timedelta -import logging from typing import Final -from bravia_tv import BraviaRC -from bravia_tv.braviarc import NoIPControl +from aiohttp import CookieJar +from pybravia import BraviaTV from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import CLIENTID_PREFIX, CONF_IGNORED_SOURCES, DOMAIN, NICKNAME - -_LOGGER = logging.getLogger(__name__) +from .const import CONF_IGNORED_SOURCES, DOMAIN +from .coordinator import BraviaTVCoordinator PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE] -SCAN_INTERVAL: Final = timedelta(seconds=10) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: @@ -31,7 +24,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b pin = config_entry.data[CONF_PIN] ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) - coordinator = BraviaTVCoordinator(hass, host, mac, pin, ignored_sources) + session = async_create_clientsession( + hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False) + ) + client = BraviaTV(host, mac, session=session) + coordinator = BraviaTVCoordinator(hass, client, pin, ignored_sources) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) await coordinator.async_config_entry_first_refresh() @@ -59,229 +56,3 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) - - -class BraviaTVCoordinator(DataUpdateCoordinator[None]): - """Representation of a Bravia TV Coordinator. - - An instance is used per device to share the same power state between - several platforms. - """ - - def __init__( - self, - hass: HomeAssistant, - host: str, - mac: str, - pin: str, - ignored_sources: list[str], - ) -> None: - """Initialize Bravia TV Client.""" - - self.braviarc = BraviaRC(host, mac) - self.pin = pin - self.ignored_sources = ignored_sources - self.muted: bool = False - self.channel_name: str | None = None - self.media_title: str | None = None - self.source: str | None = None - self.source_list: list[str] = [] - self.original_content_list: list[str] = [] - self.content_mapping: dict[str, str] = {} - self.duration: int | None = None - self.content_uri: str | None = None - self.program_media_type: str | None = None - self.audio_output: str | None = None - self.min_volume: int | None = None - self.max_volume: int | None = None - self.volume_level: float | None = None - self.is_on = False - # Assume that the TV is in Play mode - self.playing = True - self.state_lock = asyncio.Lock() - - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - request_refresh_debouncer=Debouncer( - hass, _LOGGER, cooldown=1.0, immediate=False - ), - ) - - def _send_command(self, command: Iterable[str], repeats: int = 1) -> None: - """Send a command to the TV.""" - for _ in range(repeats): - for cmd in command: - self.braviarc.send_command(cmd) - - def _get_source(self) -> str | None: - """Return the name of the source.""" - for key, value in self.content_mapping.items(): - if value == self.content_uri: - return key - return None - - def _refresh_volume(self) -> bool: - """Refresh volume information.""" - volume_info = self.braviarc.get_volume_info(self.audio_output) - if volume_info is not None: - volume = volume_info.get("volume") - self.volume_level = volume / 100 if volume is not None else None - self.audio_output = volume_info.get("target") - self.min_volume = volume_info.get("minVolume") - self.max_volume = volume_info.get("maxVolume") - self.muted = volume_info.get("mute", False) - return True - return False - - def _refresh_channels(self) -> bool: - """Refresh source and channels list.""" - if not self.source_list: - self.content_mapping = self.braviarc.load_source_list() - self.source_list = [] - if not self.content_mapping: - return False - for key in self.content_mapping: - if key not in self.ignored_sources: - self.source_list.append(key) - return True - - def _refresh_playing_info(self) -> None: - """Refresh playing information.""" - playing_info = self.braviarc.get_playing_info() - program_name = playing_info.get("programTitle") - self.channel_name = playing_info.get("title") - self.program_media_type = playing_info.get("programMediaType") - self.content_uri = playing_info.get("uri") - self.source = self._get_source() - self.duration = playing_info.get("durationSec") - if not playing_info: - self.channel_name = "App" - if self.channel_name is not None: - self.media_title = self.channel_name - if program_name is not None: - self.media_title = f"{self.media_title}: {program_name}" - else: - self.media_title = None - - def _update_tv_data(self) -> None: - """Connect and update TV info.""" - power_status = self.braviarc.get_power_status() - - if power_status != "off": - connected = self.braviarc.is_connected() - if not connected: - try: - connected = self.braviarc.connect( - self.pin, CLIENTID_PREFIX, NICKNAME - ) - except NoIPControl: - _LOGGER.error("IP Control is disabled in the TV settings") - if not connected: - power_status = "off" - - if power_status == "active": - self.is_on = True - if self._refresh_volume() and self._refresh_channels(): - self._refresh_playing_info() - return - - self.is_on = False - - async def _async_update_data(self) -> None: - """Fetch the latest data.""" - if self.state_lock.locked(): - return - - await self.hass.async_add_executor_job(self._update_tv_data) - - async def async_turn_on(self) -> None: - """Turn the device on.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.turn_on) - await self.async_request_refresh() - - async def async_turn_off(self) -> None: - """Turn off device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.turn_off) - await self.async_request_refresh() - - async def async_set_volume_level(self, volume: float) -> None: - """Set volume level, range 0..1.""" - async with self.state_lock: - await self.hass.async_add_executor_job( - self.braviarc.set_volume_level, volume, self.audio_output - ) - await self.async_request_refresh() - - async def async_volume_up(self) -> None: - """Send volume up command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job( - self.braviarc.volume_up, self.audio_output - ) - await self.async_request_refresh() - - async def async_volume_down(self) -> None: - """Send volume down command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job( - self.braviarc.volume_down, self.audio_output - ) - await self.async_request_refresh() - - async def async_volume_mute(self, mute: bool) -> None: - """Send mute command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.mute_volume, mute) - await self.async_request_refresh() - - async def async_media_play(self) -> None: - """Send play command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_play) - self.playing = True - await self.async_request_refresh() - - async def async_media_pause(self) -> None: - """Send pause command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_pause) - self.playing = False - await self.async_request_refresh() - - async def async_media_stop(self) -> None: - """Send stop command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_stop) - self.playing = False - await self.async_request_refresh() - - async def async_media_next_track(self) -> None: - """Send next track command.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_next_track) - await self.async_request_refresh() - - async def async_media_previous_track(self) -> None: - """Send previous track command.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_previous_track) - await self.async_request_refresh() - - async def async_select_source(self, source: str) -> None: - """Set the input source.""" - if source in self.content_mapping: - uri = self.content_mapping[source] - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.play_content, uri) - await self.async_request_refresh() - - async def async_send_command(self, command: Iterable[str], repeats: int) -> None: - """Send command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self._send_command, command, repeats) - await self.async_request_refresh() diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 8e59033ffc8..f89880caf89 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -1,4 +1,4 @@ -"""Adds config flow for Bravia TV integration.""" +"""Config flow to configure the Bravia TV integration.""" from __future__ import annotations from contextlib import suppress @@ -6,17 +6,19 @@ import ipaddress import re from typing import Any -from bravia_tv import BraviaRC -from bravia_tv.braviarc import NoIPControl +from aiohttp import CookieJar +from pybravia import BraviaTV, BraviaTVError, BraviaTVNotSupported import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv +from . import BraviaTVCoordinator from .const import ( ATTR_CID, ATTR_MAC, @@ -38,39 +40,15 @@ def host_valid(host: str) -> bool: class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for BraviaTV integration.""" + """Handle a config flow for Bravia TV integration.""" VERSION = 1 + client: BraviaTV + def __init__(self) -> None: - """Initialize.""" - self.braviarc: BraviaRC | None = None - self.host: str | None = None - self.title = "" - self.mac: str | None = None - - async def init_device(self, pin: str) -> None: - """Initialize Bravia TV device.""" - assert self.braviarc is not None - await self.hass.async_add_executor_job( - self.braviarc.connect, pin, CLIENTID_PREFIX, NICKNAME - ) - - connected = await self.hass.async_add_executor_job(self.braviarc.is_connected) - if not connected: - raise CannotConnect() - - system_info = await self.hass.async_add_executor_job( - self.braviarc.get_system_info - ) - if not system_info: - raise ModelNotSupported() - - await self.async_set_unique_id(system_info[ATTR_CID].lower()) - self._abort_if_unique_id_configured() - - self.title = system_info[ATTR_MODEL] - self.mac = system_info[ATTR_MAC] + """Initialize config flow.""" + self.device_config: dict[str, Any] = {} @staticmethod @callback @@ -78,6 +56,24 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Bravia TV options callback.""" return BraviaTVOptionsFlowHandler(config_entry) + async def async_init_device(self) -> FlowResult: + """Initialize and create Bravia TV device from config.""" + pin = self.device_config[CONF_PIN] + + await self.client.connect(pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME) + await self.client.set_wol_mode(True) + + system_info = await self.client.get_system_info() + cid = system_info[ATTR_CID].lower() + title = system_info[ATTR_MODEL] + + self.device_config[CONF_MAC] = system_info[ATTR_MAC] + + await self.async_set_unique_id(cid) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=title, data=self.device_config) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -85,9 +81,14 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - if host_valid(user_input[CONF_HOST]): - self.host = user_input[CONF_HOST] - self.braviarc = BraviaRC(self.host) + host = user_input[CONF_HOST] + if host_valid(host): + session = async_create_clientsession( + self.hass, + cookie_jar=CookieJar(unsafe=True, quote_cookie=False), + ) + self.client = BraviaTV(host=host, session=session) + self.device_config[CONF_HOST] = host return await self.async_step_authorize() @@ -106,23 +107,17 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: + self.device_config[CONF_PIN] = user_input[CONF_PIN] try: - await self.init_device(user_input[CONF_PIN]) - except CannotConnect: - errors["base"] = "cannot_connect" - except ModelNotSupported: + return await self.async_init_device() + except BraviaTVNotSupported: errors["base"] = "unsupported_model" - else: - user_input[CONF_HOST] = self.host - user_input[CONF_MAC] = self.mac - return self.async_create_entry(title=self.title, data=user_input) - # Connecting with th PIN "0000" to start the pairing process on the TV. + except BraviaTVError: + errors["base"] = "cannot_connect" + try: - assert self.braviarc is not None - await self.hass.async_add_executor_job( - self.braviarc.connect, "0000", CLIENTID_PREFIX, NICKNAME - ) - except NoIPControl: + await self.client.pair(CLIENTID_PREFIX, NICKNAME) + except BraviaTVError: return self.async_abort(reason="no_ip_control") return self.async_show_form( @@ -138,26 +133,20 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Bravia TV options flow.""" self.config_entry = config_entry - self.pin = config_entry.data[CONF_PIN] self.ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES) - self.source_list: dict[str, str] = {} + self.source_list: list[str] = [] async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" - coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] - braviarc = coordinator.braviarc - connected = await self.hass.async_add_executor_job(braviarc.is_connected) - if not connected: - await self.hass.async_add_executor_job( - braviarc.connect, self.pin, CLIENTID_PREFIX, NICKNAME - ) + coordinator: BraviaTVCoordinator = self.hass.data[DOMAIN][ + self.config_entry.entry_id + ] - content_mapping = await self.hass.async_add_executor_job( - braviarc.load_source_list - ) - self.source_list = {item: item for item in content_mapping} + await coordinator.async_update_sources() + sources = coordinator.source_map.values() + self.source_list = [item["title"] for item in sources] return await self.async_step_user() async def async_step_user( @@ -177,11 +166,3 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): } ), ) - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class ModelNotSupported(exceptions.HomeAssistantError): - """Error to indicate not supported model.""" diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 4aa44992cbf..6ed8efd3739 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -10,7 +10,6 @@ ATTR_MODEL: Final = "model" CONF_IGNORED_SOURCES: Final = "ignored_sources" -BRAVIA_CONFIG_FILE: Final = "bravia.conf" CLIENTID_PREFIX: Final = "HomeAssistant" DOMAIN: Final = "braviatv" NICKNAME: Final = "Home Assistant" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py new file mode 100644 index 00000000000..b5d91263b34 --- /dev/null +++ b/homeassistant/components/braviatv/coordinator.py @@ -0,0 +1,258 @@ +"""Update coordinator for Bravia TV integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Coroutine, Iterable +from datetime import timedelta +from functools import wraps +import logging +from typing import Any, Final, TypeVar + +from pybravia import BraviaTV, BraviaTVError +from typing_extensions import Concatenate, ParamSpec + +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_APP, + MEDIA_TYPE_CHANNEL, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CLIENTID_PREFIX, DOMAIN, NICKNAME + +_BraviaTVCoordinatorT = TypeVar("_BraviaTVCoordinatorT", bound="BraviaTVCoordinator") +_P = ParamSpec("_P") +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL: Final = timedelta(seconds=10) + + +def catch_braviatv_errors( + func: Callable[Concatenate[_BraviaTVCoordinatorT, _P], Awaitable[None]] +) -> Callable[Concatenate[_BraviaTVCoordinatorT, _P], Coroutine[Any, Any, None]]: + """Catch BraviaTV errors.""" + + @wraps(func) + async def wrapper( + self: _BraviaTVCoordinatorT, + *args: _P.args, + **kwargs: _P.kwargs, + ) -> None: + """Catch BraviaTV errors and log message.""" + try: + await func(self, *args, **kwargs) + except BraviaTVError as err: + _LOGGER.error("Command error: %s", err) + await self.async_request_refresh() + + return wrapper + + +class BraviaTVCoordinator(DataUpdateCoordinator[None]): + """Representation of a Bravia TV Coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + client: BraviaTV, + pin: str, + ignored_sources: list[str], + ) -> None: + """Initialize Bravia TV Client.""" + + self.client = client + self.pin = pin + self.ignored_sources = ignored_sources + self.source: str | None = None + self.source_list: list[str] = [] + self.source_map: dict[str, dict] = {} + self.media_title: str | None = None + self.media_content_id: str | None = None + self.media_content_type: str | None = None + self.media_uri: str | None = None + self.media_duration: int | None = None + self.volume_level: float | None = None + self.volume_target: str | None = None + self.volume_muted = False + self.is_on = False + self.is_channel = False + self.connected = False + # Assume that the TV is in Play mode + self.playing = True + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=1.0, immediate=False + ), + ) + + def _sources_extend(self, sources: list[dict], source_type: str) -> None: + """Extend source map and source list.""" + for item in sources: + item["type"] = source_type + title = item.get("title") + uri = item.get("uri") + if not title or not uri: + continue + self.source_map[uri] = item + if title not in self.ignored_sources: + self.source_list.append(title) + + async def _async_update_data(self) -> None: + """Connect and fetch data.""" + try: + if not self.connected: + await self.client.connect( + pin=self.pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + ) + self.connected = True + + power_status = await self.client.get_power_status() + self.is_on = power_status == "active" + + if self.is_on is False: + return + + if not self.source_map: + await self.async_update_sources() + await self.async_update_volume() + await self.async_update_playing() + except BraviaTVError as err: + self.is_on = False + self.connected = False + raise UpdateFailed("Error communicating with device") from err + + async def async_update_sources(self) -> None: + """Update sources.""" + self.source_list = [] + self.source_map = {} + + externals = await self.client.get_external_status() + self._sources_extend(externals, "input") + + apps = await self.client.get_app_list() + self._sources_extend(apps, "app") + + channels = await self.client.get_content_list_all("tv") + self._sources_extend(channels, "channel") + + async def async_update_volume(self) -> None: + """Update volume information.""" + volume_info = await self.client.get_volume_info() + volume_level = volume_info.get("volume") + if volume_level is not None: + self.volume_level = volume_level / 100 + self.volume_muted = volume_info.get("mute", False) + self.volume_target = volume_info.get("target") + + async def async_update_playing(self) -> None: + """Update current playing information.""" + playing_info = await self.client.get_playing_info() + self.media_title = playing_info.get("title") + self.media_uri = playing_info.get("uri") + self.media_duration = playing_info.get("durationSec") + if program_title := playing_info.get("programTitle"): + self.media_title = f"{self.media_title}: {program_title}" + if self.media_uri: + source = self.source_map.get(self.media_uri, {}) + self.source = source.get("title") + self.is_channel = self.media_uri[:2] == "tv" + if self.is_channel: + self.media_content_id = playing_info.get("dispNum") + self.media_content_type = MEDIA_TYPE_CHANNEL + else: + self.media_content_id = self.media_uri + self.media_content_type = None + else: + self.source = None + self.is_channel = False + self.media_content_id = None + self.media_content_type = None + if not playing_info: + self.media_title = "Smart TV" + self.media_content_type = MEDIA_TYPE_APP + + @catch_braviatv_errors + async def async_turn_on(self) -> None: + """Turn the device on.""" + await self.client.turn_on() + + @catch_braviatv_errors + async def async_turn_off(self) -> None: + """Turn off device.""" + await self.client.turn_off() + + @catch_braviatv_errors + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1.""" + await self.client.volume_level(round(volume * 100)) + + @catch_braviatv_errors + async def async_volume_up(self) -> None: + """Send volume up command to device.""" + await self.client.volume_up() + + @catch_braviatv_errors + async def async_volume_down(self) -> None: + """Send volume down command to device.""" + await self.client.volume_down() + + @catch_braviatv_errors + async def async_volume_mute(self, mute: bool) -> None: + """Send mute command to device.""" + await self.client.volume_mute() + + @catch_braviatv_errors + async def async_media_play(self) -> None: + """Send play command to device.""" + await self.client.play() + self.playing = True + + @catch_braviatv_errors + async def async_media_pause(self) -> None: + """Send pause command to device.""" + await self.client.pause() + self.playing = False + + @catch_braviatv_errors + async def async_media_stop(self) -> None: + """Send stop command to device.""" + await self.client.stop() + + @catch_braviatv_errors + async def async_media_next_track(self) -> None: + """Send next track command.""" + if self.is_channel: + await self.client.channel_up() + else: + await self.client.next_track() + + @catch_braviatv_errors + async def async_media_previous_track(self) -> None: + """Send previous track command.""" + if self.is_channel: + await self.client.channel_down() + else: + await self.client.previous_track() + + @catch_braviatv_errors + async def async_select_source(self, source: str) -> None: + """Set the input source.""" + for uri, item in self.source_map.items(): + if item.get("title") == source: + if item.get("type") == "app": + await self.client.set_active_app(uri) + else: + await self.client.set_play_content(uri) + break + + @catch_braviatv_errors + async def async_send_command(self, command: Iterable[str], repeats: int) -> None: + """Send command to device.""" + for _ in range(repeats): + for cmd in command: + await self.client.send_command(cmd) diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py index ad896ae8c5a..a947513e713 100644 --- a/homeassistant/components/braviatv/entity.py +++ b/homeassistant/components/braviatv/entity.py @@ -1,4 +1,4 @@ -"""A entity class for BraviaTV integration.""" +"""A entity class for Bravia TV integration.""" from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 4ce465abc36..8a18cac5a99 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,9 +2,9 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["bravia-tv==1.0.11"], + "requirements": ["pybravia==0.2.0"], "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, "iot_class": "local_polling", - "loggers": ["bravia_tv"] + "loggers": ["pybravia"] } diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 5d812788563..525e265d415 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -1,4 +1,4 @@ -"""Support for interface with a Bravia TV.""" +"""Media player support for Bravia TV integration.""" from __future__ import annotations from homeassistant.components.media_player import ( @@ -74,7 +74,7 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): @property def is_volume_muted(self) -> bool: """Boolean if volume is currently muted.""" - return self.coordinator.muted + return self.coordinator.volume_muted @property def media_title(self) -> str | None: @@ -84,12 +84,17 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): @property def media_content_id(self) -> str | None: """Content ID of current playing media.""" - return self.coordinator.channel_name + return self.coordinator.media_content_id + + @property + def media_content_type(self) -> str | None: + """Content type of current playing media.""" + return self.coordinator.media_content_type @property def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" - return self.coordinator.duration + return self.coordinator.media_duration async def async_turn_on(self) -> None: """Turn the device on.""" diff --git a/requirements_all.txt b/requirements_all.txt index dcc87172176..d6dec9582f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,9 +436,6 @@ boschshcpy==0.2.30 # homeassistant.components.route53 boto3==1.20.24 -# homeassistant.components.braviatv -bravia-tv==1.0.11 - # homeassistant.components.broadlink broadlink==0.18.2 @@ -1421,6 +1418,9 @@ pyblackbird==0.5 # homeassistant.components.neato pybotvac==0.0.23 +# homeassistant.components.braviatv +pybravia==0.2.0 + # homeassistant.components.nissan_leaf pycarwings2==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1df383e4ea9..185a68ddf04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,9 +343,6 @@ bond-async==0.1.22 # homeassistant.components.bosch_shc boschshcpy==0.2.30 -# homeassistant.components.braviatv -bravia-tv==1.0.11 - # homeassistant.components.broadlink broadlink==0.18.2 @@ -994,6 +991,9 @@ pyblackbird==0.5 # homeassistant.components.neato pybotvac==0.0.23 +# homeassistant.components.braviatv +pybravia==0.2.0 + # homeassistant.components.cloudflare pycfdns==1.2.2 diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index f61a8d312b2..a105f20d3ee 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,7 +1,7 @@ """Define tests for the Bravia TV config flow.""" from unittest.mock import patch -from bravia_tv.braviarc import NoIPControl +from pybravia import BraviaTVConnectionError, BraviaTVNotSupported from homeassistant import data_entry_flow from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN @@ -23,13 +23,13 @@ BRAVIA_SYSTEM_INFO = { "cid": "very_unique_string", } -BRAVIA_SOURCE_LIST = { - "HDMI 1": "extInput:hdmi?port=1", - "HDMI 2": "extInput:hdmi?port=2", - "HDMI 3/ARC": "extInput:hdmi?port=3", - "HDMI 4": "extInput:hdmi?port=4", - "AV/Component": "extInput:component?port=1", -} +BRAVIA_SOURCES = [ + {"title": "HDMI 1", "uri": "extInput:hdmi?port=1"}, + {"title": "HDMI 2", "uri": "extInput:hdmi?port=2"}, + {"title": "HDMI 3/ARC", "uri": "extInput:hdmi?port=3"}, + {"title": "HDMI 4", "uri": "extInput:hdmi?port=4"}, + {"title": "AV/Component", "uri": "extInput:component?port=1"}, +] async def test_show_form(hass): @@ -53,9 +53,10 @@ async def test_user_invalid_host(hass): async def test_authorize_cannot_connect(hass): """Test that errors are shown when cannot connect to host at the authorize step.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=False - ): + with patch( + "pybravia.BraviaTV.connect", + side_effect=BraviaTVConnectionError, + ), patch("pybravia.BraviaTV.pair"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) @@ -68,12 +69,14 @@ async def test_authorize_cannot_connect(hass): async def test_authorize_model_unsupported(hass): """Test that errors are shown when the TV is not supported at the authorize step.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True - ), patch("bravia_tv.BraviaRC.get_system_info", return_value={}): + with patch( + "pybravia.BraviaTV.connect", + side_effect=BraviaTVNotSupported, + ), patch("pybravia.BraviaTV.pair"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "10.10.10.12"} ) + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) @@ -83,13 +86,12 @@ async def test_authorize_model_unsupported(hass): async def test_authorize_no_ip_control(hass): """Test that errors are shown when IP Control is disabled on the TV.""" - with patch("bravia_tv.BraviaRC.connect", side_effect=NoIPControl("No IP Control")): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} + ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "no_ip_control" + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "no_ip_control" async def test_duplicate_error(hass): @@ -106,9 +108,12 @@ async def test_duplicate_error(hass): ) config_entry.add_to_hass(hass) - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True - ), patch("bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO): + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + "pybravia.BraviaTV.set_wol_mode" + ), patch( + "pybravia.BraviaTV.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} @@ -123,10 +128,11 @@ async def test_duplicate_error(hass): async def test_create_entry(hass): """Test that the user step works.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + "pybravia.BraviaTV.set_wol_mode" ), patch( - "bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO + "pybravia.BraviaTV.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True ): @@ -154,10 +160,11 @@ async def test_create_entry(hass): async def test_create_entry_with_ipv6_address(hass): """Test that the user step works with device IPv6 address.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + "pybravia.BraviaTV.set_wol_mode" ), patch( - "bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO + "pybravia.BraviaTV.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True ): @@ -199,19 +206,19 @@ async def test_options_flow(hass): ) config_entry.add_to_hass(hass) - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True - ), patch("bravia_tv.BraviaRC.get_power_status"), patch( - "bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO + with patch("pybravia.BraviaTV.connect"), patch( + "pybravia.BraviaTV.get_power_status", + return_value="active", + ), patch( + "pybravia.BraviaTV.get_external_status", + return_value=BRAVIA_SOURCES, + ), patch( + "pybravia.BraviaTV.send_rest_req", + return_value={}, ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=False - ), patch("bravia_tv.BraviaRC.get_power_status"), patch( - "bravia_tv.BraviaRC.load_source_list", return_value=BRAVIA_SOURCE_LIST - ): result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM From acaa20cabe316a9b325c3dd641fefb1aa59c8e43 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 10 Aug 2022 15:38:40 +0200 Subject: [PATCH 268/903] Improve MQTT warning message on illegal discovery topic (#76545) --- homeassistant/components/mqtt/discovery.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index ebc3a170aeb..8a4c4d0c542 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -106,7 +106,10 @@ async def async_start( # noqa: C901 if not (match := TOPIC_MATCHER.match(topic_trimmed)): if topic_trimmed.endswith("config"): _LOGGER.warning( - "Received message on illegal discovery topic '%s'", topic + "Received message on illegal discovery topic '%s'. The topic contains " + "not allowed characters. For more information see " + "https://www.home-assistant.io/docs/mqtt/discovery/#discovery-topic", + topic, ) return From eeff766078c94e25dc21afc928b236991e7a37ed Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 10 Aug 2022 16:25:03 +0200 Subject: [PATCH 269/903] Improve type hints in xiaomi_miio number entity (#76466) --- .../components/xiaomi_miio/number.py | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 364bd59772c..2d3106ddd4e 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -4,6 +4,8 @@ from __future__ import annotations import dataclasses from dataclasses import dataclass +from miio import Device + from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -12,6 +14,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_DEVICE, @@ -91,11 +94,17 @@ ATTR_VOLUME = "volume" @dataclass -class XiaomiMiioNumberDescription(NumberEntityDescription): +class XiaomiMiioNumberMixin: + """A class that describes number entities.""" + + method: str + + +@dataclass +class XiaomiMiioNumberDescription(NumberEntityDescription, XiaomiMiioNumberMixin): """A class that describes number entities.""" available_with_device_off: bool = True - method: str | None = None @dataclass @@ -325,7 +334,16 @@ async def async_setup_entry( class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, device, entry, unique_id, coordinator, description): + entity_description: XiaomiMiioNumberDescription + + def __init__( + self, + device: Device, + entry: ConfigEntry, + unique_id: str, + coordinator: DataUpdateCoordinator, + description: XiaomiMiioNumberDescription, + ) -> None: """Initialize the generic Xiaomi attribute selector.""" super().__init__(device, entry, unique_id, coordinator) @@ -335,7 +353,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): self.entity_description = description @property - def available(self): + def available(self) -> bool: """Return the number controller availability.""" if ( super().available @@ -345,7 +363,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): return False return super().available - async def async_set_native_value(self, value): + async def async_set_native_value(self, value: float) -> None: """Set an option of the miio device.""" method = getattr(self, self.entity_description.method) if await method(int(value)): @@ -353,7 +371,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): self.async_write_ha_state() @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Fetch state from the device.""" # On state change the device doesn't provide the new state immediately. self._attr_native_value = self._extract_value_from_attribute( @@ -407,7 +425,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): delay_off_countdown * 60, ) - async def async_set_led_brightness_level(self, level: int): + async def async_set_led_brightness_level(self, level: int) -> bool: """Set the led brightness level.""" return await self._try_command( "Setting the led brightness level of the miio device failed.", @@ -415,7 +433,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): level, ) - async def async_set_led_brightness(self, level: int): + async def async_set_led_brightness(self, level: int) -> bool: """Set the led brightness level.""" return await self._try_command( "Setting the led brightness level of the miio device failed.", @@ -423,7 +441,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): level, ) - async def async_set_favorite_rpm(self, rpm: int): + async def async_set_favorite_rpm(self, rpm: int) -> bool: """Set the target motor speed.""" return await self._try_command( "Setting the favorite rpm of the miio device failed.", From 982d197ff3c493024846f38d8688985b6acd3707 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 10 Aug 2022 16:30:58 +0200 Subject: [PATCH 270/903] Add number checks to pylint plugin (#76457) * Add number checks to pylint plugin * Adjust ancestor checks * Add tests * Add comments in tests --- pylint/plugins/hass_enforce_type_hints.py | 65 ++++++++++++++++++++++- tests/pylint/test_enforce_type_hints.py | 37 +++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 257f4fb3613..852c5b544c4 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1465,6 +1465,55 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "number": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="NumberEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["NumberDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="capability_attributes", + return_type="dict[str, Any]", + ), + TypeHintMatch( + function_name="native_min_value", + return_type="float", + ), + TypeHintMatch( + function_name="native_max_value", + return_type="float", + ), + TypeHintMatch( + function_name="native_step", + return_type=["float", None], + ), + TypeHintMatch( + function_name="mode", + return_type="NumberMode", + ), + TypeHintMatch( + function_name="native_unit_of_measurement", + return_type=["str", None], + ), + TypeHintMatch( + function_name="native_value", + return_type=["float", None], + ), + TypeHintMatch( + function_name="set_native_value", + arg_types={1: "float"}, + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "select": [ ClassTypeHintMatch( base_class="Entity", @@ -1599,6 +1648,15 @@ def _is_valid_type( and _is_valid_type(match.group(2), node.slice) ) + # Special case for float in return type + if ( + expected_type == "float" + and in_return + and isinstance(node, nodes.Name) + and node.name in ("float", "int") + ): + return True + # Name occurs when a namespace is not used, eg. "HomeAssistant" if isinstance(node, nodes.Name) and node.name == expected_type: return True @@ -1737,12 +1795,15 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ): self._class_matchers.extend(property_matches) + self._class_matchers.reverse() + def visit_classdef(self, node: nodes.ClassDef) -> None: """Called when a ClassDef node is visited.""" ancestor: nodes.ClassDef checked_class_methods: set[str] = set() - for ancestor in node.ancestors(): - for class_matches in self._class_matchers: + ancestors = list(node.ancestors()) # cache result for inside loop + for class_matches in self._class_matchers: + for ancestor in ancestors: if ancestor.name == class_matches.base_class: self._visit_class_functions( node, class_matches.matches, checked_class_methods diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index d9edde9fdee..b3c233d1c3b 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -900,3 +900,40 @@ def test_invalid_device_class( ), ): type_hint_checker.visit_classdef(class_node) + + +def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: + """Ensure valid hints are accepted for number entity.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + # Ensure that device class is valid despite Entity inheritance + # Ensure that `int` is valid for `float` return type + class_node = astroid.extract_node( + """ + class Entity(): + pass + + class RestoreEntity(Entity): + pass + + class NumberEntity(Entity): + pass + + class MyNumber( #@ + RestoreEntity, NumberEntity + ): + @property + def device_class(self) -> NumberDeviceClass: + pass + + @property + def native_value(self) -> int: + pass + """, + "homeassistant.components.pylint_test.number", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) From 0639681991a0d8cd0235ddc023ea5cb5c883c57a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 10 Aug 2022 18:56:34 +0100 Subject: [PATCH 271/903] Add new Bluetooth coordinator helper for polling mostly passive devices (#76549) --- .../bluetooth/active_update_coordinator.py | 141 +++++++ .../test_active_update_coordinator.py | 348 ++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 homeassistant/components/bluetooth/active_update_coordinator.py create mode 100644 tests/components/bluetooth/test_active_update_coordinator.py diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py new file mode 100644 index 00000000000..1ebd26f8203 --- /dev/null +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -0,0 +1,141 @@ +"""A Bluetooth passive coordinator that collects data from advertisements but can also poll.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +import logging +import time +from typing import Any, Generic, TypeVar + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer + +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak +from .passive_update_processor import PassiveBluetoothProcessorCoordinator + +POLL_DEFAULT_COOLDOWN = 10 +POLL_DEFAULT_IMMEDIATE = True + +_T = TypeVar("_T") + + +class ActiveBluetoothProcessorCoordinator( + Generic[_T], PassiveBluetoothProcessorCoordinator[_T] +): + """ + A coordinator that parses passive data from advertisements but can also poll. + + Every time an advertisement is received, needs_poll_method is called to work + out if a poll is needed. This should return True if it is and False if it is + not needed. + + def needs_poll_method(svc_info: BluetoothServiceInfoBleak, last_poll: float | None) -> bool: + return True + + If there has been no poll since HA started, `last_poll` will be None. Otherwise it is + the number of seconds since one was last attempted. + + If a poll is needed, the coordinator will call poll_method. This is a coroutine. + It should return the same type of data as your update_method. The expectation is that + data from advertisements and from polling are being parsed and fed into a shared + object that represents the current state of the device. + + async def poll_method(svc_info: BluetoothServiceInfoBleak) -> YourDataType: + return YourDataType(....) + + BluetoothServiceInfoBleak.device contains a BLEDevice. You should use this in + your poll function, as it is the most efficient way to get a BleakClient. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + address: str, + mode: BluetoothScanningMode, + update_method: Callable[[BluetoothServiceInfoBleak], _T], + needs_poll_method: Callable[[BluetoothServiceInfoBleak, float | None], bool], + poll_method: Callable[ + [BluetoothServiceInfoBleak], + Coroutine[Any, Any, _T], + ] + | None = None, + poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, + ) -> None: + """Initialize the processor.""" + super().__init__(hass, logger, address, mode, update_method) + + self._needs_poll_method = needs_poll_method + self._poll_method = poll_method + self._last_poll: float | None = None + self.last_poll_successful = True + + # We keep the last service info in case the poller needs to refer to + # e.g. its BLEDevice + self._last_service_info: BluetoothServiceInfoBleak | None = None + + if poll_debouncer is None: + poll_debouncer = Debouncer( + hass, + logger, + cooldown=POLL_DEFAULT_COOLDOWN, + immediate=POLL_DEFAULT_IMMEDIATE, + function=self._async_poll, + ) + else: + poll_debouncer.function = self._async_poll + + self._debounced_poll = poll_debouncer + + def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool: + """Return true if time to try and poll.""" + poll_age: float | None = None + if self._last_poll: + poll_age = time.monotonic() - self._last_poll + return self._needs_poll_method(service_info, poll_age) + + async def _async_poll_data( + self, last_service_info: BluetoothServiceInfoBleak + ) -> _T: + """Fetch the latest data from the source.""" + if self._poll_method is None: + raise NotImplementedError("Poll method not implemented") + return await self._poll_method(last_service_info) + + async def _async_poll(self) -> None: + """Poll the device to retrieve any extra data.""" + assert self._last_service_info + + try: + update = await self._async_poll_data(self._last_service_info) + except Exception: # pylint: disable=broad-except + if self.last_poll_successful: + self.logger.exception("%s: Failure while polling", self.address) + self.last_poll_successful = False + return + finally: + self._last_poll = time.monotonic() + + if not self.last_poll_successful: + self.logger.debug("%s: Polling recovered") + self.last_poll_successful = True + + for processor in self._processors: + processor.async_handle_update(update) + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + super()._async_handle_bluetooth_event(service_info, change) + + self._last_service_info = service_info + + # See if its time to poll + # We use bluetooth events to trigger the poll so that we scan as soon as + # possible after a device comes online or back in range, if a poll is due + if self.needs_poll(service_info): + self.hass.async_create_task(self._debounced_poll.async_call()) diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py new file mode 100644 index 00000000000..24ad96c523e --- /dev/null +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -0,0 +1,348 @@ +"""Tests for the Bluetooth integration PassiveBluetoothDataUpdateCoordinator.""" +from __future__ import annotations + +import asyncio +import logging +from unittest.mock import MagicMock, call, patch + +from homeassistant.components.bluetooth import ( + DOMAIN, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, +) +from homeassistant.components.bluetooth.active_update_coordinator import ( + ActiveBluetoothProcessorCoordinator, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.setup import async_setup_component + +_LOGGER = logging.getLogger(__name__) + + +GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) + + +async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start): + """Test basic usage of the ActiveBluetoothProcessorCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": 0} + + def _poll_needed(*args, **kwargs): + return True + + async def _poll(*args, **kwargs): + return {"testdata": 1} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + + assert coordinator.available is True + + # async_handle_update should have been called twice + # The first time, it was passed the data from parsing the advertisement + # The second time, it was passed the data from polling + assert len(async_handle_update.mock_calls) == 2 + assert async_handle_update.mock_calls[0] == call({"testdata": 0}) + assert async_handle_update.mock_calls[1] == call({"testdata": 1}) + + cancel() + + +async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start): + """Test need_poll callback works and can skip a poll if its not needed.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + flag = True + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + def _poll_needed(*args, **kwargs): + nonlocal flag + return flag + + async def _poll(*args, **kwargs): + return {"testdata": flag} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + poll_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=0, + immediate=True, + ), + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": True}) + + flag = False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": None}) + + flag = True + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": True}) + + cancel() + + +async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_start): + """Test error handling and recovery.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + flag = True + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + def _poll_needed(*args, **kwargs): + return True + + async def _poll(*args, **kwargs): + nonlocal flag + if flag: + raise RuntimeError("Poll failure") + return {"testdata": flag} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + poll_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=0, + immediate=True, + ), + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + # First poll fails + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": None}) + + # Second poll works + flag = False + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": False}) + + cancel() + + +async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start): + """If a poll is queued, by the time it starts it may no longer be needed.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + count = 0 + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + # Only poll once + def _poll_needed(*args, **kwargs): + nonlocal count + return count == 0 + + async def _poll(*args, **kwargs): + nonlocal count + count += 1 + return {"testdata": count} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + # First poll gets queued + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second poll gets stuck behind first poll + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) + + cancel() + + +async def test_rate_limit(hass: HomeAssistant, mock_bleak_scanner_start): + """Test error handling and recovery.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + count = 0 + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + def _poll_needed(*args, **kwargs): + return True + + async def _poll(*args, **kwargs): + nonlocal count + count += 1 + await asyncio.sleep(0) + return {"testdata": count} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + # First poll gets queued + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second poll gets stuck behind first poll + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Third poll gets stuck behind first poll doesn't get queued + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) + + cancel() From 54fc17e10de0752c03d6b95153c3d8168f76ea44 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 10 Aug 2022 20:40:38 +0200 Subject: [PATCH 272/903] Improve type hints in xiaomi_miio vacuum entities (#76563) Co-authored-by: Teemu R. --- .../components/xiaomi_miio/vacuum.py | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 7b866d5ff71..7df38109d16 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -3,6 +3,7 @@ from __future__ import annotations from functools import partial import logging +from typing import Any from miio import DeviceException import voluptuous as vol @@ -214,7 +215,7 @@ class MiroboVacuum( self._handle_coordinator_update() @property - def state(self): + def state(self) -> str | None: """Return the status of the vacuum cleaner.""" # The vacuum reverts back to an idle state after erroring out. # We want to keep returning an error until it has been cleared. @@ -247,7 +248,7 @@ class MiroboVacuum( return [] @property - def timers(self): + def timers(self) -> list[dict[str, Any]]: """Get the list of added timers of the vacuum cleaner.""" return [ { @@ -259,9 +260,9 @@ class MiroboVacuum( ] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the specific state attributes of this vacuum cleaner.""" - attrs = {} + attrs: dict[str, Any] = {} attrs[ATTR_STATUS] = str(self.coordinator.data.status.state) if self.coordinator.data.status.got_error: @@ -281,27 +282,27 @@ class MiroboVacuum( _LOGGER.error(mask_error, exc) return False - async def async_start(self): + async def async_start(self) -> None: """Start or resume the cleaning task.""" await self._try_command( "Unable to start the vacuum: %s", self._device.resume_or_start ) - async def async_pause(self): + async def async_pause(self) -> None: """Pause the cleaning task.""" await self._try_command("Unable to set start/pause: %s", self._device.pause) - async def async_stop(self, **kwargs): + async def async_stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" await self._try_command("Unable to stop: %s", self._device.stop) - async def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" if fan_speed in self.coordinator.data.fan_speeds: - fan_speed = self.coordinator.data.fan_speeds[fan_speed] + fan_speed_int = self.coordinator.data.fan_speeds[fan_speed] else: try: - fan_speed = int(fan_speed) + fan_speed_int = int(fan_speed) except ValueError as exc: _LOGGER.error( "Fan speed step not recognized (%s). Valid speeds are: %s", @@ -310,24 +311,26 @@ class MiroboVacuum( ) return await self._try_command( - "Unable to set fan speed: %s", self._device.set_fan_speed, fan_speed + "Unable to set fan speed: %s", self._device.set_fan_speed, fan_speed_int ) - async def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" await self._try_command("Unable to return home: %s", self._device.home) - async def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" await self._try_command( "Unable to start the vacuum for a spot clean-up: %s", self._device.spot ) - async def async_locate(self, **kwargs): + async def async_locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" await self._try_command("Unable to locate the botvac: %s", self._device.find) - async def async_send_command(self, command, params=None, **kwargs): + async def async_send_command( + self, command: str, params: dict | list | None = None, **kwargs: Any + ) -> None: """Send raw command.""" await self._try_command( "Unable to send command to the vacuum: %s", @@ -336,13 +339,13 @@ class MiroboVacuum( params, ) - async def async_remote_control_start(self): + async def async_remote_control_start(self) -> None: """Start remote control mode.""" await self._try_command( "Unable to start remote control the vacuum: %s", self._device.manual_start ) - async def async_remote_control_stop(self): + async def async_remote_control_stop(self) -> None: """Stop remote control mode.""" await self._try_command( "Unable to stop remote control the vacuum: %s", self._device.manual_stop @@ -350,7 +353,7 @@ class MiroboVacuum( async def async_remote_control_move( self, rotation: int = 0, velocity: float = 0.3, duration: int = 1500 - ): + ) -> None: """Move vacuum with remote control mode.""" await self._try_command( "Unable to move with remote control the vacuum: %s", @@ -362,7 +365,7 @@ class MiroboVacuum( async def async_remote_control_move_step( self, rotation: int = 0, velocity: float = 0.2, duration: int = 1500 - ): + ) -> None: """Move vacuum one step with remote control mode.""" await self._try_command( "Unable to remote control the vacuum: %s", @@ -372,7 +375,7 @@ class MiroboVacuum( duration=duration, ) - async def async_goto(self, x_coord: int, y_coord: int): + async def async_goto(self, x_coord: int, y_coord: int) -> None: """Goto the specified coordinates.""" await self._try_command( "Unable to send the vacuum cleaner to the specified coordinates: %s", @@ -381,7 +384,7 @@ class MiroboVacuum( y_coord=y_coord, ) - async def async_clean_segment(self, segments): + async def async_clean_segment(self, segments) -> None: """Clean the specified segments(s).""" if isinstance(segments, int): segments = [segments] @@ -392,7 +395,7 @@ class MiroboVacuum( segments=segments, ) - async def async_clean_zone(self, zone, repeats=1): + async def async_clean_zone(self, zone: list[Any], repeats: int = 1) -> None: """Clean selected area for the number of repeats indicated.""" for _zone in zone: _zone.append(repeats) From b1497b08579e3a35cd3a478418adb200633cc8cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 09:02:08 -1000 Subject: [PATCH 273/903] Simplify switchbot config flow (#76272) --- .../components/switchbot/__init__.py | 44 ++--- .../components/switchbot/binary_sensor.py | 24 +-- .../components/switchbot/config_flow.py | 171 ++++++++++++----- homeassistant/components/switchbot/const.py | 38 ++-- .../components/switchbot/coordinator.py | 4 + homeassistant/components/switchbot/cover.py | 30 +-- homeassistant/components/switchbot/entity.py | 17 +- homeassistant/components/switchbot/sensor.py | 31 +-- .../components/switchbot/strings.json | 12 +- homeassistant/components/switchbot/switch.py | 31 +-- .../components/switchbot/translations/en.json | 21 ++- tests/components/switchbot/__init__.py | 58 +++--- .../components/switchbot/test_config_flow.py | 177 +++++++++++++++--- 13 files changed, 412 insertions(+), 246 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 7eec785233f..4d9fd2af7b6 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ADDRESS, CONF_MAC, + CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE, Platform, @@ -17,31 +18,26 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr -from .const import ( - ATTR_BOT, - ATTR_CONTACT, - ATTR_CURTAIN, - ATTR_HYGROMETER, - ATTR_MOTION, - ATTR_PLUG, - CONF_RETRY_COUNT, - DEFAULT_RETRY_COUNT, - DOMAIN, -) +from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SupportedModels from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { - ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], - ATTR_PLUG: [Platform.SWITCH, Platform.SENSOR], - ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], - ATTR_HYGROMETER: [Platform.SENSOR], - ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], - ATTR_MOTION: [Platform.BINARY_SENSOR, Platform.SENSOR], + SupportedModels.BULB.value: [Platform.SENSOR], + SupportedModels.BOT.value: [Platform.SWITCH, Platform.SENSOR], + SupportedModels.PLUG.value: [Platform.SWITCH, Platform.SENSOR], + SupportedModels.CURTAIN.value: [ + Platform.COVER, + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], + SupportedModels.HYGROMETER.value: [Platform.SENSOR], + SupportedModels.CONTACT.value: [Platform.BINARY_SENSOR, Platform.SENSOR], + SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR], } CLASS_BY_DEVICE = { - ATTR_CURTAIN: switchbot.SwitchbotCurtain, - ATTR_BOT: switchbot.Switchbot, - ATTR_PLUG: switchbot.SwitchbotPlugMini, + SupportedModels.CURTAIN.value: switchbot.SwitchbotCurtain, + SupportedModels.BOT.value: switchbot.Switchbot, + SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini, } _LOGGER = logging.getLogger(__name__) @@ -49,6 +45,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Switchbot from a config entry.""" + assert entry.unique_id is not None hass.data.setdefault(DOMAIN, {}) if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data: # Bleak uses addresses not mac addresses which are are actually @@ -81,7 +78,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: retry_count=entry.options[CONF_RETRY_COUNT], ) coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator( - hass, _LOGGER, ble_device, device + hass, + _LOGGER, + ble_device, + device, + entry.unique_id, + entry.data.get(CONF_NAME, entry.title), ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index 4da4ed531b0..bf071d64a2d 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -7,7 +7,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,20 +52,10 @@ async def async_setup_entry( ) -> None: """Set up Switchbot curtain based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = entry.unique_id - assert unique_id is not None async_add_entities( - [ - SwitchBotBinarySensor( - coordinator, - unique_id, - binary_sensor, - entry.data[CONF_ADDRESS], - entry.data[CONF_NAME], - ) - for binary_sensor in coordinator.data["data"] - if binary_sensor in BINARY_SENSOR_TYPES - ] + SwitchBotBinarySensor(coordinator, binary_sensor) + for binary_sensor in coordinator.data["data"] + if binary_sensor in BINARY_SENSOR_TYPES ) @@ -78,15 +67,12 @@ class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, binary_sensor: str, - mac: str, - switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, unique_id, mac, name=switchbot_name) + super().__init__(coordinator) self._sensor = binary_sensor - self._attr_unique_id = f"{unique_id}-{binary_sensor}" + self._attr_unique_id = f"{coordinator.base_unique_id}-{binary_sensor}" self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] self._attr_name = self.entity_description.name diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index eaad573d370..0d7e91648f2 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -12,9 +12,9 @@ from homeassistant.components.bluetooth import ( async_discovered_service_info, ) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.const import CONF_ADDRESS, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES @@ -26,6 +26,17 @@ def format_unique_id(address: str) -> str: return address.replace(":", "").lower() +def short_address(address: str) -> str: + """Convert a Bluetooth address to a short address.""" + results = address.replace("-", ":").split(":") + return f"{results[-2].upper()}{results[-1].upper()}"[-4:] + + +def name_from_discovery(discovery: SwitchBotAdvertisement) -> str: + """Get the name from a discovery.""" + return f'{discovery.data["modelFriendlyName"]} {short_address(discovery.address)}' + + class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Switchbot.""" @@ -59,62 +70,128 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_adv = parsed data = parsed.data self.context["title_placeholders"] = { - "name": data["modelName"], - "address": discovery_info.address, + "name": data["modelFriendlyName"], + "address": short_address(discovery_info.address), } - return await self.async_step_user() + if self._discovered_adv.data["isEncrypted"]: + return await self.async_step_password() + return await self.async_step_confirm() + + async def _async_create_entry_from_discovery( + self, user_input: dict[str, Any] + ) -> FlowResult: + """Create an entry from a discovery.""" + assert self._discovered_adv is not None + discovery = self._discovered_adv + name = name_from_discovery(discovery) + model_name = discovery.data["modelName"] + return self.async_create_entry( + title=name, + data={ + **user_input, + CONF_ADDRESS: discovery.address, + CONF_SENSOR_TYPE: str(SUPPORTED_MODEL_TYPES[model_name]), + }, + ) + + async def async_step_confirm(self, user_input: dict[str, Any] = None) -> FlowResult: + """Confirm a single device.""" + assert self._discovered_adv is not None + if user_input is not None: + return await self._async_create_entry_from_discovery(user_input) + + self._set_confirm_only() + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders={ + "name": name_from_discovery(self._discovered_adv) + }, + ) + + async def async_step_password( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the password step.""" + assert self._discovered_adv is not None + if user_input is not None: + # There is currently no api to validate the password + # that does not operate the device so we have + # to accept it as-is + return await self._async_create_entry_from_discovery(user_input) + + return self.async_show_form( + step_id="password", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + description_placeholders={ + "name": name_from_discovery(self._discovered_adv) + }, + ) + + @callback + def _async_discover_devices(self) -> None: + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if ( + format_unique_id(address) in current_addresses + or address in self._discovered_advs + ): + continue + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + if parsed and parsed.data.get("modelName") in SUPPORTED_MODEL_TYPES: + self._discovered_advs[address] = parsed + + if not self._discovered_advs: + raise AbortFlow("no_unconfigured_devices") + + async def _async_set_device(self, discovery: SwitchBotAdvertisement) -> None: + """Set the device to work with.""" + self._discovered_adv = discovery + address = discovery.address + await self.async_set_unique_id( + format_unique_id(address), raise_on_progress=False + ) + self._abort_if_unique_id_configured() async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} - + device_adv: SwitchBotAdvertisement | None = None if user_input is not None: - address = user_input[CONF_ADDRESS] - await self.async_set_unique_id( - format_unique_id(address), raise_on_progress=False - ) - self._abort_if_unique_id_configured() - user_input[CONF_SENSOR_TYPE] = SUPPORTED_MODEL_TYPES[ - self._discovered_advs[address].data["modelName"] - ] - return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + device_adv = self._discovered_advs[user_input[CONF_ADDRESS]] + await self._async_set_device(device_adv) + if device_adv.data["isEncrypted"]: + return await self.async_step_password() + return await self._async_create_entry_from_discovery(user_input) - if discovery := self._discovered_adv: - self._discovered_advs[discovery.address] = discovery - else: - current_addresses = self._async_current_ids() - for discovery_info in async_discovered_service_info(self.hass): - address = discovery_info.address - if ( - format_unique_id(address) in current_addresses - or address in self._discovered_advs - ): - continue - parsed = parse_advertisement_data( - discovery_info.device, discovery_info.advertisement - ) - if parsed and parsed.data.get("modelName") in SUPPORTED_MODEL_TYPES: - self._discovered_advs[address] = parsed + self._async_discover_devices() + if len(self._discovered_advs) == 1: + # If there is only one device we can ask for a password + # or simply confirm it + device_adv = list(self._discovered_advs.values())[0] + await self._async_set_device(device_adv) + if device_adv.data["isEncrypted"]: + return await self.async_step_password() + return await self.async_step_confirm() - if not self._discovered_advs: - return self.async_abort(reason="no_unconfigured_devices") - - data_schema = vol.Schema( - { - vol.Required(CONF_ADDRESS): vol.In( - { - address: f"{parsed.data['modelName']} ({address})" - for address, parsed in self._discovered_advs.items() - } - ), - vol.Required(CONF_NAME): str, - vol.Optional(CONF_PASSWORD): str, - } - ) return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + address: name_from_discovery(parsed) + for address, parsed in self._discovered_advs.items() + } + ), + } + ), + errors=errors, ) diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index a8ec3433f84..6463b9fb4a3 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -1,25 +1,39 @@ """Constants for the switchbot integration.""" +from switchbot import SwitchbotModel + +from homeassistant.backports.enum import StrEnum + DOMAIN = "switchbot" MANUFACTURER = "switchbot" # Config Attributes -ATTR_BOT = "bot" -ATTR_CURTAIN = "curtain" -ATTR_HYGROMETER = "hygrometer" -ATTR_CONTACT = "contact" -ATTR_PLUG = "plug" -ATTR_MOTION = "motion" + DEFAULT_NAME = "Switchbot" + +class SupportedModels(StrEnum): + """Supported Switchbot models.""" + + BOT = "bot" + BULB = "bulb" + CURTAIN = "curtain" + HYGROMETER = "hygrometer" + CONTACT = "contact" + PLUG = "plug" + MOTION = "motion" + + SUPPORTED_MODEL_TYPES = { - "WoHand": ATTR_BOT, - "WoCurtain": ATTR_CURTAIN, - "WoSensorTH": ATTR_HYGROMETER, - "WoContact": ATTR_CONTACT, - "WoPlug": ATTR_PLUG, - "WoPresence": ATTR_MOTION, + SwitchbotModel.BOT: SupportedModels.BOT, + SwitchbotModel.CURTAIN: SupportedModels.CURTAIN, + SwitchbotModel.METER: SupportedModels.HYGROMETER, + SwitchbotModel.CONTACT_SENSOR: SupportedModels.CONTACT, + SwitchbotModel.PLUG_MINI: SupportedModels.PLUG, + SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION, + SwitchbotModel.COLOR_BULB: SupportedModels.BULB, } + # Config Defaults DEFAULT_RETRY_COUNT = 3 diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 43c576249df..ad9aff8c53b 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -37,6 +37,8 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): logger: logging.Logger, ble_device: BLEDevice, device: switchbot.SwitchbotDevice, + base_unique_id: str, + device_name: str, ) -> None: """Initialize global switchbot data updater.""" super().__init__( @@ -45,6 +47,8 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): self.ble_device = ble_device self.device = device self.data: dict[str, Any] = {} + self.device_name = device_name + self.base_unique_id = base_unique_id self._ready_event = asyncio.Event() @callback diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 0ae225f55d7..c2b6cb1a4c7 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -4,8 +4,6 @@ from __future__ import annotations import logging from typing import Any -from switchbot import SwitchbotCurtain - from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, @@ -14,7 +12,6 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -33,19 +30,7 @@ async def async_setup_entry( ) -> None: """Set up Switchbot curtain based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = entry.unique_id - assert unique_id is not None - async_add_entities( - [ - SwitchBotCurtainEntity( - coordinator, - unique_id, - entry.data[CONF_ADDRESS], - entry.data[CONF_NAME], - coordinator.device, - ) - ] - ) + async_add_entities([SwitchBotCurtainEntity(coordinator)]) class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): @@ -59,19 +44,10 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): | CoverEntityFeature.SET_POSITION ) - def __init__( - self, - coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, - address: str, - name: str, - device: SwitchbotCurtain, - ) -> None: + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, unique_id, address, name) - self._attr_unique_id = unique_id + super().__init__(coordinator) self._attr_is_closed = None - self._device = device async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index 4e69da4ec11..2e5ba78dcc8 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -20,24 +20,19 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): coordinator: SwitchbotDataUpdateCoordinator - def __init__( - self, - coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, - address: str, - name: str, - ) -> None: + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the entity.""" super().__init__(coordinator) + self._device = coordinator.device self._last_run_success: bool | None = None - self._unique_id = unique_id - self._address = address - self._attr_name = name + self._address = coordinator.ble_device.address + self._attr_unique_id = coordinator.base_unique_id + self._attr_name = coordinator.device_name self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_BLUETOOTH, self._address)}, manufacturer=MANUFACTURER, model=self.data["modelName"], - name=name, + name=coordinator.device_name, ) if ":" not in self._address: # MacOS Bluetooth addresses are not mac addresses diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index fb24ae22679..886da1051b7 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -9,8 +9,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_ADDRESS, - CONF_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, @@ -73,20 +71,13 @@ async def async_setup_entry( ) -> None: """Set up Switchbot sensor based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = entry.unique_id - assert unique_id is not None async_add_entities( - [ - SwitchBotSensor( - coordinator, - unique_id, - sensor, - entry.data[CONF_ADDRESS], - entry.data[CONF_NAME], - ) - for sensor in coordinator.data["data"] - if sensor in SENSOR_TYPES - ] + SwitchBotSensor( + coordinator, + sensor, + ) + for sensor in coordinator.data["data"] + if sensor in SENSOR_TYPES ) @@ -96,16 +87,14 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, sensor: str, - address: str, - switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, unique_id, address, name=switchbot_name) + super().__init__(coordinator) self._sensor = sensor - self._attr_unique_id = f"{unique_id}-{sensor}" - self._attr_name = f"{switchbot_name} {sensor.replace('_', ' ').title()}" + self._attr_unique_id = f"{coordinator.base_unique_id}-{sensor}" + name = coordinator.device_name + self._attr_name = f"{name} {sensor.replace('_', ' ').title()}" self.entity_description = SENSOR_TYPES[sensor] @property diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 797d1d7613c..c7b0744c579 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -3,10 +3,16 @@ "flow_title": "{name} ({address})", "step": { "user": { - "title": "Setup Switchbot device", "data": { - "address": "Device address", - "name": "[%key:common::config_flow::data::name%]", + "address": "Device address" + } + }, + "confirm": { + "description": "Do you want to setup {name}?" + }, + "password": { + "description": "The {name} device requires a password", + "data": { "password": "[%key:common::config_flow::data::password%]" } } diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 65c7588acbd..17235135cfa 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -4,11 +4,9 @@ from __future__ import annotations import logging from typing import Any -from switchbot import Switchbot - from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_NAME, STATE_ON +from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.restore_state import RestoreEntity @@ -29,19 +27,7 @@ async def async_setup_entry( ) -> None: """Set up Switchbot based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = entry.unique_id - assert unique_id is not None - async_add_entities( - [ - SwitchBotSwitch( - coordinator, - unique_id, - entry.data[CONF_ADDRESS], - entry.data[CONF_NAME], - coordinator.device, - ) - ] - ) + async_add_entities([SwitchBotSwitch(coordinator)]) class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): @@ -49,18 +35,9 @@ class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): _attr_device_class = SwitchDeviceClass.SWITCH - def __init__( - self, - coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, - address: str, - name: str, - device: Switchbot, - ) -> None: + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, unique_id, address, name) - self._attr_unique_id = unique_id - self._device = device + super().__init__(coordinator) self._attr_is_on = False async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index b583c60061b..7e58d169856 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,16 +7,22 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, + "error": {}, "flow_title": "{name} ({address})", "step": { - "user": { + "confirm": { + "description": "Do you want to setup {name}?" + }, + "password": { "data": { - "address": "Device address", - "mac": "Device MAC address", - "name": "Name", "password": "Password" }, - "title": "Setup Switchbot device" + "description": "The {name} device requires a password" + }, + "user": { + "data": { + "address": "Device address" + } } } }, @@ -24,10 +30,7 @@ "step": { "init": { "data": { - "retry_count": "Retry count", - "retry_timeout": "Timeout between retries", - "scan_timeout": "How long to scan for advertisement data", - "update_time": "Time between updates (seconds)" + "retry_count": "Retry count" } } } diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index b4b2e56b39c..fbf764fa4eb 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -5,7 +5,7 @@ from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD +from homeassistant.const import CONF_ADDRESS from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -13,38 +13,18 @@ from tests.common import MockConfigEntry DOMAIN = "switchbot" ENTRY_CONFIG = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_ADDRESS: "e7:89:43:99:99:99", } USER_INPUT = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", -} - -USER_INPUT_CURTAIN = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", -} - -USER_INPUT_SENSOR = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_UNSUPPORTED_DEVICE = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_ADDRESS: "test", } USER_INPUT_INVALID = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_ADDRESS: "invalid-mac", } @@ -90,6 +70,42 @@ WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), ) + + +WOHAND_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoHand", + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"\xc8\x10\xcf"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="798A8547-2A3D-C609-55FF-73FA824B923B", + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoHand", + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"\xc8\x10\xcf"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("798A8547-2A3D-C609-55FF-73FA824B923B", "WoHand"), +) + + +WOHAND_SERVICE_ALT_ADDRESS_INFO = BluetoothServiceInfoBleak( + name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="cc:cc:cc:cc:cc:cc", + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), +) WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak( name="WoCurtain", address="aa:bb:cc:dd:ee:ff", diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 0ae3430eeb1..7ad863cc355 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -10,9 +10,9 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( NOT_SWITCHBOT_INFO, USER_INPUT, - USER_INPUT_CURTAIN, - USER_INPUT_SENSOR, WOCURTAIN_SERVICE_INFO, + WOHAND_ENCRYPTED_SERVICE_INFO, + WOHAND_SERVICE_ALT_ADDRESS_INFO, WOHAND_SERVICE_INFO, WOSENSORTH_SERVICE_INFO, init_integration, @@ -32,27 +32,53 @@ async def test_bluetooth_discovery(hass): data=WOHAND_SERVICE_INFO, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "confirm" with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT, + {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test-name" + assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", } assert len(mock_setup_entry.mock_calls) == 1 +async def test_bluetooth_discovery_requires_password(hass): + """Test discovery via bluetooth with a valid device that needs a password.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOHAND_ENCRYPTED_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "password" + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "abc123"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Bot 923B" + assert result["data"] == { + CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", + CONF_SENSOR_TYPE: "bot", + CONF_PASSWORD: "abc123", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_bluetooth_discovery_already_setup(hass): """Test discovery via bluetooth with a valid device when already setup.""" entry = MockConfigEntry( @@ -97,22 +123,20 @@ async def test_user_setup_wohand(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} + assert result["step_id"] == "confirm" + assert result["errors"] is None with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT, + {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test-name" + assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", } @@ -154,28 +178,129 @@ async def test_user_setup_wocurtain(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["errors"] is None + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Curtain EEFF" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_SENSOR_TYPE: "curtain", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_setup_wocurtain_or_bot(hass): + """Test the user initiated form with valid address.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO, WOHAND_SERVICE_ALT_ADDRESS_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT_CURTAIN, + USER_INPUT, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test-name" + assert result["title"] == "Curtain EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "curtain", } assert len(mock_setup_entry.mock_calls) == 1 +async def test_user_setup_wocurtain_or_bot_with_password(hass): + """Test the user initiated form and valid address and a bot with a password.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO, WOHAND_ENCRYPTED_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "password" + assert result2["errors"] is None + + with patch_async_setup_entry() as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_PASSWORD: "abc123"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Bot 923B" + assert result3["data"] == { + CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", + CONF_PASSWORD: "abc123", + CONF_SENSOR_TYPE: "bot", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_setup_single_bot_with_password(hass): + """Test the user initiated form for a bot with a password.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOHAND_ENCRYPTED_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "password" + assert result["errors"] is None + + with patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "abc123"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Bot 923B" + assert result2["data"] == { + CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", + CONF_PASSWORD: "abc123", + CONF_SENSOR_TYPE: "bot", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_user_setup_wosensor(hass): """Test the user initiated form with password and valid mac.""" with patch( @@ -186,22 +311,20 @@ async def test_user_setup_wosensor(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} + assert result["step_id"] == "confirm" + assert result["errors"] is None with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT_SENSOR, + {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test-name" + assert result["title"] == "Meter EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "hygrometer", } @@ -229,7 +352,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): data=WOCURTAIN_SERVICE_INFO, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "confirm" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", @@ -244,15 +367,13 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): with patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=USER_INPUT, + user_input={}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "test-name" + assert result2["title"] == "Curtain EEFF" assert result2["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "curtain", } From 2f99d6a32d641d1f476e765528e827c79b22302c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 10 Aug 2022 13:51:31 -0600 Subject: [PATCH 274/903] Bump ZHA dependencies (#76565) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index bad84054f1f..1eb2536fed6 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.3", + "bellows==0.32.0", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.49.0", + "zigpy==0.49.1", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.1", "zigpy-znp==0.8.1" diff --git a/requirements_all.txt b/requirements_all.txt index d6dec9582f0..7a8fae075f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.3 +bellows==0.32.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -2545,7 +2545,7 @@ zigpy-zigate==0.9.1 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.49.0 +zigpy==0.49.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 185a68ddf04..d029efd2880 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.3 +bellows==0.32.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -1725,7 +1725,7 @@ zigpy-zigate==0.9.1 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.49.0 +zigpy==0.49.1 # homeassistant.components.zwave_js zwave-js-server-python==0.40.0 From bb0038319dd467ac519e8d2de1e78c3e7c092241 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 10:08:02 -1000 Subject: [PATCH 275/903] Add Yale Access Bluetooth integration (#76182) --- .coveragerc | 4 + CODEOWNERS | 2 + .../components/yalexs_ble/__init__.py | 91 +++ .../components/yalexs_ble/config_flow.py | 245 ++++++ homeassistant/components/yalexs_ble/const.py | 9 + homeassistant/components/yalexs_ble/entity.py | 76 ++ homeassistant/components/yalexs_ble/lock.py | 62 ++ .../components/yalexs_ble/manifest.json | 11 + homeassistant/components/yalexs_ble/models.py | 14 + .../components/yalexs_ble/strings.json | 30 + .../yalexs_ble/translations/en.json | 30 + homeassistant/components/yalexs_ble/util.py | 69 ++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/yalexs_ble/__init__.py | 67 ++ tests/components/yalexs_ble/conftest.py | 8 + .../components/yalexs_ble/test_config_flow.py | 754 ++++++++++++++++++ 19 files changed, 1483 insertions(+) create mode 100644 homeassistant/components/yalexs_ble/__init__.py create mode 100644 homeassistant/components/yalexs_ble/config_flow.py create mode 100644 homeassistant/components/yalexs_ble/const.py create mode 100644 homeassistant/components/yalexs_ble/entity.py create mode 100644 homeassistant/components/yalexs_ble/lock.py create mode 100644 homeassistant/components/yalexs_ble/manifest.json create mode 100644 homeassistant/components/yalexs_ble/models.py create mode 100644 homeassistant/components/yalexs_ble/strings.json create mode 100644 homeassistant/components/yalexs_ble/translations/en.json create mode 100644 homeassistant/components/yalexs_ble/util.py create mode 100644 tests/components/yalexs_ble/__init__.py create mode 100644 tests/components/yalexs_ble/conftest.py create mode 100644 tests/components/yalexs_ble/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 7e60c9ae891..2616f4b6e16 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1483,6 +1483,10 @@ omit = homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* + homeassistant/components/yalexs_ble/__init__.py + homeassistant/components/yalexs_ble/entity.py + homeassistant/components/yalexs_ble/lock.py + homeassistant/components/yalexs_ble/util.py homeassistant/components/yale_smart_alarm/__init__.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 59835aed315..8de29fd5ede 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1246,6 +1246,8 @@ build.json @home-assistant/supervisor /homeassistant/components/xmpp/ @fabaff @flowolf /homeassistant/components/yale_smart_alarm/ @gjohansson-ST /tests/components/yale_smart_alarm/ @gjohansson-ST +/homeassistant/components/yalexs_ble/ @bdraco +/tests/components/yalexs_ble/ @bdraco /homeassistant/components/yamaha_musiccast/ @vigonotion @micha91 /tests/components/yamaha_musiccast/ @vigonotion @micha91 /homeassistant/components/yandex_transport/ @rishatik92 @devbis diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py new file mode 100644 index 00000000000..5a1cf461e5d --- /dev/null +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -0,0 +1,91 @@ +"""The Yale Access Bluetooth integration.""" +from __future__ import annotations + +import asyncio + +import async_timeout +from yalexs_ble import PushLock, local_name_is_unique + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN +from .models import YaleXSBLEData +from .util import async_find_existing_service_info, bluetooth_callback_matcher + +PLATFORMS: list[Platform] = [Platform.LOCK] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Yale Access Bluetooth from a config entry.""" + local_name = entry.data[CONF_LOCAL_NAME] + address = entry.data[CONF_ADDRESS] + key = entry.data[CONF_KEY] + slot = entry.data[CONF_SLOT] + has_unique_local_name = local_name_is_unique(local_name) + push_lock = PushLock(local_name, address, None, key, slot) + id_ = local_name if has_unique_local_name else address + push_lock.set_name(f"{entry.title} ({id_})") + + startup_event = asyncio.Event() + + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a ble callback.""" + push_lock.update_advertisement(service_info.device, service_info.advertisement) + + cancel_first_update = push_lock.register_callback(lambda *_: startup_event.set()) + entry.async_on_unload(await push_lock.start()) + + # We may already have the advertisement, so check for it. + if service_info := async_find_existing_service_info(hass, local_name, address): + push_lock.update_advertisement(service_info.device, service_info.advertisement) + + entry.async_on_unload( + bluetooth.async_register_callback( + hass, + _async_update_ble, + bluetooth_callback_matcher(local_name, push_lock.address), + bluetooth.BluetoothScanningMode.PASSIVE, + ) + ) + + try: + async with async_timeout.timeout(DEVICE_TIMEOUT): + await startup_event.wait() + except asyncio.TimeoutError as ex: + raise ConfigEntryNotReady( + f"{push_lock.last_error}; " + f"Try moving the Bluetooth adapter closer to {local_name}" + ) from ex + finally: + cancel_first_update() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = YaleXSBLEData( + entry.title, push_lock + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] + if entry.title != data.title: + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/yalexs_ble/config_flow.py b/homeassistant/components/yalexs_ble/config_flow.py new file mode 100644 index 00000000000..7f632ebfab0 --- /dev/null +++ b/homeassistant/components/yalexs_ble/config_flow.py @@ -0,0 +1,245 @@ +"""Config flow for Yale Access Bluetooth integration.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +from bleak_retry_connector import BleakError, BLEDevice +import voluptuous as vol +from yalexs_ble import ( + AuthError, + DisconnectedError, + PushLock, + ValidatedLockConfig, + local_name_is_unique, +) +from yalexs_ble.const import YALE_MFR_ID + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.loader import async_get_integration + +from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN +from .util import async_get_service_info, human_readable_name + +_LOGGER = logging.getLogger(__name__) + + +async def validate_lock( + local_name: str, device: BLEDevice, key: str, slot: int +) -> None: + """Validate a lock.""" + if len(key) != 32: + raise InvalidKeyFormat + try: + bytes.fromhex(key) + except ValueError as ex: + raise InvalidKeyFormat from ex + if not isinstance(slot, int) or slot < 0 or slot > 255: + raise InvalidKeyIndex + await PushLock(local_name, device.address, device, key, slot).validate() + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Yale Access Bluetooth.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {} + self._lock_cfg: ValidatedLockConfig | None = None + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + self.context["local_name"] = discovery_info.name + self._discovery_info = discovery_info + self.context["title_placeholders"] = { + "name": human_readable_name( + None, discovery_info.name, discovery_info.address + ), + } + return await self.async_step_user() + + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle a discovered integration.""" + lock_cfg = ValidatedLockConfig( + discovery_info["name"], + discovery_info["address"], + discovery_info["serial"], + discovery_info["key"], + discovery_info["slot"], + ) + # We do not want to raise on progress as integration_discovery takes + # precedence over other discovery flows since we already have the keys. + await self.async_set_unique_id(lock_cfg.address, raise_on_progress=False) + new_data = {CONF_KEY: lock_cfg.key, CONF_SLOT: lock_cfg.slot} + self._abort_if_unique_id_configured(updates=new_data) + for entry in self._async_current_entries(): + if entry.data.get(CONF_LOCAL_NAME) == lock_cfg.local_name: + if self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **new_data} + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + raise AbortFlow(reason="already_configured") + + try: + self._discovery_info = await async_get_service_info( + self.hass, lock_cfg.local_name, lock_cfg.address + ) + except asyncio.TimeoutError: + return self.async_abort(reason="no_devices_found") + + for progress in self._async_in_progress(include_uninitialized=True): + # Integration discovery should abort other discovery types + # since it already has the keys and slots, and the other + # discovery types do not. + context = progress["context"] + if ( + not context.get("active") + and context.get("local_name") == lock_cfg.local_name + or context.get("unique_id") == lock_cfg.address + ): + self.hass.config_entries.flow.async_abort(progress["flow_id"]) + + self._lock_cfg = lock_cfg + self.context["title_placeholders"] = { + "name": human_readable_name( + lock_cfg.name, lock_cfg.local_name, self._discovery_info.address + ) + } + return await self.async_step_integration_discovery_confirm() + + async def async_step_integration_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a confirmation of discovered integration.""" + assert self._discovery_info is not None + assert self._lock_cfg is not None + if user_input is not None: + return self.async_create_entry( + title=self._lock_cfg.name, + data={ + CONF_LOCAL_NAME: self._discovery_info.name, + CONF_ADDRESS: self._discovery_info.address, + CONF_KEY: self._lock_cfg.key, + CONF_SLOT: self._lock_cfg.slot, + }, + ) + + self._set_confirm_only() + return self.async_show_form( + step_id="integration_discovery_confirm", + description_placeholders={ + "name": self._lock_cfg.name, + "address": self._discovery_info.address, + }, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + errors: dict[str, str] = {} + + if user_input is not None: + self.context["active"] = True + address = user_input[CONF_ADDRESS] + discovery_info = self._discovered_devices[address] + local_name = discovery_info.name + key = user_input[CONF_KEY] + slot = user_input[CONF_SLOT] + await self.async_set_unique_id( + discovery_info.address, raise_on_progress=False + ) + self._abort_if_unique_id_configured() + try: + await validate_lock(local_name, discovery_info.device, key, slot) + except InvalidKeyFormat: + errors[CONF_KEY] = "invalid_key_format" + except InvalidKeyIndex: + errors[CONF_SLOT] = "invalid_key_index" + except (DisconnectedError, AuthError, ValueError): + errors[CONF_KEY] = "invalid_auth" + except BleakError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=local_name, + data={ + CONF_LOCAL_NAME: discovery_info.name, + CONF_ADDRESS: discovery_info.address, + CONF_KEY: key, + CONF_SLOT: slot, + }, + ) + + if discovery := self._discovery_info: + self._discovered_devices[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + current_unique_names = { + entry.data.get(CONF_LOCAL_NAME) + for entry in self._async_current_entries() + if local_name_is_unique(entry.data.get(CONF_LOCAL_NAME)) + } + for discovery in async_discovered_service_info(self.hass): + if ( + discovery.address in current_addresses + or discovery.name in current_unique_names + or discovery.address in self._discovered_devices + or YALE_MFR_ID not in discovery.manufacturer_data + ): + continue + self._discovered_devices[discovery.address] = discovery + + if not self._discovered_devices: + return self.async_abort(reason="no_unconfigured_devices") + + data_schema = vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + service_info.address: f"{service_info.name} ({service_info.address})" + for service_info in self._discovered_devices.values() + } + ), + vol.Required(CONF_KEY): str, + vol.Required(CONF_SLOT): int, + } + ) + integration = await async_get_integration(self.hass, DOMAIN) + return self.async_show_form( + step_id="user", + data_schema=data_schema, + errors=errors, + description_placeholders={"docs_url": integration.documentation}, + ) + + +class InvalidKeyFormat(HomeAssistantError): + """Invalid key format.""" + + +class InvalidKeyIndex(HomeAssistantError): + """Invalid key index.""" diff --git a/homeassistant/components/yalexs_ble/const.py b/homeassistant/components/yalexs_ble/const.py new file mode 100644 index 00000000000..f38a376a717 --- /dev/null +++ b/homeassistant/components/yalexs_ble/const.py @@ -0,0 +1,9 @@ +"""Constants for the Yale Access Bluetooth integration.""" + +DOMAIN = "yalexs_ble" + +CONF_LOCAL_NAME = "local_name" +CONF_KEY = "key" +CONF_SLOT = "slot" + +DEVICE_TIMEOUT = 55 diff --git a/homeassistant/components/yalexs_ble/entity.py b/homeassistant/components/yalexs_ble/entity.py new file mode 100644 index 00000000000..fa80698831b --- /dev/null +++ b/homeassistant/components/yalexs_ble/entity.py @@ -0,0 +1,76 @@ +"""The yalexs_ble integration entities.""" +from __future__ import annotations + +from yalexs_ble import ConnectionInfo, LockInfo, LockState + +from homeassistant.components import bluetooth +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN +from .models import YaleXSBLEData + + +class YALEXSBLEEntity(Entity): + """Base class for yale xs ble entities.""" + + _attr_should_poll = False + + def __init__(self, data: YaleXSBLEData) -> None: + """Initialize the entity.""" + self._data = data + self._device = device = data.lock + self._attr_available = False + lock_state = device.lock_state + lock_info = device.lock_info + connection_info = device.connection_info + assert lock_state is not None + assert connection_info is not None + assert lock_info is not None + self._attr_unique_id = device.address + self._attr_device_info = DeviceInfo( + name=data.title, + manufacturer=lock_info.manufacturer, + model=lock_info.model, + connections={(dr.CONNECTION_BLUETOOTH, device.address)}, + identifiers={(DOMAIN, lock_info.serial)}, + sw_version=lock_info.firmware, + ) + if device.lock_state: + self._async_update_state(lock_state, lock_info, connection_info) + + @callback + def _async_update_state( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Update the state.""" + self._attr_available = True + + @callback + def _async_state_changed( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Handle state changed.""" + self._async_update_state(new_state, lock_info, connection_info) + self.async_write_ha_state() + + @callback + def _async_device_unavailable(self, _address: str) -> None: + """Handle device not longer being seen by the bluetooth stack.""" + self._attr_available = False + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + bluetooth.async_track_unavailable( + self.hass, self._async_device_unavailable, self._device.address + ) + ) + self.async_on_remove(self._device.register_callback(self._async_state_changed)) + return await super().async_added_to_hass() + + async def async_update(self) -> None: + """Request a manual update.""" + await self._device.update() diff --git a/homeassistant/components/yalexs_ble/lock.py b/homeassistant/components/yalexs_ble/lock.py new file mode 100644 index 00000000000..3f75a282f67 --- /dev/null +++ b/homeassistant/components/yalexs_ble/lock.py @@ -0,0 +1,62 @@ +"""Support for Yale Access Bluetooth locks.""" +from __future__ import annotations + +from typing import Any + +from yalexs_ble import ConnectionInfo, LockInfo, LockState, LockStatus + +from homeassistant.components.lock import LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import YALEXSBLEEntity +from .models import YaleXSBLEData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up locks.""" + data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities([YaleXSBLELock(data)]) + + +class YaleXSBLELock(YALEXSBLEEntity, LockEntity): + """A yale xs ble lock.""" + + _attr_has_entity_name = True + + @callback + def _async_update_state( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Update the state.""" + self._attr_is_locked = False + self._attr_is_locking = False + self._attr_is_unlocking = False + self._attr_is_jammed = False + lock_state = new_state.lock + if lock_state == LockStatus.LOCKED: + self._attr_is_locked = True + elif lock_state == LockStatus.LOCKING: + self._attr_is_locking = True + elif lock_state == LockStatus.UNLOCKING: + self._attr_is_unlocking = True + elif lock_state in ( + LockStatus.UNKNOWN_01, + LockStatus.UNKNOWN_06, + ): + self._attr_is_jammed = True + super()._async_update_state(new_state, lock_info, connection_info) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the lock.""" + return await self._device.unlock() + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the lock.""" + return await self._device.lock() diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json new file mode 100644 index 00000000000..8f7838792ff --- /dev/null +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "yalexs_ble", + "name": "Yale Access Bluetooth", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", + "requirements": ["yalexs-ble==1.1.2"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "bluetooth": [{ "manufacturer_id": 465 }], + "iot_class": "local_push" +} diff --git a/homeassistant/components/yalexs_ble/models.py b/homeassistant/components/yalexs_ble/models.py new file mode 100644 index 00000000000..d79668f1c70 --- /dev/null +++ b/homeassistant/components/yalexs_ble/models.py @@ -0,0 +1,14 @@ +"""The yalexs_ble integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from yalexs_ble import PushLock + + +@dataclass +class YaleXSBLEData: + """Data for the yale xs ble integration.""" + + title: str + lock: PushLock diff --git a/homeassistant/components/yalexs_ble/strings.json b/homeassistant/components/yalexs_ble/strings.json new file mode 100644 index 00000000000..4d867474dbe --- /dev/null +++ b/homeassistant/components/yalexs_ble/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Check the documentation at {docs_url} for how to find the offline key.", + "data": { + "address": "Bluetooth address", + "key": "Offline Key (32-byte hex string)", + "slot": "Offline Key Slot (Integer between 0 and 255)" + } + }, + "integration_discovery_confirm": { + "description": "Do you want to setup {name} over Bluetooth with address {address}?" + } + }, + "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%]", + "invalid_key_format": "The offline key must be a 32-byte hex string.", + "invalid_key_index": "The offline key slot must be an integer between 0 and 255." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_unconfigured_devices": "No unconfigured devices found.", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/yalexs_ble/translations/en.json b/homeassistant/components/yalexs_ble/translations/en.json new file mode 100644 index 00000000000..6d817499270 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "no_devices_found": "No devices found on the network", + "no_unconfigured_devices": "No unconfigured devices found." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_key_format": "The offline key must be a 32-byte hex string.", + "invalid_key_index": "The offline key slot must be an integer between 0 and 255.", + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Do you want to setup {name} over Bluetooth with address {address}?" + }, + "user": { + "data": { + "address": "Bluetooth address", + "key": "Offline Key (32-byte hex string)", + "slot": "Offline Key Slot (Integer between 0 and 255)" + }, + "description": "Check the documentation at {docs_url} for how to find the offline key." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/util.py b/homeassistant/components/yalexs_ble/util.py new file mode 100644 index 00000000000..a1c6fdf7d32 --- /dev/null +++ b/homeassistant/components/yalexs_ble/util.py @@ -0,0 +1,69 @@ +"""The yalexs_ble integration models.""" +from __future__ import annotations + +from yalexs_ble import local_name_is_unique + +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_discovered_service_info, + async_process_advertisements, +) +from homeassistant.components.bluetooth.match import ( + ADDRESS, + LOCAL_NAME, + BluetoothCallbackMatcher, +) +from homeassistant.core import HomeAssistant, callback + +from .const import DEVICE_TIMEOUT + + +def bluetooth_callback_matcher( + local_name: str, address: str +) -> BluetoothCallbackMatcher: + """Return a BluetoothCallbackMatcher for the given local_name and address.""" + if local_name_is_unique(local_name): + return BluetoothCallbackMatcher({LOCAL_NAME: local_name}) + return BluetoothCallbackMatcher({ADDRESS: address}) + + +@callback +def async_find_existing_service_info( + hass: HomeAssistant, local_name: str, address: str +) -> BluetoothServiceInfoBleak | None: + """Return the service info for the given local_name and address.""" + has_unique_local_name = local_name_is_unique(local_name) + for service_info in async_discovered_service_info(hass): + device = service_info.device + if ( + has_unique_local_name and device.name == local_name + ) or device.address == address: + return service_info + return None + + +async def async_get_service_info( + hass: HomeAssistant, local_name: str, address: str +) -> BluetoothServiceInfoBleak: + """Wait for the service info for the given local_name and address.""" + if service_info := async_find_existing_service_info(hass, local_name, address): + return service_info + return await async_process_advertisements( + hass, + lambda service_info: True, + bluetooth_callback_matcher(local_name, address), + BluetoothScanningMode.ACTIVE, + DEVICE_TIMEOUT, + ) + + +def short_address(address: str) -> str: + """Convert a Bluetooth address to a short address.""" + split_address = address.replace("-", ":").split(":") + return f"{split_address[-2].upper()}{split_address[-1].upper()}"[-4:] + + +def human_readable_name(name: str | None, local_name: str, address: str) -> str: + """Return a human readable name for the given name, local_name, and address.""" + return f"{name or local_name} ({short_address(address)})" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index d7af6e6ee11..7f0696f1a76 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -107,5 +107,9 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ { "domain": "xiaomi_ble", "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "yalexs_ble", + "manufacturer_id": 465 } ] diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a582f2b719c..a20f1229a39 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -430,6 +430,7 @@ FLOWS = { "xiaomi_ble", "xiaomi_miio", "yale_smart_alarm", + "yalexs_ble", "yamaha_musiccast", "yeelight", "yolink", diff --git a/requirements_all.txt b/requirements_all.txt index 7a8fae075f4..1927739987c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2499,6 +2499,9 @@ xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm yalesmartalarmclient==0.3.8 +# homeassistant.components.yalexs_ble +yalexs-ble==1.1.2 + # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d029efd2880..72bf0741f25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1694,6 +1694,9 @@ xmltodict==0.13.0 # homeassistant.components.yale_smart_alarm yalesmartalarmclient==0.3.8 +# homeassistant.components.yalexs_ble +yalexs-ble==1.1.2 + # homeassistant.components.august yalexs==1.2.1 diff --git a/tests/components/yalexs_ble/__init__.py b/tests/components/yalexs_ble/__init__.py new file mode 100644 index 00000000000..eb6800ff83a --- /dev/null +++ b/tests/components/yalexs_ble/__init__.py @@ -0,0 +1,67 @@ +"""Tests for the Yale Access Bluetooth integration.""" +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + +YALE_ACCESS_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="M1012LU", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={ + 465: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 76: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="M1012LU"), + advertisement=AdvertisementData(), +) + + +LOCK_DISCOVERY_INFO_UUID_ADDRESS = BluetoothServiceInfoBleak( + name="M1012LU", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-60, + manufacturer_data={ + 465: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 76: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="M1012LU"), + advertisement=AdvertisementData(), +) + +OLD_FIRMWARE_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="Aug", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={ + 465: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 76: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"), + advertisement=AdvertisementData(), +) + + +NOT_YALE_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="Not", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={ + 33: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 21: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"), + advertisement=AdvertisementData(), +) diff --git a/tests/components/yalexs_ble/conftest.py b/tests/components/yalexs_ble/conftest.py new file mode 100644 index 00000000000..c2b947cc863 --- /dev/null +++ b/tests/components/yalexs_ble/conftest.py @@ -0,0 +1,8 @@ +"""yalexs_ble session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py new file mode 100644 index 00000000000..7607b710934 --- /dev/null +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -0,0 +1,754 @@ +"""Test the Yale Access Bluetooth config flow.""" +import asyncio +from unittest.mock import patch + +from bleak import BleakError +from yalexs_ble import AuthError + +from homeassistant import config_entries +from homeassistant.components.yalexs_ble.const import ( + CONF_KEY, + CONF_LOCAL_NAME, + CONF_SLOT, + DOMAIN, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + LOCK_DISCOVERY_INFO_UUID_ADDRESS, + NOT_YALE_DISCOVERY_INFO, + OLD_FIRMWARE_LOCK_DISCOVERY_INFO, + YALE_ACCESS_LOCK_DISCOVERY_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_user_step_success(hass: HomeAssistant) -> None: + """Test user step success path.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[NOT_YALE_DISCOVERY_INFO, YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: + """Test user step with no devices found.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[NOT_YALE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: + """Test user step with only existing devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + unique_id=YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: + """Test user step with invalid keys tried first.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "dog", + CONF_SLOT: 66, + }, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {CONF_KEY: "invalid_key_format"} + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "qfd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + assert result3["type"] == FlowResultType.FORM + assert result3["step_id"] == "user" + assert result3["errors"] == {CONF_KEY: "invalid_key_format"} + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 999, + }, + ) + assert result4["type"] == FlowResultType.FORM + assert result4["step_id"] == "user" + assert result4["errors"] == {CONF_SLOT: "invalid_key_index"} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result5["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result5["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: + """Test user step and we cannot connect.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + side_effect=BleakError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_auth_exception(hass: HomeAssistant) -> None: + """Test user step with an authentication exception.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO, NOT_YALE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + side_effect=AuthError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {CONF_KEY: "invalid_auth"} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: + """Test user step with an unknown exception.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[NOT_YALE_DISCOVERY_INFO, YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + side_effect=RuntimeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_step_success(hass: HomeAssistant) -> None: + """Test bluetooth step success path.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_integration_discovery_success(hass: HomeAssistant) -> None: + """Test integration discovery step success path.""" + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "integration_discovery_confirm" + assert result["errors"] is None + + with patch( + "homeassistant.components.yalexs_ble.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"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Front Door" + assert result2["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_integration_discovery_device_not_found(hass: HomeAssistant) -> None: + """Test integration discovery when the device is not found.""" + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_integration_discovery_takes_precedence_over_bluetooth( + hass: HomeAssistant, +) -> None: + """Test integration discovery dismisses bluetooth discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + assert flows[0]["context"]["unique_id"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert flows[0]["context"]["local_name"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "integration_discovery_confirm" + assert result["errors"] is None + + # the bluetooth flow should get dismissed in favor + # of the integration discovery flow since the integration + # discovery flow will have the keys and the bluetooth + # flow will not + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + + with patch( + "homeassistant.components.yalexs_ble.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"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Front Door" + assert result2["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 0 + + +async def test_integration_discovery_updates_key_unique_local_name( + hass: HomeAssistant, +) -> None: + """Test integration discovery updates the key with a unique local name.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LOCAL_NAME: LOCK_DISCOVERY_INFO_UUID_ADDRESS.name, + CONF_ADDRESS: "61DE521B-F0BF-9F44-64D4-75BBE1738105", + CONF_KEY: "5fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 11, + }, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": "AA:BB:CC:DD:EE:FF", + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" + assert entry.data[CONF_SLOT] == 66 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_integration_discovery_updates_key_without_unique_local_name( + hass: HomeAssistant, +) -> None: + """Test integration discovery updates the key without a unique local name.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LOCAL_NAME: OLD_FIRMWARE_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "5fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 11, + }, + unique_id=OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" + assert entry.data[CONF_SLOT] == 66 + + +async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_address( + hass: HomeAssistant, +) -> None: + """Test integration discovery dismisses bluetooth discovery with a uuid address.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + assert flows[0]["context"]["unique_id"] == LOCK_DISCOVERY_INFO_UUID_ADDRESS.address + assert flows[0]["context"]["local_name"] == LOCK_DISCOVERY_INFO_UUID_ADDRESS.name + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": "AA:BB:CC:DD:EE:FF", + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "integration_discovery_confirm" + assert result["errors"] is None + + # the bluetooth flow should get dismissed in favor + # of the integration discovery flow since the integration + # discovery flow will have the keys and the bluetooth + # flow will not + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + + with patch( + "homeassistant.components.yalexs_ble.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"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Front Door" + assert result2["data"] == { + CONF_LOCAL_NAME: LOCK_DISCOVERY_INFO_UUID_ADDRESS.name, + CONF_ADDRESS: LOCK_DISCOVERY_INFO_UUID_ADDRESS.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 0 + + +async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_local_name( + hass: HomeAssistant, +) -> None: + """Test integration discovery dismisses bluetooth discovery with a non unique local name.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=OLD_FIRMWARE_LOCK_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + assert flows[0]["context"]["unique_id"] == OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address + assert flows[0]["context"]["local_name"] == OLD_FIRMWARE_LOCK_DISCOVERY_INFO.name + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=OLD_FIRMWARE_LOCK_DISCOVERY_INFO, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "integration_discovery_confirm" + assert result["errors"] is None + + # the bluetooth flow should get dismissed in favor + # of the integration discovery flow since the integration + # discovery flow will have the keys and the bluetooth + # flow will not + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 From 9555df88c812863084f85116772c44157407f1f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 10 Aug 2022 23:49:02 +0200 Subject: [PATCH 276/903] Improve type hints in zwave_me number entity (#76469) --- homeassistant/components/zwave_me/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_me/number.py b/homeassistant/components/zwave_me/number.py index 2fa82514626..4e9acc1f76b 100644 --- a/homeassistant/components/zwave_me/number.py +++ b/homeassistant/components/zwave_me/number.py @@ -40,7 +40,7 @@ class ZWaveMeNumber(ZWaveMeEntity, NumberEntity): """Representation of a ZWaveMe Multilevel Switch.""" @property - def native_value(self): + def native_value(self) -> float: """Return the unit of measurement.""" if self.device.level == 99: # Scale max value return 100 From 58ac3eee3b8c69b8f54b8d40187a94ecbbfa7e4c Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 10 Aug 2022 17:56:20 -0400 Subject: [PATCH 277/903] Always round down for Mazda odometer entity (#76500) --- homeassistant/components/mazda/sensor.py | 3 ++- tests/components/mazda/test_sensor.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index c688ac62637..715b274b6f5 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -116,7 +116,8 @@ def _fuel_distance_remaining_value(data, unit_system): def _odometer_value(data, unit_system): """Get the odometer value.""" - return round(unit_system.length(data["status"]["odometerKm"], LENGTH_KILOMETERS)) + # In order to match the behavior of the Mazda mobile app, we always round down + return int(unit_system.length(data["status"]["odometerKm"], LENGTH_KILOMETERS)) def _front_left_tire_pressure_value(data, unit_system): diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index 763e1490e89..2284101fa84 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -63,7 +63,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:speedometer" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.state == "2796" + assert state.state == "2795" entry = entity_registry.async_get("sensor.my_mazda3_odometer") assert entry assert entry.unique_id == "JM000000000000000_odometer" From 2bab6447a924d63d253b210f9a6ab3ea3ca67e7d Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 10 Aug 2022 23:57:58 +0200 Subject: [PATCH 278/903] Replaces aiohttp.hdrs CONTENT_TYPE with plain string for the Swisscom integration (#76568) --- homeassistant/components/swisscom/device_tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 3c8e7341be2..d95067e6b33 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -4,7 +4,6 @@ from __future__ import annotations from contextlib import suppress import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -79,7 +78,7 @@ class SwisscomDeviceScanner(DeviceScanner): def get_swisscom_data(self): """Retrieve data from Swisscom and return parsed result.""" url = f"http://{self.host}/ws" - headers = {CONTENT_TYPE: "application/x-sah-ws-4-call+json"} + headers = {"Content-Type": "application/x-sah-ws-4-call+json"} data = """ {"service":"Devices", "method":"get", "parameters":{"expression":"lan and not self"}}""" From d81298a2d655493b31b43b0fde3062b5322696c2 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Wed, 10 Aug 2022 17:59:50 -0400 Subject: [PATCH 279/903] Add sensor state class for SleepIQ sensors (#76372) Co-authored-by: Inca --- homeassistant/components/sleepiq/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index b101aee8a6e..71618dab056 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from asyncsleepiq import SleepIQBed, SleepIQSleeper -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -45,6 +45,7 @@ class SleepIQSensorEntity(SleepIQSleeperEntity, SensorEntity): ) -> None: """Initialize the sensor.""" self.sensor_type = sensor_type + self._attr_state_class = SensorStateClass.MEASUREMENT super().__init__(coordinator, bed, sleeper, sensor_type) @callback From 52fd63acbce6d5b31f751cf82bbdca5ae39b8803 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 10 Aug 2022 18:05:32 -0400 Subject: [PATCH 280/903] Use generators for async_add_entities in Accuweather (#76574) --- .../components/accuweather/sensor.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 72182d4d635..c13dedcdceb 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -320,19 +320,19 @@ async def async_setup_entry( coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - sensors: list[AccuWeatherSensor] = [] - for description in SENSOR_TYPES: - sensors.append(AccuWeatherSensor(coordinator, description)) + sensors = [ + AccuWeatherSensor(coordinator, description) for description in SENSOR_TYPES + ] if coordinator.forecast: - for description in FORECAST_SENSOR_TYPES: - for day in range(MAX_FORECAST_DAYS + 1): - # Some air quality/allergy sensors are only available for certain - # locations. - if description.key in coordinator.data[ATTR_FORECAST][0]: - sensors.append( - AccuWeatherSensor(coordinator, description, forecast_day=day) - ) + # Some air quality/allergy sensors are only available for certain + # locations. + sensors.extend( + AccuWeatherSensor(coordinator, description, forecast_day=day) + for description in FORECAST_SENSOR_TYPES + for day in range(MAX_FORECAST_DAYS + 1) + if description.key in coordinator.data[ATTR_FORECAST][0] + ) async_add_entities(sensors) From ca3033b84cd5a982ac50388c7a2828a08c14862f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 10 Aug 2022 18:09:05 -0400 Subject: [PATCH 281/903] Use generators for async_add_entities in Abode (#76569) --- .../components/abode/binary_sensor.py | 10 ++++------ homeassistant/components/abode/camera.py | 9 ++++----- homeassistant/components/abode/cover.py | 10 ++++------ homeassistant/components/abode/light.py | 10 ++++------ homeassistant/components/abode/lock.py | 10 ++++------ homeassistant/components/abode/sensor.py | 19 ++++++------------- homeassistant/components/abode/switch.py | 16 +++++++++------- 7 files changed, 35 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 58bb101cb5d..4f7af8af640 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -30,12 +30,10 @@ async def async_setup_entry( CONST.TYPE_OPENING, ] - entities = [] - - for device in data.abode.get_devices(generic_type=device_types): - entities.append(AbodeBinarySensor(data, device)) - - async_add_entities(entities) + async_add_entities( + AbodeBinarySensor(data, device) + for device in data.abode.get_devices(generic_type=device_types) + ) class AbodeBinarySensor(AbodeDevice, BinarySensorEntity): diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 9885ccb54ef..d0f428a45fa 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -28,12 +28,11 @@ async def async_setup_entry( ) -> None: """Set up Abode camera devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): - entities.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) - - async_add_entities(entities) + async_add_entities( + AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) + for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA) + ) class AbodeCamera(AbodeDevice, Camera): diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 7943056f8ac..b48f00209ec 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -19,12 +19,10 @@ async def async_setup_entry( """Set up Abode cover devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - - for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): - entities.append(AbodeCover(data, device)) - - async_add_entities(entities) + async_add_entities( + AbodeCover(data, device) + for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER) + ) class AbodeCover(AbodeDevice, CoverEntity): diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 1bb9d41f461..030e5744ce8 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -32,12 +32,10 @@ async def async_setup_entry( """Set up Abode light devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - - for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): - entities.append(AbodeLight(data, device)) - - async_add_entities(entities) + async_add_entities( + AbodeLight(data, device) + for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT) + ) class AbodeLight(AbodeDevice, LightEntity): diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 368769003b0..12258a45aaf 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -19,12 +19,10 @@ async def async_setup_entry( """Set up Abode lock devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - - for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): - entities.append(AbodeLock(data, device)) - - async_add_entities(entities) + async_add_entities( + AbodeLock(data, device) + for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK) + ) class AbodeLock(AbodeDevice, LockEntity): diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 2564b775cf9..da854153293 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -42,19 +42,12 @@ async def async_setup_entry( """Set up Abode sensor devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - - for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): - conditions = device.get_value(CONST.STATUSES_KEY) - entities.extend( - [ - AbodeSensor(data, device, description) - for description in SENSOR_TYPES - if description.key in conditions - ] - ) - - async_add_entities(entities) + async_add_entities( + AbodeSensor(data, device, description) + for description in SENSOR_TYPES + for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR) + if description.key in device.get_value(CONST.STATUSES_KEY) + ) class AbodeSensor(AbodeDevice, SensorEntity): diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 5ed32b4f83c..f472a5028c0 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -25,14 +25,16 @@ async def async_setup_entry( """Set up Abode switch devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities: list[SwitchEntity] = [] + entities: list[SwitchEntity] = [ + AbodeSwitch(data, device) + for device_type in DEVICE_TYPES + for device in data.abode.get_devices(generic_type=device_type) + ] - for device_type in DEVICE_TYPES: - for device in data.abode.get_devices(generic_type=device_type): - entities.append(AbodeSwitch(data, device)) - - for automation in data.abode.get_automations(): - entities.append(AbodeAutomationSwitch(data, automation)) + entities.extend( + AbodeAutomationSwitch(data, automation) + for automation in data.abode.get_automations() + ) async_add_entities(entities) From f7c23fe19363ab16a2a9c33e1eb9baf1e08e4eb9 Mon Sep 17 00:00:00 2001 From: Alastair D'Silva Date: Thu, 11 Aug 2022 08:09:37 +1000 Subject: [PATCH 282/903] Handle EmonCMS feeds that return NULL gracefully (#76074) --- homeassistant/components/emoncms/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index af684067ec8..0aab21458f4 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -192,8 +192,10 @@ class EmonCmsSensor(SensorEntity): self._state = self._value_template.render_with_possible_json_value( elem["value"], STATE_UNKNOWN ) - else: + elif elem["value"] is not None: self._state = round(float(elem["value"]), DECIMALS) + else: + self._state = None @property def name(self): @@ -255,8 +257,10 @@ class EmonCmsSensor(SensorEntity): self._state = self._value_template.render_with_possible_json_value( elem["value"], STATE_UNKNOWN ) - else: + elif elem["value"] is not None: self._state = round(float(elem["value"]), DECIMALS) + else: + self._state = None class EmonCmsData: From c8f11d65d2a4ddf729107c9a6dabec5bfaabc173 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 00:10:28 +0200 Subject: [PATCH 283/903] Improve type hints in demo and mqtt number entity (#76464) --- homeassistant/components/demo/number.py | 2 +- homeassistant/components/mqtt/number.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 17382ab9962..9613aa247fb 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -131,7 +131,7 @@ class DemoNumber(NumberEntity): name=self.name, ) - async def async_set_native_value(self, value): + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" self._attr_native_value = value self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index fbadd653df7..4e9f237431a 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -272,7 +272,7 @@ class MqttNumber(MqttEntity, RestoreNumber): return self._config.get(CONF_UNIT_OF_MEASUREMENT) @property - def native_value(self): + def native_value(self) -> float | None: """Return the current value.""" return self._current_number From 46d3f2e14c90026c0c99f1ebd0ef7d350d8d44ba Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 00:12:47 +0200 Subject: [PATCH 284/903] Improve type hints in freedompro lights (#76045) --- homeassistant/components/freedompro/light.py | 32 ++++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index 7659a136927..d3f99cbd4e0 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -1,5 +1,8 @@ """Support for Freedompro light.""" +from __future__ import annotations + import json +from typing import Any from pyfreedompro import put_state @@ -17,6 +20,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FreedomproDataUpdateCoordinator from .const import DOMAIN @@ -24,8 +28,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Freedompro light.""" - api_key = entry.data[CONF_API_KEY] - coordinator = hass.data[DOMAIN][entry.entry_id] + api_key: str = entry.data[CONF_API_KEY] + coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data @@ -36,7 +40,13 @@ async def async_setup_entry( class Device(CoordinatorEntity, LightEntity): """Representation of an Freedompro light.""" - def __init__(self, hass, api_key, device, coordinator): + def __init__( + self, + hass: HomeAssistant, + api_key: str, + device: dict[str, Any], + coordinator: FreedomproDataUpdateCoordinator, + ) -> None: """Initialize the Freedompro light.""" super().__init__(coordinator) self._session = aiohttp_client.async_get_clientsession(hass) @@ -44,9 +54,7 @@ class Device(CoordinatorEntity, LightEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, self.unique_id), - }, + identifiers={(DOMAIN, device["uid"])}, manufacturer="Freedompro", model=device["type"], name=self.name, @@ -87,31 +95,29 @@ class Device(CoordinatorEntity, LightEntity): await super().async_added_to_hass() self._handle_coordinator_update() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Async function to set on to light.""" - payload = {"on": True} + payload: dict[str, Any] = {"on": True} if ATTR_BRIGHTNESS in kwargs: payload["brightness"] = round(kwargs[ATTR_BRIGHTNESS] / 255 * 100) if ATTR_HS_COLOR in kwargs: payload["saturation"] = round(kwargs[ATTR_HS_COLOR][1]) payload["hue"] = round(kwargs[ATTR_HS_COLOR][0]) - payload = json.dumps(payload) await put_state( self._session, self._api_key, self._attr_unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Async function to set off to light.""" payload = {"on": False} - payload = json.dumps(payload) await put_state( self._session, self._api_key, self._attr_unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() From 0edf82fcb4fd38d31d4b6b871fe5fd85e17f2e69 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 00:16:38 +0200 Subject: [PATCH 285/903] Improve type hints in yamaha_musiccast number (#76467) --- homeassistant/components/yamaha_musiccast/number.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/number.py b/homeassistant/components/yamaha_musiccast/number.py index b05c47ce279..98cda92ffea 100644 --- a/homeassistant/components/yamaha_musiccast/number.py +++ b/homeassistant/components/yamaha_musiccast/number.py @@ -1,4 +1,5 @@ """Number entities for musiccast.""" +from __future__ import annotations from aiomusiccast.capabilities import NumberSetter @@ -50,10 +51,10 @@ class NumberCapability(MusicCastCapabilityEntity, NumberEntity): self._attr_native_step = capability.value_range.step @property - def native_value(self): + def native_value(self) -> float | None: """Return the current value.""" return self.capability.current - async def async_set_native_value(self, value: float): + async def async_set_native_value(self, value: float) -> None: """Set a new value.""" await self.capability.set(value) From e0d8f0cc950509ee666cf0b8b3774f76bc76a397 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 10 Aug 2022 16:18:11 -0600 Subject: [PATCH 286/903] Add persistent repair items for deprecated Guardian services (#76312) Co-authored-by: Franck Nijhof --- homeassistant/components/guardian/__init__.py | 29 +++++++++++++++++-- .../components/guardian/manifest.json | 1 + .../components/guardian/strings.json | 13 +++++++++ .../components/guardian/translations/en.json | 13 +++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 58d70667cdf..909752b5b33 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -10,6 +10,7 @@ from aioguardian import Client from aioguardian.errors import GuardianError import voluptuous as vol +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( ATTR_DEVICE_ID, @@ -116,14 +117,34 @@ def async_log_deprecated_service_call( call: ServiceCall, alternate_service: str, alternate_target: str, + breaks_in_ha_version: str, ) -> None: """Log a warning about a deprecated service call.""" + deprecated_service = f"{call.domain}.{call.service}" + + async_create_issue( + hass, + DOMAIN, + f"deprecated_service_{deprecated_service}", + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_service", + translation_placeholders={ + "alternate_service": alternate_service, + "alternate_target": alternate_target, + "deprecated_service": deprecated_service, + }, + ) + LOGGER.warning( ( - 'The "%s" service is deprecated and will be removed in a future version; ' - 'use the "%s" service and pass it a target entity ID of "%s"' + 'The "%s" service is deprecated and will be removed in %s; use the "%s" ' + 'service and pass it a target entity ID of "%s"' ), - f"{call.domain}.{call.service}", + deprecated_service, + breaks_in_ha_version, alternate_service, alternate_target, ) @@ -235,6 +256,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: call, "button.press", f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reboot", + "2022.10.0", ) await data.client.system.reboot() @@ -248,6 +270,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: call, "button.press", f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reset_valve_diagnostics", + "2022.10.0", ) await data.client.valve.reset() diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 7fab487563c..24dfbad13fe 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,6 +3,7 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", + "dependencies": ["repairs"], "requirements": ["aioguardian==2022.07.0"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index 4c60bfe4572..1665cf9f678 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -17,5 +17,18 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "issues": { + "deprecated_service": { + "title": "The {deprecated_service} service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {deprecated_service} service is being removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved." + } + } + } + } } } diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 310f550bcc1..ad6d0a4b7dc 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -17,5 +17,18 @@ "description": "Configure a local Elexa Guardian device." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved.", + "title": "The {deprecated_service} service is being removed" + } + } + }, + "title": "The {deprecated_service} service is being removed" + } } } \ No newline at end of file From 519d478d612dabfd4fe77705cee8103fbcf81bf0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 11 Aug 2022 00:26:23 +0000 Subject: [PATCH 287/903] [ci skip] Translation update --- .../accuweather/translations/es.json | 2 +- .../components/adguard/translations/es.json | 4 +- .../components/agent_dvr/translations/es.json | 2 +- .../components/airzone/translations/es.json | 2 +- .../aladdin_connect/translations/es.json | 8 +- .../components/ambee/translations/es.json | 6 ++ .../components/ambee/translations/et.json | 6 ++ .../components/ambee/translations/tr.json | 6 ++ .../android_ip_webcam/translations/ca.json | 26 +++++++ .../android_ip_webcam/translations/de.json | 26 +++++++ .../android_ip_webcam/translations/el.json | 26 +++++++ .../android_ip_webcam/translations/es.json | 26 +++++++ .../android_ip_webcam/translations/et.json | 26 +++++++ .../android_ip_webcam/translations/no.json | 26 +++++++ .../android_ip_webcam/translations/pt-BR.json | 26 +++++++ .../android_ip_webcam/translations/tr.json | 26 +++++++ .../components/anthemav/translations/es.json | 25 +++++++ .../components/anthemav/translations/et.json | 6 ++ .../components/anthemav/translations/tr.json | 6 ++ .../components/apple_tv/translations/es.json | 2 +- .../components/arcam_fmj/translations/es.json | 2 +- .../components/asuswrt/translations/es.json | 4 +- .../components/august/translations/es.json | 2 +- .../aussie_broadband/translations/es.json | 2 +- .../components/awair/translations/es.json | 7 ++ .../components/axis/translations/es.json | 2 +- .../components/baf/translations/es.json | 6 +- .../components/blink/translations/es.json | 2 +- .../components/bluetooth/translations/es.json | 32 ++++++++ .../components/bluetooth/translations/tr.json | 32 ++++++++ .../components/broadlink/translations/es.json | 2 +- .../components/bsblan/translations/es.json | 2 +- .../components/canary/translations/es.json | 2 +- .../components/coinbase/translations/es.json | 2 +- .../components/deconz/translations/es.json | 4 +- .../components/deluge/translations/es.json | 2 +- .../components/demo/translations/el.json | 11 +++ .../components/demo/translations/es.json | 32 ++++++++ .../components/demo/translations/et.json | 13 +++- .../components/demo/translations/tr.json | 13 +++- .../components/denonavr/translations/es.json | 4 +- .../derivative/translations/es.json | 6 +- .../deutsche_bahn/translations/el.json | 8 ++ .../deutsche_bahn/translations/es.json | 8 ++ .../deutsche_bahn/translations/et.json | 8 ++ .../deutsche_bahn/translations/tr.json | 8 ++ .../components/dexcom/translations/es.json | 2 +- .../components/discord/translations/es.json | 8 +- .../components/dlna_dmr/translations/es.json | 4 +- .../components/doorbird/translations/es.json | 4 +- .../enphase_envoy/translations/es.json | 2 +- .../environment_canada/translations/es.json | 2 +- .../components/escea/translations/ca.json | 5 ++ .../components/escea/translations/el.json | 13 ++++ .../components/escea/translations/es.json | 13 ++++ .../components/escea/translations/et.json | 13 ++++ .../components/escea/translations/pt-BR.json | 13 ++++ .../components/escea/translations/tr.json | 13 ++++ .../components/esphome/translations/es.json | 4 +- .../components/ezviz/translations/es.json | 6 +- .../components/fan/translations/es.json | 2 +- .../components/fibaro/translations/es.json | 2 +- .../fireservicerota/translations/es.json | 2 +- .../flick_electric/translations/es.json | 2 +- .../components/flume/translations/es.json | 2 +- .../flunearyou/translations/el.json | 13 ++++ .../flunearyou/translations/es.json | 13 ++++ .../flunearyou/translations/et.json | 13 ++++ .../flunearyou/translations/tr.json | 13 ++++ .../components/flux_led/translations/es.json | 2 +- .../components/foscam/translations/es.json | 2 +- .../components/fritz/translations/es.json | 12 +-- .../components/fritzbox/translations/es.json | 10 +-- .../fritzbox_callmonitor/translations/es.json | 2 +- .../components/generic/translations/es.json | 74 ++++++++++--------- .../geocaching/translations/es.json | 12 +-- .../components/glances/translations/es.json | 4 +- .../components/gogogate2/translations/es.json | 2 +- .../components/google/translations/es.json | 16 +++- .../components/google/translations/et.json | 10 +++ .../components/google/translations/tr.json | 10 +++ .../components/govee_ble/translations/es.json | 21 ++++++ .../components/govee_ble/translations/tr.json | 21 ++++++ .../components/group/translations/es.json | 6 +- .../growatt_server/translations/es.json | 2 +- .../components/guardian/translations/es.json | 13 ++++ .../here_travel_time/translations/es.json | 23 ++++-- .../components/hive/translations/es.json | 13 +++- .../components/hlk_sw16/translations/es.json | 2 +- .../home_plus_control/translations/es.json | 2 +- .../homeassistant/translations/es.json | 1 + .../homeassistant_alerts/translations/es.json | 8 ++ .../homeassistant_alerts/translations/et.json | 8 ++ .../homeassistant_alerts/translations/tr.json | 8 ++ .../homekit_controller/translations/es.json | 4 +- .../translations/sensor.ca.json | 1 + .../translations/sensor.el.json | 21 ++++++ .../translations/sensor.es.json | 21 ++++++ .../translations/sensor.et.json | 21 ++++++ .../translations/sensor.tr.json | 21 ++++++ .../components/honeywell/translations/es.json | 2 +- .../components/hue/translations/es.json | 2 +- .../huisbaasje/translations/es.json | 2 +- .../hvv_departures/translations/es.json | 2 +- .../components/hyperion/translations/es.json | 2 +- .../components/iaqualink/translations/es.json | 2 +- .../components/inkbird/translations/es.json | 21 ++++++ .../components/inkbird/translations/tr.json | 21 ++++++ .../components/insteon/translations/es.json | 4 +- .../integration/translations/es.json | 2 +- .../intellifire/translations/es.json | 4 +- .../components/ipp/translations/es.json | 2 +- .../components/isy994/translations/es.json | 8 +- .../isy994/translations/zh-Hant.json | 2 +- .../components/jellyfin/translations/es.json | 2 +- .../justnimbus/translations/ca.json | 19 +++++ .../justnimbus/translations/de.json | 19 +++++ .../justnimbus/translations/el.json | 19 +++++ .../justnimbus/translations/es.json | 19 +++++ .../justnimbus/translations/et.json | 19 +++++ .../justnimbus/translations/pt-BR.json | 19 +++++ .../justnimbus/translations/tr.json | 19 +++++ .../components/knx/translations/es.json | 42 +++++------ .../components/konnected/translations/es.json | 2 +- .../lacrosse_view/translations/es.json | 20 +++++ .../lacrosse_view/translations/et.json | 20 +++++ .../lacrosse_view/translations/tr.json | 20 +++++ .../components/laundrify/translations/es.json | 12 +-- .../components/lcn/translations/es.json | 1 + .../lg_soundbar/translations/es.json | 18 +++++ .../lg_soundbar/translations/et.json | 2 +- .../lg_soundbar/translations/tr.json | 2 +- .../components/life360/translations/es.json | 27 ++++++- .../components/lifx/translations/es.json | 20 +++++ .../litterrobot/translations/sensor.es.json | 8 +- .../components/lookin/translations/es.json | 2 +- .../components/lyric/translations/es.json | 6 ++ .../components/lyric/translations/et.json | 6 ++ .../components/lyric/translations/tr.json | 6 ++ .../components/meater/translations/es.json | 6 +- .../media_player/translations/es.json | 6 +- .../components/miflora/translations/es.json | 8 ++ .../components/miflora/translations/et.json | 8 ++ .../components/miflora/translations/tr.json | 8 ++ .../components/mikrotik/translations/es.json | 2 +- .../components/min_max/translations/es.json | 6 +- .../components/mitemp_bt/translations/es.json | 8 ++ .../components/mitemp_bt/translations/et.json | 8 ++ .../components/mitemp_bt/translations/tr.json | 8 ++ .../components/moat/translations/es.json | 21 ++++++ .../components/moat/translations/tr.json | 21 ++++++ .../modem_callerid/translations/es.json | 2 +- .../motion_blinds/translations/es.json | 2 +- .../components/motioneye/translations/es.json | 4 +- .../components/mqtt/translations/es.json | 4 +- .../components/myq/translations/es.json | 2 +- .../components/mysensors/translations/ca.json | 1 + .../components/mysensors/translations/el.json | 9 +++ .../components/mysensors/translations/es.json | 9 +++ .../components/mysensors/translations/et.json | 9 +++ .../components/mysensors/translations/tr.json | 9 +++ .../components/nam/translations/es.json | 4 +- .../components/nest/translations/el.json | 10 +++ .../components/nest/translations/es.json | 40 ++++++++++ .../components/nest/translations/et.json | 10 +++ .../components/nest/translations/tr.json | 10 +++ .../components/netgear/translations/es.json | 2 +- .../components/nextdns/translations/es.json | 29 ++++++++ .../components/nina/translations/es.json | 22 ++++++ .../components/notion/translations/es.json | 2 +- .../components/nuheat/translations/es.json | 2 +- .../components/nut/translations/es.json | 2 +- .../components/nzbget/translations/es.json | 4 +- .../components/octoprint/translations/es.json | 2 +- .../components/omnilogic/translations/es.json | 2 +- .../components/onvif/translations/es.json | 2 +- .../openalpr_local/translations/es.json | 8 ++ .../openalpr_local/translations/et.json | 8 ++ .../openalpr_local/translations/tr.json | 8 ++ .../openexchangerates/translations/ca.json | 11 ++- .../openexchangerates/translations/el.json | 33 +++++++++ .../openexchangerates/translations/es.json | 33 +++++++++ .../openexchangerates/translations/et.json | 33 +++++++++ .../openexchangerates/translations/tr.json | 33 +++++++++ .../opengarage/translations/es.json | 2 +- .../opentherm_gw/translations/es.json | 3 +- .../opentherm_gw/translations/et.json | 3 +- .../opentherm_gw/translations/tr.json | 3 +- .../components/overkiz/translations/es.json | 2 +- .../overkiz/translations/sensor.es.json | 5 ++ .../ovo_energy/translations/es.json | 2 +- .../components/picnic/translations/es.json | 2 +- .../components/plex/translations/es.json | 4 +- .../components/plugwise/translations/es.json | 7 +- .../components/prosegur/translations/es.json | 4 +- .../components/ps4/translations/es.json | 4 +- .../components/qnap_qsw/translations/es.json | 12 ++- .../radiotherm/translations/es.json | 10 ++- .../radiotherm/translations/tr.json | 6 ++ .../remote/translations/zh-Hant.json | 2 +- .../components/rhasspy/translations/es.json | 12 +++ .../components/ring/translations/es.json | 2 +- .../components/roku/translations/es.json | 2 +- .../components/roon/translations/es.json | 2 +- .../ruckus_unleashed/translations/es.json | 2 +- .../components/sabnzbd/translations/es.json | 4 +- .../components/samsungtv/translations/es.json | 2 +- .../components/scrape/translations/es.json | 60 ++++++++++++++- .../script/translations/zh-Hant.json | 2 +- .../sensor/translations/zh-Hant.json | 2 +- .../sensorpush/translations/es.json | 21 ++++++ .../sensorpush/translations/tr.json | 21 ++++++ .../components/senz/translations/es.json | 14 +++- .../components/senz/translations/et.json | 6 ++ .../components/senz/translations/tr.json | 6 ++ .../components/sharkiq/translations/es.json | 4 +- .../components/shelly/translations/es.json | 4 +- .../simplepush/translations/ca.json | 1 + .../simplepush/translations/el.json | 4 + .../simplepush/translations/es.json | 31 ++++++++ .../simplepush/translations/et.json | 10 +++ .../simplepush/translations/tr.json | 10 +++ .../simplisafe/translations/el.json | 1 + .../simplisafe/translations/es.json | 14 ++-- .../simplisafe/translations/et.json | 3 +- .../simplisafe/translations/tr.json | 8 +- .../components/skybell/translations/es.json | 6 +- .../components/slack/translations/es.json | 6 +- .../components/slimproto/translations/es.json | 2 +- .../components/sma/translations/es.json | 4 +- .../smart_meter_texas/translations/es.json | 2 +- .../components/sms/translations/es.json | 2 +- .../components/sonarr/translations/es.json | 2 +- .../soundtouch/translations/es.json | 27 +++++++ .../soundtouch/translations/et.json | 6 ++ .../soundtouch/translations/tr.json | 6 ++ .../components/spider/translations/es.json | 2 +- .../components/spotify/translations/es.json | 6 ++ .../components/spotify/translations/et.json | 6 ++ .../components/spotify/translations/tr.json | 6 ++ .../components/sql/translations/es.json | 14 ++-- .../squeezebox/translations/es.json | 2 +- .../srp_energy/translations/es.json | 2 +- .../components/starline/translations/es.json | 2 +- .../steam_online/translations/es.json | 20 +++-- .../steam_online/translations/et.json | 6 ++ .../steam_online/translations/tr.json | 6 ++ .../components/subaru/translations/es.json | 6 +- .../surepetcare/translations/es.json | 2 +- .../switch/translations/zh-Hant.json | 2 +- .../switch_as_x/translations/es.json | 2 +- .../components/switchbot/translations/ca.json | 9 +++ .../components/switchbot/translations/en.json | 14 +++- .../components/switchbot/translations/es.json | 12 ++- .../components/switchbot/translations/fr.json | 9 +++ .../components/switchbot/translations/hu.json | 9 +++ .../switchbot/translations/pt-BR.json | 9 +++ .../components/switchbot/translations/tr.json | 12 ++- .../components/syncthing/translations/es.json | 2 +- .../synology_dsm/translations/es.json | 8 +- .../components/tado/translations/es.json | 2 +- .../tankerkoenig/translations/es.json | 8 +- .../components/tautulli/translations/es.json | 10 +-- .../components/tod/translations/es.json | 2 +- .../totalconnect/translations/es.json | 4 +- .../components/tradfri/translations/es.json | 2 +- .../trafikverket_ferry/translations/es.json | 8 +- .../trafikverket_train/translations/es.json | 8 +- .../transmission/translations/es.json | 12 ++- .../components/tuya/translations/es.json | 2 +- .../ukraine_alarm/translations/es.json | 10 +-- .../components/unifi/translations/es.json | 2 +- .../unifiprotect/translations/de.json | 1 + .../unifiprotect/translations/el.json | 1 + .../unifiprotect/translations/es.json | 3 +- .../unifiprotect/translations/et.json | 1 + .../unifiprotect/translations/pt-BR.json | 1 + .../unifiprotect/translations/tr.json | 1 + .../components/upcloud/translations/es.json | 2 +- .../components/update/translations/es.json | 4 +- .../components/uscis/translations/es.json | 8 ++ .../components/uscis/translations/tr.json | 8 ++ .../utility_meter/translations/es.json | 12 +-- .../components/venstar/translations/es.json | 2 +- .../components/verisure/translations/es.json | 15 +++- .../components/vulcan/translations/es.json | 18 ++--- .../components/wallbox/translations/es.json | 4 +- .../components/whirlpool/translations/es.json | 2 +- .../components/withings/translations/es.json | 4 + .../components/ws66i/translations/es.json | 6 +- .../components/xbox/translations/es.json | 6 ++ .../components/xbox/translations/et.json | 6 ++ .../components/xbox/translations/tr.json | 6 ++ .../xiaomi_aqara/translations/es.json | 2 +- .../xiaomi_ble/translations/el.json | 14 +++- .../xiaomi_ble/translations/es.json | 48 ++++++++++++ .../xiaomi_ble/translations/et.json | 14 +++- .../xiaomi_ble/translations/tr.json | 48 ++++++++++++ .../xiaomi_miio/translations/es.json | 2 +- .../yale_smart_alarm/translations/es.json | 4 +- .../yalexs_ble/translations/ca.json | 27 +++++++ .../yalexs_ble/translations/es.json | 30 ++++++++ .../yalexs_ble/translations/fr.json | 22 ++++++ .../yalexs_ble/translations/hu.json | 29 ++++++++ .../yalexs_ble/translations/pt-BR.json | 30 ++++++++ .../components/yolink/translations/es.json | 20 ++--- .../components/zha/translations/es.json | 3 + .../components/zha/translations/et.json | 2 + .../components/zha/translations/tr.json | 3 + .../zoneminder/translations/es.json | 4 +- .../components/zwave_js/translations/es.json | 4 +- 311 files changed, 2578 insertions(+), 391 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/translations/ca.json create mode 100644 homeassistant/components/android_ip_webcam/translations/de.json create mode 100644 homeassistant/components/android_ip_webcam/translations/el.json create mode 100644 homeassistant/components/android_ip_webcam/translations/es.json create mode 100644 homeassistant/components/android_ip_webcam/translations/et.json create mode 100644 homeassistant/components/android_ip_webcam/translations/no.json create mode 100644 homeassistant/components/android_ip_webcam/translations/pt-BR.json create mode 100644 homeassistant/components/android_ip_webcam/translations/tr.json create mode 100644 homeassistant/components/anthemav/translations/es.json create mode 100644 homeassistant/components/bluetooth/translations/es.json create mode 100644 homeassistant/components/bluetooth/translations/tr.json create mode 100644 homeassistant/components/deutsche_bahn/translations/el.json create mode 100644 homeassistant/components/deutsche_bahn/translations/es.json create mode 100644 homeassistant/components/deutsche_bahn/translations/et.json create mode 100644 homeassistant/components/deutsche_bahn/translations/tr.json create mode 100644 homeassistant/components/escea/translations/el.json create mode 100644 homeassistant/components/escea/translations/es.json create mode 100644 homeassistant/components/escea/translations/et.json create mode 100644 homeassistant/components/escea/translations/pt-BR.json create mode 100644 homeassistant/components/escea/translations/tr.json create mode 100644 homeassistant/components/govee_ble/translations/es.json create mode 100644 homeassistant/components/govee_ble/translations/tr.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/es.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/et.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/tr.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.el.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.es.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.et.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.tr.json create mode 100644 homeassistant/components/inkbird/translations/es.json create mode 100644 homeassistant/components/inkbird/translations/tr.json create mode 100644 homeassistant/components/justnimbus/translations/ca.json create mode 100644 homeassistant/components/justnimbus/translations/de.json create mode 100644 homeassistant/components/justnimbus/translations/el.json create mode 100644 homeassistant/components/justnimbus/translations/es.json create mode 100644 homeassistant/components/justnimbus/translations/et.json create mode 100644 homeassistant/components/justnimbus/translations/pt-BR.json create mode 100644 homeassistant/components/justnimbus/translations/tr.json create mode 100644 homeassistant/components/lacrosse_view/translations/es.json create mode 100644 homeassistant/components/lacrosse_view/translations/et.json create mode 100644 homeassistant/components/lacrosse_view/translations/tr.json create mode 100644 homeassistant/components/lg_soundbar/translations/es.json create mode 100644 homeassistant/components/miflora/translations/es.json create mode 100644 homeassistant/components/miflora/translations/et.json create mode 100644 homeassistant/components/miflora/translations/tr.json create mode 100644 homeassistant/components/mitemp_bt/translations/es.json create mode 100644 homeassistant/components/mitemp_bt/translations/et.json create mode 100644 homeassistant/components/mitemp_bt/translations/tr.json create mode 100644 homeassistant/components/moat/translations/es.json create mode 100644 homeassistant/components/moat/translations/tr.json create mode 100644 homeassistant/components/nextdns/translations/es.json create mode 100644 homeassistant/components/openalpr_local/translations/es.json create mode 100644 homeassistant/components/openalpr_local/translations/et.json create mode 100644 homeassistant/components/openalpr_local/translations/tr.json create mode 100644 homeassistant/components/openexchangerates/translations/el.json create mode 100644 homeassistant/components/openexchangerates/translations/es.json create mode 100644 homeassistant/components/openexchangerates/translations/et.json create mode 100644 homeassistant/components/openexchangerates/translations/tr.json create mode 100644 homeassistant/components/rhasspy/translations/es.json create mode 100644 homeassistant/components/sensorpush/translations/es.json create mode 100644 homeassistant/components/sensorpush/translations/tr.json create mode 100644 homeassistant/components/simplepush/translations/es.json create mode 100644 homeassistant/components/soundtouch/translations/es.json create mode 100644 homeassistant/components/uscis/translations/es.json create mode 100644 homeassistant/components/uscis/translations/tr.json create mode 100644 homeassistant/components/xiaomi_ble/translations/es.json create mode 100644 homeassistant/components/xiaomi_ble/translations/tr.json create mode 100644 homeassistant/components/yalexs_ble/translations/ca.json create mode 100644 homeassistant/components/yalexs_ble/translations/es.json create mode 100644 homeassistant/components/yalexs_ble/translations/fr.json create mode 100644 homeassistant/components/yalexs_ble/translations/hu.json create mode 100644 homeassistant/components/yalexs_ble/translations/pt-BR.json diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index ef91348a727..36ccc3f0cca 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { - "default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\n El pron\u00f3stico del tiempo no est\u00e1 habilitado de forma predeterminada. Puedes habilitarlo en las opciones de integraci\u00f3n." + "default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puedes habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado de forma predeterminada. Puedes habilitarlo en las opciones de integraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index 7fc2f972fa1..f7bfb1203d7 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -18,8 +18,8 @@ "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." } diff --git a/homeassistant/components/agent_dvr/translations/es.json b/homeassistant/components/agent_dvr/translations/es.json index c1771d7e80b..996e006898d 100644 --- a/homeassistant/components/agent_dvr/translations/es.json +++ b/homeassistant/components/agent_dvr/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar" }, "step": { diff --git a/homeassistant/components/airzone/translations/es.json b/homeassistant/components/airzone/translations/es.json index fbb00d71bf0..1fb525c9851 100644 --- a/homeassistant/components/airzone/translations/es.json +++ b/homeassistant/components/airzone/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_system_id": "ID de sistema Airzone inv\u00e1lido" + "invalid_system_id": "ID del sistema Airzone no v\u00e1lido" }, "step": { "user": { diff --git a/homeassistant/components/aladdin_connect/translations/es.json b/homeassistant/components/aladdin_connect/translations/es.json index ac10503ab3c..5621a1c69e9 100644 --- a/homeassistant/components/aladdin_connect/translations/es.json +++ b/homeassistant/components/aladdin_connect/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { @@ -13,8 +13,8 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "La integraci\u00f3n de Aladdin Connect necesita volver a autenticar su cuenta", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "description": "La integraci\u00f3n Aladdin Connect necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json index 7f4f8b75de5..6484ac8c3a0 100644 --- a/homeassistant/components/ambee/translations/es.json +++ b/homeassistant/components/ambee/translations/es.json @@ -24,5 +24,11 @@ "description": "Configure Ambee para que se integre con Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n Ambee est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque Ambee elimin\u00f3 sus cuentas gratuitas (limitadas) y ya no proporciona una forma para que los usuarios regulares se registren en un plan pago. \n\nElimina la entrada de la integraci\u00f3n Ambee de tu instancia para solucionar este problema.", + "title": "Se va a eliminar la integraci\u00f3n Ambee" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/et.json b/homeassistant/components/ambee/translations/et.json index 085f13d6926..abb41497581 100644 --- a/homeassistant/components/ambee/translations/et.json +++ b/homeassistant/components/ambee/translations/et.json @@ -24,5 +24,11 @@ "description": "Seadista Ambee sidumine Home Assistantiga." } } + }, + "issues": { + "pending_removal": { + "description": "Ambee integratsioon on Home Assistantist eemaldamisel ja ei ole enam saadaval alates Home Assistant 2022.10.\n\nIntegratsioon eemaldatakse, sest Ambee eemaldas oma tasuta (piiratud) kontod ja ei paku tavakasutajatele enam v\u00f5imalust tasulisele plaanile registreeruda.\n\nSelle probleemi lahendamiseks eemaldage Ambee integratsiooni kirje oma instantsist.", + "title": "Ambee integratsioon eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/tr.json b/homeassistant/components/ambee/translations/tr.json index 45eacf30987..0163ea40bae 100644 --- a/homeassistant/components/ambee/translations/tr.json +++ b/homeassistant/components/ambee/translations/tr.json @@ -24,5 +24,11 @@ "description": "Ambee'yi Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." } } + }, + "issues": { + "pending_removal": { + "description": "Ambee entegrasyonu Home Assistant'tan kald\u0131r\u0131lmay\u0131 beklemektedir ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacakt\u0131r.\n\nEntegrasyon kald\u0131r\u0131l\u0131yor \u00e7\u00fcnk\u00fc Ambee \u00fccretsiz (s\u0131n\u0131rl\u0131) hesaplar\u0131n\u0131 kald\u0131rd\u0131 ve art\u0131k normal kullan\u0131c\u0131lar\u0131n \u00fccretli bir plana kaydolmas\u0131 i\u00e7in bir yol sa\u011flam\u0131yor.\n\nBu sorunu gidermek i\u00e7in Ambee entegrasyon giri\u015fini \u00f6rne\u011finizden kald\u0131r\u0131n.", + "title": "Ambee entegrasyonu kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/ca.json b/homeassistant/components/android_ip_webcam/translations/ca.json new file mode 100644 index 00000000000..daebd1c3cd6 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/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" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 d'Android IP Webcam mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'Android IP Webcam del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'Android IP Webcam est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/de.json b/homeassistant/components/android_ip_webcam/translations/de.json new file mode 100644 index 00000000000..3fe34f4a259 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration der Android IP Webcam mit YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Android IP Webcam YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Android IP Webcam YAML Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/el.json b/homeassistant/components/android_ip_webcam/translations/el.json new file mode 100644 index 00000000000..a4ae676b8a0 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/el.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2 Web Android IP \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03b7\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2 Web Android IP \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03b7\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2 Web Android IP \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/es.json b/homeassistant/components/android_ip_webcam/translations/es.json new file mode 100644 index 00000000000..71c0d6cc5bc --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se eliminar\u00e1 la configuraci\u00f3n de la c\u00e1mara web IP de Android mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/et.json b/homeassistant/components/android_ip_webcam/translations/et.json new file mode 100644 index 00000000000..1b2d03a2d0a --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Android IP veebikaamera konfigureerimine YAML-i abil eemaldatakse.\n\nOlemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nProbleemi lahendamiseks eemalda Android IP Webcam YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivita Home Assistant uuesti.", + "title": "Android IP veebikaamera YAML konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/no.json b/homeassistant/components/android_ip_webcam/translations/no.json new file mode 100644 index 00000000000..e9d7be1d409 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Android IP Webcam med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Android IP Webcam YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Android IP Webcam YAML-konfigurasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/pt-BR.json b/homeassistant/components/android_ip_webcam/translations/pt-BR.json new file mode 100644 index 00000000000..95b5ffa7166 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha", + "port": "Porta", + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Android IP Webcam usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Android IP Webcam do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o de YAML do Android IP Webcam est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/tr.json b/homeassistant/components/android_ip_webcam/translations/tr.json new file mode 100644 index 00000000000..cac43d82490 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Android IP Web Kameras\u0131n\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Android IP Webcam YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Android IP Webcam YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/es.json b/homeassistant/components/anthemav/translations/es.json new file mode 100644 index 00000000000..7759dbd8607 --- /dev/null +++ b/homeassistant/components/anthemav/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "cannot_receive_deviceinfo": "No se pudo recuperar la direcci\u00f3n MAC. Aseg\u00farate de que el dispositivo est\u00e9 encendido" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de los Receptores A/V Anthem mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de los Receptores A/V Anthem de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de los Receptores A/V Anthem" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/et.json b/homeassistant/components/anthemav/translations/et.json index 4ec356c8902..d5b9d4f224f 100644 --- a/homeassistant/components/anthemav/translations/et.json +++ b/homeassistant/components/anthemav/translations/et.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Anthem A/V-vastuv\u00f5tjate konfigureerimine YAML-i abil eemaldatakse. \n\n Teie olemasolev YAML-i konfiguratsioon imporditi kasutajaliidesesse automaatselt. \n\n Selle probleemi lahendamiseks eemaldage failist configuration.yaml konfiguratsioon Anthem A/V Receivers YAML ja taask\u00e4ivitage Home Assistant.", + "title": "Anthem A/V-vastuv\u00f5tjate YAML-konfiguratsioon eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/tr.json b/homeassistant/components/anthemav/translations/tr.json index cbe85a5319c..c77f5a1f14a 100644 --- a/homeassistant/components/anthemav/translations/tr.json +++ b/homeassistant/components/anthemav/translations/tr.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Anthem A/V Al\u0131c\u0131lar\u0131n\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131.\n\nAnthem A/V Al\u0131c\u0131lar\u0131 YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Anthem A/V Al\u0131c\u0131lar\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 3fe2345d6e1..6b136463aa0 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", "device_not_found": "No se ha encontrado el dispositivo durante la detecci\u00f3n, por favor, intente a\u00f1adirlo de nuevo.", diff --git a/homeassistant/components/arcam_fmj/translations/es.json b/homeassistant/components/arcam_fmj/translations/es.json index 6959ee85ab1..33c0558a4ce 100644 --- a/homeassistant/components/arcam_fmj/translations/es.json +++ b/homeassistant/components/arcam_fmj/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar" }, "flow_title": "Arcam FMJ en {host}", diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index a2e899ef113..c714c26129a 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "invalid_unique_id": "No se pudo determinar ning\u00fan identificador \u00fanico v\u00e1lido del dispositivo", - "no_unique_id": "Un dispositivo sin una identificaci\u00f3n \u00fanica v\u00e1lida ya est\u00e1 configurado. La configuraci\u00f3n de una instancia m\u00faltiple no es posible" + "invalid_unique_id": "Imposible determinar una identificaci\u00f3n \u00fanica v\u00e1lida para el dispositivo", + "no_unique_id": "Ya se configur\u00f3 un dispositivo sin una identificaci\u00f3n \u00fanica v\u00e1lida. La configuraci\u00f3n de varias instancias no es posible" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index cd50020bb2a..f2c7a10d0e0 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -21,7 +21,7 @@ "data": { "login_method": "M\u00e9todo de inicio de sesi\u00f3n", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es \"correo electr\u00f3nico\", el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es \"tel\u00e9fono\", el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato \"+NNNNNNN\".", "title": "Configurar una cuenta de August" diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json index 497215525cb..e1ae211ca8b 100644 --- a/homeassistant/components/aussie_broadband/translations/es.json +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -27,7 +27,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index 1a3240d3802..5b203948a7c 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -17,6 +17,13 @@ }, "description": "Por favor, vuelve a introducir tu token de acceso de desarrollador Awair." }, + "reauth_confirm": { + "data": { + "access_token": "Token de acceso", + "email": "Correo electr\u00f3nico" + }, + "description": "Por favor, vuelve a introducir tu token de acceso de desarrollador Awair." + }, "user": { "data": { "access_token": "Token de acceso", diff --git a/homeassistant/components/axis/translations/es.json b/homeassistant/components/axis/translations/es.json index 4a47b17c528..6a406614b9f 100644 --- a/homeassistant/components/axis/translations/es.json +++ b/homeassistant/components/axis/translations/es.json @@ -18,7 +18,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Configurar dispositivo Axis" } diff --git a/homeassistant/components/baf/translations/es.json b/homeassistant/components/baf/translations/es.json index 4e2800090d2..472c2e22c2c 100644 --- a/homeassistant/components/baf/translations/es.json +++ b/homeassistant/components/baf/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "ipv6_not_supported": "IPv6 no est\u00e1 soportado." + "ipv6_not_supported": "IPv6 no es compatible." }, "error": { - "cannot_connect": "Error al conectar", - "unknown": "Error Inesperado" + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" }, "flow_title": "{name} - {model} ({ip_address})", "step": { diff --git a/homeassistant/components/blink/translations/es.json b/homeassistant/components/blink/translations/es.json index 1b25e1a52e2..7ff0b349555 100644 --- a/homeassistant/components/blink/translations/es.json +++ b/homeassistant/components/blink/translations/es.json @@ -20,7 +20,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Iniciar sesi\u00f3n con cuenta Blink" } diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json new file mode 100644 index 00000000000..ec970720228 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "no_adapters": "No se encontraron adaptadores Bluetooth" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "enable_bluetooth": { + "description": "\u00bfQuieres configurar Bluetooth?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "El adaptador Bluetooth que se utilizar\u00e1 para escanear" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json new file mode 100644 index 00000000000..a464d65dd93 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_adapters": "Bluetooth adapt\u00f6r\u00fc bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "enable_bluetooth": { + "description": "Bluetooth'u kurmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Tarama i\u00e7in kullan\u0131lacak Bluetooth Adapt\u00f6r\u00fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json index d0020c55bca..96f791979d4 100644 --- a/homeassistant/components/broadlink/translations/es.json +++ b/homeassistant/components/broadlink/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "not_supported": "Dispositivo no compatible", diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 6e533b5916b..4b36080dd7c 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -15,7 +15,7 @@ "passkey": "Clave de acceso", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Configura tu dispositivo BSB-Lan para integrarse con Home Assistant.", "title": "Conectar con el dispositivo BSB-Lan" diff --git a/homeassistant/components/canary/translations/es.json b/homeassistant/components/canary/translations/es.json index 76a439da49d..2a1b3333b3d 100644 --- a/homeassistant/components/canary/translations/es.json +++ b/homeassistant/components/canary/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar a Canary" } diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json index 7ecd0a753c9..32eb2af4d5c 100644 --- a/homeassistant/components/coinbase/translations/es.json +++ b/homeassistant/components/coinbase/translations/es.json @@ -33,7 +33,7 @@ "account_balance_currencies": "Saldos de la cartera para informar.", "exchange_base": "Moneda base para sensores de tipo de cambio.", "exchange_rate_currencies": "Tipos de cambio a informar.", - "exchnage_rate_precision": "N\u00famero de posiciones decimales para los tipos de cambio." + "exchnage_rate_precision": "N\u00famero de decimales para los tipos de cambio." }, "description": "Ajustar las opciones de Coinbase" } diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 2921dc2f2bf..0974be9ff9d 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "La pasarela ya est\u00e1 configurada", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_bridges": "No se han descubierto pasarelas deCONZ", "no_hardware_available": "No hay hardware de radio conectado a deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, "error": { - "linking_not_possible": "No se ha podido enlazar con la pasarela", + "linking_not_possible": "No se pudo vincular con la puerta de enlace", "no_key": "No se pudo obtener una clave API" }, "flow_title": "{host}", diff --git a/homeassistant/components/deluge/translations/es.json b/homeassistant/components/deluge/translations/es.json index 1724791221f..c09a1ef5ef5 100644 --- a/homeassistant/components/deluge/translations/es.json +++ b/homeassistant/components/deluge/translations/es.json @@ -13,7 +13,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario", + "username": "Nombre de usuario", "web_port": "Puerto web (para el servicio de visita)" }, "description": "Para poder usar esta integraci\u00f3n, debe habilitar la siguiente opci\u00f3n en la configuraci\u00f3n de diluvio: Daemon > Permitir controles remotos" diff --git a/homeassistant/components/demo/translations/el.json b/homeassistant/components/demo/translations/el.json index c4c539034bd..ea1787ab8f0 100644 --- a/homeassistant/components/demo/translations/el.json +++ b/homeassistant/components/demo/translations/el.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u03a0\u03b9\u03ad\u03c3\u03c4\u03b5 \u03a5\u03a0\u039f\u0392\u039f\u039b\u0397 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b9\u03ba\u03cc \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af.", + "title": "\u03a4\u03bf \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af" + } + } + }, + "title": "\u03a4\u03bf \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03cc" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/es.json b/homeassistant/components/demo/translations/es.json index 19ebc05d089..ba1e31265b9 100644 --- a/homeassistant/components/demo/translations/es.json +++ b/homeassistant/components/demo/translations/es.json @@ -1,4 +1,36 @@ { + "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pulsa ENVIAR para confirmar que la fuente de alimentaci\u00f3n ha sido sustituida", + "title": "La fuente de alimentaci\u00f3n necesita ser reemplazada" + } + } + }, + "title": "La fuente de alimentaci\u00f3n no es estable." + }, + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pulsa ENVIAR cuando se haya rellenado el l\u00edquido de las luces intermitentes.", + "title": "Es necesario rellenar el l\u00edquido de los intermitentes" + } + } + }, + "title": "El l\u00edquido de la luz intermitente est\u00e1 vac\u00edo y necesita ser rellenado" + }, + "transmogrifier_deprecated": { + "description": "El componente transfigurador ahora est\u00e1 obsoleto debido a la falta de control local disponible en la nueva API", + "title": "El componente transfigurador est\u00e1 obsoleto" + }, + "unfixable_problem": { + "description": "Este problema no se va a rendir nunca.", + "title": "Este no es un problema solucionable" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/et.json b/homeassistant/components/demo/translations/et.json index 0e4c89dba01..ad7d7e361b6 100644 --- a/homeassistant/components/demo/translations/et.json +++ b/homeassistant/components/demo/translations/et.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Vajuta nuppu ESITA, et kinnitada toiteallika v\u00e4ljavahetamist", + "title": "Toiteallikas tuleb v\u00e4lja vahetada" + } + } + }, + "title": "Toiteallikas ei ole stabiilne" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Vajuta OK kui Blinkeri vedelik on uuesti t\u00e4idetud", + "description": "Vajuta ESITA kui Blinkeri vedelik on uuesti t\u00e4idetud", "title": "Blinkeri vedelikku on vaja uuesti t\u00e4ita" } } diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json index b7ff98f9cd4..2c3f9a44200 100644 --- a/homeassistant/components/demo/translations/tr.json +++ b/homeassistant/components/demo/translations/tr.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "G\u00fc\u00e7 kayna\u011f\u0131n\u0131n de\u011fi\u015ftirildi\u011fini onaylamak i\u00e7in G\u00d6NDER'e bas\u0131n", + "title": "G\u00fc\u00e7 kayna\u011f\u0131n\u0131n de\u011fi\u015ftirilmesi gerekiyor" + } + } + }, + "title": "G\u00fc\u00e7 kayna\u011f\u0131 stabil de\u011fil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Fla\u015f\u00f6r yeniden dolduruldu\u011funda Tamam'a bas\u0131n", + "description": "Fla\u015f\u00f6r s\u0131v\u0131s\u0131 yeniden dolduruldu\u011funda G\u00d6NDER d\u00fc\u011fmesine bas\u0131n", "title": "Fla\u015f\u00f6r\u00fcn yeniden doldurulmas\u0131 gerekiyor" } } diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index f5993f46cf7..d601c6a9ff0 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar, int\u00e9ntelo de nuevo, desconectar los cables de alimentaci\u00f3n y Ethernet y volver a conectarlos puede ayudar", "not_denonavr_manufacturer": "No es un Receptor AVR Denon AVR en Red, el fabricante detectado no concuerda", "not_denonavr_missing": "No es un Receptor AVR Denon AVR en Red, la informaci\u00f3n detectada no est\u00e1 completa" @@ -27,7 +27,7 @@ "host": "Direcci\u00f3n IP" }, "data_description": { - "host": "D\u00e9jelo en blanco para usar el descubrimiento autom\u00e1tico" + "host": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico" } } } diff --git a/homeassistant/components/derivative/translations/es.json b/homeassistant/components/derivative/translations/es.json index e9b0919f06f..d0722b70400 100644 --- a/homeassistant/components/derivative/translations/es.json +++ b/homeassistant/components/derivative/translations/es.json @@ -33,11 +33,11 @@ }, "data_description": { "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida.", - "time_window": "Si se establece, el valor del sensor es una media m\u00f3vil ponderada en el tiempo de las derivadas dentro de esta ventana.", - "unit_prefix": "a salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico y la unidad de tiempo de la derivada seleccionados." + "time_window": "Si se establece, el valor del sensor es un promedio m\u00f3vil ponderado en el tiempo de las derivadas dentro de esta ventana.", + "unit_prefix": "La salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico seleccionado y la unidad de tiempo de la derivada." } } } }, - "title": "Sensor derivatiu" + "title": "Sensor derivado" } \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/el.json b/homeassistant/components/deutsche_bahn/translations/el.json new file mode 100644 index 00000000000..cc1cd6eb001 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 Deutsche Bahn \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03c0\u03c1\u03bf\u03c2 \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.11.\n\n\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 webscraping, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9.\n\n\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd YAML \u03c4\u03b7\u03c2 Deutsche Bahn \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 Deutsche Bahn \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/es.json b/homeassistant/components/deutsche_bahn/translations/es.json new file mode 100644 index 00000000000..32aaf5a3893 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la integraci\u00f3n Deutsche Bahn" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/et.json b/homeassistant/components/deutsche_bahn/translations/et.json new file mode 100644 index 00000000000..a6029593a7b --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahni integratsioon on Home Assistantist eemaldamisel ja see ei ole enam saadaval alates Home Assistant 2022.11.\n\nIntegratsioon eemaldatakse, sest see p\u00f5hineb veebiotsingul, mis on keelatud.\n\nProbleemi lahendamiseks eemaldage Deutsche Bahni YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Deutsche Bahni integratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/tr.json b/homeassistant/components/deutsche_bahn/translations/tr.json new file mode 100644 index 00000000000..1aeda9c4d18 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahn entegrasyonu, Home Assistant'tan kald\u0131r\u0131lmay\u0131 bekliyor ve Home Assistant 2022.11'den itibaren art\u0131k kullan\u0131lamayacak. \n\n Entegrasyon kald\u0131r\u0131l\u0131yor, \u00e7\u00fcnk\u00fc izin verilmeyen web taramaya dayan\u0131yor. \n\n Deutsche Bahn YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Deutsche Bahn entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/es.json b/homeassistant/components/dexcom/translations/es.json index 24d06db3ee6..58b4a014f50 100644 --- a/homeassistant/components/dexcom/translations/es.json +++ b/homeassistant/components/dexcom/translations/es.json @@ -13,7 +13,7 @@ "data": { "password": "Contrase\u00f1a", "server": "Servidor", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introducir las credenciales de Dexcom Share", "title": "Configurar integraci\u00f3n de Dexcom" diff --git a/homeassistant/components/discord/translations/es.json b/homeassistant/components/discord/translations/es.json index f4f7bd49fc5..df4bc5a7fa3 100644 --- a/homeassistant/components/discord/translations/es.json +++ b/homeassistant/components/discord/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -14,13 +14,13 @@ "data": { "api_token": "Token API" }, - "description": "Consulta la documentaci\u00f3n para obtener tu clave de bote de Discord.\n\n{url}" + "description": "Consulta la documentaci\u00f3n sobre c\u00f3mo obtener tu clave de bot de Discord. \n\n{url}" }, "user": { "data": { "api_token": "Token API" }, - "description": "Consulte la documentaci\u00f3n sobre c\u00f3mo obtener su clave de bot de Discord. \n\n {url}" + "description": "Consulta la documentaci\u00f3n sobre c\u00f3mo obtener tu clave de bot de Discord. \n\n{url}" } } } diff --git a/homeassistant/components/dlna_dmr/translations/es.json b/homeassistant/components/dlna_dmr/translations/es.json index 6bb9dd83dbd..6d16766de03 100644 --- a/homeassistant/components/dlna_dmr/translations/es.json +++ b/homeassistant/components/dlna_dmr/translations/es.json @@ -32,7 +32,7 @@ "data": { "host": "Host" }, - "description": "Elija un dispositivo para configurar o d\u00e9jelo en blanco para introducir una URL", + "description": "Elige un dispositivo para configurar o d\u00e9jalo en blanco para introducir una URL", "title": "Dispositivos DLNA DMR descubiertos" } } @@ -44,7 +44,7 @@ "step": { "init": { "data": { - "browse_unfiltered": "Muestra archivos multimedia incompatibles mientras navega", + "browse_unfiltered": "Mostrar medios incompatibles al navegar", "callback_url_override": "URL de devoluci\u00f3n de llamada del detector de eventos", "listen_port": "Puerto de escucha de eventos (aleatorio si no se establece)", "poll_availability": "Sondeo para la disponibilidad del dispositivo" diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 355a48e9191..83ec7ed7a6a 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -17,7 +17,7 @@ "host": "Host", "name": "Nombre del dispositivo", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } @@ -29,7 +29,7 @@ "events": "Lista de eventos separados por comas." }, "data_description": { - "events": "A\u00f1ade un nombre de evento separado por comas para cada evento que desee rastrear. Despu\u00e9s de ingresarlos aqu\u00ed, use la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. \n\n Ejemplo: alguien_puls\u00f3_el_bot\u00f3n, movimiento" + "events": "A\u00f1ade un nombre de evento separado por comas para cada evento que desees rastrear. Despu\u00e9s de introducirlos aqu\u00ed, usa la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. \n\nEjemplo: alguien_puls\u00f3_el_bot\u00f3n, movimiento" } } } diff --git a/homeassistant/components/enphase_envoy/translations/es.json b/homeassistant/components/enphase_envoy/translations/es.json index 8bf1359cefa..24a63fa6c73 100644 --- a/homeassistant/components/enphase_envoy/translations/es.json +++ b/homeassistant/components/enphase_envoy/translations/es.json @@ -15,7 +15,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Para los modelos m\u00e1s nuevos, introduzca el nombre de usuario `envoy` sin contrase\u00f1a. Para los modelos m\u00e1s antiguos, introduzca el nombre de usuario `installer` sin contrase\u00f1a. Para todos los dem\u00e1s modelos, introduzca un nombre de usuario y una contrase\u00f1a v\u00e1lidos." } diff --git a/homeassistant/components/environment_canada/translations/es.json b/homeassistant/components/environment_canada/translations/es.json index 2c476c6c90f..7cbfb8caed9 100644 --- a/homeassistant/components/environment_canada/translations/es.json +++ b/homeassistant/components/environment_canada/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "station": "ID de la estaci\u00f3n meteorol\u00f3gica" }, - "description": "Se debe especificar un ID de estaci\u00f3n o una latitud/longitud. La latitud/longitud utilizada por defecto son los valores configurados en su instalaci\u00f3n de Home Assistant. Si se especifican coordenadas, se utilizar\u00e1 la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana a las mismas. Si se utiliza un c\u00f3digo de estaci\u00f3n debe seguir el formato PP/c\u00f3digo, donde PP es la provincia de dos letras y c\u00f3digo es el ID de la estaci\u00f3n. La lista de IDs de estaciones se puede encontrar aqu\u00ed: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. La informaci\u00f3n meteorol\u00f3gica puede obtenerse en ingl\u00e9s o en franc\u00e9s.", + "description": "Se debe especificar un ID de estaci\u00f3n o una latitud/longitud. La latitud/longitud utilizada por defecto son los valores configurados en tu instalaci\u00f3n de Home Assistant. Si se especifican coordenadas, se utilizar\u00e1 la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana a las mismas. Si se utiliza un c\u00f3digo de estaci\u00f3n debe seguir el formato PP/c\u00f3digo, donde PP es la provincia de dos letras y c\u00f3digo es el ID de la estaci\u00f3n. La lista de IDs de estaciones se puede encontrar aqu\u00ed: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. La informaci\u00f3n meteorol\u00f3gica puede obtenerse en ingl\u00e9s o en franc\u00e9s.", "title": "Environment Canada: ubicaci\u00f3n meteorol\u00f3gica e idioma" } } diff --git a/homeassistant/components/escea/translations/ca.json b/homeassistant/components/escea/translations/ca.json index d28b9050682..dcc35592fff 100644 --- a/homeassistant/components/escea/translations/ca.json +++ b/homeassistant/components/escea/translations/ca.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "No s'han trobat dispositius a la xarxa", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Vols configurar la llar de foc Escea?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/escea/translations/el.json b/homeassistant/components/escea/translations/el.json new file mode 100644 index 00000000000..340d28fe43a --- /dev/null +++ b/homeassistant/components/escea/translations/el.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c4\u03b6\u03ac\u03ba\u03b9 Escea;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/es.json b/homeassistant/components/escea/translations/es.json new file mode 100644 index 00000000000..0f208dac321 --- /dev/null +++ b/homeassistant/components/escea/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos en la red", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres configurar una chimenea Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/et.json b/homeassistant/components/escea/translations/et.json new file mode 100644 index 00000000000..a52a0d2c22c --- /dev/null +++ b/homeassistant/components/escea/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine" + }, + "step": { + "confirm": { + "description": "Kas luua Escea kamina sidumine?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/pt-BR.json b/homeassistant/components/escea/translations/pt-BR.json new file mode 100644 index 00000000000..5bcd3a25634 --- /dev/null +++ b/homeassistant/components/escea/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 foi configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Quer montar uma lareira Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/tr.json b/homeassistant/components/escea/translations/tr.json new file mode 100644 index 00000000000..1e2ba7839a0 --- /dev/null +++ b/homeassistant/components/escea/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "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": "Escea \u015f\u00f6minesi kurmak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index c682ddab294..68e53160322 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -9,7 +9,7 @@ "connection_error": "No se puede conectar a ESP. Aseg\u00farate de que tu archivo YAML contenga una l\u00ednea 'api:'.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_psk": "La clave de transporte cifrado no es v\u00e1lida. Por favor, aseg\u00farese de que coincide con la que tiene en su configuraci\u00f3n", - "resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "No se puede resolver la direcci\u00f3n del ESP. Si este error persiste, por favor, configura una direcci\u00f3n IP est\u00e1tica" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Puerto" }, - "description": "Introduce la configuraci\u00f3n de la conexi\u00f3n de tu nodo [ESPHome](https://esphomelib.com/)." + "description": "Por favor, introduce la configuraci\u00f3n de conexi\u00f3n de tu nodo [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/ezviz/translations/es.json b/homeassistant/components/ezviz/translations/es.json index eef8343c17a..f8a80614fb0 100644 --- a/homeassistant/components/ezviz/translations/es.json +++ b/homeassistant/components/ezviz/translations/es.json @@ -15,7 +15,7 @@ "confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introduce las credenciales RTSP para la c\u00e1mara Ezviz {serial} con IP {ip_address}", "title": "Descubierta c\u00e1mara Ezviz" @@ -24,7 +24,7 @@ "data": { "password": "Contrase\u00f1a", "url": "URL", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con Ezviz Cloud" }, @@ -32,7 +32,7 @@ "data": { "password": "Contrase\u00f1a", "url": "URL", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Especificar manualmente la URL de tu regi\u00f3n", "title": "Conectar con la URL personalizada de Ezviz" diff --git a/homeassistant/components/fan/translations/es.json b/homeassistant/components/fan/translations/es.json index b66ca0b0256..e2bc0b1611d 100644 --- a/homeassistant/components/fan/translations/es.json +++ b/homeassistant/components/fan/translations/es.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "toggle": "Cambiar {entity_name}", + "toggle": "Alternar {entity_name}", "turn_off": "Desactivar {entity_name}", "turn_on": "Activar {entity_name}" }, diff --git a/homeassistant/components/fibaro/translations/es.json b/homeassistant/components/fibaro/translations/es.json index 99f29f3bee5..567ae7e59a2 100644 --- a/homeassistant/components/fibaro/translations/es.json +++ b/homeassistant/components/fibaro/translations/es.json @@ -14,7 +14,7 @@ "import_plugins": "\u00bfImportar entidades desde los plugins de fibaro?", "password": "Contrase\u00f1a", "url": "URL en el format http://HOST/api/", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json index 085bce8f343..b86ba3b2164 100644 --- a/homeassistant/components/fireservicerota/translations/es.json +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -21,7 +21,7 @@ "data": { "password": "Contrase\u00f1a", "url": "Sitio web", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/flick_electric/translations/es.json b/homeassistant/components/flick_electric/translations/es.json index 424c73da24f..10b77911016 100644 --- a/homeassistant/components/flick_electric/translations/es.json +++ b/homeassistant/components/flick_electric/translations/es.json @@ -14,7 +14,7 @@ "client_id": "ID de cliente (Opcional)", "client_secret": "Secreto de Cliente (Opcional)", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Credenciales de Inicio de Sesi\u00f3n de Flick" } diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index e8baedf243c..5789a2eccc0 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -22,7 +22,7 @@ "client_id": "Client ID", "client_secret": "Client Secret", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Para acceder a la API Personal de Flume, tendr\u00e1s que solicitar un 'Client ID' y un 'Client Secret' en https://portal.flumetech.com/settings#token", "title": "Conectar con tu cuenta de Flume" diff --git a/homeassistant/components/flunearyou/translations/el.json b/homeassistant/components/flunearyou/translations/el.json index 972611ff6b1..ca6fae97190 100644 --- a/homeassistant/components/flunearyou/translations/el.json +++ b/homeassistant/components/flunearyou/translations/el.json @@ -16,5 +16,18 @@ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0397 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ae \u03c0\u03b7\u03b3\u03ae \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Flu Near You \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7. \u0395\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03b7 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd. \n\n \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 SUBMIT \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b3\u03c1\u03af\u03c0\u03b7 \u03ba\u03bf\u03bd\u03c4\u03ac \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant.", + "title": "\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf Flu Near You" + } + } + }, + "title": "\u03a4\u03bf Flu Near You \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json index 5d7b8fb6a6f..b205be8270c 100644 --- a/homeassistant/components/flunearyou/translations/es.json +++ b/homeassistant/components/flunearyou/translations/es.json @@ -16,5 +16,18 @@ "title": "Configurar Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "La fuente de datos externa que alimenta la integraci\u00f3n Flu Near You ya no est\u00e1 disponible; por lo tanto, la integraci\u00f3n ya no funciona. \n\nPulsar ENVIAR para eliminar Flu Near You de tu instancia Home Assistant.", + "title": "Eliminar Flu Near You" + } + } + }, + "title": "Flu Near You ya no est\u00e1 disponible" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/et.json b/homeassistant/components/flunearyou/translations/et.json index 9d79d166ef7..447ad54c25a 100644 --- a/homeassistant/components/flunearyou/translations/et.json +++ b/homeassistant/components/flunearyou/translations/et.json @@ -16,5 +16,18 @@ "title": "Seadista Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Flu Near You integratsiooni k\u00e4ivitav v\u00e4line andmeallikas ei ole enam saadaval; seega ei t\u00f6\u00f6ta integratsioon enam.\n\nVajutage SUBMIT, et eemaldada Flu Near You oma Home Assistant'i instantsist.", + "title": "Eemalda Flu Near You" + } + } + }, + "title": "Flu Near You pole enam saadaval" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/tr.json b/homeassistant/components/flunearyou/translations/tr.json index 3a21364502e..1e762c75f7a 100644 --- a/homeassistant/components/flunearyou/translations/tr.json +++ b/homeassistant/components/flunearyou/translations/tr.json @@ -16,5 +16,18 @@ "title": "Flu Near You'yu Yap\u0131land\u0131r\u0131n" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Flu Near You entegrasyonuna g\u00fc\u00e7 sa\u011flayan harici veri kayna\u011f\u0131 art\u0131k mevcut de\u011fil; bu nedenle, entegrasyon art\u0131k \u00e7al\u0131\u015fm\u0131yor. \n\n Home Assistant \u00f6rne\u011finden Flu Near You'yu kald\u0131rmak i\u00e7in G\u00d6NDER'e bas\u0131n.", + "title": "Yak\u0131n\u0131n\u0131zdaki Flu Near You'yu Kald\u0131r\u0131n" + } + } + }, + "title": "Flu Near You art\u0131k mevcut de\u011fil" + } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/es.json b/homeassistant/components/flux_led/translations/es.json index e4431b7a1d1..54fef29c371 100644 --- a/homeassistant/components/flux_led/translations/es.json +++ b/homeassistant/components/flux_led/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { diff --git a/homeassistant/components/foscam/translations/es.json b/homeassistant/components/foscam/translations/es.json index f80ef3335d1..93e01fabbc9 100644 --- a/homeassistant/components/foscam/translations/es.json +++ b/homeassistant/components/foscam/translations/es.json @@ -17,7 +17,7 @@ "port": "Puerto", "rtsp_port": "Puerto RTSP", "stream": "Stream", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index f1386a9e87f..21dafaec367 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", - "ignore_ip6_link_local": "El enlace con direcciones IPv6 locales no est\u00e1 permitido", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "ignore_ip6_link_local": "La direcci\u00f3n local del enlace IPv6 no es compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "upnp_not_configured": "Falta la configuraci\u00f3n de UPnP en el dispositivo." @@ -18,7 +18,7 @@ "confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Descubierto FRITZ!Box: {name}\n\nConfigurar FRITZ!Box Tools para controlar tu {name}", "title": "Configurar FRITZ!Box Tools" @@ -26,7 +26,7 @@ "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Actualizar credenciales de FRITZ!Box Tools para: {host}.\n\n FRITZ!Box Tools no puede iniciar sesi\u00f3n en tu FRITZ!Box.", "title": "Actualizando FRITZ!Box Tools - credenciales" @@ -36,7 +36,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Configure las herramientas de FRITZ! Box para controlar su FRITZ! Box.\n M\u00ednimo necesario: nombre de usuario, contrase\u00f1a.", "title": "Configurar las herramientas de FRITZ! Box" diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index 77c8bb64ee5..33dfc59d543 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "Este AVM FRITZ!Box ya est\u00e1 configurado.", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", - "ignore_ip6_link_local": "El enlace con direcciones IPv6 locales no est\u00e1 permitido", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "ignore_ip6_link_local": "La direcci\u00f3n local del enlace IPv6 no es compatible.", "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.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" @@ -16,14 +16,14 @@ "confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "\u00bfQuieres configurar {name}?" }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Actualice la informaci\u00f3n de inicio de sesi\u00f3n para {name}." }, @@ -31,7 +31,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introduce tu informaci\u00f3n de AVM FRITZ!Box." } diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json index 61e295d3b99..3be51f3bb6d 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -20,7 +20,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json index 8e1189f30f1..ac5c16c3120 100644 --- a/homeassistant/components/generic/translations/es.json +++ b/homeassistant/components/generic/translations/es.json @@ -5,62 +5,66 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "already_exists": "Ya hay una c\u00e1mara con esa URL de configuraci\u00f3n.", - "invalid_still_image": "La URL no ha devuelto una imagen fija v\u00e1lida", - "no_still_image_or_stream_url": "Tienes que especificar al menos una imagen una URL de flujo", + "already_exists": "Ya existe una c\u00e1mara con esta configuraci\u00f3n de URL.", + "invalid_still_image": "La URL no devolvi\u00f3 una imagen fija v\u00e1lida", + "malformed_url": "URL con formato incorrecto", + "no_still_image_or_stream_url": "Debes especificar al menos una imagen fija o URL de transmisi\u00f3n", + "relative_url": "No se permiten URLs relativas", "stream_file_not_found": "No se encontr\u00f3 el archivo al intentar conectarse a la transmisi\u00f3n (\u00bfest\u00e1 instalado ffmpeg?)", - "stream_http_not_found": "HTTP 404 'Not found' al intentar conectarse al flujo de datos ('stream')", + "stream_http_not_found": "HTTP 404 Not found al intentar conectarse a la transmisi\u00f3n", "stream_io_error": "Error de entrada/salida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_no_route_to_host": "No se pudo encontrar el anfitri\u00f3n mientras intentaba conectar al flujo de datos", - "stream_no_video": "El flujo no contiene v\u00eddeo", + "stream_no_route_to_host": "No se pudo encontrar el host al intentar conectarse a la transmisi\u00f3n", + "stream_no_video": "La transmisi\u00f3n no tiene video", "stream_not_permitted": "Operaci\u00f3n no permitida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_unauthorised": "La autorizaci\u00f3n ha fallado mientras se intentaba conectar con el flujo de datos", - "template_error": "Error al renderizar la plantilla. Revise el registro para m\u00e1s informaci\u00f3n.", - "timeout": "El tiempo m\u00e1ximo de carga de la URL ha expirado", - "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (p. ej., host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revise el registro para obtener m\u00e1s informaci\u00f3n.", + "stream_unauthorised": "La autorizaci\u00f3n fall\u00f3 al intentar conectarse a la transmisi\u00f3n", + "template_error": "Error al renderizar la plantilla. Revisa el registro para obtener m\u00e1s informaci\u00f3n.", + "timeout": "Tiempo de espera al cargar la URL", + "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (p. ej., host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revisa el registro para obtener m\u00e1s informaci\u00f3n.", "unknown": "Error inesperado" }, "step": { "confirm": { - "description": "\u00bfQuiere empezar a configurar?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, "content_type": { "data": { "content_type": "Tipos de contenido" }, - "description": "Especifique el tipo de contenido para el flujo de datos (stream)." + "description": "Especifica el tipo de contenido para el flujo." }, "user": { "data": { "authentication": "Autenticaci\u00f3n", - "framerate": "Frecuencia de visualizaci\u00f3n (Hz)", - "limit_refetch_to_url_change": "Limita la lectura al cambio de URL", + "framerate": "Velocidad de fotogramas (Hz)", + "limit_refetch_to_url_change": "Limitar recuperaci\u00f3n al cambio de URL", "password": "Contrase\u00f1a", "rtsp_transport": "Protocolo de transporte RTSP", - "still_image_url": "URL de imagen fija (ej. http://...)", - "stream_source": "URL origen del flux (p. ex. rtsp://...)", - "username": "Usuario", - "verify_ssl": "Verifica el certificat SSL" + "still_image_url": "URL de la imagen fija (p. ej., http://...)", + "stream_source": "URL de origen de la transmisi\u00f3n (p. ej., rtsp://...)", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, - "description": "Introduzca los ajustes para conectarse a la c\u00e1mara." + "description": "Introduce los ajustes para conectarte a la c\u00e1mara." } } }, "options": { "error": { - "already_exists": "Ya hay una c\u00e1mara con esa URL de configuraci\u00f3n.", + "already_exists": "Ya existe una c\u00e1mara con esta configuraci\u00f3n de URL.", "invalid_still_image": "La URL no devolvi\u00f3 una imagen fija v\u00e1lida", - "no_still_image_or_stream_url": "Debe especificar al menos una imagen fija o URL de transmisi\u00f3n", + "malformed_url": "URL con formato incorrecto", + "no_still_image_or_stream_url": "Debes especificar al menos una imagen fija o URL de transmisi\u00f3n", + "relative_url": "No se permiten URLs relativas", "stream_file_not_found": "No se encontr\u00f3 el archivo al intentar conectarse a la transmisi\u00f3n (\u00bfest\u00e1 instalado ffmpeg?)", - "stream_http_not_found": "HTTP 404 No encontrado al intentar conectarse a la transmisi\u00f3n", + "stream_http_not_found": "HTTP 404 Not found al intentar conectarse a la transmisi\u00f3n", "stream_io_error": "Error de entrada/salida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_no_route_to_host": "No se pudo encontrar el anfitri\u00f3n mientras intentaba conectar al flujo de datos", + "stream_no_route_to_host": "No se pudo encontrar el host al intentar conectarse a la transmisi\u00f3n", "stream_no_video": "La transmisi\u00f3n no tiene video", "stream_not_permitted": "Operaci\u00f3n no permitida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_unauthorised": "La autorizaci\u00f3n ha fallado mientras se intentaba conectar con el flujo de datos", - "template_error": "Error al renderizar la plantilla. Revise el registro para m\u00e1s informaci\u00f3n.", - "timeout": "El tiempo m\u00e1ximo de carga de la URL ha expirado", - "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (por ejemplo, host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revise el registro para obtener m\u00e1s informaci\u00f3n.", + "stream_unauthorised": "La autorizaci\u00f3n fall\u00f3 al intentar conectarse a la transmisi\u00f3n", + "template_error": "Error al renderizar la plantilla. Revisa el registro para obtener m\u00e1s informaci\u00f3n.", + "timeout": "Tiempo de espera al cargar la URL", + "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (p. ej., host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revisa el registro para obtener m\u00e1s informaci\u00f3n.", "unknown": "Error inesperado" }, "step": { @@ -68,23 +72,23 @@ "data": { "content_type": "Tipos de contenido" }, - "description": "Especifique el tipo de contenido para el flujo de datos (stream)." + "description": "Especifica el tipo de contenido para el flujo." }, "init": { "data": { "authentication": "Autenticaci\u00f3n", - "framerate": "Frecuencia de visualizaci\u00f3n (Hz)", - "limit_refetch_to_url_change": "Limita la lectura al cambio de URL", + "framerate": "Velocidad de fotogramas (Hz)", + "limit_refetch_to_url_change": "Limitar recuperaci\u00f3n al cambio de URL", "password": "Contrase\u00f1a", "rtsp_transport": "Protocolo de transporte RTSP", - "still_image_url": "URL de imagen fija (ej. http://...)", - "stream_source": "URL de origen de la transmisi\u00f3n (por ejemplo, rtsp://...)", + "still_image_url": "URL de la imagen fija (p. ej., http://...)", + "stream_source": "URL de origen de la transmisi\u00f3n (p. ej., rtsp://...)", "use_wallclock_as_timestamps": "Usar el reloj de pared como marca de tiempo", - "username": "Usuario", - "verify_ssl": "Verifica el certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "data_description": { - "use_wallclock_as_timestamps": "Esta opci\u00f3n puede corregir los problemas de segmentaci\u00f3n o bloqueo que surgen de las implementaciones de marcas de tiempo defectuosas en algunas c\u00e1maras" + "use_wallclock_as_timestamps": "Esta opci\u00f3n puede corregir problemas de segmentaci\u00f3n o bloqueo que surgen de implementaciones de marcas de tiempo con errores en algunas c\u00e1maras." } } } diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json index 8fd4800c588..712f554ac19 100644 --- a/homeassistant/components/geocaching/translations/es.json +++ b/homeassistant/components/geocaching/translations/es.json @@ -4,13 +4,13 @@ "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [compruebe la secci\u00f3n de ayuda]({docs_url})", - "oauth_error": "Se han recibido datos token inv\u00e1lidos.", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { - "default": "Autenticaci\u00f3n exitosa" + "default": "Autenticado correctamente" }, "step": { "pick_implementation": { @@ -18,7 +18,7 @@ }, "reauth_confirm": { "description": "La integraci\u00f3n Geocaching debe volver a autenticarse con tu cuenta", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/glances/translations/es.json b/homeassistant/components/glances/translations/es.json index 5560276ff3d..01f2a132136 100644 --- a/homeassistant/components/glances/translations/es.json +++ b/homeassistant/components/glances/translations/es.json @@ -15,8 +15,8 @@ "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL", "version": "Versi\u00f3n API Glances (2 o 3)" }, "title": "Configurar Glances" diff --git a/homeassistant/components/gogogate2/translations/es.json b/homeassistant/components/gogogate2/translations/es.json index 65d14508ce6..877147e46a0 100644 --- a/homeassistant/components/gogogate2/translations/es.json +++ b/homeassistant/components/gogogate2/translations/es.json @@ -13,7 +13,7 @@ "data": { "ip_address": "Direcci\u00f3n IP", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Proporciona la informaci\u00f3n requerida a continuaci\u00f3n.", "title": "Configurar GotoGate2" diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 9e777e6b377..5bd628e1f6e 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -1,16 +1,18 @@ { "application_credentials": { - "description": "Sigue las [instrucciones]({more_info_url}) para la [pantalla de consentimiento de OAuth]({oauth_consent_url}) para dar acceso a Home Assistant a tu Google Calendar. Tambi\u00e9n necesita crear credenciales de aplicaci\u00f3n vinculadas a su calendario:\n1. Vaya a [Credenciales]({oauth_creds_url}) y haga clic en **Crear credenciales**.\n1. En la lista desplegable, seleccione **ID de cliente de OAuth**.\n1. Seleccione **TV y dispositivos de entrada limitada** para el tipo de aplicaci\u00f3n." + "description": "Sigue las [instrucciones]({more_info_url}) para la [pantalla de consentimiento de OAuth]({oauth_consent_url}) para dar acceso a Home Assistant a tu Google Calendar. Tambi\u00e9n necesitas crear credenciales de aplicaci\u00f3n vinculadas a tu calendario:\n 1. Ve a [Credenciales]( {oauth_creds_url} ) y haz clic en **Crear credenciales**.\n 1. En la lista desplegable, selecciona **ID de cliente de OAuth**.\n 1. Selecciona **TV y dispositivos de entrada limitada** para el tipo de aplicaci\u00f3n." }, "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El proceso de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", "code_expired": "El c\u00f3digo de autenticaci\u00f3n caduc\u00f3 o la configuraci\u00f3n de la credencial no es v\u00e1lida, int\u00e9ntelo de nuevo.", "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "oauth_error": "Se han recibido datos token inv\u00e1lidos.", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "create_entry": { "default": "Autenticaci\u00f3n exitosa" @@ -31,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de Google Calendar en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.9. \n\nTs credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Google Calendar" + }, + "removed_track_new_yaml": { + "description": "Has inhabilitado el seguimiento de entidades para Google Calendar en configuration.yaml, que ya no es compatible. Debes cambiar manualmente las opciones del sistema de integraci\u00f3n en la IU para deshabilitar las entidades reci\u00e9n descubiertas en el futuro. Elimina la configuraci\u00f3n track_new de configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "El seguimiento de entidades de Google Calendar ha cambiado" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json index c516c9201e2..83dda4e151c 100644 --- a/homeassistant/components/google/translations/et.json +++ b/homeassistant/components/google/translations/et.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Google'i kalendri konfigureerimine failis configuration.yaml eemaldatakse versioonis Home Assistant 2022.9.\n\nTeie olemasolevad OAuth-rakenduse volitused ja juurdep\u00e4\u00e4su seaded on automaatselt kasutajaliidesesse imporditud. Probleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", + "title": "Google'i kalendri YAML-i konfiguratsioon eemaldatakse" + }, + "removed_track_new_yaml": { + "description": "Oled keelanud Google'i kalendri olemite j\u00e4lgimise rakenduses configuration.yaml, mida enam ei toetata. Peate kasutajaliideses integratsioonis\u00fcsteemi suvandeid k\u00e4sitsi muutma, et \u00e4sja avastatud olemid edaspidi keelata. Eemaldage saidilt configuration.yaml s\u00e4te track_new ja taask\u00e4ivitage home assistant selle probleemi lahendamiseks.", + "title": "Google'i kalendri olemi j\u00e4lgimine on muutunud" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/tr.json b/homeassistant/components/google/translations/tr.json index 7d67018630f..a7074b69127 100644 --- a/homeassistant/components/google/translations/tr.json +++ b/homeassistant/components/google/translations/tr.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Google Takvim'in configuration.yaml dosyas\u0131nda yap\u0131land\u0131r\u0131lmas\u0131 Home Assistant 2022.9'da kald\u0131r\u0131l\u0131yor.\n\nMevcut OAuth Uygulama Kimlik Bilgileriniz ve eri\u015fim ayarlar\u0131n\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131lm\u0131\u015ft\u0131r. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Google Takvim YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + }, + "removed_track_new_yaml": { + "description": "Art\u0131k desteklenmeyen configuration.yaml dosyas\u0131nda Google Takvim i\u00e7in varl\u0131k izlemeyi devre d\u0131\u015f\u0131 b\u0131rakt\u0131n\u0131z. \u0130leride yeni ke\u015ffedilen varl\u0131klar\u0131 devre d\u0131\u015f\u0131 b\u0131rakmak i\u00e7in kullan\u0131c\u0131 aray\u00fcz\u00fcndeki entegrasyon Sistem Se\u00e7eneklerini manuel olarak de\u011fi\u015ftirmeniz gerekir. Bu sorunu \u00e7\u00f6zmek i\u00e7in configuration.yaml dosyas\u0131ndan track_new ayar\u0131n\u0131 kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Google Takvim varl\u0131k takibi de\u011fi\u015fti" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/govee_ble/translations/es.json b/homeassistant/components/govee_ble/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/govee_ble/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/tr.json b/homeassistant/components/govee_ble/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/govee_ble/translations/tr.json @@ -0,0 +1,21 @@ +{ + "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", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index c58cb744a92..bea5a398f8c 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -5,7 +5,7 @@ "data": { "all": "Todas las entidades", "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, "description": "Si \"todas las entidades\" est\u00e1n habilitadas, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1n deshabilitadas, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado.", @@ -38,10 +38,10 @@ "lock": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, - "title": "Agregar grupo" + "title": "A\u00f1adir grupo" }, "media_player": { "data": { diff --git a/homeassistant/components/growatt_server/translations/es.json b/homeassistant/components/growatt_server/translations/es.json index 041f52b19a0..f26b7c1f173 100644 --- a/homeassistant/components/growatt_server/translations/es.json +++ b/homeassistant/components/growatt_server/translations/es.json @@ -18,7 +18,7 @@ "name": "Nombre", "password": "Contrase\u00f1a", "url": "URL", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Introduce tu informaci\u00f3n de Growatt." } diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index fe2367232a4..42c08aba617 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -17,5 +17,18 @@ "description": "Configurar un dispositivo local Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con una ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", + "title": "El servicio {deprecated_service} ser\u00e1 eliminado" + } + } + }, + "title": "El servicio {deprecated_service} ser\u00e1 eliminado" + } } } \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/es.json b/homeassistant/components/here_travel_time/translations/es.json index c1a8d9cef11..04563358781 100644 --- a/homeassistant/components/here_travel_time/translations/es.json +++ b/homeassistant/components/here_travel_time/translations/es.json @@ -12,32 +12,39 @@ "data": { "destination": "Destino como coordenadas GPS" }, - "title": "Elija el destino" + "title": "Elige el destino" }, "destination_entity_id": { "data": { "destination_entity_id": "Destino usando una entidad" }, - "title": "Elija el destino" + "title": "Elige el destino" }, "destination_menu": { "menu_options": { "destination_coordinates": "Usando una ubicaci\u00f3n en el mapa", "destination_entity": "Usando una entidad" }, - "title": "Elija el destino" + "title": "Elige el destino" }, "origin_coordinates": { "data": { "origin": "Origen como coordenadas GPS" }, - "title": "Elija el origen" + "title": "Elige el origen" }, "origin_entity_id": { "data": { "origin_entity_id": "Origen usando una entidad" }, - "title": "Elija el origen" + "title": "Elige el origen" + }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Usando una ubicaci\u00f3n en el mapa", + "origin_entity": "Usando una entidad" + }, + "title": "Elige el origen" }, "user": { "data": { @@ -54,13 +61,13 @@ "data": { "arrival_time": "Hora de llegada" }, - "title": "Elija la hora de llegada" + "title": "Elige la hora de llegada" }, "departure_time": { "data": { "departure_time": "Hora de salida" }, - "title": "Elija la hora de salida" + "title": "Elige la hora de salida" }, "init": { "data": { @@ -75,7 +82,7 @@ "departure_time": "Configurar una hora de salida", "no_time": "No configurar una hora" }, - "title": "Elija el tipo de hora" + "title": "Elige el tipo de hora" } } } diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index 09acc273536..ccca2b3ea4d 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -20,10 +20,17 @@ "description": "Introduzca su c\u00f3digo de autentificaci\u00f3n Hive. \n \n Introduzca el c\u00f3digo 0000 para solicitar otro c\u00f3digo.", "title": "Autenticaci\u00f3n de dos factores de Hive." }, + "configuration": { + "data": { + "device_name": "Nombre del dispositivo" + }, + "description": "Introduce tu configuraci\u00f3n de Hive", + "title": "Configuraci\u00f3n de Hive." + }, "reauth": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Vuelva a introducir sus datos de acceso a Hive.", "title": "Inicio de sesi\u00f3n en Hive" @@ -32,9 +39,9 @@ "data": { "password": "Contrase\u00f1a", "scan_interval": "Intervalo de exploraci\u00f3n (segundos)", - "username": "Usuario" + "username": "Nombre de usuario" }, - "description": "Ingrese su configuraci\u00f3n e informaci\u00f3n de inicio de sesi\u00f3n de Hive.", + "description": "Introduce tus datos de acceso a Hive.", "title": "Inicio de sesi\u00f3n en Hive" } } diff --git a/homeassistant/components/hlk_sw16/translations/es.json b/homeassistant/components/hlk_sw16/translations/es.json index 2609ee07eaf..c1d3e57b02f 100644 --- a/homeassistant/components/hlk_sw16/translations/es.json +++ b/homeassistant/components/hlk_sw16/translations/es.json @@ -13,7 +13,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json index 194eff4bb8c..ff4e671fb65 100644 --- a/homeassistant/components/home_plus_control/translations/es.json +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index 6b00a1b3b51..db63c818f61 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arquitectura de CPU", + "config_dir": "Directorio de configuraci\u00f3n", "dev": "Desarrollo", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant_alerts/translations/es.json b/homeassistant/components/homeassistant_alerts/translations/es.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/et.json b/homeassistant/components/homeassistant_alerts/translations/et.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/tr.json b/homeassistant/components/homeassistant_alerts/translations/tr.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index a528d66ea06..54dd93f8a55 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -18,7 +18,7 @@ "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. La vinculaci\u00f3n ha fallado." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Interrumpe el emparejamiento en todos los controladores o intenta reiniciar el dispositivo y luego contin\u00faa con el emparejamiento.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Permitir el emparejamiento con c\u00f3digos de configuraci\u00f3n inseguros.", "pairing_code": "C\u00f3digo de vinculaci\u00f3n" }, - "description": "El controlador de HomeKit se comunica con {name} a trav\u00e9s de la red de \u00e1rea local usando una conexi\u00f3n encriptada segura sin un controlador HomeKit separado o iCloud. Introduce el c\u00f3digo de vinculaci\u00f3n de tu HomeKit (con el formato XXX-XX-XXX) para usar este accesorio. Este c\u00f3digo suele encontrarse en el propio dispositivo o en el embalaje.", + "description": "El controlador HomeKit se comunica con {name} ({category}) a trav\u00e9s de la red de \u00e1rea local mediante una conexi\u00f3n cifrada segura sin un controlador HomeKit o iCloud por separado. Introduce tu c\u00f3digo de emparejamiento de HomeKit (en el formato XXX-XX-XXX) para usar este accesorio. Este c\u00f3digo suele encontrarse en el propio dispositivo o en el embalaje.", "title": "Vincular un dispositivo a trav\u00e9s del protocolo de accesorios HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/sensor.ca.json b/homeassistant/components/homekit_controller/translations/sensor.ca.json index dde4926406c..5ee2100c59b 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.ca.json +++ b/homeassistant/components/homekit_controller/translations/sensor.ca.json @@ -1,6 +1,7 @@ { "state": { "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Pot funcionar com a encaminador (router) frontera", "full": "Dispositiu final complet", "minimal": "Dispositiu final redu\u00eft", "none": "Cap", diff --git a/homeassistant/components/homekit_controller/translations/sensor.el.json b/homeassistant/components/homekit_controller/translations/sensor.el.json new file mode 100644 index 00000000000..c1c02c08bff --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.el.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "\u0394\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03c5\u03bd\u03cc\u03c1\u03c9\u03bd", + "full": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bb\u03ae\u03c1\u03bf\u03c5\u03c2 \u03bb\u03ae\u03be\u03b7\u03c2", + "minimal": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03c4\u03b5\u03bb\u03b9\u03ba\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1", + "router_eligible": "\u039a\u03b1\u03c4\u03ac\u03bb\u03bb\u03b7\u03bb\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c4\u03b5\u03c1\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae", + "sleepy": "Sleepy End Device" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "child": "\u03a0\u03b1\u03b9\u03b4\u03af", + "detached": "\u0391\u03c0\u03bf\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03bf", + "disabled": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", + "joining": "\u03a3\u03c5\u03bc\u03bc\u03b5\u03c4\u03bf\u03c7\u03ae", + "leader": "\u0397\u03b3\u03ad\u03c4\u03b7\u03c2", + "router": "\u0394\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.es.json b/homeassistant/components/homekit_controller/translations/sensor.es.json new file mode 100644 index 00000000000..e3ba6322d3f --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.es.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Capacidad de router fronterizo", + "full": "Dispositivo final completo", + "minimal": "Dispositivo final m\u00ednimo", + "none": "Ninguna", + "router_eligible": "Dispositivo final elegible como router", + "sleepy": "Dispositivo final dormido" + }, + "homekit_controller__thread_status": { + "border_router": "Router fronterizo", + "child": "Hijo", + "detached": "Separado", + "disabled": "Deshabilitado", + "joining": "Uniendo", + "leader": "L\u00edder", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.et.json b/homeassistant/components/homekit_controller/translations/sensor.et.json new file mode 100644 index 00000000000..7594576edad --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.et.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Piiriruuteri v\u00f5imekus", + "full": "T\u00e4ielik l\u00f5ppseade", + "minimal": "Minimaalne l\u00f5ppseade", + "none": "Puudub", + "router_eligible": "Ruuteriks sobiv l\u00f5ppseade", + "sleepy": "Unine l\u00f5ppseade" + }, + "homekit_controller__thread_status": { + "border_router": "Rajaruuter", + "child": "Alamseade", + "detached": "Eraldatud", + "disabled": "Keelatud", + "joining": "Liitun", + "leader": "Juhtseade", + "router": "Ruuter" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.tr.json b/homeassistant/components/homekit_controller/translations/sensor.tr.json new file mode 100644 index 00000000000..4f295f42ce1 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.tr.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "S\u0131n\u0131r Y\u00f6nlendirici \u00d6zelli\u011fi", + "full": "Tam Son Cihaz", + "minimal": "Minimal Son Cihaz", + "none": "Hi\u00e7biri", + "router_eligible": "Y\u00f6nlendiriciye Uygun Son Cihaz", + "sleepy": "Uykudaki Son Cihaz" + }, + "homekit_controller__thread_status": { + "border_router": "S\u0131n\u0131r Y\u00f6nlendirici", + "child": "\u00c7ocuk", + "detached": "Tarafs\u0131z", + "disabled": "Devre d\u0131\u015f\u0131", + "joining": "Kat\u0131l\u0131yor", + "leader": "Lider", + "router": "Y\u00f6nlendirici" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index 98ad6871b81..c08c02c3632 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -7,7 +7,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." } diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index 7017cca99c1..6b72df25f65 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -3,7 +3,7 @@ "abort": { "all_configured": "Ya se han configurado todas las pasarelas Philips Hue", "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "discover_timeout": "Imposible encontrar pasarelas Philips Hue", "invalid_host": "Host inv\u00e1lido", diff --git a/homeassistant/components/huisbaasje/translations/es.json b/homeassistant/components/huisbaasje/translations/es.json index d537185eb68..942024cf167 100644 --- a/homeassistant/components/huisbaasje/translations/es.json +++ b/homeassistant/components/huisbaasje/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/hvv_departures/translations/es.json b/homeassistant/components/hvv_departures/translations/es.json index d73002c5a4b..8cfa90d6367 100644 --- a/homeassistant/components/hvv_departures/translations/es.json +++ b/homeassistant/components/hvv_departures/translations/es.json @@ -25,7 +25,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con el API de HVV" } diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index 496064b1543..d96584ee532 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "auth_new_token_not_granted_error": "El token reci\u00e9n creado no se aprob\u00f3 en la interfaz de usuario de Hyperion", "auth_new_token_not_work_error": "Error al autenticarse con el token reci\u00e9n creado", "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n", diff --git a/homeassistant/components/iaqualink/translations/es.json b/homeassistant/components/iaqualink/translations/es.json index c95f9d51927..7587b393c3f 100644 --- a/homeassistant/components/iaqualink/translations/es.json +++ b/homeassistant/components/iaqualink/translations/es.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introduce el nombre de usuario y contrase\u00f1a de tu cuenta de iAqualink.", "title": "Conexi\u00f3n con iAqualink" diff --git a/homeassistant/components/inkbird/translations/es.json b/homeassistant/components/inkbird/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/inkbird/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/tr.json b/homeassistant/components/inkbird/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/inkbird/translations/tr.json @@ -0,0 +1,21 @@ +{ + "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", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 5434bc77a8a..768c590151e 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -27,7 +27,7 @@ "host": "Direcci\u00f3n IP", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Configure el Insteon Hub versi\u00f3n 2.", "title": "Insteon Hub Versi\u00f3n 2" @@ -76,7 +76,7 @@ "host": "Direcci\u00f3n IP", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Cambiar la informaci\u00f3n de la conexi\u00f3n del Hub Insteon. Debes reiniciar el Home Assistant despu\u00e9s de hacer este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n del Hub usa la aplicaci\u00f3n Hub." }, diff --git a/homeassistant/components/integration/translations/es.json b/homeassistant/components/integration/translations/es.json index 4b4f1306dc9..7fee4f392ce 100644 --- a/homeassistant/components/integration/translations/es.json +++ b/homeassistant/components/integration/translations/es.json @@ -32,5 +32,5 @@ } } }, - "title": "Integraci\u00f3n - Sensor integral de suma de Riemann" + "title": "Integraci\u00f3n - Sensor de suma integral de Riemann" } \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/es.json b/homeassistant/components/intellifire/translations/es.json index 4b61f7f4b3e..c44475be1a1 100644 --- a/homeassistant/components/intellifire/translations/es.json +++ b/homeassistant/components/intellifire/translations/es.json @@ -6,7 +6,7 @@ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "api_error": "Ha Fallado el inicio de sesi\u00f3n", + "api_error": "Error de inicio de sesi\u00f3n", "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "iftapi_connect": "Se ha producido un error al conectar a iftapi.net" }, @@ -31,7 +31,7 @@ "data": { "host": "Host" }, - "description": "Se han descubierto los siguientes dispositivos IntelliFire. Selecciona lo que quieras configurar.", + "description": "Se han descubierto los siguientes dispositivos IntelliFire. Por favor, selecciona el que quieras configurar.", "title": "Selecci\u00f3n de dispositivo" } } diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index be1b38f08db..f6948374561 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -21,7 +21,7 @@ "host": "Host", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" }, "description": "Configura tu impresora a trav\u00e9s del Protocolo de Impresi\u00f3n de Internet (IPP) para integrarla con Home Assistant.", "title": "Vincula tu impresora" diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index 7fd765fb437..dd5e97923d6 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "Error al conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", @@ -17,15 +17,15 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Las credenciales de {host} ya no son v\u00e1lidas.", - "title": "Re-autenticaci\u00f3n de ISY" + "description": "Las credenciales para {host} ya no son v\u00e1lidas.", + "title": "Vuelve a autenticar tu ISY" }, "user": { "data": { "host": "URL", "password": "Contrase\u00f1a", "tls": "La versi\u00f3n de TLS del controlador ISY.", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "La entrada del host debe estar en formato URL completo, por ejemplo, http://192.168.10.100:80", "title": "Conexi\u00f3n con ISY" diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index f6625c0bb60..b70d9601ba2 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -41,7 +41,7 @@ "sensor_string": "\u7bc0\u9ede\u611f\u6e2c\u5668\u5b57\u4e32", "variable_sensor_string": "\u53ef\u8b8a\u611f\u6e2c\u5668\u5b57\u4e32" }, - "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u611f\u6e2c\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u8996\u70ba\u611f\u6e2c\u5668\u6216\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u611f\u6e2c\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u611f\u6e2c\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u5553\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u88dd\u7f6e\u9810\u8a2d\u4eae\u5ea6\u3002", + "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u611f\u6e2c\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u8996\u70ba\u611f\u6e2c\u5668\u6216\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u611f\u6e2c\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u611f\u6e2c\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u555f\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u88dd\u7f6e\u9810\u8a2d\u4eae\u5ea6\u3002", "title": "ISY \u9078\u9805" } } diff --git a/homeassistant/components/jellyfin/translations/es.json b/homeassistant/components/jellyfin/translations/es.json index 1cc7ab64c75..b6e0c52c667 100644 --- a/homeassistant/components/jellyfin/translations/es.json +++ b/homeassistant/components/jellyfin/translations/es.json @@ -13,7 +13,7 @@ "data": { "password": "Contrase\u00f1a", "url": "URL", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/justnimbus/translations/ca.json b/homeassistant/components/justnimbus/translations/ca.json new file mode 100644 index 00000000000..679f8726b1b --- /dev/null +++ b/homeassistant/components/justnimbus/translations/ca.json @@ -0,0 +1,19 @@ +{ + "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": { + "client_id": "ID de client" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/de.json b/homeassistant/components/justnimbus/translations/de.json new file mode 100644 index 00000000000..d60cbfd45d6 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/de.json @@ -0,0 +1,19 @@ +{ + "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": { + "client_id": "Client-ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/el.json b/homeassistant/components/justnimbus/translations/el.json new file mode 100644 index 00000000000..a26b9f1a466 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "client_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/es.json b/homeassistant/components/justnimbus/translations/es.json new file mode 100644 index 00000000000..2b718ffd7bd --- /dev/null +++ b/homeassistant/components/justnimbus/translations/es.json @@ -0,0 +1,19 @@ +{ + "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": { + "client_id": "ID de cliente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/et.json b/homeassistant/components/justnimbus/translations/et.json new file mode 100644 index 00000000000..327b1a41eda --- /dev/null +++ b/homeassistant/components/justnimbus/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "client_id": "Kliendi ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/pt-BR.json b/homeassistant/components/justnimbus/translations/pt-BR.json new file mode 100644 index 00000000000..c6fec98d719 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "client_id": "ID do Cliente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/tr.json b/homeassistant/components/justnimbus/translations/tr.json new file mode 100644 index 00000000000..017fbed5f8a --- /dev/null +++ b/homeassistant/components/justnimbus/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": { + "client_id": "\u0130stemci Kimli\u011fi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index d982d96b330..627c26ab72a 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -7,8 +7,8 @@ "error": { "cannot_connect": "Error al conectar", "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", - "invalid_individual_address": "El valor no coincide con el patr\u00f3n de direcci\u00f3n KNX individual. 'area.line.device'", - "invalid_ip_address": "Direcci\u00f3n IPv4 inv\u00e1lida.", + "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", + "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta." }, "step": { @@ -20,9 +20,9 @@ "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { - "host": "Direcci\u00f3n IP del dispositivo de t\u00fanel KNX/IP.", + "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", "local_ip": "D\u00e9jalo en blanco para utilizar el descubrimiento autom\u00e1tico.", - "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP.Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." + "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." }, "description": "Introduzca la informaci\u00f3n de conexi\u00f3n de su dispositivo de tunelizaci\u00f3n." }, @@ -35,39 +35,39 @@ }, "data_description": { "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", - "local_ip": "D\u00e9jelo en blanco para usar el descubrimiento autom\u00e1tico." + "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico." }, "description": "Por favor, configure las opciones de enrutamiento." }, "secure_knxkeys": { "data": { - "knxkeys_filename": "El nombre de su archivo `.knxkeys` (incluyendo la extensi\u00f3n)", + "knxkeys_filename": "El nombre de tu archivo `.knxkeys` (incluyendo la extensi\u00f3n)", "knxkeys_password": "Contrase\u00f1a para descifrar el archivo `.knxkeys`." }, "data_description": { - "knxkeys_filename": "Se espera que el archivo se encuentre en su directorio de configuraci\u00f3n en `.storage/knx/`.\n En el sistema operativo Home Assistant, ser\u00eda `/config/.storage/knx/`\n Ejemplo: `mi_proyecto.knxkeys`", - "knxkeys_password": "Se ha definido durante la exportaci\u00f3n del archivo desde ETS.Se ha definido durante la exportaci\u00f3n del archivo desde ETS." + "knxkeys_filename": "Se espera que el archivo se encuentre en tu directorio de configuraci\u00f3n en `.storage/knx/`.\nEn Home Assistant OS ser\u00eda `/config/.storage/knx/`\nEjemplo: `mi_proyecto.knxkeys`", + "knxkeys_password": "Esto se configur\u00f3 al exportar el archivo desde ETS." }, - "description": "Introduce la informaci\u00f3n de tu archivo `.knxkeys`." + "description": "Por favor, introduce la informaci\u00f3n de tu archivo `.knxkeys`." }, "secure_manual": { "data": { "device_authentication": "Contrase\u00f1a de autenticaci\u00f3n del dispositivo", "user_id": "ID de usuario", - "user_password": "Contrase\u00f1a del usuario" + "user_password": "Contrase\u00f1a de usuario" }, "data_description": { "device_authentication": "Esto se configura en el panel 'IP' de la interfaz en ETS.", - "user_id": "A menudo, es el n\u00famero del t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda el ID de usuario '3'.", - "user_password": "Contrase\u00f1a para la conexi\u00f3n espec\u00edfica del t\u00fanel establecida en el panel de \"Propiedades\" del t\u00fanel en ETS." + "user_id": "Este suele ser el n\u00famero de t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda ID de usuario '3'.", + "user_password": "Contrase\u00f1a para la conexi\u00f3n de t\u00fanel espec\u00edfica establecida en el panel 'Propiedades' del t\u00fanel en ETS." }, - "description": "Introduce la informaci\u00f3n de seguridad IP (IP Secure)." + "description": "Introduce tu informaci\u00f3n de IP segura." }, "secure_tunneling": { "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", "menu_options": { - "secure_knxkeys": "Use un archivo `.knxkeys` que contenga claves seguras de IP", - "secure_manual": "Configura manualmente las claves de seguridad IP (IP Secure)" + "secure_knxkeys": "Utilizar un archivo `.knxkeys` que contenga claves seguras de IP", + "secure_manual": "Configurar claves seguras de IP manualmente" } }, "tunnel": { @@ -97,12 +97,12 @@ "state_updater": "Actualizador de estado" }, "data_description": { - "individual_address": "Direcci\u00f3n KNX para utilizar con Home Assistant, ej. `0.0.4`", - "local_ip": "Usar `0.0.0.0` para el descubrimiento autom\u00e1tico.", - "multicast_group": "Se usa para el enrutamiento y el descubrimiento. Predeterminado: `224.0.23.12`", - "multicast_port": "Se usa para el enrutamiento y el descubrimiento. Predeterminado: `3671`", - "rate_limit": "Telegramas de salida m\u00e1ximos por segundo. \nRecomendado: de 20 a 40", - "state_updater": "Establece los valores predeterminados para leer los estados del bus KNX. Cuando est\u00e1 deshabilitado, Home Assistant no recuperar\u00e1 activamente estados de entidad del bus KNX. Puede ser anulado por las opciones de entidad `sync_state`." + "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", + "local_ip": "Usa `0.0.0.0` para el descubrimiento autom\u00e1tico.", + "multicast_group": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `224.0.23.12`", + "multicast_port": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `3671`", + "rate_limit": "N\u00famero m\u00e1ximo de telegramas salientes por segundo.\nRecomendado: 20 a 40", + "state_updater": "Establece los valores predeterminados para leer los estados del bus KNX. Cuando est\u00e1 deshabilitado, Home Assistant no recuperar\u00e1 activamente los estados de entidad del bus KNX. Puede ser anulado por las opciones de entidad `sync_state`." } }, "tunnel": { diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 1074711901c..901c858b33c 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "not_konn_panel": "No es un dispositivo Konnected.io reconocido", "unknown": "Se produjo un error desconocido" diff --git a/homeassistant/components/lacrosse_view/translations/es.json b/homeassistant/components/lacrosse_view/translations/es.json new file mode 100644 index 00000000000..1f341b0f44e --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_locations": "No se encontraron ubicaciones", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/et.json b/homeassistant/components/lacrosse_view/translations/et.json new file mode 100644 index 00000000000..8f21ccff5f6 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus", + "no_locations": "Asukohti ei leitud", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/tr.json b/homeassistant/components/lacrosse_view/translations/tr.json new file mode 100644 index 00000000000..cf82d698150 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_locations": "Konum bulunamad\u0131", + "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/laundrify/translations/es.json b/homeassistant/components/laundrify/translations/es.json index 05c019700bc..8c55a8cf441 100644 --- a/homeassistant/components/laundrify/translations/es.json +++ b/homeassistant/components/laundrify/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "Fallo en la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_format": "Formato no v\u00e1lido. Por favor, especif\u00edquelo como xxx-xxx.", + "invalid_format": "Formato no v\u00e1lido. Por favor, especif\u00edcalo como xxx-xxx.", "unknown": "Error inesperado" }, "step": { @@ -14,11 +14,11 @@ "data": { "code": "C\u00f3digo de autenticaci\u00f3n (xxx-xxx)" }, - "description": "Por favor, introduzca su c\u00f3digo de autenticaci\u00f3n personal que se muestra en la aplicaci\u00f3n Laundrify." + "description": "Por favor, introduce tu c\u00f3digo de autenticaci\u00f3n personal que se muestra en la aplicaci\u00f3n laundrify." }, "reauth_confirm": { - "description": "La integraci\u00f3n de laundrify necesita volver a autentificarse.", - "title": "Integraci\u00f3n de la reautenticaci\u00f3n" + "description": "La integraci\u00f3n laundrify necesita volver a autentificarse.", + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/lcn/translations/es.json b/homeassistant/components/lcn/translations/es.json index cc9f91601c9..045f87a1927 100644 --- a/homeassistant/components/lcn/translations/es.json +++ b/homeassistant/components/lcn/translations/es.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "c\u00f3digo de bloqueo de c\u00f3digo recibido", "fingerprint": "c\u00f3digo de huella dactilar recibido", "send_keys": "enviar claves recibidas", "transmitter": "c\u00f3digo de transmisor recibido", diff --git a/homeassistant/components/lg_soundbar/translations/es.json b/homeassistant/components/lg_soundbar/translations/es.json new file mode 100644 index 00000000000..dc78afa232b --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/et.json b/homeassistant/components/lg_soundbar/translations/et.json index 227250382c0..f5c73126124 100644 --- a/homeassistant/components/lg_soundbar/translations/et.json +++ b/homeassistant/components/lg_soundbar/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "existing_instance_updated": "V\u00e4rskendati olemasolevat konfiguratsiooni." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/tr.json b/homeassistant/components/lg_soundbar/translations/tr.json index 5eb581847fb..c80f5540643 100644 --- a/homeassistant/components/lg_soundbar/translations/tr.json +++ b/homeassistant/components/lg_soundbar/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "existing_instance_updated": "Mevcut yap\u0131land\u0131rma g\u00fcncellendi." }, "error": { diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index 02a4a349ee9..b8495b5e916 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "La cuenta ya est\u00e1 configurada", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "invalid_username": "Nombre de usuario no v\u00e1lido", "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Para configurar las opciones avanzadas, revisa la [documentaci\u00f3n de Life360]({docs_url}).\nDeber\u00edas hacerlo antes de a\u00f1adir alguna cuenta.", - "title": "Informaci\u00f3n de la cuenta de Life360" + "title": "Configurar la cuenta de Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostrar conducci\u00f3n como estado", + "driving_speed": "Velocidad de conducci\u00f3n", + "limit_gps_acc": "Limitar la precisi\u00f3n del GPS", + "max_gps_accuracy": "Precisi\u00f3n m\u00e1xima del GPS (metros)", + "set_drive_speed": "Establecer el umbral de velocidad de conducci\u00f3n" + }, + "title": "Opciones de la cuenta" } } } diff --git a/homeassistant/components/lifx/translations/es.json b/homeassistant/components/lifx/translations/es.json index 484d59ba55f..a308d5fbb9e 100644 --- a/homeassistant/components/lifx/translations/es.json +++ b/homeassistant/components/lifx/translations/es.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_devices_found": "No se encontraron dispositivos en la red", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u00bfQuieres configurar LIFX?" + }, + "discovery_confirm": { + "description": "\u00bfQuieres configurar {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Si deja el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/litterrobot/translations/sensor.es.json b/homeassistant/components/litterrobot/translations/sensor.es.json index ca5695d1347..a67a15c6820 100644 --- a/homeassistant/components/litterrobot/translations/sensor.es.json +++ b/homeassistant/components/litterrobot/translations/sensor.es.json @@ -6,18 +6,18 @@ "ccp": "Ciclo de limpieza en curso", "csf": "Fallo del sensor de gatos", "csi": "Sensor de gatos interrumpido", - "cst": "Tiempo del sensor de gatos", + "cst": "Sincronizaci\u00f3n del sensor de gatos", "df1": "Caj\u00f3n casi lleno - Quedan 2 ciclos", "df2": "Caj\u00f3n casi lleno - Queda 1 ciclo", "dfs": "Caj\u00f3n lleno", - "dhf": "Error de posici\u00f3n de vertido + inicio", + "dhf": "Fallo de volcado + posici\u00f3n inicial", "dpf": "Fallo de posici\u00f3n de descarga", "ec": "Ciclo vac\u00edo", "hpf": "Fallo de posici\u00f3n inicial", "off": "Apagado", - "offline": "Desconectado", + "offline": "Sin conexi\u00f3n", "otf": "Fallo de par excesivo", - "p": "Pausada", + "p": "En pausa", "pd": "Detecci\u00f3n de pellizcos", "rdy": "Listo", "scf": "Fallo del sensor de gatos al inicio", diff --git a/homeassistant/components/lookin/translations/es.json b/homeassistant/components/lookin/translations/es.json index c1e443bddcf..d160513d0ff 100644 --- a/homeassistant/components/lookin/translations/es.json +++ b/homeassistant/components/lookin/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "no_devices_found": "No se encontraron dispositivos en la red" }, diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index 12692849ce3..5405ca19ffa 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -17,5 +17,11 @@ "title": "Volver a autenticar la integraci\u00f3n" } } + }, + "issues": { + "removed_yaml": { + "description": "Se elimin\u00f3 la configuraci\u00f3n de Honeywell Lyric mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se elimin\u00f3 la configuraci\u00f3n YAML de Honeywell Lyric" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/et.json b/homeassistant/components/lyric/translations/et.json index b3e19a93b26..e5e7c970e13 100644 --- a/homeassistant/components/lyric/translations/et.json +++ b/homeassistant/components/lyric/translations/et.json @@ -17,5 +17,11 @@ "title": "Taastuvastamine" } } + }, + "issues": { + "removed_yaml": { + "description": "Honeywell Lyric'i konfigureerimine YAML-i abil on eemaldatud.\n\nTeie olemasolevat YAML-konfiguratsiooni ei kasuta Home Assistant.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Honeywell Lyric YAML-i konfiguratsioon on eemaldatud" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/tr.json b/homeassistant/components/lyric/translations/tr.json index b910327ed7e..b49ffa53e18 100644 --- a/homeassistant/components/lyric/translations/tr.json +++ b/homeassistant/components/lyric/translations/tr.json @@ -17,5 +17,11 @@ "title": "Entegrasyonu Yeniden Do\u011frula" } } + }, + "issues": { + "removed_yaml": { + "description": "Honeywell Lyric'in YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Honeywell Lyric YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/meater/translations/es.json b/homeassistant/components/meater/translations/es.json index 44e5f3984f4..8c4be00c610 100644 --- a/homeassistant/components/meater/translations/es.json +++ b/homeassistant/components/meater/translations/es.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "service_unavailable_error": "La API no est\u00e1 disponible actualmente, vuelva a intentarlo m\u00e1s tarde.", + "service_unavailable_error": "La API no est\u00e1 disponible en este momento, por favor int\u00e9ntalo m\u00e1s tarde.", "unknown_auth_error": "Error inesperado" }, "step": { @@ -15,12 +15,12 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "data_description": { "username": "Nombre de usuario de Meater Cloud, normalmente una direcci\u00f3n de correo electr\u00f3nico." }, - "description": "Configure su cuenta de Meater Cloud." + "description": "Configura tu cuenta de Meater Cloud." } } } diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index fd60d09f562..ca2a633eb21 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_buffering": "{entity_name} se est\u00e1 cargando en memoria", + "is_buffering": "{entity_name} est\u00e1 almacenando en b\u00fafer", "is_idle": "{entity_name} est\u00e1 inactivo", "is_off": "{entity_name} est\u00e1 apagado", "is_on": "{entity_name} est\u00e1 activado", @@ -9,7 +9,7 @@ "is_playing": "{entity_name} est\u00e1 reproduciendo" }, "trigger_type": { - "buffering": "{entity_name} comienza a cargarse en memoria", + "buffering": "{entity_name} comienza a almacenar en b\u00fafer", "changed_states": "{entity_name} ha cambiado de estado", "idle": "{entity_name} est\u00e1 inactivo", "paused": "{entity_name} est\u00e1 en pausa", @@ -20,7 +20,7 @@ }, "state": { "_": { - "buffering": "Cargando", + "buffering": "almacenamiento en b\u00fafer", "idle": "Inactivo", "off": "Apagado", "on": "Encendido", diff --git a/homeassistant/components/miflora/translations/es.json b/homeassistant/components/miflora/translations/es.json new file mode 100644 index 00000000000..93a1e82aed6 --- /dev/null +++ b/homeassistant/components/miflora/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "La integraci\u00f3n Mi Flora dej\u00f3 de funcionar en Home Assistant 2022.7 y se reemplaz\u00f3 por la integraci\u00f3n de Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes agregar tu dispositivo Mi Flora usando la nueva integraci\u00f3n manualmente. \n\nHome Assistant ya no usa la configuraci\u00f3n YAML existente de Mi Flora. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "La integraci\u00f3n Mi Flora ha sido reemplazada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/et.json b/homeassistant/components/miflora/translations/et.json new file mode 100644 index 00000000000..f340d3263ca --- /dev/null +++ b/homeassistant/components/miflora/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Mi Flora integratsioon lakkas t\u00f6\u00f6tamast versioonis Home Assistant 2022.7 ja asendati Xiaomi BLE integratsiooniga versioonis 2022.8.\n\nMigratsiooniteed ei ole v\u00f5imalik, seega peate oma Mi Flora seadme uue integratsiooni abil k\u00e4sitsi lisama.\n\nTeie olemasolevat Mi Flora YAML-konfiguratsiooni ei kasuta Home Assistant enam. Probleemi lahendamiseks eemaldage YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Mi Flora sidumine on asendatud" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/tr.json b/homeassistant/components/miflora/translations/tr.json new file mode 100644 index 00000000000..8d6fe8a625d --- /dev/null +++ b/homeassistant/components/miflora/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Mi Flora entegrasyonu Home Assistant 2022.7'de \u00e7al\u0131\u015fmay\u0131 durdurdu ve 2022.8 s\u00fcr\u00fcm\u00fcnde Xiaomi BLE entegrasyonu ile de\u011fi\u015ftirildi.\n\nGe\u00e7i\u015f yolu m\u00fcmk\u00fcn de\u011fildir, bu nedenle Mi Flora cihaz\u0131n\u0131z\u0131 yeni entegrasyonu kullanarak manuel olarak eklemeniz gerekir.\n\nMevcut Mi Flora YAML yap\u0131land\u0131rman\u0131z art\u0131k Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. Bu sorunu gidermek i\u00e7in YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Mi Flora entegrasyonu de\u011fi\u015ftirildi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/es.json b/homeassistant/components/mikrotik/translations/es.json index e252c492c47..d4751c19a9a 100644 --- a/homeassistant/components/mikrotik/translations/es.json +++ b/homeassistant/components/mikrotik/translations/es.json @@ -15,7 +15,7 @@ "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario", + "username": "Nombre de usuario", "verify_ssl": "Usar ssl" }, "title": "Configurar el router Mikrotik" diff --git a/homeassistant/components/min_max/translations/es.json b/homeassistant/components/min_max/translations/es.json index fadad650eaa..2be7203d0d4 100644 --- a/homeassistant/components/min_max/translations/es.json +++ b/homeassistant/components/min_max/translations/es.json @@ -9,10 +9,10 @@ "type": "Caracter\u00edstica estad\u00edstica" }, "data_description": { - "round_digits": "Controla el n\u00famero de d\u00edgitos decimales cuando la caracter\u00edstica estad\u00edstica es la media o mediana." + "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media o mediana." }, "description": "Cree un sensor que calcule un valor m\u00ednimo, m\u00e1ximo, medio o mediano a partir de una lista de sensores de entrada.", - "title": "Agregar sensor m\u00edn / m\u00e1x / media / mediana" + "title": "A\u00f1adir sensor m\u00edn / m\u00e1x / media / mediana" } } }, @@ -25,7 +25,7 @@ "type": "Caracter\u00edstica estad\u00edstica" }, "data_description": { - "round_digits": "Controla el n\u00famero de d\u00edgitos decimales cuando la caracter\u00edstica estad\u00edstica es la media o mediana." + "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media o mediana." } } } diff --git a/homeassistant/components/mitemp_bt/translations/es.json b/homeassistant/components/mitemp_bt/translations/es.json new file mode 100644 index 00000000000..6ae1e300961 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE dej\u00f3 de funcionar en Home Assistant 2022.7 y fue reemplazada por la integraci\u00f3n Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes agregar tu dispositivo Xiaomi Mijia BLE utilizando la nueva integraci\u00f3n manualmente. \n\nHome Assistant ya no utiliza la configuraci\u00f3n YAML del sensor de temperatura y humedad BLE de Xiaomi Mijia. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE ha sido reemplazada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/et.json b/homeassistant/components/mitemp_bt/translations/et.json new file mode 100644 index 00000000000..2dd28b9cc4e --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Xiaomi Mijia BLE temperatuuri- ja niiskusanduri integratsioon lakkas t\u00f6\u00f6tamast Home Assistant 2022.7 versioonis ja asendati Xiaomi BLE integratsiooniga 2022.8 versioonis.\n\nMigratsiooniteed ei ole v\u00f5imalik, seega peate oma Xiaomi Mijia BLE-seadme lisama uue integratsiooni abil k\u00e4sitsi.\n\nTeie olemasolevat Xiaomi Mijia BLE temperatuuri ja \u00f5huniiskuse anduri YAML-konfiguratsiooni ei kasuta enam Home Assistant. Eemaldage YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", + "title": "Xiaomi Mijia BLE temperatuuri ja niiskusanduri sidumine on asendatud" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/tr.json b/homeassistant/components/mitemp_bt/translations/tr.json new file mode 100644 index 00000000000..5e7c18a9544 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Xiaomi Mijia BLE S\u0131cakl\u0131k ve Nem Sens\u00f6r\u00fc entegrasyonu Home Assistant 2022.7'de \u00e7al\u0131\u015fmay\u0131 durdurdu ve 2022.8 s\u00fcr\u00fcm\u00fcnde Xiaomi BLE entegrasyonu ile de\u011fi\u015ftirildi.\n\nGe\u00e7i\u015f yolu m\u00fcmk\u00fcn de\u011fildir, bu nedenle Xiaomi Mijia BLE cihaz\u0131n\u0131z\u0131 yeni entegrasyonu kullanarak manuel olarak eklemeniz gerekir.\n\nMevcut Xiaomi Mijia BLE S\u0131cakl\u0131k ve Nem Sens\u00f6r\u00fc YAML yap\u0131land\u0131rman\u0131z art\u0131k Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Xiaomi Mijia BLE S\u0131cakl\u0131k ve Nem Sens\u00f6r\u00fc entegrasyonu de\u011fi\u015ftirildi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/es.json b/homeassistant/components/moat/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/moat/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/tr.json b/homeassistant/components/moat/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/moat/translations/tr.json @@ -0,0 +1,21 @@ +{ + "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", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/es.json b/homeassistant/components/modem_callerid/translations/es.json index 6c62f64a1d6..bc1b20cbbe0 100644 --- a/homeassistant/components/modem_callerid/translations/es.json +++ b/homeassistant/components/modem_callerid/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_devices_found": "No se encontraron dispositivos restantes" }, "error": { diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index 1a4312d4fe0..11e8c2a2e71 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "connection_error": "No se pudo conectar" }, "error": { diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json index e9480019bb6..881972cf5c4 100644 --- a/homeassistant/components/motioneye/translations/es.json +++ b/homeassistant/components/motioneye/translations/es.json @@ -18,9 +18,9 @@ "user": { "data": { "admin_password": "Contrase\u00f1a administrador", - "admin_username": "Usuario administrador", + "admin_username": "Nombre de usuario administrador", "surveillance_password": "Contrase\u00f1a vigilancia", - "surveillance_username": "Usuario vigilancia", + "surveillance_username": "Nombre de usuario vigilancia", "url": "URL" } } diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 89a5ce04d97..93fe4b53933 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -14,7 +14,7 @@ "discovery": "Habilitar descubrimiento", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT." }, @@ -61,7 +61,7 @@ "broker": "Br\u00f3ker", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT.", "title": "Opciones del br\u00f3ker" diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index 98d02b56280..d8520cd2b6f 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -20,7 +20,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con el Gateway " } diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 1527f2b3307..94ba8b80c2f 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -70,6 +70,7 @@ "description": "Configuraci\u00f3 de passarel\u00b7la Ethernet" }, "select_gateway_type": { + "description": "Selecciona la passarel\u00b7la (gateway) a configurar.", "menu_options": { "gw_mqtt": "Configura passarel\u00b7la (gateway) MQTT", "gw_serial": "Configura passarel\u00b7la (gateway) s\u00e8rie", diff --git a/homeassistant/components/mysensors/translations/el.json b/homeassistant/components/mysensors/translations/el.json index 81ba72eea7e..a761f148c27 100644 --- a/homeassistant/components/mysensors/translations/el.json +++ b/homeassistant/components/mysensors/translations/el.json @@ -14,6 +14,7 @@ "invalid_serial": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1", "invalid_subscribe_topic": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2", "invalid_version": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 MySensors", + "mqtt_required": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 MQTT \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", "not_a_number": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc", "port_out_of_range": "\u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd 1 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03c0\u03bf\u03bb\u03cd 65535", "same_topic": "\u03a4\u03b1 \u03b8\u03ad\u03bc\u03b1\u03c4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b7\u03bc\u03bf\u03c3\u03af\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b1 \u03af\u03b4\u03b9\u03b1", @@ -68,6 +69,14 @@ }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2 Ethernet" }, + "select_gateway_type": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b8\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5.", + "menu_options": { + "gw_mqtt": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2 MQTT", + "gw_serial": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2", + "gw_tcp": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2 TCP" + } + }, "user": { "data": { "gateway_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2" diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 91f2b7f0d1e..15234537136 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -14,6 +14,7 @@ "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", + "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", "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", @@ -68,6 +69,14 @@ }, "description": "Configuraci\u00f3n de la pasarela Ethernet" }, + "select_gateway_type": { + "description": "Selecciona qu\u00e9 puerta de enlace configurar.", + "menu_options": { + "gw_mqtt": "Configurar una puerta de enlace MQTT", + "gw_serial": "Configurar una puerta de enlace serie", + "gw_tcp": "Configurar una puerta de enlace TCP" + } + }, "user": { "data": { "gateway_type": "Tipo de pasarela" diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index 00614e57252..a1bcacc1852 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -14,6 +14,7 @@ "invalid_serial": "Sobimatu jadaport", "invalid_subscribe_topic": "Kehtetu tellimisteema", "invalid_version": "Sobimatu MySensors versioon", + "mqtt_required": "MQTT sidumine on loomata", "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", @@ -68,6 +69,14 @@ }, "description": "Etherneti l\u00fc\u00fcsi seadistamine" }, + "select_gateway_type": { + "description": "Vali seadistatav l\u00fc\u00fcs", + "menu_options": { + "gw_mqtt": "Seadista MQTT l\u00fc\u00fcs", + "gw_serial": "Seadista jadal\u00fc\u00fcs", + "gw_tcp": "TCP l\u00fc\u00fcsi seadistamine" + } + }, "user": { "data": { "gateway_type": "L\u00fc\u00fcsi t\u00fc\u00fcp" diff --git a/homeassistant/components/mysensors/translations/tr.json b/homeassistant/components/mysensors/translations/tr.json index 9f99525c7b2..9fc26a7119b 100644 --- a/homeassistant/components/mysensors/translations/tr.json +++ b/homeassistant/components/mysensors/translations/tr.json @@ -14,6 +14,7 @@ "invalid_serial": "Ge\u00e7ersiz seri ba\u011flant\u0131 noktas\u0131", "invalid_subscribe_topic": "Ge\u00e7ersiz abone konusu", "invalid_version": "Ge\u00e7ersiz MySensors s\u00fcr\u00fcm\u00fc", + "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f", "not_a_number": "L\u00fctfen bir numara giriniz", "port_out_of_range": "Port numaras\u0131 en az 1, en fazla 65535 olmal\u0131d\u0131r", "same_topic": "Abone olma ve yay\u0131nlama konular\u0131 ayn\u0131", @@ -68,6 +69,14 @@ }, "description": "Ethernet a\u011f ge\u00e7idi kurulumu" }, + "select_gateway_type": { + "description": "Hangi a\u011f ge\u00e7idinin yap\u0131land\u0131r\u0131laca\u011f\u0131n\u0131 se\u00e7in.", + "menu_options": { + "gw_mqtt": "Bir MQTT a\u011f ge\u00e7idini yap\u0131land\u0131r\u0131n", + "gw_serial": "Bir seri a\u011f ge\u00e7idi yap\u0131land\u0131r\u0131n", + "gw_tcp": "Bir TCP a\u011f ge\u00e7idi yap\u0131land\u0131r\u0131n" + } + }, "user": { "data": { "gateway_type": "A\u011f ge\u00e7idi t\u00fcr\u00fc" diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index feeff2015f9..b6494327077 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -19,14 +19,14 @@ "credentials": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a." }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a correctos para el host: {host}" }, diff --git a/homeassistant/components/nest/translations/el.json b/homeassistant/components/nest/translations/el.json index b94ec0ee9df..69b6f096d8a 100644 --- a/homeassistant/components/nest/translations/el.json +++ b/homeassistant/components/nest/translations/el.json @@ -96,5 +96,15 @@ "camera_sound": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03c7\u03bf\u03c2", "doorbell_chime": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03b4\u03bf\u03cd\u03bd\u03b9 \u03c4\u03b7\u03c2 \u03c0\u03cc\u03c1\u03c4\u03b1\u03c2 \u03c0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5" } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Nest \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.10. \n\n \u03a4\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 OAuth \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Nest YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "removed_app_auth": { + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03bb\u03c4\u03b9\u03ce\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c3\u03c6\u03ac\u03bb\u03b5\u03b9\u03b1 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b5\u03b9\u03ce\u03c3\u03b5\u03b9 \u03c4\u03bf\u03bd \u03ba\u03af\u03bd\u03b4\u03c5\u03bd\u03bf \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c8\u03b1\u03c1\u03ad\u03bc\u03b1\u03c4\u03bf\u03c2, \u03b7 Google \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n **\u0391\u03c5\u03c4\u03cc \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 \u03b1\u03c0\u03cc \u03b5\u03c3\u03ac\u03c2 \u03b3\u03b9\u03b1 \u03b5\u03c0\u03af\u03bb\u03c5\u03c3\u03b7** ([\u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2]( {more_info_url} )) \n\n 1. \u0395\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03ce\u03c3\u03b5\u03c9\u03bd\n 1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u0395\u03c0\u03b1\u03bd\u03b1\u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Nest.\n 1. \u03a4\u03bf Home Assistant \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03ba\u03b1\u03b8\u03bf\u03b4\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Web. \n\n \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf Nest [\u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2]( {documentation_url} ) \u03b3\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b1\u03bd\u03c4\u03b9\u03bc\u03b5\u03c4\u03ce\u03c0\u03b9\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b2\u03bb\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd.", + "title": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Nest \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03bf\u03cd\u03bd" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index ea65d2fc78a..a04dc0baab6 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -1,6 +1,10 @@ { + "application_credentials": { + "description": "Sigue las [instrucciones]({more_info_url}) para configurar Cloud Console: \n\n 1. Ve a la [pantalla de consentimiento de OAuth]( {oauth_consent_url} ) y configura\n 1. Ve a [Credenciales]( {oauth_creds_url} ) y haz clic en **Crear credenciales**.\n 1. En la lista desplegable, selecciona **ID de cliente de OAuth**.\n 1. Selecciona **Aplicaci\u00f3n web** para el Tipo de aplicaci\u00f3n.\n 1. A\u00f1ade `{redirect_url}` debajo de *URI de redirecci\u00f3n autorizada*." + }, "config": { "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", @@ -29,6 +33,32 @@ "description": "Para vincular tu cuenta de Google, [autoriza tu cuenta]({url}).\n\nDespu\u00e9s de la autorizaci\u00f3n, copie y pegue el c\u00f3digo Auth Token proporcionado a continuaci\u00f3n.", "title": "Vincular cuenta de Google" }, + "auth_upgrade": { + "description": "Google ha dejado de usar App Auth para mejorar la seguridad, y debes tomar medidas creando nuevas credenciales de aplicaci\u00f3n. \n\nAbre la [documentaci\u00f3n]({more_info_url}) para seguir, ya que los siguientes pasos te guiar\u00e1n a trav\u00e9s de los pasos que debes seguir para restaurar el acceso a tus dispositivos Nest.", + "title": "Nest: desactivaci\u00f3n de App Auth" + }, + "cloud_project": { + "data": { + "cloud_project_id": "ID de proyecto de Google Cloud" + }, + "description": "Introduce el ID del Cloud Project a continuaci\u00f3n, por ejemplo, *example-project-12345*. Consulta la [Consola de Google Cloud]({cloud_console_url}) o la documentaci\u00f3n para obtener [m\u00e1s informaci\u00f3n]({more_info_url}).", + "title": "Nest: Introduce el ID del Cloud Project" + }, + "create_cloud_project": { + "description": "La integraci\u00f3n Nest te permite integrar tus termostatos, c\u00e1maras y timbres Nest mediante la API de administraci\u00f3n de dispositivos inteligentes (SDM). La API de SDM **requiere una tarifa de configuraci\u00f3n \u00fanica de 5$**. Consulta la documentaci\u00f3n para obtener [m\u00e1s informaci\u00f3n]({more_info_url}).\n\n1. Ve a [Google Cloud Console]({cloud_console_url}).\n1. Si este es tu primer proyecto, haz clic en **Crear proyecto** y luego en **Nuevo proyecto**.\n1. Asigna un nombre a tu Cloud Project y, a continuaci\u00f3n, haz clic en **Crear**.\n1. Guarda el ID del Cloud Project, por ejemplo, *example-project-12345* ya que lo necesitar\u00e1s m\u00e1s adelante\n1. Ve a la Biblioteca de API de [API de administraci\u00f3n de dispositivos inteligentes]({sdm_api_url}) y haz clic en **Habilitar**.\n1. Ve a la biblioteca de API de [Cloud Pub/Sub API]({pubsub_api_url}) y haz clic en **Habilitar**.\n\nContin\u00faa cuando tu proyecto en la nube est\u00e9 configurado.", + "title": "Nest: Crear y configurar un Cloud Project" + }, + "device_project": { + "data": { + "project_id": "ID de proyecto de acceso a dispositivos" + }, + "description": "Crea un proyecto de acceso a dispositivos Nest que **requiere una tarifa de 5$** para configurarlo.\n 1. Ve a la [Consola de acceso al dispositivo] ({device_access_console_url}) y sigue el flujo de pago.\n 1. Haz clic en **Crear proyecto**\n 1. Asigna un nombre a tu proyecto de acceso a dispositivos y haz clic en **Siguiente**.\n 1. Introduce tu ID de cliente de OAuth\n 1. Habilita los eventos haciendo clic en **Habilitar** y **Crear proyecto**. \n\n Introduce tu ID de proyecto de acceso a dispositivos a continuaci\u00f3n ([m\u00e1s informaci\u00f3n]({more_info_url})).", + "title": "Nest: Crear un proyecto de acceso a dispositivos" + }, + "device_project_upgrade": { + "description": "Actualiza el Proyecto de acceso a dispositivos Nest con tu nuevo ID de cliente de OAuth ([m\u00e1s informaci\u00f3n]({more_info_url}))\n 1. Ve a la [Consola de acceso al dispositivo]({device_access_console_url}).\n 1. Haz clic en el icono de la papelera junto a *ID de cliente de OAuth*.\n 1. Haz clic en el men\u00fa adicional `...` y *A\u00f1adir ID de cliente*.\n 1. Introduce tu nuevo ID de cliente de OAuth y haz clic en **A\u00f1adir**. \n\n Tu ID de cliente de OAuth es: `{client_id}`", + "title": "Nest: Actualizar el proyecto de acceso a dispositivos" + }, "init": { "data": { "flow_impl": "Proveedor" @@ -66,5 +96,15 @@ "camera_sound": "Sonido detectado", "doorbell_chime": "Timbre pulsado" } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de Nest en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.10. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Nest" + }, + "removed_app_auth": { + "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de utilizar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n de su parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de integraci\u00f3n]({documentation_url}) de Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", + "title": "Las credenciales de autenticaci\u00f3n de Nest deben actualizarse" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index b07845f7dab..b79c320a35b 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -96,5 +96,15 @@ "camera_sound": "Tuvastati heli", "doorbell_chime": "Uksekell helises" } + }, + "issues": { + "deprecated_yaml": { + "description": "Nesti seadistamine rakenduses configuration.yaml eemaldatakse koduabilisest 2022.10.\n\nTeie olemasolevad OAuthi rakenduse identimis- ja juurdep\u00e4\u00e4sus\u00e4tted on automaatselt kasutajaliidesesse imporditud. Eemaldage FAILIST CONFIGURATION.yaml YAML-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Nesti YAML-i konfiguratsioon eemaldatakse" + }, + "removed_app_auth": { + "description": "Turvalisuse parandamiseks ja andmep\u00fc\u00fcgiriski v\u00e4hendamiseks katkestas Google Home Assistanti autentimismeetodi. \n\n **Selle lahendamiseks peate midagi ette v\u00f5tma** ([rohkem teavet]( {more_info_url} )) \n\n 1. K\u00fclastage integreerimise lehte\n 1. Kl\u00f5psake Nesti integratsioonil nuppu Konfigureeri uuesti.\n 1. Koduassistent juhendab teid veebiautentimisele \u00fcleminekuks. \n\n Veaotsingu teabe saamiseks vaadake Nesti [integreerimisjuhiseid]( {documentation_url} ).", + "title": "Nesti autentimise mandaate tuleb v\u00e4rskendada" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index bc69b928ba1..986412798dc 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -96,5 +96,15 @@ "camera_sound": "Ses alg\u0131land\u0131", "doorbell_chime": "Kap\u0131 zili bas\u0131ld\u0131" } + }, + "issues": { + "deprecated_yaml": { + "description": "Nest'i configuration.yaml'de yap\u0131land\u0131rma, Home Assistant 2022.10'da kald\u0131r\u0131l\u0131yor. \n\n Mevcut OAuth Uygulama Kimlik Bilgileriniz ve eri\u015fim ayarlar\u0131n\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Nest YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + }, + "removed_app_auth": { + "description": "G\u00fcvenli\u011fi art\u0131rmak ve kimlik av\u0131 riskini azaltmak i\u00e7in Google, Home Assistant taraf\u0131ndan kullan\u0131lan kimlik do\u011frulama y\u00f6ntemini kullan\u0131mdan kald\u0131rm\u0131\u015ft\u0131r. \n\n **Bu, \u00e7\u00f6zmek i\u00e7in sizin taraf\u0131n\u0131zdan bir i\u015flem yap\u0131lmas\u0131n\u0131 gerektirir** ([daha fazla bilgi]( {more_info_url} )) \n\n 1. Entegrasyon sayfas\u0131n\u0131 ziyaret edin\n 1. Nest entegrasyonunda Yeniden Yap\u0131land\u0131r'\u0131 t\u0131klay\u0131n.\n 1. Home Assistant, Web Kimlik Do\u011frulamas\u0131na y\u00fckseltme ad\u0131mlar\u0131nda size yol g\u00f6sterecektir. \n\n Sorun giderme bilgileri i\u00e7in Nest [entegrasyon talimatlar\u0131na]( {documentation_url} ) bak\u0131n.", + "title": "Nest Kimlik Do\u011frulama Kimlik Bilgileri g\u00fcncellenmelidir" + } } } \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/es.json b/homeassistant/components/netgear/translations/es.json index 9edbd1101d0..69bea9a5de6 100644 --- a/homeassistant/components/netgear/translations/es.json +++ b/homeassistant/components/netgear/translations/es.json @@ -11,7 +11,7 @@ "data": { "host": "Host (Opcional)", "password": "Contrase\u00f1a", - "username": "Usuario (Opcional)" + "username": "Nombre de usuario (Opcional)" }, "description": "Host predeterminado: {host} \nNombre de usuario predeterminado: {username}" } diff --git a/homeassistant/components/nextdns/translations/es.json b/homeassistant/components/nextdns/translations/es.json new file mode 100644 index 00000000000..428b25128d8 --- /dev/null +++ b/homeassistant/components/nextdns/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Este perfil de NextDNS ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "profiles": { + "data": { + "profile": "Perfil" + } + }, + "user": { + "data": { + "api_key": "Clave API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Llegar al servidor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/es.json b/homeassistant/components/nina/translations/es.json index e7e20c0908a..a605bfb7a9a 100644 --- a/homeassistant/components/nina/translations/es.json +++ b/homeassistant/components/nina/translations/es.json @@ -23,5 +23,27 @@ "title": "Seleccionar ciudad/pa\u00eds" } } + }, + "options": { + "error": { + "cannot_connect": "No se pudo conectar", + "no_selection": "Por favor, selecciona al menos una ciudad/condado", + "unknown": "Error inesperado" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Ciudad/condado (A-D)", + "_e_to_h": "Ciudad/condado (E-H)", + "_i_to_l": "Ciudad/condado (I-L)", + "_m_to_q": "Ciudad/condado (M-Q)", + "_r_to_u": "Ciudad/condado (R-U)", + "_v_to_z": "Ciudad/condado (V-Z)", + "corona_filter": "Eliminar las advertencias de Corona", + "slots": "Advertencias m\u00e1ximas por ciudad/condado" + }, + "title": "Opciones" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/es.json b/homeassistant/components/notion/translations/es.json index 62a21b06022..3eb17fa606d 100644 --- a/homeassistant/components/notion/translations/es.json +++ b/homeassistant/components/notion/translations/es.json @@ -19,7 +19,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Completa tu informaci\u00f3n" } diff --git a/homeassistant/components/nuheat/translations/es.json b/homeassistant/components/nuheat/translations/es.json index 088978d4c50..a64a68e2e70 100644 --- a/homeassistant/components/nuheat/translations/es.json +++ b/homeassistant/components/nuheat/translations/es.json @@ -14,7 +14,7 @@ "data": { "password": "Contrase\u00f1a", "serial_number": "N\u00famero de serie del termostato.", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Necesitas obtener el n\u00famero de serie o el ID de tu termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando tu(s) termostato(s).", "title": "ConectarNuHeat" diff --git a/homeassistant/components/nut/translations/es.json b/homeassistant/components/nut/translations/es.json index 898bf1f027d..edb49ebcbcd 100644 --- a/homeassistant/components/nut/translations/es.json +++ b/homeassistant/components/nut/translations/es.json @@ -19,7 +19,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con el servidor NUT" } diff --git a/homeassistant/components/nzbget/translations/es.json b/homeassistant/components/nzbget/translations/es.json index e3b1a595c90..eeb2b46ba8b 100644 --- a/homeassistant/components/nzbget/translations/es.json +++ b/homeassistant/components/nzbget/translations/es.json @@ -16,8 +16,8 @@ "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "title": "Conectarse a NZBGet" } diff --git a/homeassistant/components/octoprint/translations/es.json b/homeassistant/components/octoprint/translations/es.json index 241b3a4111e..e9135b25be8 100644 --- a/homeassistant/components/octoprint/translations/es.json +++ b/homeassistant/components/octoprint/translations/es.json @@ -21,7 +21,7 @@ "path": "Ruta de aplicaci\u00f3n", "port": "N\u00famero de puerto", "ssl": "Usar SSL", - "username": "Usuario", + "username": "Nombre de usuario", "verify_ssl": "Verificar certificado SSL" } } diff --git a/homeassistant/components/omnilogic/translations/es.json b/homeassistant/components/omnilogic/translations/es.json index 54aef5f1892..1755942e5df 100644 --- a/homeassistant/components/omnilogic/translations/es.json +++ b/homeassistant/components/omnilogic/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 6ba7dbe0ee8..7858a4561fe 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -17,7 +17,7 @@ "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Configurar dispositivo ONVIF" }, diff --git a/homeassistant/components/openalpr_local/translations/es.json b/homeassistant/components/openalpr_local/translations/es.json new file mode 100644 index 00000000000..4c1b1de6e05 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n OpenALPR Local est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la integraci\u00f3n OpenALPR Local" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/et.json b/homeassistant/components/openalpr_local/translations/et.json new file mode 100644 index 00000000000..aca98183950 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPRi kohalik integratsioon on Home Assistantist eemaldamisel ja see ei ole enam saadaval alates Home Assistant 2022.10.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "OpenALPR Locali integratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/tr.json b/homeassistant/components/openalpr_local/translations/tr.json new file mode 100644 index 00000000000..479e5385980 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR Yerel entegrasyonu Home Assistant'tan kald\u0131r\u0131lmay\u0131 beklemektedir ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacakt\u0131r.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "OpenALPR Yerel entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ca.json b/homeassistant/components/openexchangerates/translations/ca.json index f5a93caa71d..98c74ae55b3 100644 --- a/homeassistant/components/openexchangerates/translations/ca.json +++ b/homeassistant/components/openexchangerates/translations/ca.json @@ -15,9 +15,18 @@ "step": { "user": { "data": { - "api_key": "Clau API" + "api_key": "Clau API", + "base": "Moneda base" + }, + "data_description": { + "base": "L'\u00fas d'una moneda base que no sigui USD requereix un [pla de pagament]({signup})." } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuraci\u00f3 YAML d'Open Exchange Rates est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/el.json b/homeassistant/components/openexchangerates/translations/el.json new file mode 100644 index 00000000000..dee7e836d01 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/el.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "base": "\u0392\u03b1\u03c3\u03b9\u03ba\u03cc \u03bd\u03cc\u03bc\u03b9\u03c3\u03bc\u03b1" + }, + "data_description": { + "base": "\u0393\u03b9\u03b1 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03ac\u03bb\u03bb\u03bf\u03c5 \u03b2\u03b1\u03c3\u03b9\u03ba\u03bf\u03cd \u03bd\u03bf\u03bc\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf USD \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1 [\u03b5\u03c0\u03af \u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03ae \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1]({\u03c3\u03c5\u03bd\u03b4\u03c1\u03bf\u03bc\u03ae})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b1\u03bd\u03bf\u03b9\u03ba\u03c4\u03ce\u03bd \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03b9\u03ce\u03bd \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03ac\u03b3\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Open Exchange Rates YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Open Exchange Rates YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/es.json b/homeassistant/components/openexchangerates/translations/es.json new file mode 100644 index 00000000000..fb5897846db --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/es.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "base": "Moneda base" + }, + "data_description": { + "base": "El uso de una moneda base distinta al USD requiere un [plan de pago]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Open Exchange Rates mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Open Exchange Rates de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Open Exchange Rates" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/et.json b/homeassistant/components/openexchangerates/translations/et.json new file mode 100644 index 00000000000..fbeed4f8443 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/et.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "base": "P\u00f5hivaluuta" + }, + "data_description": { + "base": "Teise baasvaluuta kui USD kasutamine n\u00f5uab [tasulist plaani]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Open Exchange Rates konfigureerimine YAML-i abil eemaldatakse.\n\nOlemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nEemalda Open Exchange Rates YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivita Home Assistant uuesti, et see probleem lahendada.", + "title": "Open Exchange Rates YAML-konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/tr.json b/homeassistant/components/openexchangerates/translations/tr.json new file mode 100644 index 00000000000..436e6bbb07b --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/tr.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "base": "Temel para birimi" + }, + "data_description": { + "base": "USD'den ba\u015fka bir temel para birimi kullanmak, bir [\u00fccretli plan]( {signup} ) gerektirir." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak A\u00e7\u0131k D\u00f6viz Kurlar\u0131n\u0131 yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Open Exchange Rates YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "A\u00e7\u0131k D\u00f6viz Kurlar\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/es.json b/homeassistant/components/opengarage/translations/es.json index d282e706afc..cc8a51dc8fc 100644 --- a/homeassistant/components/opengarage/translations/es.json +++ b/homeassistant/components/opengarage/translations/es.json @@ -14,7 +14,7 @@ "device_key": "Clave del dispositivo", "host": "Host", "port": "Puerto", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index 2b6cc0afd7f..b464549ed71 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -3,7 +3,8 @@ "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "id_exists": "El ID del Gateway ya existe" + "id_exists": "El ID del Gateway ya existe", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/et.json b/homeassistant/components/opentherm_gw/translations/et.json index 2458c8f1d52..27c44ccd620 100644 --- a/homeassistant/components/opentherm_gw/translations/et.json +++ b/homeassistant/components/opentherm_gw/translations/et.json @@ -3,7 +3,8 @@ "error": { "already_configured": "L\u00fc\u00fcs on juba m\u00e4\u00e4ratud", "cannot_connect": "\u00dchendamine nurjus", - "id_exists": "L\u00fc\u00fcsi ID on juba olemas" + "id_exists": "L\u00fc\u00fcsi ID on juba olemas", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/tr.json b/homeassistant/components/opentherm_gw/translations/tr.json index 72a603cc827..11b1db771ad 100644 --- a/homeassistant/components/opentherm_gw/translations/tr.json +++ b/homeassistant/components/opentherm_gw/translations/tr.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131", - "id_exists": "A\u011f ge\u00e7idi kimli\u011fi zaten var" + "id_exists": "A\u011f ge\u00e7idi kimli\u011fi zaten var", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131" }, "step": { "init": { diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index 817864deba8..ac9d3f72ebc 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -9,7 +9,7 @@ "cannot_connect": "Error al conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "server_in_maintenance": "El servidor est\u00e1 inactivo por mantenimiento", - "too_many_attempts": "Demasiados intentos con un 'token' inv\u00e1lido, bloqueado temporalmente", + "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", "too_many_requests": "Demasiadas solicitudes, int\u00e9ntalo de nuevo m\u00e1s tarde.", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/overkiz/translations/sensor.es.json b/homeassistant/components/overkiz/translations/sensor.es.json index 8d0c475586b..523ddf7fe7b 100644 --- a/homeassistant/components/overkiz/translations/sensor.es.json +++ b/homeassistant/components/overkiz/translations/sensor.es.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Limpiar", "dirty": "Sucio" + }, + "overkiz__three_way_handle_direction": { + "closed": "Cerrado", + "open": "Abierto", + "tilt": "Inclinaci\u00f3n" } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index 549f3af16ca..a060f0f9552 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -17,7 +17,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Configurar una instancia de OVO Energy para acceder a su consumo de energ\u00eda.", "title": "A\u00f1adir cuenta de OVO Energy" diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json index 5054ae22f5b..7ecfc37d97d 100644 --- a/homeassistant/components/picnic/translations/es.json +++ b/homeassistant/components/picnic/translations/es.json @@ -15,7 +15,7 @@ "data": { "country_code": "C\u00f3digo del pa\u00eds", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 7099f63a9e8..8071e9e11fe 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -3,7 +3,7 @@ "abort": { "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Error inesperado" @@ -23,7 +23,7 @@ "port": "Puerto", "ssl": "Utiliza un certificado SSL", "token": "Token (Opcional)", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" }, "title": "Configuraci\u00f3n Manual de Plex" }, diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 16fae70586d..284bfe97943 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servicio ya est\u00e1 configurado" + "already_configured": "El servicio ya est\u00e1 configurado", + "anna_with_adam": "Tanto Anna como Adam han sido detectados. A\u00f1ade tu Adam en lugar de tu Anna" }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", @@ -15,9 +16,9 @@ "data": { "flow_type": "Tipo de conexi\u00f3n", "host": "Direcci\u00f3n IP", - "password": "ID Smile", + "password": "ID de Smile", "port": "Puerto", - "username": "Nombre de usuario de la sonrisa" + "username": "Nombre de usuario de Smile" }, "description": "Producto:", "title": "Conectarse a Smile" diff --git a/homeassistant/components/prosegur/translations/es.json b/homeassistant/components/prosegur/translations/es.json index af4f61d6fdc..1cbb1e25dc9 100644 --- a/homeassistant/components/prosegur/translations/es.json +++ b/homeassistant/components/prosegur/translations/es.json @@ -14,14 +14,14 @@ "data": { "description": "Vuelva a autenticarse con su cuenta Prosegur.", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { "data": { "country": "Pa\u00eds", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/ps4/translations/es.json b/homeassistant/components/ps4/translations/es.json index 4eb7636d0a2..3655b72110c 100644 --- a/homeassistant/components/ps4/translations/es.json +++ b/homeassistant/components/ps4/translations/es.json @@ -25,7 +25,7 @@ "region": "Regi\u00f3n" }, "data_description": { - "code": "Vaya a 'Configuraci\u00f3n' en su consola PlayStation 4. Luego navegue hasta 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y seleccione 'Agregar dispositivo' para obtener el pin." + "code": "Ve a 'Configuraci\u00f3n' en tu consola PlayStation 4. Luego navega hasta 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y selecciona 'Agregar dispositivo' para obtener el PIN." } }, "mode": { @@ -34,7 +34,7 @@ "mode": "Modo configuraci\u00f3n" }, "data_description": { - "ip_address": "D\u00e9jelo en blanco si selecciona la detecci\u00f3n autom\u00e1tica." + "ip_address": "D\u00e9jalo en blanco si seleccionas la detecci\u00f3n autom\u00e1tica." } } } diff --git a/homeassistant/components/qnap_qsw/translations/es.json b/homeassistant/components/qnap_qsw/translations/es.json index b58fcb71fc7..e149b08b403 100644 --- a/homeassistant/components/qnap_qsw/translations/es.json +++ b/homeassistant/components/qnap_qsw/translations/es.json @@ -2,13 +2,19 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "invalid_id": "El dispositivo ha devuelto un ID \u00fanico inv\u00e1lido" + "invalid_id": "El dispositivo devolvi\u00f3 un ID \u00fanico no v\u00e1lido" }, "error": { - "cannot_connect": "Ha fallado la conexi\u00f3n", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { + "discovered_connection": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/radiotherm/translations/es.json b/homeassistant/components/radiotherm/translations/es.json index dbb84376ff9..165068f38fa 100644 --- a/homeassistant/components/radiotherm/translations/es.json +++ b/homeassistant/components/radiotherm/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{name} {model} ({host})", @@ -19,11 +19,17 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de la plataforma clim\u00e1tica Radio Thermostat mediante YAML se eliminar\u00e1 en Home Assistant 2022.9. \n\nTu configuraci\u00f3n existente se ha importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Radio Thermostat" + } + }, "options": { "step": { "init": { "data": { - "hold_temp": "Establezca una retenci\u00f3n permanente al ajustar la temperatura." + "hold_temp": "Establecer una retenci\u00f3n permanente al ajustar la temperatura." } } } diff --git a/homeassistant/components/radiotherm/translations/tr.json b/homeassistant/components/radiotherm/translations/tr.json index f8e6b4f7a6d..99e57c1bf6b 100644 --- a/homeassistant/components/radiotherm/translations/tr.json +++ b/homeassistant/components/radiotherm/translations/tr.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Radyo Termostat iklim platformunun YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 Home Assistant 2022.9'da kald\u0131r\u0131l\u0131yor.\n\nMevcut yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131lm\u0131\u015ft\u0131r. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Radyo Termostat\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/remote/translations/zh-Hant.json b/homeassistant/components/remote/translations/zh-Hant.json index 259121d7a4f..a10eb3b8f61 100644 --- a/homeassistant/components/remote/translations/zh-Hant.json +++ b/homeassistant/components/remote/translations/zh-Hant.json @@ -18,7 +18,7 @@ "state": { "_": { "off": "\u95dc\u9589", - "on": "\u958b\u5553" + "on": "\u958b\u555f" } }, "title": "\u9059\u63a7\u5668" diff --git a/homeassistant/components/rhasspy/translations/es.json b/homeassistant/components/rhasspy/translations/es.json new file mode 100644 index 00000000000..5510c9e38f4 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfQuieres habilitar el soporte de Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/es.json b/homeassistant/components/ring/translations/es.json index d98a61474cc..4a3947c5efc 100644 --- a/homeassistant/components/ring/translations/es.json +++ b/homeassistant/components/ring/translations/es.json @@ -17,7 +17,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Iniciar sesi\u00f3n con cuenta de Ring" } diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 817f1d970cc..015360d76c9 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/roon/translations/es.json b/homeassistant/components/roon/translations/es.json index 098ba60234a..d63856b8f8d 100644 --- a/homeassistant/components/roon/translations/es.json +++ b/homeassistant/components/roon/translations/es.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Puerto" }, - "description": "No se ha podrido descubrir el servidor Roon, introduce el anfitri\u00f3n y el puerto." + "description": "No se pudo descubrir el servidor Roon, por favor, introduce su nombre de host y el puerto." }, "link": { "description": "Debes autorizar Home Assistant en Roon. Despu\u00e9s de pulsar en Enviar, ve a la aplicaci\u00f3n Roon Core, abre Configuraci\u00f3n y activa HomeAssistant en la pesta\u00f1a Extensiones.", diff --git a/homeassistant/components/ruckus_unleashed/translations/es.json b/homeassistant/components/ruckus_unleashed/translations/es.json index 2609ee07eaf..c1d3e57b02f 100644 --- a/homeassistant/components/ruckus_unleashed/translations/es.json +++ b/homeassistant/components/ruckus_unleashed/translations/es.json @@ -13,7 +13,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/sabnzbd/translations/es.json b/homeassistant/components/sabnzbd/translations/es.json index f38e28a3287..c3e690941bb 100644 --- a/homeassistant/components/sabnzbd/translations/es.json +++ b/homeassistant/components/sabnzbd/translations/es.json @@ -1,8 +1,8 @@ { "config": { "error": { - "cannot_connect": "Ha fallado la conexi\u00f3n", - "invalid_api_key": "Clave API inv\u00e1lida" + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida" }, "step": { "user": { diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 29b2a97027d..ebcaa398949 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", diff --git a/homeassistant/components/scrape/translations/es.json b/homeassistant/components/scrape/translations/es.json index 660d687344c..f5c07aa30b4 100644 --- a/homeassistant/components/scrape/translations/es.json +++ b/homeassistant/components/scrape/translations/es.json @@ -1,10 +1,68 @@ { + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "attribute": "Atributo", + "authentication": "Autenticaci\u00f3n", + "device_class": "Clase de dispositivo", + "headers": "Cabeceras", + "index": "\u00cdndice", + "name": "Nombre", + "password": "Contrase\u00f1a", + "resource": "Recurso", + "select": "Seleccionar", + "state_class": "Clase de estado", + "unit_of_measurement": "Unidad de medida", + "username": "Nombre de usuario", + "value_template": "Plantilla de valor", + "verify_ssl": "Verificar el certificado SSL" + }, + "data_description": { + "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", + "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", + "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", + "headers": "Cabeceras a utilizar para la petici\u00f3n web", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", + "resource": "La URL del sitio web que contiene el valor.", + "select": "Define qu\u00e9 etiqueta buscar. Consulta los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", + "state_class": "El state_class del sensor", + "value_template": "Define una plantilla para obtener el estado del sensor", + "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" + } + } + } + }, "options": { "step": { "init": { + "data": { + "attribute": "Atributo", + "authentication": "Autenticaci\u00f3n", + "device_class": "Clase de dispositivo", + "headers": "Cabeceras", + "index": "\u00cdndice", + "name": "Nombre", + "password": "Contrase\u00f1a", + "resource": "Recurso", + "select": "Seleccionar", + "state_class": "Clase de estado", + "unit_of_measurement": "Unidad de medida", + "username": "Nombre de usuario", + "value_template": "Plantilla de valor", + "verify_ssl": "Verificar el certificado SSL" + }, "data_description": { + "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", + "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", + "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", + "headers": "Cabeceras a utilizar para la petici\u00f3n web", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", "resource": "La URL del sitio web que contiene el valor.", - "select": "Define qu\u00e9 etiqueta buscar. Consulte los selectores de CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", + "select": "Define qu\u00e9 etiqueta buscar. Consulta los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", "state_class": "El state_class del sensor", "value_template": "Define una plantilla para obtener el estado del sensor", "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" diff --git a/homeassistant/components/script/translations/zh-Hant.json b/homeassistant/components/script/translations/zh-Hant.json index 4840b6f8ab0..9e117e082d6 100644 --- a/homeassistant/components/script/translations/zh-Hant.json +++ b/homeassistant/components/script/translations/zh-Hant.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "\u95dc\u9589", - "on": "\u958b\u5553" + "on": "\u958b\u555f" } }, "title": "\u8173\u672c" diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index 1dcb63d4052..344f9d6119e 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -62,7 +62,7 @@ "state": { "_": { "off": "\u95dc\u9589", - "on": "\u958b\u5553" + "on": "\u958b\u555f" } }, "title": "\u611f\u6e2c\u5668" diff --git a/homeassistant/components/sensorpush/translations/es.json b/homeassistant/components/sensorpush/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/sensorpush/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/tr.json b/homeassistant/components/sensorpush/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/sensorpush/translations/tr.json @@ -0,0 +1,21 @@ +{ + "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", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/es.json b/homeassistant/components/senz/translations/es.json index f81af28f4d9..fc1c1ae2d13 100644 --- a/homeassistant/components/senz/translations/es.json +++ b/homeassistant/components/senz/translations/es.json @@ -2,19 +2,25 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", - "no_url_available": "No hay ninguna URL disponible. Para m\u00e1s informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", - "oauth_error": "Se han recibido datos token inv\u00e1lidos." + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos." }, "create_entry": { - "default": "Autenticaci\u00f3n exitosa" + "default": "Autenticado correctamente" }, "step": { "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } + }, + "issues": { + "removed_yaml": { + "description": "Se elimin\u00f3 la configuraci\u00f3n de nVent RAYCHEM SENZ mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se elimin\u00f3 la configuraci\u00f3n YAML de nVent RAYCHEM SENZ" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/et.json b/homeassistant/components/senz/translations/et.json index 1ea0537a1b7..7424b6e2967 100644 --- a/homeassistant/components/senz/translations/et.json +++ b/homeassistant/components/senz/translations/et.json @@ -16,5 +16,11 @@ "title": "Vali tuvastusmeetod" } } + }, + "issues": { + "removed_yaml": { + "description": "nVent RAYCHEM SENZi konfigureerimine YAMLi abil on eemaldatud.\n\nTeie olemasolevat YAML-konfiguratsiooni ei kasuta Home Assistant.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti.", + "title": "NVent RAYCHEM SENZ YAML konfiguratsioon on eemaldatud" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/tr.json b/homeassistant/components/senz/translations/tr.json index 3f6fa6f27ba..a9f7ea9b718 100644 --- a/homeassistant/components/senz/translations/tr.json +++ b/homeassistant/components/senz/translations/tr.json @@ -16,5 +16,11 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } + }, + "issues": { + "removed_yaml": { + "description": "nVent RAYCHEM SENZ'in YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "nVent RAYCHEM SENZ YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/es.json b/homeassistant/components/sharkiq/translations/es.json index 951537484bc..976840e0a9f 100644 --- a/homeassistant/components/sharkiq/translations/es.json +++ b/homeassistant/components/sharkiq/translations/es.json @@ -15,13 +15,13 @@ "reauth": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 0c0011f7297..876d64093a0 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "firmware_not_fully_provisioned": "El dispositivo no est\u00e1 completamente aprovisionado. P\u00f3ngase en contacto con el servicio de asistencia de Shelly", + "firmware_not_fully_provisioned": "El dispositivo no est\u00e1 completamente aprovisionado. Por favor, ponte en contacto con el servicio de asistencia de Shelly", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -18,7 +18,7 @@ "credentials": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { diff --git a/homeassistant/components/simplepush/translations/ca.json b/homeassistant/components/simplepush/translations/ca.json index 8755c00a076..4252be0764e 100644 --- a/homeassistant/components/simplepush/translations/ca.json +++ b/homeassistant/components/simplepush/translations/ca.json @@ -24,6 +24,7 @@ "title": "La configuraci\u00f3 YAML de Simplepush est\u00e0 sent eliminada" }, "removed_yaml": { + "description": "La configuraci\u00f3 de Simplepush mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML de Simplepush del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de Simplepush s'ha eliminat" } } diff --git a/homeassistant/components/simplepush/translations/el.json b/homeassistant/components/simplepush/translations/el.json index 8f8c4691045..13a961c6aff 100644 --- a/homeassistant/components/simplepush/translations/el.json +++ b/homeassistant/components/simplepush/translations/el.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/es.json b/homeassistant/components/simplepush/translations/es.json new file mode 100644 index 00000000000..acf2da9e5d2 --- /dev/null +++ b/homeassistant/components/simplepush/translations/es.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "device_key": "La clave de dispositivo de tu dispositivo", + "event": "El evento para los eventos.", + "name": "Nombre", + "password": "La contrase\u00f1a de cifrado utilizada por tu dispositivo", + "salt": "La semilla utilizada por tu dispositivo." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Simplepush mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Simplepush" + }, + "removed_yaml": { + "description": "Se ha eliminado la configuraci\u00f3n de Simplepush mediante YAML.\n\nTu configuraci\u00f3n YAML existente no es utilizada por Home Assistant.\n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Simplepush" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/et.json b/homeassistant/components/simplepush/translations/et.json index 2501d992c83..7cae6c69edf 100644 --- a/homeassistant/components/simplepush/translations/et.json +++ b/homeassistant/components/simplepush/translations/et.json @@ -17,5 +17,15 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Simplepushi konfigureerimine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nEemaldage Simplepushi YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", + "title": "Simplepush YAML-i konfiguratsioon eemaldatakse" + }, + "removed_yaml": { + "description": "Simplepushi konfigureerimine YAMLi abil on eemaldatud.\n\nTeie olemasolevat YAML-konfiguratsiooni ei kasuta Home Assistant.\n\nEemaldage Simplepushi YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", + "title": "Simplepush YAML-i konfiguratsioon on eemaldatud" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/tr.json b/homeassistant/components/simplepush/translations/tr.json index 0c969465da9..0a3183d7c90 100644 --- a/homeassistant/components/simplepush/translations/tr.json +++ b/homeassistant/components/simplepush/translations/tr.json @@ -17,5 +17,15 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Simplepush'un YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Simplepush YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Simplepush YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + }, + "removed_yaml": { + "description": "Simplepush'u YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n Simplepush YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Simplepush YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index ecc263e6821..d9c9123b391 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "invalid_auth_code_length": "\u039f\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03af \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 SimpliSafe \u03ad\u03c7\u03bf\u03c5\u03bd \u03bc\u03ae\u03ba\u03bf\u03c2 45 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 25d8de8bbb3..db73c6ce042 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -2,15 +2,18 @@ "config": { "abort": { "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso.", - "email_2fa_timed_out": "Se ha agotado el tiempo de espera de la autenticaci\u00f3n de dos factores basada en el correo electr\u00f3nico.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "email_2fa_timed_out": "Se agot\u00f3 el tiempo de espera para la autenticaci\u00f3n de dos factores basada en correo electr\u00f3nico.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "wrong_account": "Las credenciales de usuario proporcionadas no coinciden con esta cuenta de SimpliSafe." }, "error": { + "identifier_exists": "Cuenta ya registrada", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth_code_length": "Los c\u00f3digos de autorizaci\u00f3n de SimpliSafe tienen 45 caracteres de longitud", "unknown": "Error inesperado" }, "progress": { - "email_2fa": "Mira el correo electr\u00f3nico donde deber\u00edas encontrar el enlace de verificaci\u00f3n de Simplisafe." + "email_2fa": "Revisa tu correo electr\u00f3nico para obtener un enlace de verificaci\u00f3n de Simplisafe." }, "step": { "reauth_confirm": { @@ -24,14 +27,15 @@ "data": { "code": "C\u00f3digo" }, - "description": "Introduce el c\u00f3digo de autenticaci\u00f3n de dos factores enviado por SMS." + "description": "Introduce el c\u00f3digo de autenticaci\u00f3n de dos factores que se te envi\u00f3 por SMS." }, "user": { "data": { + "auth_code": "C\u00f3digo de Autorizaci\u00f3n", "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "description": "SimpliSafe se autentifica con Home Assistant a trav\u00e9s de la aplicaci\u00f3n web de SimpliSafe. Debido a limitaciones t\u00e9cnicas, hay un paso manual al final de este proceso; aseg\u00farese de leer la [documentaci\u00f3n]({docs_url}) antes de empezar.\n\n1. Haga clic en [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web de SimpliSafe e introduzca sus credenciales.\n\n2. Cuando el proceso de inicio de sesi\u00f3n haya finalizado, vuelva aqu\u00ed e introduzca el c\u00f3digo de autorizaci\u00f3n que aparece a continuaci\u00f3n." + "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\n Cuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 073d20555c1..03356fc7d6e 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Konto on juba registreeritud", "invalid_auth": "Tuvastamise viga", + "invalid_auth_code_length": "SimpliSafe autoriseerimiskoodid on 45 t\u00e4hem\u00e4rki pikad.", "unknown": "Tundmatu viga" }, "progress": { @@ -34,7 +35,7 @@ "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "SimpliSafe autendib kasutajaid oma veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus k\u00e4sitsi samm; palun veendu, et loed enne alustamist l\u00e4bi [dokumendid](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKui oled valmis, kl\u00f5psa veebirakenduse SimpliSafe avamiseks ja mandaadi sisestamiseks kl\u00f5psa [here]({url}). Kui protsess on l\u00f5pule j\u00f5udnud, naase siia ja sisesta autoriseerimiskood SimpliSafe veebirakenduse URL-ist." + "description": "SimpliSafe autentib kasutajad veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus manuaalne samm; palun lugege enne alustamist kindlasti [dokumentatsiooni](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKui olete valmis, kl\u00f5psake [siin]({url}), et avada SimpliSafe veebirakendus ja sisestada oma volitused. Kui olete juba SimpliSafe'ile oma brauseris sisse loginud, siis avage uus vahekaart ja kopeerige/liidke \u00fclaltoodud URL sellesse vahekaarti.\n\nKui protsess on l\u00f5pule viidud, naaske siia ja sisestage autoriseerimiskood `com.simplisafe.mobile` URL-i." } } }, diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 74c260e2d3d..02153b0b90c 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -3,10 +3,13 @@ "abort": { "already_configured": "Bu SimpliSafe hesab\u0131 zaten kullan\u0131mda.", "email_2fa_timed_out": "E-posta tabanl\u0131 iki fakt\u00f6rl\u00fc kimlik do\u011frulama i\u00e7in beklerken zaman a\u015f\u0131m\u0131na u\u011frad\u0131.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "wrong_account": "Sa\u011flanan kullan\u0131c\u0131 kimlik bilgileri bu SimpliSafe hesab\u0131yla e\u015fle\u015fmiyor." }, "error": { + "identifier_exists": "Hesap zaten kay\u0131tl\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_auth_code_length": "SimpliSafe yetkilendirme kodlar\u0131 45 karakter uzunlu\u011fundad\u0131r", "unknown": "Beklenmeyen hata" }, "progress": { @@ -28,10 +31,11 @@ }, "user": { "data": { + "auth_code": "Yetkilendirme Kodu", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin." + "description": "SimpliSafe, web uygulamas\u0131 arac\u0131l\u0131\u011f\u0131yla kullan\u0131c\u0131lar\u0131n kimli\u011fini do\u011frular. Teknik s\u0131n\u0131rlamalar nedeniyle bu i\u015flemin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) okudu\u011funuzdan emin olun. \n\n Haz\u0131r oldu\u011funuzda SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]( {url} ) t\u0131klay\u0131n. Taray\u0131c\u0131n\u0131zda SimpliSafe'e zaten giri\u015f yapt\u0131ysan\u0131z, yeni bir sekme a\u00e7mak ve ard\u0131ndan yukar\u0131daki URL'yi o sekmeye kopyalay\u0131p/yap\u0131\u015ft\u0131rmak isteyebilirsiniz. \n\n \u0130\u015flem tamamland\u0131\u011f\u0131nda buraya d\u00f6n\u00fcn ve \"com.simplisafe.mobile\" URL'sinden yetkilendirme kodunu girin." } } }, diff --git a/homeassistant/components/skybell/translations/es.json b/homeassistant/components/skybell/translations/es.json index cc93b536c38..68633c72180 100644 --- a/homeassistant/components/skybell/translations/es.json +++ b/homeassistant/components/skybell/translations/es.json @@ -2,17 +2,17 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { "user": { "data": { - "email": "Correo electronico", + "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" } } diff --git a/homeassistant/components/slack/translations/es.json b/homeassistant/components/slack/translations/es.json index 52aee38b4f9..e4680a3e7ae 100644 --- a/homeassistant/components/slack/translations/es.json +++ b/homeassistant/components/slack/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -19,10 +19,10 @@ "data_description": { "api_key": "El token de la API de Slack que se usar\u00e1 para enviar mensajes de Slack.", "default_channel": "Canal al que publicar en caso de que no se especifique ning\u00fan canal al enviar un mensaje.", - "icon": "Utilice uno de los emojis de Slack como icono para el nombre de usuario proporcionado.", + "icon": "Utiliza uno de los emojis de Slack como icono para el nombre de usuario proporcionado.", "username": "Home Assistant publicar\u00e1 en Slack con el nombre de usuario especificado." }, - "description": "Consulte la documentaci\u00f3n sobre c\u00f3mo obtener su clave API de Slack." + "description": "Consulta la documentaci\u00f3n sobre c\u00f3mo obtener tu clave API de Slack." } } } diff --git a/homeassistant/components/slimproto/translations/es.json b/homeassistant/components/slimproto/translations/es.json index 352f82f605f..ac3c9b2bb41 100644 --- a/homeassistant/components/slimproto/translations/es.json +++ b/homeassistant/components/slimproto/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. S\u00f3lo es posible una sola configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." } } } \ No newline at end of file diff --git a/homeassistant/components/sma/translations/es.json b/homeassistant/components/sma/translations/es.json index 76edc25241c..216745647fb 100644 --- a/homeassistant/components/sma/translations/es.json +++ b/homeassistant/components/sma/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso" + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso" }, "error": { "cannot_connect": "No se pudo conectar", @@ -17,7 +17,7 @@ "host": "Host", "password": "Contrase\u00f1a", "ssl": "Utiliza un certificado SSL", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" }, "description": "Introduce la informaci\u00f3n de tu dispositivo SMA.", "title": "Configurar SMA Solar" diff --git a/homeassistant/components/smart_meter_texas/translations/es.json b/homeassistant/components/smart_meter_texas/translations/es.json index d537185eb68..942024cf167 100644 --- a/homeassistant/components/smart_meter_texas/translations/es.json +++ b/homeassistant/components/smart_meter_texas/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/sms/translations/es.json b/homeassistant/components/sms/translations/es.json index 27669a2b52f..660fb0db4fa 100644 --- a/homeassistant/components/sms/translations/es.json +++ b/homeassistant/components/sms/translations/es.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "baud_speed": "Velocidad en Baudis", + "baud_speed": "Velocidad en baudios", "device": "Dispositivo" }, "title": "Conectar con el m\u00f3dem" diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index 3c1b7c0fa9a..440ab7bf21f 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -19,7 +19,7 @@ "data": { "api_key": "Clave API", "url": "URL", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/soundtouch/translations/es.json b/homeassistant/components/soundtouch/translations/es.json new file mode 100644 index 00000000000..0ee2968bf09 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Est\u00e1s a punto de a\u00f1adir el dispositivo SoundTouch llamado `{name}` a Home Assistant.", + "title": "Confirma la adici\u00f3n del dispositivo Bose SoundTouch" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Bose SoundTouch mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimine la configuraci\u00f3n YAML de Bose SoundTouch de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Bose SoundTouch" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/et.json b/homeassistant/components/soundtouch/translations/et.json index 0adb9ddba8b..fdbc2b9090f 100644 --- a/homeassistant/components/soundtouch/translations/et.json +++ b/homeassistant/components/soundtouch/translations/et.json @@ -17,5 +17,11 @@ "title": "Kinnita Bose SoundTouchi seadme lisamine" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Bose SoundTouchi seadistamine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nEemaldage bose SoundTouch YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Bose SoundTouchi YAML-konfiguratsioon eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/tr.json b/homeassistant/components/soundtouch/translations/tr.json index 957215dd8b3..a3a176e58d8 100644 --- a/homeassistant/components/soundtouch/translations/tr.json +++ b/homeassistant/components/soundtouch/translations/tr.json @@ -17,5 +17,11 @@ "title": "Bose SoundTouch cihaz\u0131 eklemeyi onaylay\u0131n" } } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak Bose SoundTouch'\u0131 yap\u0131land\u0131rmak kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Bu sorunu \u00e7\u00f6zmek i\u00e7in Bose SoundTouch YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Bose SoundTouch YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/spider/translations/es.json b/homeassistant/components/spider/translations/es.json index c538d4fa5b5..12fe8db0e73 100644 --- a/homeassistant/components/spider/translations/es.json +++ b/homeassistant/components/spider/translations/es.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Iniciar sesi\u00f3n con la cuenta de mijn.ithodaalderop.nl" } diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index ce1966a8edd..09de377d195 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Se elimin\u00f3 la configuraci\u00f3n de Spotify usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Spotify" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API de Spotify" diff --git a/homeassistant/components/spotify/translations/et.json b/homeassistant/components/spotify/translations/et.json index c5cee44acca..5365a66df1c 100644 --- a/homeassistant/components/spotify/translations/et.json +++ b/homeassistant/components/spotify/translations/et.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Spotify seadistamine YAML-i abil on eemaldatud.\n\nHome Assistant ei kasuta teie olemasolevat YAML-i konfiguratsiooni.\n\nEemaldage FAILIST CONFIGURATION.yaml YAML-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Spotify YAML konfiguratsioon on eemaldatud" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API l\u00f5pp-punkt on k\u00e4ttesaadav" diff --git a/homeassistant/components/spotify/translations/tr.json b/homeassistant/components/spotify/translations/tr.json index 32c21899d52..ba52631e96d 100644 --- a/homeassistant/components/spotify/translations/tr.json +++ b/homeassistant/components/spotify/translations/tr.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Spotify'\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Spotify YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API u\u00e7 noktas\u0131na ula\u015f\u0131labilir" diff --git a/homeassistant/components/sql/translations/es.json b/homeassistant/components/sql/translations/es.json index 6811fc498f9..322485ae446 100644 --- a/homeassistant/components/sql/translations/es.json +++ b/homeassistant/components/sql/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { - "db_url_invalid": "URL de la base de datos inv\u00e1lido", + "db_url_invalid": "URL de la base de datos no v\u00e1lida", "query_invalid": "Consulta SQL no v\u00e1lida" }, "step": { @@ -12,7 +12,7 @@ "data": { "column": "Columna", "db_url": "URL de la base de datos", - "name": "Nombre", + "name": "[%key:component::sql::config::step::user::data::name%]", "query": "Selecciona la consulta", "unit_of_measurement": "Unidad de medida", "value_template": "Plantilla de valor" @@ -20,7 +20,7 @@ "data_description": { "column": "Columna de respuesta de la consulta para presentar como estado", "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", - "name": "Nombre que se utilizar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", + "name": "Nombre que se usar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", "query": "Consulta a ejecutar, debe empezar por 'SELECT'", "unit_of_measurement": "Unidad de medida (opcional)", "value_template": "Plantilla de valor (opcional)" @@ -30,15 +30,15 @@ }, "options": { "error": { - "db_url_invalid": "URL de la base de datos inv\u00e1lido", - "query_invalid": "Consulta SQL inv\u00e1lida" + "db_url_invalid": "URL de la base de datos no v\u00e1lida", + "query_invalid": "Consulta SQL no v\u00e1lida" }, "step": { "init": { "data": { "column": "Columna", "db_url": "URL de la base de datos", - "name": "Nombre", + "name": "[%key:component::sql::config::step::user::data::name%]", "query": "Selecciona la consulta", "unit_of_measurement": "Unidad de medida", "value_template": "Plantilla de valor" @@ -46,7 +46,7 @@ "data_description": { "column": "Columna de respuesta de la consulta para presentar como estado", "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", - "name": "Nombre que se utilizar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", + "name": "Nombre que se usar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", "query": "Consulta a ejecutar, debe empezar por 'SELECT'", "unit_of_measurement": "Unidad de medida (opcional)", "value_template": "Plantilla de valor (opcional)" diff --git a/homeassistant/components/squeezebox/translations/es.json b/homeassistant/components/squeezebox/translations/es.json index 7bf247c2976..b6745920acd 100644 --- a/homeassistant/components/squeezebox/translations/es.json +++ b/homeassistant/components/squeezebox/translations/es.json @@ -17,7 +17,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Editar la informaci\u00f3n de conexi\u00f3n" }, diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json index ebd4583fe43..b82a4e6f6f6 100644 --- a/homeassistant/components/srp_energy/translations/es.json +++ b/homeassistant/components/srp_energy/translations/es.json @@ -15,7 +15,7 @@ "id": "ID de la cuenta", "is_tou": "Es el plan de tiempo de uso", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/starline/translations/es.json b/homeassistant/components/starline/translations/es.json index 6df1cdf8f01..eff1da9773a 100644 --- a/homeassistant/components/starline/translations/es.json +++ b/homeassistant/components/starline/translations/es.json @@ -31,7 +31,7 @@ "auth_user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Correo electr\u00f3nico y contrase\u00f1a de la cuenta StarLine", "title": "Credenciales de usuario" diff --git a/homeassistant/components/steam_online/translations/es.json b/homeassistant/components/steam_online/translations/es.json index 26ee994acde..7aeca102cc4 100644 --- a/homeassistant/components/steam_online/translations/es.json +++ b/homeassistant/components/steam_online/translations/es.json @@ -2,17 +2,17 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_account": "ID de la cuenta inv\u00e1lida", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "invalid_account": "ID de cuenta inv\u00e1lido", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { "reauth_confirm": { - "description": "La integraci\u00f3n de Steam debe volver a autenticarse manualmente \n\n Puede encontrar su clave aqu\u00ed: {api_key_url}", + "description": "La integraci\u00f3n de Steam debe volver a autenticarse manualmente \n\nPuedes encontrar tu clave aqu\u00ed: {api_key_url}", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { @@ -24,14 +24,20 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Se elimin\u00f3 la configuraci\u00f3n de Steam usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Steam" + } + }, "options": { "error": { - "unauthorized": "Lista de amigos restringida: Por favor, consulte la documentaci\u00f3n sobre c\u00f3mo ver a todos los dem\u00e1s amigos" + "unauthorized": "Lista de amigos restringida: Por favor, consulta la documentaci\u00f3n sobre c\u00f3mo ver a todos los dem\u00e1s amigos" }, "step": { "init": { "data": { - "accounts": "Nombres de las cuentas a controlar" + "accounts": "Nombres de las cuentas a supervisar" } } } diff --git a/homeassistant/components/steam_online/translations/et.json b/homeassistant/components/steam_online/translations/et.json index 62e38eb4088..b33e994f3e8 100644 --- a/homeassistant/components/steam_online/translations/et.json +++ b/homeassistant/components/steam_online/translations/et.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Steami seadistamine YAML-i abil on eemaldatud.\n\nHome Assistant ei kasuta teie olemasolevat YAML-i konfiguratsiooni.\n\nEemaldage FAILIST CONFIGURATION.yaml YAML-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Steam YAML konfiguratsioon on eemaldatud" + } + }, "options": { "error": { "unauthorized": "S\u00f5prade nimekiri on piiratud: vaata dokumentatsiooni kuidas n\u00e4ha k\u00f5iki teisi s\u00f5pru" diff --git a/homeassistant/components/steam_online/translations/tr.json b/homeassistant/components/steam_online/translations/tr.json index a1717857080..eabdabc24a0 100644 --- a/homeassistant/components/steam_online/translations/tr.json +++ b/homeassistant/components/steam_online/translations/tr.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Steam'i YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Steam YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } + }, "options": { "error": { "unauthorized": "Arkada\u015f listesi k\u0131s\u0131tland\u0131: L\u00fctfen di\u011fer t\u00fcm arkada\u015flar\u0131 nas\u0131l g\u00f6rece\u011finizle ilgili belgelere bak\u0131n" diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index 7b3a32c0213..fe02d508241 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -25,21 +25,21 @@ "data": { "contact_method": "Selecciona un m\u00e9todo de contacto:" }, - "description": "Autenticaci\u00f3n de dos factores requerida", + "description": "Se requiere autenticaci\u00f3n de dos factores", "title": "Configuraci\u00f3n de Subaru Starlink" }, "two_factor_validate": { "data": { "validation_code": "C\u00f3digo de validaci\u00f3n" }, - "description": "Introduce el c\u00f3digo de validaci\u00f3n recibido", + "description": "Por favor, introduce el c\u00f3digo de validaci\u00f3n recibido", "title": "Configuraci\u00f3n de Subaru Starlink" }, "user": { "data": { "country": "Seleccionar pa\u00eds", "password": "Contrase\u00f1a", - "username": "Usuario" + "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" diff --git a/homeassistant/components/surepetcare/translations/es.json b/homeassistant/components/surepetcare/translations/es.json index 13f2eb38bef..f92417d76a0 100644 --- a/homeassistant/components/surepetcare/translations/es.json +++ b/homeassistant/components/surepetcare/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/switch/translations/zh-Hant.json b/homeassistant/components/switch/translations/zh-Hant.json index a1f38544f67..c8c6f688cab 100644 --- a/homeassistant/components/switch/translations/zh-Hant.json +++ b/homeassistant/components/switch/translations/zh-Hant.json @@ -18,7 +18,7 @@ "state": { "_": { "off": "\u95dc\u9589", - "on": "\u958b\u5553" + "on": "\u958b\u555f" } }, "title": "\u958b\u95dc" diff --git a/homeassistant/components/switch_as_x/translations/es.json b/homeassistant/components/switch_as_x/translations/es.json index ad0f9f52bbf..0228d86d0fe 100644 --- a/homeassistant/components/switch_as_x/translations/es.json +++ b/homeassistant/components/switch_as_x/translations/es.json @@ -6,7 +6,7 @@ "entity_id": "Conmutador", "target_domain": "Nuevo tipo" }, - "description": "Elija un interruptor que desee que aparezca en Home Assistant como luz, cubierta o cualquier otra cosa. El interruptor original se ocultar\u00e1." + "description": "Elige un interruptor que desees que aparezca en Home Assistant como luz, cubierta o cualquier otra cosa. El interruptor original se ocultar\u00e1." } } }, diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index a3ba38e2f24..04c1af6d69f 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Vols configurar {name}?" + }, + "password": { + "data": { + "password": "Contrasenya" + }, + "description": "El dispositiu {name} necessita una contrasenya" + }, "user": { "data": { "address": "Adre\u00e7a del dispositiu", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 7e58d169856..6a0231c9bed 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,7 +7,6 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, - "error": {}, "flow_title": "{name} ({address})", "step": { "confirm": { @@ -21,8 +20,12 @@ }, "user": { "data": { - "address": "Device address" - } + "address": "Device address", + "mac": "Device MAC address", + "name": "Name", + "password": "Password" + }, + "title": "Setup Switchbot device" } } }, @@ -30,7 +33,10 @@ "step": { "init": { "data": { - "retry_count": "Retry count" + "retry_count": "Retry count", + "retry_timeout": "Timeout between retries", + "scan_timeout": "How long to scan for advertisement data", + "update_time": "Time between updates (seconds)" } } } diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index 6fc4d59f693..d0f908df646 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -7,10 +7,20 @@ "switchbot_unsupported_type": "Tipo de Switchbot no compatible.", "unknown": "Error inesperado" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "password": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "El dispositivo {name} requiere una contrase\u00f1a" + }, "user": { "data": { + "address": "Direcci\u00f3n del dispositivo", "mac": "Direcci\u00f3n MAC del dispositivo", "name": "Nombre", "password": "Contrase\u00f1a" diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index ea070f9f3c2..2b35ec9634e 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "password": { + "data": { + "password": "Mot de passe" + }, + "description": "L'appareil {name} requiert un mot de passe" + }, "user": { "data": { "address": "Adresse de l'appareil", diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index bd363de0738..88f5756c5fa 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -13,6 +13,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "password": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "A {name} eszk\u00f6z jelsz\u00f3t k\u00e9r" + }, "user": { "data": { "address": "Eszk\u00f6z c\u00edme", diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 3edc04ba6f3..bcc97a120af 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Deseja configurar {name}?" + }, + "password": { + "data": { + "password": "Senha" + }, + "description": "O dispositivo {name} requer uma senha" + }, "user": { "data": { "address": "Endere\u00e7o do dispositivo", diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index 40b80dc4a3c..d60be4c0d73 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -11,10 +11,20 @@ "one": "Bo\u015f", "other": "Bo\u015f" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "password": { + "data": { + "password": "Parola" + }, + "description": "{name} cihaz\u0131 bir \u015fifre gerektiriyor" + }, "user": { "data": { + "address": "Cihaz adresi", "mac": "Cihaz MAC adresi", "name": "Ad", "password": "Parola" diff --git a/homeassistant/components/syncthing/translations/es.json b/homeassistant/components/syncthing/translations/es.json index e39a1436c2a..dd156f7edb4 100644 --- a/homeassistant/components/syncthing/translations/es.json +++ b/homeassistant/components/syncthing/translations/es.json @@ -13,7 +13,7 @@ "title": "Configurar integraci\u00f3n de Syncthing", "token": "Token", "url": "URL", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index 779996d7023..d7f1f16197c 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -25,15 +25,15 @@ "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Usar SSL/TLS para conectar con tu NAS", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "description": "\u00bfQuieres configurar {name} ({host})?" }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Volver a autenticar la integraci\u00f3n Synology DSM" }, @@ -44,7 +44,7 @@ "port": "Puerto", "ssl": "Usar SSL/TLS para conectar con tu NAS", "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/tado/translations/es.json b/homeassistant/components/tado/translations/es.json index db71d41f789..58745783774 100644 --- a/homeassistant/components/tado/translations/es.json +++ b/homeassistant/components/tado/translations/es.json @@ -13,7 +13,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con tu cuenta de Tado" } diff --git a/homeassistant/components/tankerkoenig/translations/es.json b/homeassistant/components/tankerkoenig/translations/es.json index bda6c43ce6e..ec0f079cbea 100644 --- a/homeassistant/components/tankerkoenig/translations/es.json +++ b/homeassistant/components/tankerkoenig/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", - "no_stations": "No se pudo encontrar ninguna estaci\u00f3n al alcance." + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_stations": "No se pudo encontrar ninguna estaci\u00f3n dentro del rango." }, "step": { "reauth_confirm": { @@ -18,7 +18,7 @@ "data": { "stations": "Estaciones" }, - "description": "encontr\u00f3 {stations_count} estaciones en el radio", + "description": "se encontraron {stations_count} estaciones en el radio", "title": "Selecciona las estaciones a a\u00f1adir" }, "user": { diff --git a/homeassistant/components/tautulli/translations/es.json b/homeassistant/components/tautulli/translations/es.json index 295683bf358..2d9a09d3432 100644 --- a/homeassistant/components/tautulli/translations/es.json +++ b/homeassistant/components/tautulli/translations/es.json @@ -5,8 +5,8 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "Fallo en la conexi\u00f3n", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -14,16 +14,16 @@ "data": { "api_key": "Clave API" }, - "description": "Para encontrar su clave API, abra la p\u00e1gina web de Tautulli y navegue a Configuraci\u00f3n y luego a la interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina.", + "description": "Para encontrar tu clave API, abre la p\u00e1gina web de Tautulli y navega a Configuraci\u00f3n y luego a Interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina.", "title": "Re-autenticaci\u00f3n de Tautulli" }, "user": { "data": { "api_key": "Clave API", "url": "URL", - "verify_ssl": "Verifica el certificat SSL" + "verify_ssl": "Verificar el certificado SSL" }, - "description": "Para encontrar su clave de API, abra la p\u00e1gina web de Tautulli y navegue hasta Configuraci\u00f3n y luego hasta Interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina.\n\nEjemplo de la URL: ```http://192.168.0.10:8181`` con 8181 como puerto predeterminado." + "description": "Para encontrar tu clave API, abre la p\u00e1gina web de Tautulli y navega a Configuraci\u00f3n y luego a Interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina. \n\nEjemplo de URL: ```http://192.168.0.10:8181``` siendo 8181 el puerto predeterminado." } } } diff --git a/homeassistant/components/tod/translations/es.json b/homeassistant/components/tod/translations/es.json index c1ea525e10c..a5451faf145 100644 --- a/homeassistant/components/tod/translations/es.json +++ b/homeassistant/components/tod/translations/es.json @@ -22,5 +22,5 @@ } } }, - "title": "Sensor tiempo del d\u00eda" + "title": "Sensor de horas del d\u00eda" } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 66f5be9ebc2..a25a204eec4 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -24,7 +24,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } @@ -35,7 +35,7 @@ "data": { "auto_bypass_low_battery": "Ignorar autom\u00e1ticamente bater\u00eda baja" }, - "description": "Ignorar autom\u00e1ticamente las zonas en el momento en que informan de que la bater\u00eda est\u00e1 baja.", + "description": "Omite zonas autom\u00e1ticamente en el momento en que informan que la bater\u00eda est\u00e1 baja.", "title": "Opciones de TotalConnect" } } diff --git a/homeassistant/components/tradfri/translations/es.json b/homeassistant/components/tradfri/translations/es.json index 531b11cc0a1..caadd9b7903 100644 --- a/homeassistant/components/tradfri/translations/es.json +++ b/homeassistant/components/tradfri/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso" + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso" }, "error": { "cannot_authenticate": "No se puede autenticar, \u00bfGateway est\u00e1 emparejado con otro servidor como, por ejemplo, Homekit?", diff --git a/homeassistant/components/trafikverket_ferry/translations/es.json b/homeassistant/components/trafikverket_ferry/translations/es.json index 0ab89ac0fa8..93996b1923c 100644 --- a/homeassistant/components/trafikverket_ferry/translations/es.json +++ b/homeassistant/components/trafikverket_ferry/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fallo en la conexi\u00f3n", - "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", + "cannot_connect": "No se pudo conectar", + "incorrect_api_key": "Clave de API no v\u00e1lida para la cuenta seleccionada", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_route": "No se pudo encontrar la ruta con la informaci\u00f3n proporcionada" }, @@ -19,7 +19,7 @@ "user": { "data": { "api_key": "Clave API", - "from": "Des del puerto", + "from": "Desde el puerto", "time": "Hora", "to": "Al puerto", "weekday": "D\u00edas entre semana" diff --git a/homeassistant/components/trafikverket_train/translations/es.json b/homeassistant/components/trafikverket_train/translations/es.json index 2aca5e59745..9f546bd1676 100644 --- a/homeassistant/components/trafikverket_train/translations/es.json +++ b/homeassistant/components/trafikverket_train/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "incorrect_api_key": "Clave API no v\u00e1lida para la cuenta seleccionada", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_station": "No se pudo encontrar una estaci\u00f3n con el nombre especificado", "invalid_time": "Hora proporcionada no v\u00e1lida", "more_stations": "Se encontraron varias estaciones con el nombre especificado" diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index c83904bf5db..6c293a59d4c 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado." + "already_configured": "El dispositivo ya est\u00e1 configurado.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -9,13 +10,20 @@ "name_exists": "El nombre ya existe" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "La contrase\u00f1a para {username} no es v\u00e1lida.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Configuraci\u00f3n del cliente de transmisi\u00f3n" } diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index a15408bf96d..d51024c5640 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -11,7 +11,7 @@ "access_secret": "Tuya IoT Access Secret", "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introduce tus credencial de Tuya" } diff --git a/homeassistant/components/ukraine_alarm/translations/es.json b/homeassistant/components/ukraine_alarm/translations/es.json index 3e1b8f322bb..6a4e2d806a5 100644 --- a/homeassistant/components/ukraine_alarm/translations/es.json +++ b/homeassistant/components/ukraine_alarm/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "max_regions": "Se pueden configurar un m\u00e1ximo de 5 regiones", "rate_limit": "Demasiadas peticiones", - "timeout": "Tiempo m\u00e1ximo de espera para establecer la conexi\u00f3n agotado", + "timeout": "Tiempo de espera agotado para establecer la conexi\u00f3n", "unknown": "Error inesperado" }, "step": { @@ -13,19 +13,19 @@ "data": { "region": "Regi\u00f3n" }, - "description": "Si desea monitorear no solo el estado y el distrito, elija su comunidad espec\u00edfica" + "description": "Si quieres supervisar no solo el estado y el distrito, elige tu comunidad espec\u00edfica" }, "district": { "data": { "region": "Regi\u00f3n" }, - "description": "Si quieres monitorear no s\u00f3lo el estado, elige el distrito espec\u00edfico" + "description": "Si quieres supervisar no s\u00f3lo el estado, elige el distrito espec\u00edfico" }, "user": { "data": { "region": "Regi\u00f3n" }, - "description": "Escoja el estado a monitorear" + "description": "Elige el estado para supervisar" } } } diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index ebaf1d3c127..c6d17b369b4 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -18,7 +18,7 @@ "password": "Contrase\u00f1a", "port": "Puerto", "site": "ID del sitio", - "username": "Usuario", + "username": "Nombre de usuario", "verify_ssl": "Controlador usando el certificado adecuado" }, "title": "Configuraci\u00f3n de UniFi Network" diff --git a/homeassistant/components/unifiprotect/translations/de.json b/homeassistant/components/unifiprotect/translations/de.json index 0f6ffd77793..01867c2b76d 100644 --- a/homeassistant/components/unifiprotect/translations/de.json +++ b/homeassistant/components/unifiprotect/translations/de.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Echtzeitmetriken (WARNUNG: Erh\u00f6ht die CPU-Auslastung erheblich)", "disable_rtsp": "RTSP-Stream deaktivieren", + "max_media": "Maximale Anzahl von Ereignissen, die f\u00fcr den Medienbrowser geladen werden (erh\u00f6ht die RAM-Nutzung)", "override_connection_host": "Verbindungshost \u00fcberschreiben" }, "description": "Die Option Echtzeit-Metriken sollte nur aktiviert werden, wenn du die Diagnosesensoren aktiviert hast und diese in Echtzeit aktualisiert werden sollen. Wenn sie nicht aktiviert ist, werden sie nur einmal alle 15 Minuten aktualisiert.", diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index 8cede72d32b..4ad61ad47bb 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -47,6 +47,7 @@ "data": { "all_updates": "\u039c\u03b5\u03c4\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf (\u03a0\u03a1\u039f\u0395\u0399\u0394\u039f\u03a0\u039f\u0399\u0397\u03a3\u0397: \u0391\u03c5\u03be\u03ac\u03bd\u03b5\u03b9 \u03c3\u03b7\u03bc\u03b1\u03bd\u03c4\u03b9\u03ba\u03ac \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 CPU)", "disable_rtsp": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03bf\u03ae RTSP", + "max_media": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd \u03c0\u03c1\u03bf\u03c2 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd (\u03b1\u03c5\u03be\u03ac\u03bd\u03b5\u03b9 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 RAM)", "override_connection_host": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "description": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03c4\u03c1\u03ae\u03c3\u03b5\u03c9\u03bd \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf\u03c5\u03c2 \u03b4\u03b9\u03b1\u03b3\u03bd\u03c9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03bf\u03cd\u03bd \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1, \u03b8\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c6\u03bf\u03c1\u03ac \u03ba\u03ac\u03b8\u03b5 15 \u03bb\u03b5\u03c0\u03c4\u03ac.", diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index eb52d0222fc..8a1cdd9afcd 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -14,7 +14,7 @@ "discovery_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "\u00bfQuieres configurar {name} ({ip_address})? Necesitar\u00e1 un usuario local creado en su consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", "title": "UniFi Protect descubierto" @@ -47,6 +47,7 @@ "data": { "all_updates": "M\u00e9tricas en tiempo real (ADVERTENCIA: Aumenta en gran medida el uso de la CPU)", "disable_rtsp": "Deshabilitar la transmisi\u00f3n RTSP", + "max_media": "N\u00famero m\u00e1ximo de eventos a cargar para el Navegador de Medios (aumenta el uso de RAM)", "override_connection_host": "Anular la conexi\u00f3n del host" }, "description": "La opci\u00f3n de m\u00e9tricas en tiempo real s\u00f3lo debe estar activada si ha habilitado los sensores de diagn\u00f3stico y quiere que se actualicen en tiempo real. Si no est\u00e1 activada, s\u00f3lo se actualizar\u00e1n una vez cada 15 minutos.", diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json index 9abd63559ee..31b70f41dec 100644 --- a/homeassistant/components/unifiprotect/translations/et.json +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Reaalajas m\u00f5\u00f5dikud (HOIATUS: suurendab oluliselt CPU kasutust)", "disable_rtsp": "Keela RTSP voog", + "max_media": "Meediumibrauserisse laaditavate s\u00fcndmuste maksimaalne arv (suurendab RAM-i kasutamist)", "override_connection_host": "\u00dchenduse hosti alistamine" }, "description": "Reaalajas m\u00f5\u00f5dikute valik tuleks lubada ainult siis kui oled diagnostikaandurid sisse l\u00fclitanud ja soovid, et neid uuendatakse reaalajas. Kui see ei ole lubatud, uuendatakse neid ainult \u00fcks kord 15 minuti tagant.", diff --git a/homeassistant/components/unifiprotect/translations/pt-BR.json b/homeassistant/components/unifiprotect/translations/pt-BR.json index 2c0c1270add..e2951d47938 100644 --- a/homeassistant/components/unifiprotect/translations/pt-BR.json +++ b/homeassistant/components/unifiprotect/translations/pt-BR.json @@ -47,6 +47,7 @@ "data": { "all_updates": "M\u00e9tricas em tempo real (AVISO: aumenta muito o uso da CPU)", "disable_rtsp": "Desativar o fluxo RTSP", + "max_media": "N\u00famero m\u00e1ximo de eventos a serem carregados para o Media Browser (aumenta o uso de RAM)", "override_connection_host": "Anular o host de conex\u00e3o" }, "description": "A op\u00e7\u00e3o de m\u00e9tricas em tempo real s\u00f3 deve ser habilitada se voc\u00ea tiver habilitado os sensores de diagn\u00f3stico e quiser que eles sejam atualizados em tempo real. Se n\u00e3o estiver ativado, eles ser\u00e3o atualizados apenas uma vez a cada 15 minutos.", diff --git a/homeassistant/components/unifiprotect/translations/tr.json b/homeassistant/components/unifiprotect/translations/tr.json index 869c13608ce..d26f6af41ce 100644 --- a/homeassistant/components/unifiprotect/translations/tr.json +++ b/homeassistant/components/unifiprotect/translations/tr.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Ger\u00e7ek zamanl\u0131 \u00f6l\u00e7\u00fcmler (UYARI: CPU kullan\u0131m\u0131n\u0131 b\u00fcy\u00fck \u00f6l\u00e7\u00fcde art\u0131r\u0131r)", "disable_rtsp": "RTSP ak\u0131\u015f\u0131n\u0131 devre d\u0131\u015f\u0131 b\u0131rak\u0131n", + "max_media": "Medya Taray\u0131c\u0131 i\u00e7in y\u00fcklenecek maksimum olay say\u0131s\u0131 (RAM kullan\u0131m\u0131n\u0131 art\u0131r\u0131r)", "override_connection_host": "Ba\u011flant\u0131 Ana Bilgisayar\u0131n\u0131 Ge\u00e7ersiz K\u0131l" }, "description": "Ger\u00e7ek zamanl\u0131 \u00f6l\u00e7\u00fcmler se\u00e7ene\u011fi, yaln\u0131zca tan\u0131lama sens\u00f6rlerini etkinle\u015ftirdiyseniz ve bunlar\u0131n ger\u00e7ek zamanl\u0131 olarak g\u00fcncellenmesini istiyorsan\u0131z etkinle\u015ftirilmelidir. Etkinle\u015ftirilmezlerse, yaln\u0131zca her 15 dakikada bir g\u00fcncellenirler.", diff --git a/homeassistant/components/upcloud/translations/es.json b/homeassistant/components/upcloud/translations/es.json index 96c659ccad4..f4cccb3f0e2 100644 --- a/homeassistant/components/upcloud/translations/es.json +++ b/homeassistant/components/upcloud/translations/es.json @@ -8,7 +8,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/update/translations/es.json b/homeassistant/components/update/translations/es.json index ee92c01c938..49012350f47 100644 --- a/homeassistant/components/update/translations/es.json +++ b/homeassistant/components/update/translations/es.json @@ -2,8 +2,8 @@ "device_automation": { "trigger_type": { "changed_states": "La disponibilidad de la actualizaci\u00f3n de {entity_name} cambie", - "turned_off": "{entity_name} se actualice", - "turned_on": "{entity_name} tenga una actualizaci\u00f3n disponible" + "turned_off": "{entity_name} se actualiz\u00f3", + "turned_on": "{entity_name} tiene una actualizaci\u00f3n disponible" } }, "title": "Actualizar" diff --git a/homeassistant/components/uscis/translations/es.json b/homeassistant/components/uscis/translations/es.json new file mode 100644 index 00000000000..8dd3ad76874 --- /dev/null +++ b/homeassistant/components/uscis/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la integraci\u00f3n USCIS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/tr.json b/homeassistant/components/uscis/translations/tr.json new file mode 100644 index 00000000000..0e27f5bdac5 --- /dev/null +++ b/homeassistant/components/uscis/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "ABD Vatanda\u015fl\u0131k ve G\u00f6\u00e7menlik Hizmetleri (USCIS) entegrasyonu, Home Assistant'tan kald\u0131r\u0131lmay\u0131 bekliyor ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacak. \n\n Entegrasyon kald\u0131r\u0131l\u0131yor, \u00e7\u00fcnk\u00fc izin verilmeyen web taramaya dayan\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "USCIS entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/es.json b/homeassistant/components/utility_meter/translations/es.json index a38687741d8..f4a3ca04104 100644 --- a/homeassistant/components/utility_meter/translations/es.json +++ b/homeassistant/components/utility_meter/translations/es.json @@ -12,13 +12,13 @@ "tariffs": "Tarifas soportadas" }, "data_description": { - "delta_values": "Habilitar si los valores de origen son valores delta desde la \u00faltima lectura en lugar de valores absolutos.", - "net_consumption": "Act\u00edvalo si es un contador limpio, es decir, puede aumentar y disminuir.", - "offset": "Desplaza el d\u00eda de restablecimiento mensual del contador.", - "tariffs": "Lista de tarifas admitidas, d\u00e9jala en blanco si utilizas una \u00fanica tarifa." + "delta_values": "Habil\u00edtalo si los valores de origen son valores delta desde la \u00faltima lectura en lugar de valores absolutos.", + "net_consumption": "Habil\u00edtalo si la fuente es un medidor neto, lo que significa que puede aumentar y disminuir.", + "offset": "Compensar el d\u00eda de reinicio del medidor mensual.", + "tariffs": "Una lista de tarifas admitidas, d\u00e9jala en blanco si solo necesitas una tarifa." }, - "description": "Cree un sensor que rastree el consumo de varios servicios p\u00fablicos (p. ej., energ\u00eda, gas, agua, calefacci\u00f3n) durante un per\u00edodo de tiempo configurado, generalmente mensual. El sensor del medidor de servicios admite opcionalmente dividir el consumo por tarifas, en ese caso se crea un sensor para cada tarifa, as\u00ed como una entidad de selecci\u00f3n para elegir la tarifa actual.", - "title": "A\u00f1adir medidor de utilidades" + "description": "Crea un sensor que rastree el consumo de varios servicios p\u00fablicos (p. ej., energ\u00eda, gas, agua, calefacci\u00f3n) durante un per\u00edodo de tiempo configurado, generalmente mensual. El sensor del medidor de servicios admite opcionalmente dividir el consumo por tarifas, en ese caso se crea un sensor para cada tarifa, as\u00ed como una entidad de selecci\u00f3n para elegir la tarifa actual.", + "title": "A\u00f1adir contador" } } }, diff --git a/homeassistant/components/venstar/translations/es.json b/homeassistant/components/venstar/translations/es.json index 0401a0365d3..90d63f5aa0d 100644 --- a/homeassistant/components/venstar/translations/es.json +++ b/homeassistant/components/venstar/translations/es.json @@ -14,7 +14,7 @@ "password": "Contrase\u00f1a", "pin": "C\u00f3digo PIN", "ssl": "Utiliza un certificado SSL", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con el termostato Venstar" } diff --git a/homeassistant/components/verisure/translations/es.json b/homeassistant/components/verisure/translations/es.json index 19517bb7ed0..c7ea0af6f41 100644 --- a/homeassistant/components/verisure/translations/es.json +++ b/homeassistant/components/verisure/translations/es.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unknown": "Error inesperado" + "unknown": "Error inesperado", + "unknown_mfa": "Se produjo un error desconocido durante la configuraci\u00f3n MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant encontr\u00f3 varias instalaciones de Verisure en su cuenta de Mis p\u00e1ginas. Por favor, seleccione la instalaci\u00f3n para agregar a Home Assistant." }, + "mfa": { + "data": { + "code": "C\u00f3digo de verificaci\u00f3n", + "description": "Tu cuenta tiene activada la verificaci\u00f3n en dos pasos. Por favor, introduce el c\u00f3digo de verificaci\u00f3n que te env\u00eda Verisure." + } + }, "reauth_confirm": { "data": { "description": "Vuelva a autenticarse con su cuenta Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Contrase\u00f1a" } }, + "reauth_mfa": { + "data": { + "code": "C\u00f3digo de verificaci\u00f3n", + "description": "Tu cuenta tiene activada la verificaci\u00f3n en dos pasos. Por favor, introduce el c\u00f3digo de verificaci\u00f3n que te env\u00eda Verisure." + } + }, "user": { "data": { "description": "Inicia sesi\u00f3n con tu cuenta Verisure My Pages.", diff --git a/homeassistant/components/vulcan/translations/es.json b/homeassistant/components/vulcan/translations/es.json index 7538c6411df..7e8b3b8b067 100644 --- a/homeassistant/components/vulcan/translations/es.json +++ b/homeassistant/components/vulcan/translations/es.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "all_student_already_configured": "Ya se han a\u00f1adido todos los estudiantes.", - "already_configured": "Ya se ha a\u00f1adido a este alumno.", - "no_matching_entries": "No se encontraron entradas que coincidan, use una cuenta diferente o elimine la integraci\u00f3n con el estudiante obsoleto.", + "all_student_already_configured": "Todos los estudiantes ya han sido a\u00f1adidos.", + "already_configured": "Ese estudiante ya ha sido a\u00f1adido.", + "no_matching_entries": "No se encontraron entradas que coincidan, usa una cuenta diferente o elimina la integraci\u00f3n con el estudiante obsoleto.", "reauth_successful": "Re-autenticaci\u00f3n exitosa" }, "error": { - "cannot_connect": "Error de conexi\u00f3n - compruebe su conexi\u00f3n a Internet", - "expired_credentials": "Credenciales caducadas - cree nuevas en la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil de Vulcan", - "expired_token": "Token caducado, genera un nuevo token", + "cannot_connect": "Error de conexi\u00f3n - por favor, comprueba tu conexi\u00f3n a Internet", + "expired_credentials": "Credenciales caducadas - por favor, crea unas nuevas en la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil de Vulcan", + "expired_token": "Token caducado - por favor, genera un token nuevo", "invalid_pin": "Pin no v\u00e1lido", "invalid_symbol": "S\u00edmbolo inv\u00e1lido", "invalid_token": "Token inv\u00e1lido", @@ -28,7 +28,7 @@ "region": "S\u00edmbolo", "token": "Token" }, - "description": "Acceda a su cuenta de Vulcan a trav\u00e9s de la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." + "description": "Inicia sesi\u00f3n en tu cuenta de Vulcan utilizando la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." }, "reauth_confirm": { "data": { @@ -36,7 +36,7 @@ "region": "S\u00edmbolo", "token": "Token" }, - "description": "Acceda a su cuenta de Vulcan a trav\u00e9s de la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." + "description": "Inicie sesi\u00f3n en tu cuenta de Vulcan utilizando la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." }, "select_saved_credentials": { "data": { @@ -48,7 +48,7 @@ "data": { "student_name": "Selecciona al alumno" }, - "description": "Seleccione el estudiante, puede a\u00f1adir m\u00e1s estudiantes a\u00f1adiendo de nuevo la integraci\u00f3n." + "description": "Selecciona el estudiante, puedes a\u00f1adir m\u00e1s estudiantes a\u00f1adiendo de nuevo la integraci\u00f3n." } } } diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json index 1c5315d6745..451e55ed41e 100644 --- a/homeassistant/components/wallbox/translations/es.json +++ b/homeassistant/components/wallbox/translations/es.json @@ -14,14 +14,14 @@ "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { "data": { "password": "Contrase\u00f1a", "station": "N\u00famero de serie de la estaci\u00f3n", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/whirlpool/translations/es.json b/homeassistant/components/whirlpool/translations/es.json index d26c25c3548..b5ec4d85cb0 100644 --- a/homeassistant/components/whirlpool/translations/es.json +++ b/homeassistant/components/whirlpool/translations/es.json @@ -9,7 +9,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index d3a3f60b12f..1303ca27690 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -27,6 +27,10 @@ "reauth": { "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", "title": "Re-autentificar el perfil" + }, + "reauth_confirm": { + "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/ws66i/translations/es.json b/homeassistant/components/ws66i/translations/es.json index d19017fdb95..075dd41ed56 100644 --- a/homeassistant/components/ws66i/translations/es.json +++ b/homeassistant/components/ws66i/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Ha fallado la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { @@ -12,7 +12,7 @@ "data": { "ip_address": "Direcci\u00f3n IP" }, - "title": "Conexi\u00f3n con el dispositivo" + "title": "Conectar al dispositivo" } } }, @@ -27,7 +27,7 @@ "source_5": "Nombre de la fuente #5", "source_6": "Nombre de la fuente #6" }, - "title": "Configuraci\u00f3n de las fuentes" + "title": "Configurar fuentes" } } } diff --git a/homeassistant/components/xbox/translations/es.json b/homeassistant/components/xbox/translations/es.json index 27cbeaec139..7b7fe2d32fe 100644 --- a/homeassistant/components/xbox/translations/es.json +++ b/homeassistant/components/xbox/translations/es.json @@ -13,5 +13,11 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de Xbox en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.9. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Xbox" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/et.json b/homeassistant/components/xbox/translations/et.json index d7bb66c6cf1..2b14aa56762 100644 --- a/homeassistant/components/xbox/translations/et.json +++ b/homeassistant/components/xbox/translations/et.json @@ -13,5 +13,11 @@ "title": "Vali tuvastusmeetod" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Xboxi konfigureerimine failis configuration.yaml eemaldatakse versioonis Home Assistant 2022.9.\n\nTeie olemasolevad OAuth-rakenduse volitused ja juurdep\u00e4\u00e4su seaded on automaatselt kasutajaliidesesse imporditud. Probleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Xboxi YAML-i konfiguratsioon eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/tr.json b/homeassistant/components/xbox/translations/tr.json index 3f4025ade5d..39a9c199371 100644 --- a/homeassistant/components/xbox/translations/tr.json +++ b/homeassistant/components/xbox/translations/tr.json @@ -13,5 +13,11 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Xbox'\u0131 configuration.yaml'de yap\u0131land\u0131rma, Home Assistant 2022.9'da kald\u0131r\u0131l\u0131yor. \n\n Mevcut OAuth Uygulama Kimlik Bilgileriniz ve eri\u015fim ayarlar\u0131n\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Xbox YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index e06806bbd9a..d8a36306af7 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "not_xiaomi_aqara": "No es un Xiaomi Aqara Gateway, el dispositivo descubierto no coincide con los gateways conocidos" }, "error": { diff --git a/homeassistant/components/xiaomi_ble/translations/el.json b/homeassistant/components/xiaomi_ble/translations/el.json index 30dc61cb5dc..e6c5efce91f 100644 --- a/homeassistant/components/xiaomi_ble/translations/el.json +++ b/homeassistant/components/xiaomi_ble/translations/el.json @@ -6,13 +6,22 @@ "decryption_failed": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5, \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b1\u03bd \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "expected_24_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", "expected_32_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", - "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "decryption_failed": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5, \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b1\u03bd \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "expected_24_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", + "expected_32_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "confirm_slow": { + "description": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b3\u03af\u03bd\u03b5\u03b9 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bc\u03b1\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9 \u03b1\u03bd \u03b1\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03ae \u03cc\u03c7\u03b9. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bf\u03c6\u03b5\u03af\u03bb\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03b1\u03c1\u03b3\u03cc \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2. \u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bf\u03cd\u03c4\u03c9\u03c2 \u03ae \u03ac\u03bb\u03bb\u03c9\u03c2, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03bb\u03b7\u03c6\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c4\u03b7\u03c2, \u03b5\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -25,6 +34,9 @@ }, "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03ad\u03c3\u03bc\u03b5\u03c5\u03c3\u03b7\u03c2 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd." }, + "slow_confirm": { + "description": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b3\u03af\u03bd\u03b5\u03b9 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bc\u03b1\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9 \u03b1\u03bd \u03b1\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03ae \u03cc\u03c7\u03b9. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bf\u03c6\u03b5\u03af\u03bb\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03b1\u03c1\u03b3\u03cc \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2. \u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bf\u03cd\u03c4\u03c9\u03c2 \u03ae \u03ac\u03bb\u03bb\u03c9\u03c2, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03bb\u03b7\u03c6\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c4\u03b7\u03c2, \u03b5\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9." + }, "user": { "data": { "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/xiaomi_ble/translations/es.json b/homeassistant/components/xiaomi_ble/translations/es.json new file mode 100644 index 00000000000..504c7bc845c --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/es.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "decryption_failed": "La clave de enlace proporcionada no funcion\u00f3, los datos del sensor no se pudieron descifrar. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", + "expected_24_characters": "Se esperaba una clave de enlace hexadecimal de 24 caracteres.", + "expected_32_characters": "Se esperaba una clave de enlace hexadecimal de 32 caracteres.", + "no_devices_found": "No se encontraron dispositivos en la red", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "decryption_failed": "La clave de enlace proporcionada no funcion\u00f3, los datos del sensor no se pudieron descifrar. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", + "expected_24_characters": "Se esperaba una clave de enlace hexadecimal de 24 caracteres.", + "expected_32_characters": "Se esperaba una clave de enlace hexadecimal de 32 caracteres." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "confirm_slow": { + "description": "No ha habido una transmisi\u00f3n desde este dispositivo en el \u00faltimo minuto, por lo que no estamos seguros de si este dispositivo usa cifrado o no. Esto puede deberse a que el dispositivo utiliza un intervalo de transmisi\u00f3n lento. Confirma para agregar este dispositivo de todos modos, luego, la pr\u00f3xima vez que se reciba una transmisi\u00f3n, se te pedir\u00e1 que ingreses su clave de enlace si es necesario." + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Clave de enlace" + }, + "description": "Los datos del sensor transmitidos por el sensor est\u00e1n cifrados. Para descifrarlos necesitamos una clave de enlace hexadecimal de 32 caracteres." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Clave de enlace" + }, + "description": "Los datos del sensor transmitidos por el sensor est\u00e1n cifrados. Para descifrarlos necesitamos una clave de enlace hexadecimal de 24 caracteres." + }, + "slow_confirm": { + "description": "No ha habido una transmisi\u00f3n desde este dispositivo en el \u00faltimo minuto, por lo que no estamos seguros de si este dispositivo usa cifrado o no. Esto puede deberse a que el dispositivo utiliza un intervalo de transmisi\u00f3n lento. Confirma para agregar este dispositivo de todos modos, luego, la pr\u00f3xima vez que se reciba una transmisi\u00f3n, se te pedir\u00e1 que ingreses su clave de enlace si es necesario." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json index 1895097e7b1..41bf99207e2 100644 --- a/homeassistant/components/xiaomi_ble/translations/et.json +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -6,13 +6,22 @@ "decryption_failed": "Esitatud sidumisv\u00f5ti ei t\u00f6\u00f6tanud, sensori andmeid ei saanud dekr\u00fcpteerida. Palun kontrolli seda ja proovi uuesti.", "expected_24_characters": "Eeldati 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", "expected_32_characters": "Eeldati 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", - "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + "no_devices_found": "V\u00f6rgust seadmeid ei leitud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "decryption_failed": "Esitatud sidumisv\u00f5ti ei t\u00f6\u00f6tanud, sensori andmeid ei saanud dekr\u00fcpteerida. Palun kontrolli seda ja proovi uuesti.", + "expected_24_characters": "Eeldati 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", + "expected_32_characters": "Eeldati 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Kas seadistada {name}?" }, + "confirm_slow": { + "description": "Sellest seadmest ei ole viimasel minutil \u00fchtegi saadet olnud, nii et me ei ole kindlad, kas see seade kasutab kr\u00fcpteerimist v\u00f5i mitte. See v\u00f5ib olla tingitud sellest, et seade kasutab aeglast saateintervalli. Kinnita, et lisate selle seadme ikkagi, siis j\u00e4rgmisel korral, kui saade saabub, palutakse sisestada selle sidumisv\u00f5ti, kui seda on vaja." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Sidumisv\u00f5ti" @@ -25,6 +34,9 @@ }, "description": "Anduri edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks vajame 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." }, + "slow_confirm": { + "description": "Sellest seadmest ei ole viimasel minutil \u00fchtegi saadet olnud, nii et me ei ole kindlad, kas see seade kasutab kr\u00fcpteerimist v\u00f5i mitte. See v\u00f5ib olla tingitud sellest, et seade kasutab aeglast saateintervalli. Kinnita, et lisate selle seadme ikkagi, siis j\u00e4rgmisel korral, kui saade saabub, palutakse sisestada selle sidumisv\u00f5ti, kui seda on vaja." + }, "user": { "data": { "address": "Seade" diff --git a/homeassistant/components/xiaomi_ble/translations/tr.json b/homeassistant/components/xiaomi_ble/translations/tr.json new file mode 100644 index 00000000000..9d1b1931e79 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/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", + "decryption_failed": "Sa\u011flanan ba\u011flama anahtar\u0131 \u00e7al\u0131\u015fmad\u0131, sens\u00f6r verilerinin \u015fifresi \u00e7\u00f6z\u00fclemedi. L\u00fctfen kontrol edin ve tekrar deneyin.", + "expected_24_characters": "24 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor.", + "expected_32_characters": "32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor.", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "decryption_failed": "Sa\u011flanan ba\u011flama anahtar\u0131 \u00e7al\u0131\u015fmad\u0131, sens\u00f6r verilerinin \u015fifresi \u00e7\u00f6z\u00fclemedi. L\u00fctfen kontrol edin ve tekrar deneyin.", + "expected_24_characters": "24 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor.", + "expected_32_characters": "32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "confirm_slow": { + "description": "Son dakikada bu cihazdan bir yay\u0131n olmad\u0131\u011f\u0131 i\u00e7in bu cihaz\u0131n \u015fifreleme kullan\u0131p kullanmad\u0131\u011f\u0131ndan emin de\u011filiz. Bunun nedeni, cihaz\u0131n yava\u015f bir yay\u0131n aral\u0131\u011f\u0131 kullanmas\u0131 olabilir. Yine de bu cihaz\u0131 eklemeyi onaylay\u0131n, ard\u0131ndan bir sonraki yay\u0131n al\u0131nd\u0131\u011f\u0131nda gerekirse bindkey'i girmeniz istenecektir." + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sens\u00f6r taraf\u0131ndan yay\u0131nlanan sens\u00f6r verileri \u015fifrelenmi\u015ftir. \u015eifreyi \u00e7\u00f6zmek i\u00e7in 32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131na ihtiyac\u0131m\u0131z var." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sens\u00f6r taraf\u0131ndan yay\u0131nlanan sens\u00f6r verileri \u015fifrelenmi\u015ftir. \u015eifreyi \u00e7\u00f6zmek i\u00e7in 24 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131na ihtiyac\u0131m\u0131z var." + }, + "slow_confirm": { + "description": "Son dakikada bu cihazdan bir yay\u0131n olmad\u0131\u011f\u0131 i\u00e7in bu cihaz\u0131n \u015fifreleme kullan\u0131p kullanmad\u0131\u011f\u0131ndan emin de\u011filiz. Bunun nedeni, cihaz\u0131n yava\u015f bir yay\u0131n aral\u0131\u011f\u0131 kullanmas\u0131 olabilir. Yine de bu cihaz\u0131 eklemeyi onaylay\u0131n, ard\u0131ndan bir sonraki yay\u0131n al\u0131nd\u0131\u011f\u0131nda gerekirse bindkey'i girmeniz istenecektir." + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ 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 c1a3eae784b..6152da62e47 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se ha suministrado ning\u00fan host o token.", "not_xiaomi_miio": "El dispositivo no es (todav\u00eda) compatible con Xiaomi Miio.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" diff --git a/homeassistant/components/yale_smart_alarm/translations/es.json b/homeassistant/components/yale_smart_alarm/translations/es.json index e9c24aab7f2..59824bfdb3b 100644 --- a/homeassistant/components/yale_smart_alarm/translations/es.json +++ b/homeassistant/components/yale_smart_alarm/translations/es.json @@ -14,7 +14,7 @@ "area_id": "ID de \u00e1rea", "name": "Nombre", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { @@ -22,7 +22,7 @@ "area_id": "ID de \u00e1rea", "name": "Nombre", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/yalexs_ble/translations/ca.json b/homeassistant/components/yalexs_ble/translations/ca.json new file mode 100644 index 00000000000..5b4b014c4ac --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "no_unconfigured_devices": "No s'han trobat dispositius no configurats." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Vols configurar {name} per Bluetooth amb l'adre\u00e7a {address}?" + }, + "user": { + "data": { + "address": "Adre\u00e7a Bluetooth", + "key": "Clau fora de l\u00ednia (cadena hexadecimal de 32 bytes)" + }, + "description": "Consulta la documentaci\u00f3 a {docs_url} per saber com trobar la clau de fora de l\u00ednia." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/es.json b/homeassistant/components/yalexs_ble/translations/es.json new file mode 100644 index 00000000000..32863d750d2 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "no_devices_found": "No se encontraron dispositivos en la red", + "no_unconfigured_devices": "No se encontraron dispositivos no configurados." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_key_format": "La clave sin conexi\u00f3n debe ser una cadena hexadecimal de 32 bytes.", + "invalid_key_index": "La ranura de la clave sin conexi\u00f3n debe ser un n\u00famero entero entre 0 y 255.", + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "\u00bfQuieres configurar {name} a trav\u00e9s de Bluetooth con la direcci\u00f3n {address}?" + }, + "user": { + "data": { + "address": "Direcci\u00f3n Bluetooth", + "key": "Clave sin conexi\u00f3n (cadena hexadecimal de 32 bytes)", + "slot": "Ranura de clave sin conexi\u00f3n (entero entre 0 y 255)" + }, + "description": "Consulta la documentaci\u00f3n en {docs_url} para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/fr.json b/homeassistant/components/yalexs_ble/translations/fr.json new file mode 100644 index 00000000000..7ec2edfc86e --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adresse Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/hu.json b/homeassistant/components/yalexs_ble/translations/hu.json new file mode 100644 index 00000000000..44e14971cc7 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "no_unconfigured_devices": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan eszk\u00f6z." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_key_format": "Az offline kulcsnak 32 b\u00e1jtos hexadecim\u00e1lis karakterl\u00e1ncnak kell lennie.", + "invalid_key_index": "Az offline kulcshelynek 0 \u00e9s 255 k\u00f6z\u00f6tti eg\u00e9sz sz\u00e1mnak kell lennie.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "integration_discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {nane}, Bluetooth-on kereszt\u00fcl, {address} c\u00edmmel?" + }, + "user": { + "data": { + "address": "Bluetooth-c\u00edm", + "key": "Offline kulcs (32 b\u00e1jtos hexa karakterl\u00e1nc)", + "slot": "Offline kulcshely (eg\u00e9sz sz\u00e1m 0 \u00e9s 255 k\u00f6z\u00f6tt)" + }, + "description": "Tekintse meg a {docs_url} c\u00edmen tal\u00e1lhat\u00f3 dokument\u00e1ci\u00f3t, hogy hogyan szerezze meg az offline kulcsot." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/pt-BR.json b/homeassistant/components/yalexs_ble/translations/pt-BR.json new file mode 100644 index 00000000000..68a52803cbe --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado." + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_key_format": "A chave offline deve ser uma string hexadecimal de 32 bytes.", + "invalid_key_index": "O slot de chave offline deve ser um n\u00famero inteiro entre 0 e 255.", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Deseja configurar {name} por Bluetooth com o endere\u00e7o {address}?" + }, + "user": { + "data": { + "address": "Endere\u00e7o Bluetooth", + "key": "Chave offline (sequ\u00eancia hexadecimal de 32 bytes)", + "slot": "Slot de chave offline (inteiro entre 0 e 255)" + }, + "description": "Verifique a documenta\u00e7\u00e3o em {docs_url} para saber como encontrar a chave off-line." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/es.json b/homeassistant/components/yolink/translations/es.json index 71df6ac31e3..c4bcfa6e75b 100644 --- a/homeassistant/components/yolink/translations/es.json +++ b/homeassistant/components/yolink/translations/es.json @@ -2,23 +2,23 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en progreso", - "authorize_url_timeout": "Tiempo de espera para generar la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [compruebe la secci\u00f3n de ayuda]({docs_url})", - "oauth_error": "Se han recibido datos no v\u00e1lidos del token.", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { - "default": "Autentificado con \u00e9xito" + "default": "Autenticado correctamente" }, "step": { "pick_implementation": { - "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { - "description": "La integraci\u00f3n de yolink necesita volver a autenticar su cuenta", - "title": "Integraci\u00f3n de la reautenticaci\u00f3n" + "description": "La integraci\u00f3n yolink necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index d8162401f3d..9056708fd96 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -46,10 +46,13 @@ "title": "Opciones del panel de control de la alarma" }, "zha_options": { + "always_prefer_xy_color_mode": "Preferir siempre el modo de color XY", "consider_unavailable_battery": "Considere que los dispositivos alimentados por bater\u00eda no est\u00e1n disponibles despu\u00e9s de (segundos)", "consider_unavailable_mains": "Considere que los dispositivos alimentados por la red el\u00e9ctrica no est\u00e1n disponibles despu\u00e9s de (segundos)", "default_light_transition": "Tiempo de transici\u00f3n de la luz por defecto (segundos)", "enable_identify_on_join": "Activar el efecto de identificaci\u00f3n cuando los dispositivos se unen a la red", + "enhanced_light_transition": "Habilitar la transici\u00f3n mejorada de color de luz/temperatura desde un estado apagado", + "light_transitioning_flag": "Habilitar el control deslizante de brillo mejorado durante la transici\u00f3n de luz", "title": "Opciones globales" } }, diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 567ee5f359c..cbcc3adfa51 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -46,11 +46,13 @@ "title": "Valvekeskuse juhtpaneeli s\u00e4tted" }, "zha_options": { + "always_prefer_xy_color_mode": "Eelista alati XY v\u00e4rvire\u017eiimi", "consider_unavailable_battery": "Arvesta, et patareitoitega seadmed pole p\u00e4rast (sekundit) saadaval", "consider_unavailable_mains": "Arvesta, et v\u00f5rgutoitega seadmed pole p\u00e4rast (sekundit) saadaval", "default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)", "enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga", "enhanced_light_transition": "Luba t\u00e4iustatud valguse v\u00e4rvi/temperatuuri \u00fcleminek v\u00e4ljal\u00fclitatud olekust", + "light_transitioning_flag": "Luba t\u00e4iustatud heleduse liugur valguse \u00fclemineku ajal", "title": "\u00dcldised valikud" } }, diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index 39eb68d8d53..391b9315b48 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -46,10 +46,13 @@ "title": "Alarm Kontrol Paneli Se\u00e7enekleri" }, "zha_options": { + "always_prefer_xy_color_mode": "Her zaman XY renk modunu tercih edin", "consider_unavailable_battery": "Pille \u00e7al\u0131\u015fan ayg\u0131tlar\u0131n kullan\u0131lamad\u0131\u011f\u0131n\u0131 g\u00f6z \u00f6n\u00fcnde bulundurun (saniye)", "consider_unavailable_mains": "\u015eebekeyle \u00e7al\u0131\u015fan ayg\u0131tlar\u0131n kullan\u0131lamad\u0131\u011f\u0131n\u0131 g\u00f6z \u00f6n\u00fcnde bulundurun (saniye)", "default_light_transition": "Varsay\u0131lan \u0131\u015f\u0131k ge\u00e7i\u015f s\u00fcresi (saniye)", "enable_identify_on_join": "Cihazlar a\u011fa kat\u0131ld\u0131\u011f\u0131nda tan\u0131mlama efektini etkinle\u015ftir", + "enhanced_light_transition": "Kapal\u0131 durumdan geli\u015fmi\u015f \u0131\u015f\u0131k rengi/s\u0131cakl\u0131\u011f\u0131 ge\u00e7i\u015fini etkinle\u015ftirme", + "light_transitioning_flag": "I\u015f\u0131k ge\u00e7i\u015fi s\u0131ras\u0131nda geli\u015fmi\u015f parlakl\u0131k kayd\u0131r\u0131c\u0131s\u0131n\u0131 etkinle\u015ftirin", "title": "Genel Se\u00e7enekler" } }, diff --git a/homeassistant/components/zoneminder/translations/es.json b/homeassistant/components/zoneminder/translations/es.json index 6e12f9fd8fe..4425b179ccb 100644 --- a/homeassistant/components/zoneminder/translations/es.json +++ b/homeassistant/components/zoneminder/translations/es.json @@ -24,8 +24,8 @@ "path": "Ruta ZM", "path_zms": "Ruta ZMS", "ssl": "Utiliza un certificado SSL", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "title": "A\u00f1adir Servidor ZoneMinder" } diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 5d3efb0e7f4..3101d804b43 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -7,7 +7,7 @@ "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", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "discovery_requires_supervisor": "El descubrimiento requiere del supervisor.", "not_zwave_device": "El dispositivo descubierto no es un dispositivo Z-Wave." @@ -60,7 +60,7 @@ "description": "\u00bfQuieres configurar {name} con el complemento Z-Wave JS?" }, "zeroconf_confirm": { - "description": "\u00bfQuieres a\u00f1adir el servidor Z-Wave JS con ID {home_id} que se encuentra en {url} en Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el servidor Z-Wave JS con el ID de casa {home_id} que se encuentra en {url} a Home Assistant?", "title": "Servidor Z-Wave JS descubierto" } } From 8ecbb858524145dd7bef9be74a3d8fc9d8366c1c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 10 Aug 2022 21:05:09 -0400 Subject: [PATCH 288/903] Use generators for async_add_entities in Ambient Station (#76586) --- .../components/ambient_station/binary_sensor.py | 14 ++++++-------- homeassistant/components/ambient_station/sensor.py | 10 ++++------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 4380e1839f2..bcc2ae60404 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -333,14 +333,12 @@ async def async_setup_entry( ambient = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - AmbientWeatherBinarySensor( - ambient, mac_address, station[ATTR_NAME], description - ) - for mac_address, station in ambient.stations.items() - for description in BINARY_SENSOR_DESCRIPTIONS - if description.key in station[ATTR_LAST_DATA] - ] + AmbientWeatherBinarySensor( + ambient, mac_address, station[ATTR_NAME], description + ) + for mac_address, station in ambient.stations.items() + for description in BINARY_SENSOR_DESCRIPTIONS + if description.key in station[ATTR_LAST_DATA] ) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 4eae53b6a03..1a51e57bfa3 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -646,12 +646,10 @@ async def async_setup_entry( ambient = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - AmbientWeatherSensor(ambient, mac_address, station[ATTR_NAME], description) - for mac_address, station in ambient.stations.items() - for description in SENSOR_DESCRIPTIONS - if description.key in station[ATTR_LAST_DATA] - ] + AmbientWeatherSensor(ambient, mac_address, station[ATTR_NAME], description) + for mac_address, station in ambient.stations.items() + for description in SENSOR_DESCRIPTIONS + if description.key in station[ATTR_LAST_DATA] ) From dbfba3a951c505372594b4a7cf8f91ff6c84f3db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 03:24:12 +0200 Subject: [PATCH 289/903] Remove attribution from extra state attributes (#76580) --- .../components/brottsplatskartan/sensor.py | 14 ++++---------- homeassistant/components/fixer/sensor.py | 5 +++-- homeassistant/components/gitlab_ci/sensor.py | 11 +++-------- homeassistant/components/ring/camera.py | 4 ++-- homeassistant/components/rmvtransport/sensor.py | 5 +++-- homeassistant/components/speedtestdotnet/sensor.py | 4 ++-- 6 files changed, 17 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 1a321b3f173..171986ffd1c 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -10,12 +10,7 @@ import brottsplatskartan import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -90,6 +85,8 @@ def setup_platform( class BrottsplatskartanSensor(SensorEntity): """Representation of a Brottsplatskartan Sensor.""" + _attr_attribution = brottsplatskartan.ATTRIBUTION + def __init__(self, bpk, name): """Initialize the Brottsplatskartan sensor.""" self._brottsplatskartan = bpk @@ -109,8 +106,5 @@ class BrottsplatskartanSensor(SensorEntity): incident_type = incident.get("title_type") incident_counts[incident_type] += 1 - self._attr_extra_state_attributes = { - ATTR_ATTRIBUTION: brottsplatskartan.ATTRIBUTION - } - self._attr_extra_state_attributes.update(incident_counts) + self._attr_extra_state_attributes = incident_counts self._attr_native_value = len(incidents) diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index a4260efeda7..c05bd5a756f 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -9,7 +9,7 @@ from fixerio.exceptions import FixerioException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, CONF_TARGET +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_TARGET from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -61,6 +61,8 @@ def setup_platform( class ExchangeRateSensor(SensorEntity): """Representation of a Exchange sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, data, name, target): """Initialize the sensor.""" self.data = data @@ -88,7 +90,6 @@ class ExchangeRateSensor(SensorEntity): """Return the state attributes.""" if self.data.rate is not None: return { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_EXCHANGE_RATE: self.data.rate["rates"][self._target], ATTR_TARGET: self._target, } diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index e9dd4991d71..4206a1184ad 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -8,13 +8,7 @@ from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_NAME, - CONF_SCAN_INTERVAL, - CONF_TOKEN, - CONF_URL, -) +from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_URL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -78,6 +72,8 @@ def setup_platform( class GitLabSensor(SensorEntity): """Representation of a GitLab sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, gitlab_data, name): """Initialize the GitLab sensor.""" self._available = False @@ -111,7 +107,6 @@ class GitLabSensor(SensorEntity): def extra_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_BUILD_STATUS: self._state, ATTR_BUILD_STARTED: self._started_at, ATTR_BUILD_FINISHED: self._finished_at, diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index da2e447869a..72d8a51f01e 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -11,7 +11,6 @@ import requests from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -49,6 +48,8 @@ async def async_setup_entry( class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" + _attr_attribution = ATTRIBUTION + def __init__(self, config_entry_id, ffmpeg_manager, device): """Initialize a Ring Door Bell camera.""" super().__init__(config_entry_id, device) @@ -105,7 +106,6 @@ class RingCam(RingEntityMixin, Camera): def extra_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, "video_url": self._video_url, "last_video_id": self._last_video_id, } diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 92455329dea..3394c4ef4d3 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -13,7 +13,7 @@ from RMVtransport.rmvtransport import ( import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TIMEOUT, TIME_MINUTES +from homeassistant.const import CONF_NAME, CONF_TIMEOUT, TIME_MINUTES from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -116,6 +116,8 @@ async def async_setup_platform( class RMVDepartureSensor(SensorEntity): """Implementation of an RMV departure sensor.""" + _attr_attribution = ATTRIBUTION + def __init__( self, station, @@ -170,7 +172,6 @@ class RMVDepartureSensor(SensorEntity): "minutes": self.data.departures[0].get("minutes"), "departure_time": self.data.departures[0].get("departure_time"), "product": self.data.departures[0].get("product"), - ATTR_ATTRIBUTION: ATTRIBUTION, } except IndexError: return {} diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 44b018e1e19..e1ba0f560bb 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -5,7 +5,6 @@ from typing import Any, cast from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -49,6 +48,7 @@ class SpeedtestSensor( """Implementation of a speedtest.net sensor.""" entity_description: SpeedtestSensorEntityDescription + _attr_attribution = ATTRIBUTION _attr_has_entity_name = True _attr_icon = ICON @@ -62,7 +62,7 @@ class SpeedtestSensor( self.entity_description = description self._attr_unique_id = description.key self._state: StateType = None - self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attrs: dict[str, Any] = {} self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, name=DEFAULT_NAME, From 6e65cb4928301a8cf5ccc4e49d7ccdcd392f5f9f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 03:27:52 +0200 Subject: [PATCH 290/903] Fix Spotify deviding None value in current progress (#76581) --- homeassistant/components/spotify/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index ea41067e7e9..3db8ae7de08 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -179,7 +179,10 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @property def media_position(self) -> int | None: """Position of current playing media in seconds.""" - if not self._currently_playing: + if ( + not self._currently_playing + or self._currently_playing.get("progress_ms") is None + ): return None return self._currently_playing["progress_ms"] / 1000 From 828ea99c61c170bb096e6d64cb3fb6042d6ec920 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 15:34:48 -1000 Subject: [PATCH 291/903] Add door sensors to Yale Access Bluetooth (#76571) --- .coveragerc | 1 + .../components/yalexs_ble/__init__.py | 2 +- .../components/yalexs_ble/binary_sensor.py | 41 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/yalexs_ble/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 2616f4b6e16..4e88d171d57 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1484,6 +1484,7 @@ omit = homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* homeassistant/components/yalexs_ble/__init__.py + homeassistant/components/yalexs_ble/binary_sensor.py homeassistant/components/yalexs_ble/entity.py homeassistant/components/yalexs_ble/lock.py homeassistant/components/yalexs_ble/util.py diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 5a1cf461e5d..e38dba00ef8 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -16,7 +16,7 @@ from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN from .models import YaleXSBLEData from .util import async_find_existing_service_info, bluetooth_callback_matcher -PLATFORMS: list[Platform] = [Platform.LOCK] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/yalexs_ble/binary_sensor.py b/homeassistant/components/yalexs_ble/binary_sensor.py new file mode 100644 index 00000000000..3ee88dbaa5e --- /dev/null +++ b/homeassistant/components/yalexs_ble/binary_sensor.py @@ -0,0 +1,41 @@ +"""Support for yalexs ble binary sensors.""" +from __future__ import annotations + +from yalexs_ble import ConnectionInfo, DoorStatus, LockInfo, LockState + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import YALEXSBLEEntity +from .models import YaleXSBLEData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YALE XS binary sensors.""" + data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities([YaleXSBLEDoorSensor(data)]) + + +class YaleXSBLEDoorSensor(YALEXSBLEEntity, BinarySensorEntity): + """Yale XS BLE binary sensor.""" + + _attr_device_class = BinarySensorDeviceClass.DOOR + _attr_has_entity_name = True + + @callback + def _async_update_state( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Update the state.""" + self._attr_is_on = new_state.door == DoorStatus.OPENED + super()._async_update_state(new_state, lock_info, connection_info) From 5523b6fc9fdeea80bbd0dcd437fc3d4c9e5c9214 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 11 Aug 2022 02:35:05 +0100 Subject: [PATCH 292/903] Fix homekit_controller not noticing ip and port changes that zeroconf has found (#76570) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index cf3069e3b0d..4bd9a0b70f9 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.8"], + "requirements": ["aiohomekit==1.2.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 1927739987c..b793c357631 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.8 +aiohomekit==1.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72bf0741f25..5bb38db054b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.8 +aiohomekit==1.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http From 420084f6f17c8229b96fb69760d1daf5e39e2d3b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 03:35:13 +0200 Subject: [PATCH 293/903] Update sentry-sdk to 1.9.3 (#76573) --- 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 0cbe9eb636f..2360180d1cf 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.9.2"], + "requirements": ["sentry-sdk==1.9.3"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b793c357631..c5653e757b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2173,7 +2173,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.9.2 +sentry-sdk==1.9.3 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bb38db054b..67d73001634 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1470,7 +1470,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.9.2 +sentry-sdk==1.9.3 # homeassistant.components.sharkiq sharkiq==0.0.1 From 4c701294270995044f6b003dde6b67df3b424945 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 03:36:13 +0200 Subject: [PATCH 294/903] Improve state attributes of CityBikes (#76578) --- homeassistant/components/citybikes/sensor.py | 21 ++++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 5d833a113f2..418e206fc36 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -15,7 +15,6 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_ID, ATTR_LATITUDE, ATTR_LOCATION, @@ -275,6 +274,7 @@ class CityBikesNetwork: class CityBikesStation(SensorEntity): """CityBikes API Sensor.""" + _attr_attribution = CITYBIKES_ATTRIBUTION _attr_native_unit_of_measurement = "bikes" _attr_icon = "mdi:bike" @@ -292,15 +292,10 @@ class CityBikesStation(SensorEntity): break self._attr_name = station_data.get(ATTR_NAME) self._attr_native_value = station_data.get(ATTR_FREE_BIKES) - self._attr_extra_state_attributes = ( - { - ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION, - ATTR_UID: station_data.get(ATTR_EXTRA, {}).get(ATTR_UID), - ATTR_LATITUDE: station_data[ATTR_LATITUDE], - ATTR_LONGITUDE: station_data[ATTR_LONGITUDE], - ATTR_EMPTY_SLOTS: station_data[ATTR_EMPTY_SLOTS], - ATTR_TIMESTAMP: station_data[ATTR_TIMESTAMP], - } - if station_data - else {ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION} - ) + self._attr_extra_state_attributes = { + ATTR_UID: station_data.get(ATTR_EXTRA, {}).get(ATTR_UID), + ATTR_LATITUDE: station_data.get(ATTR_LATITUDE), + ATTR_LONGITUDE: station_data.get(ATTR_LONGITUDE), + ATTR_EMPTY_SLOTS: station_data.get(ATTR_EMPTY_SLOTS), + ATTR_TIMESTAMP: station_data.get(ATTR_TIMESTAMP), + } From bf899101ce756e2b62936ed3b5d7a410c61b7e3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 16:21:41 -1000 Subject: [PATCH 295/903] Update offline keys from august cloud for august branded yale locks (#76577) --- homeassistant/components/august/__init__.py | 34 ++++++ homeassistant/components/august/manifest.json | 3 +- .../components/yalexs_ble/__init__.py | 25 ++++- .../fixtures/get_lock.doorsense_init.json | 10 +- .../fixtures/get_lock.low_keypad_battery.json | 10 +- .../august/fixtures/get_lock.offline.json | 10 +- .../august/fixtures/get_lock.online.json | 10 +- .../fixtures/get_lock.online_with_keys.json | 100 ++++++++++++++++++ tests/components/august/mocks.py | 4 + tests/components/august/test_init.py | 26 +++++ 10 files changed, 194 insertions(+), 38 deletions(-) create mode 100644 tests/components/august/fixtures/get_lock.online_with_keys.json diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 81842f995e8..f4a0f57eb76 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -13,6 +13,7 @@ from yalexs.lock import Lock, LockDetail from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub +from homeassistant.components import yalexs_ble from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant, callback @@ -93,6 +94,26 @@ async def async_setup_august( return True +@callback +def _async_trigger_ble_lock_discovery( + hass: HomeAssistant, locks_with_offline_keys: list[LockDetail] +): + """Update keys for the yalexs-ble integration if available.""" + for lock_detail in locks_with_offline_keys: + yalexs_ble.async_discovery( + hass, + yalexs_ble.YaleXSBLEDiscovery( + { + "name": lock_detail.device_name, + "address": lock_detail.mac_address, + "serial": lock_detail.serial_number, + "key": lock_detail.offline_key, + "slot": lock_detail.offline_slot, + } + ), + ) + + class AugustData(AugustSubscriberMixin): """August data object.""" @@ -133,6 +154,19 @@ class AugustData(AugustSubscriberMixin): # detail as we cannot determine if they are usable. # This also allows us to avoid checking for # detail being None all over the place + + # Currently we know how to feed data to yalexe_ble + # but we do not know how to send it to homekit_controller + # yet + _async_trigger_ble_lock_discovery( + self._hass, + [ + lock_detail + for lock_detail in self._device_detail_by_id.values() + if isinstance(lock_detail, LockDetail) and lock_detail.offline_key + ], + ) + self._remove_inoperative_locks() self._remove_inoperative_doorbells() diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 418fa6920ad..c688aa1a775 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -24,5 +24,6 @@ ], "config_flow": true, "iot_class": "cloud_push", - "loggers": ["pubnub", "yalexs"] + "loggers": ["pubnub", "yalexs"], + "after_dependencies": ["yalexs_ble"] } diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index e38dba00ef8..265b10a502b 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -2,12 +2,13 @@ from __future__ import annotations import asyncio +from typing import TypedDict import async_timeout from yalexs_ble import PushLock, local_name_is_unique from homeassistant.components import bluetooth -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -19,6 +20,28 @@ from .util import async_find_existing_service_info, bluetooth_callback_matcher PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK] +class YaleXSBLEDiscovery(TypedDict): + """A validated discovery of a Yale XS BLE device.""" + + name: str + address: str + serial: str + key: str + slot: int + + +@callback +def async_discovery(hass: HomeAssistant, discovery: YaleXSBLEDiscovery) -> None: + """Update keys for the yalexs-ble integration if available.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + "yalexs_ble", + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data=discovery, + ) + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yale Access Bluetooth from a config entry.""" local_name = entry.data[CONF_LOCAL_NAME] diff --git a/tests/components/august/fixtures/get_lock.doorsense_init.json b/tests/components/august/fixtures/get_lock.doorsense_init.json index d85ca3b153f..1132cc61a8d 100644 --- a/tests/components/august/fixtures/get_lock.doorsense_init.json +++ b/tests/components/august/fixtures/get_lock.doorsense_init.json @@ -40,15 +40,7 @@ }, "OfflineKeys": { "created": [], - "loaded": [ - { - "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", - "slot": 1, - "key": "kkk01d4300c1dcxxx1c330f794941111", - "created": "2017-12-10T03:12:09.215Z", - "loaded": "2017-12-10T03:12:54.391Z" - } - ], + "loaded": [], "deleted": [], "loadedhk": [ { diff --git a/tests/components/august/fixtures/get_lock.low_keypad_battery.json b/tests/components/august/fixtures/get_lock.low_keypad_battery.json index b10c3f2600f..08bdfaa76ed 100644 --- a/tests/components/august/fixtures/get_lock.low_keypad_battery.json +++ b/tests/components/august/fixtures/get_lock.low_keypad_battery.json @@ -40,15 +40,7 @@ }, "OfflineKeys": { "created": [], - "loaded": [ - { - "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", - "slot": 1, - "key": "kkk01d4300c1dcxxx1c330f794941111", - "created": "2017-12-10T03:12:09.215Z", - "loaded": "2017-12-10T03:12:54.391Z" - } - ], + "loaded": [], "deleted": [], "loadedhk": [ { diff --git a/tests/components/august/fixtures/get_lock.offline.json b/tests/components/august/fixtures/get_lock.offline.json index 753a1081918..50d3d345ef8 100644 --- a/tests/components/august/fixtures/get_lock.offline.json +++ b/tests/components/august/fixtures/get_lock.offline.json @@ -19,15 +19,7 @@ } ], "deleted": [], - "loaded": [ - { - "UserID": "userid", - "created": "2000-00-00T00:00:00.447Z", - "key": "key", - "loaded": "2000-00-00T00:00:00.447Z", - "slot": 1 - } - ] + "loaded": [] }, "SerialNumber": "ABC", "Type": 3, diff --git a/tests/components/august/fixtures/get_lock.online.json b/tests/components/august/fixtures/get_lock.online.json index 7fa12fa8bcb..7abadeef4b6 100644 --- a/tests/components/august/fixtures/get_lock.online.json +++ b/tests/components/august/fixtures/get_lock.online.json @@ -40,15 +40,7 @@ }, "OfflineKeys": { "created": [], - "loaded": [ - { - "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", - "slot": 1, - "key": "kkk01d4300c1dcxxx1c330f794941111", - "created": "2017-12-10T03:12:09.215Z", - "loaded": "2017-12-10T03:12:54.391Z" - } - ], + "loaded": [], "deleted": [], "loadedhk": [ { diff --git a/tests/components/august/fixtures/get_lock.online_with_keys.json b/tests/components/august/fixtures/get_lock.online_with_keys.json new file mode 100644 index 00000000000..7fa12fa8bcb --- /dev/null +++ b/tests/components/august/fixtures/get_lock.online_with_keys.json @@ -0,0 +1,100 @@ +{ + "LockName": "Front Door Lock", + "Type": 2, + "Created": "2017-12-10T03:12:09.210Z", + "Updated": "2017-12-10T03:12:09.210Z", + "LockID": "A6697750D607098BAE8D6BAA11EF8063", + "HouseID": "000000000000", + "HouseName": "My House", + "Calibrated": false, + "skuNumber": "AUG-SL02-M02-S02", + "timeZone": "America/Vancouver", + "battery": 0.88, + "SerialNumber": "X2FSW05DGA", + "LockStatus": { + "status": "locked", + "doorState": "closed", + "dateTime": "2017-12-10T04:48:30.272Z", + "isLockStatusChanged": true, + "valid": true + }, + "currentFirmwareVersion": "109717e9-3.0.44-3.0.30", + "homeKitEnabled": false, + "zWaveEnabled": false, + "isGalileo": false, + "Bridge": { + "_id": "aaacab87f7efxa0015884999", + "mfgBridgeID": "AAGPP102XX", + "deviceModel": "august-doorbell", + "firmwareVersion": "2.3.0-RC153+201711151527", + "operative": true + }, + "keypad": { + "_id": "5bc65c24e6ef2a263e1450a8", + "serialNumber": "K1GXB0054Z", + "lockID": "92412D1B44004595B5DEB134E151A8D3", + "currentFirmwareVersion": "2.27.0", + "battery": {}, + "batteryLevel": "Medium", + "batteryRaw": 170 + }, + "OfflineKeys": { + "created": [], + "loaded": [ + { + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "slot": 1, + "key": "kkk01d4300c1dcxxx1c330f794941111", + "created": "2017-12-10T03:12:09.215Z", + "loaded": "2017-12-10T03:12:54.391Z" + } + ], + "deleted": [], + "loadedhk": [ + { + "key": "kkk01d4300c1dcxxx1c330f794941222", + "slot": 256, + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "created": "2017-12-10T03:12:09.218Z", + "loaded": "2017-12-10T03:12:55.563Z" + } + ] + }, + "parametersToSet": {}, + "users": { + "cccca94e-373e-aaaa-bbbb-333396827777": { + "UserType": "superuser", + "FirstName": "Foo", + "LastName": "Bar", + "identifiers": ["email:foo@bar.com", "phone:+177777777777"], + "imageInfo": { + "original": { + "width": 948, + "height": 949, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + }, + "thumbnail": { + "width": 128, + "height": 128, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + } + } + } + }, + "pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333", + "ruleHash": {}, + "cameras": [], + "geofenceLimits": { + "ios": { + "debounceInterval": 90, + "gpsAccuracyMultiplier": 2.5, + "maximumGeofence": 5000, + "minimumGeofence": 100, + "minGPSAccuracyRequired": 80 + } + } +} diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 932065f37da..2fa59fe964c 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -302,6 +302,10 @@ async def _mock_operative_august_lock_detail(hass): return await _mock_lock_from_fixture(hass, "get_lock.online.json") +async def _mock_lock_with_offline_key(hass): + return await _mock_lock_from_fixture(hass, "get_lock.online_with_keys.json") + + async def _mock_inoperative_august_lock_detail(hass): return await _mock_lock_from_fixture(hass, "get_lock.offline.json") diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 56113832d23..ab3269e9ac8 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -29,6 +29,7 @@ from tests.components.august.mocks import ( _mock_doorsense_missing_august_lock_detail, _mock_get_config, _mock_inoperative_august_lock_detail, + _mock_lock_with_offline_key, _mock_operative_august_lock_detail, ) @@ -323,6 +324,31 @@ async def test_load_unload(hass): await hass.async_block_till_done() +async def test_load_triggers_ble_discovery(hass): + """Test that loading a lock that supports offline ble operation passes the keys to yalexe_ble.""" + + august_lock_with_key = await _mock_lock_with_offline_key(hass) + august_lock_without_key = await _mock_operative_august_lock_detail(hass) + + with patch( + "homeassistant.components.august.yalexs_ble.async_discovery" + ) as mock_discovery: + config_entry = await _create_august_with_devices( + hass, [august_lock_with_key, august_lock_without_key] + ) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + assert len(mock_discovery.mock_calls) == 1 + assert mock_discovery.mock_calls[0][1][1] == { + "name": "Front Door Lock", + "address": None, + "serial": "X2FSW05DGA", + "key": "kkk01d4300c1dcxxx1c330f794941111", + "slot": 1, + } + + async def remove_device(ws_client, device_id, config_entry_id): """Remove config entry from a device.""" await ws_client.send_json( From 8f0ade7a68969a281a4cd2aa81d68d2dc8f8438f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 21:34:23 -1000 Subject: [PATCH 296/903] Bump yalexs-ble to 1.1.3 (#76595) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 8f7838792ff..0bf861e4d44 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.1.2"], + "requirements": ["yalexs-ble==1.1.3"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index c5653e757b0..2cccec261c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.1.2 +yalexs-ble==1.1.3 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67d73001634..7222a1778bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1695,7 +1695,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.1.2 +yalexs-ble==1.1.3 # homeassistant.components.august yalexs==1.2.1 From 7fc2a73c8806ce13d970c9f155a8c43bfba08590 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 09:50:35 +0200 Subject: [PATCH 297/903] Improve type hints in harmony (#76445) --- homeassistant/components/harmony/select.py | 27 ++++++++++--------- .../components/harmony/subscriber.py | 20 +++++++------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/harmony/select.py b/homeassistant/components/harmony/select.py index e19f6c3bc9c..3728c4b17a4 100644 --- a/homeassistant/components/harmony/select.py +++ b/homeassistant/components/harmony/select.py @@ -40,7 +40,7 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity): self._attr_name = name @property - def icon(self): + def icon(self) -> str: """Return a representative icon.""" if not self.available or self.current_option == ACTIVITY_POWER_OFF: return "mdi:remote-tv-off" @@ -52,7 +52,7 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity): return [ACTIVITY_POWER_OFF] + sorted(self._data.activity_names) @property - def current_option(self): + def current_option(self) -> str | None: """Return the current activity.""" _, activity_name = self._data.current_activity return activity_name @@ -61,18 +61,19 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity): """Change the current activity.""" await self._data.async_start_activity(option) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" - - callbacks = { - "connected": self.async_got_connected, - "disconnected": self.async_got_disconnected, - "activity_starting": self._async_activity_update, - "activity_started": self._async_activity_update, - "config_updated": None, - } - - self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) + self.async_on_remove( + self._data.async_subscribe( + HarmonyCallback( + connected=self.async_got_connected, + disconnected=self.async_got_disconnected, + activity_starting=self._async_activity_update, + activity_started=self._async_activity_update, + config_updated=None, + ) + ) + ) @callback def _async_activity_update(self, activity_info: tuple): diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index 2731d8555f0..efb46f5c6e6 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -7,12 +7,12 @@ import logging # https://bugs.python.org/issue42965 from typing import Any, Callable, NamedTuple, Optional -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback _LOGGER = logging.getLogger(__name__) -NoParamCallback = Optional[Callable[[object], Any]] -ActivityCallback = Optional[Callable[[object, tuple], Any]] +NoParamCallback = Optional[Callable[[], Any]] +ActivityCallback = Optional[Callable[[tuple], Any]] class HarmonyCallback(NamedTuple): @@ -28,11 +28,11 @@ class HarmonyCallback(NamedTuple): class HarmonySubscriberMixin: """Base implementation for a subscriber.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize an subscriber.""" super().__init__() self._hass = hass - self._subscriptions = [] + self._subscriptions: list[HarmonyCallback] = [] self._activity_lock = asyncio.Lock() async def async_lock_start_activity(self): @@ -40,23 +40,23 @@ class HarmonySubscriberMixin: await self._activity_lock.acquire() @callback - def async_unlock_start_activity(self): + def async_unlock_start_activity(self) -> None: """Release the lock.""" if self._activity_lock.locked(): self._activity_lock.release() @callback - def async_subscribe(self, update_callbacks: HarmonyCallback) -> Callable: + def async_subscribe(self, update_callbacks: HarmonyCallback) -> CALLBACK_TYPE: """Add a callback subscriber.""" self._subscriptions.append(update_callbacks) - def _unsubscribe(): + def _unsubscribe() -> None: self.async_unsubscribe(update_callbacks) return _unsubscribe @callback - def async_unsubscribe(self, update_callback: HarmonyCallback): + def async_unsubscribe(self, update_callback: HarmonyCallback) -> None: """Remove a callback subscriber.""" self._subscriptions.remove(update_callback) @@ -85,7 +85,7 @@ class HarmonySubscriberMixin: self.async_unlock_start_activity() self._call_callbacks("activity_started", activity_info) - def _call_callbacks(self, callback_func_name: str, argument: tuple = None): + def _call_callbacks(self, callback_func_name: str, argument: tuple = None) -> None: for subscription in self._subscriptions: current_callback = getattr(subscription, callback_func_name) if current_callback: From 6ad27089468e551ce4fb33021684c5366ce1d4f9 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 11 Aug 2022 09:10:18 +0100 Subject: [PATCH 298/903] Support polling the MiFlora battery (#76342) --- .../components/xiaomi_ble/__init__.py | 18 +++++-- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/__init__.py | 34 ++++++++++---- tests/components/xiaomi_ble/conftest.py | 47 +++++++++++++++++++ tests/components/xiaomi_ble/test_sensor.py | 15 ++++-- 7 files changed, 101 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index e3e30e0c79e..031490d6d68 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -11,8 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfoBleak, ) -from homeassistant.components.bluetooth.passive_update_processor import ( - PassiveBluetoothProcessorCoordinator, +from homeassistant.components.bluetooth.active_update_coordinator import ( + ActiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -56,9 +56,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: kwargs["bindkey"] = bytes.fromhex(bindkey) data = XiaomiBluetoothDeviceData(**kwargs) + def _needs_poll( + service_info: BluetoothServiceInfoBleak, last_poll: float | None + ) -> bool: + return data.poll_needed(service_info, last_poll) + + async def _async_poll(service_info: BluetoothServiceInfoBleak): + # BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it + # directly to the Xiaomi code + return await data.async_poll(service_info.device) + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( + ] = ActiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, @@ -66,6 +76,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=lambda service_info: process_service_info( hass, entry, data, service_info ), + needs_poll_method=_needs_poll, + poll_method=_async_poll, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index a901439b2c9..cdcac07b5c9 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.4"], + "requirements": ["xiaomi-ble==0.8.1"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 2cccec261c5..21fa1488f48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.4 +xiaomi-ble==0.8.1 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7222a1778bd..35f845c8046 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.4 +xiaomi-ble==0.8.1 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 1dd1eeed65a..c4424236082 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -1,21 +1,26 @@ """Tests for the SensorPush integration.""" +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfo( +NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfoBleak( name="Not it", address="00:00:00:00:00:00", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-63, manufacturer_data={3234: b"\x00\x01"}, service_data={}, service_uuids=[], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfo( +LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfoBleak( name="LYWSDCGQ", address="58:2D:34:35:93:21", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-63, manufacturer_data={}, service_data={ @@ -23,11 +28,13 @@ LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( +MMC_T201_1_SERVICE_INFO = BluetoothServiceInfoBleak( name="MMC_T201_1", address="00:81:F9:DD:6F:C1", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-56, manufacturer_data={}, service_data={ @@ -35,11 +42,13 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfo( +JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfoBleak( name="JTYJGD03MI", address="54:EF:44:E3:9C:BC", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-56, manufacturer_data={}, service_data={ @@ -47,11 +56,13 @@ JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( +YLKG07YL_SERVICE_INFO = BluetoothServiceInfoBleak( name="YLKG07YL", address="F8:24:41:C5:98:8B", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-56, manufacturer_data={}, service_data={ @@ -59,11 +70,13 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfo( +MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfoBleak( name="LYWSD02MMC", address="A4:C1:38:56:53:84", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-56, manufacturer_data={}, service_data={ @@ -71,14 +84,16 @@ MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: +def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBleak: """Make a dummy advertisement.""" - return BluetoothServiceInfo( + return BluetoothServiceInfoBleak( name="Test Device", address=address, + device=BLEDevice(address, None), rssi=-56, manufacturer_data={}, service_data={ @@ -86,4 +101,5 @@ def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Test Device"), ) diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py index 9fce8e85ea8..2997943468f 100644 --- a/tests/components/xiaomi_ble/conftest.py +++ b/tests/components/xiaomi_ble/conftest.py @@ -1,8 +1,55 @@ """Session fixtures.""" +from unittest import mock + import pytest +class MockServices: + """Mock GATTServicesCollection.""" + + def get_characteristic(self, key: str) -> str: + """Mock GATTServicesCollection.get_characteristic.""" + return key + + +class MockBleakClient: + """Mock BleakClient.""" + + services = MockServices() + + def __init__(self, *args, **kwargs): + """Mock BleakClient.""" + pass + + async def __aenter__(self, *args, **kwargs): + """Mock BleakClient.__aenter__.""" + return self + + async def __aexit__(self, *args, **kwargs): + """Mock BleakClient.__aexit__.""" + pass + + async def connect(self, *args, **kwargs): + """Mock BleakClient.connect.""" + pass + + async def disconnect(self, *args, **kwargs): + """Mock BleakClient.disconnect.""" + pass + + +class MockBleakClientBattery5(MockBleakClient): + """Mock BleakClient that returns a battery level of 5.""" + + async def read_gatt_char(self, *args, **kwargs) -> bytes: + """Mock BleakClient.read_gatt_char.""" + return b"\x05\x001.2.3" + + @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth): """Auto mock bluetooth.""" + + with mock.patch("xiaomi_ble.parser.BleakClient", MockBleakClientBattery5): + yield diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 011c6daecae..063e4a22c2d 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -78,7 +78,7 @@ async def test_xiaomi_formaldeyhde(hass): # obj type is 0x1010, payload len is 0x2 and payload is 0xf400 saved_callback( make_advertisement( - "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x10\x10\x02\xf4\x00" + "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x10\x10\x02\xf4\x00" ), BluetoothChange.ADVERTISEMENT, ) @@ -125,7 +125,7 @@ async def test_xiaomi_consumable(hass): # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 saved_callback( make_advertisement( - "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x13\x10\x02\x60\x00" + "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x13\x10\x02\x60\x00" ), BluetoothChange.ADVERTISEMENT, ) @@ -172,7 +172,7 @@ async def test_xiaomi_battery_voltage(hass): # obj type is 0x0a10, payload len is 0x2 and payload is 0x6400 saved_callback( make_advertisement( - "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x0a\x10\x02\x64\x00" + "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x0a\x10\x02\x64\x00" ), BluetoothChange.ADVERTISEMENT, ) @@ -246,7 +246,7 @@ async def test_xiaomi_HHCCJCY01(hass): BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 illum_sensor = hass.states.get("sensor.test_device_illuminance") illum_sensor_attr = illum_sensor.attributes @@ -276,6 +276,13 @@ async def test_xiaomi_HHCCJCY01(hass): assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + batt_sensor = hass.states.get("sensor.test_device_battery") + batt_sensor_attribtes = batt_sensor.attributes + assert batt_sensor.state == "5" + assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert batt_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert batt_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() From a30dfd9f3e117d4834eb6fe7f09b66b97d01c80e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 10:34:58 +0200 Subject: [PATCH 299/903] Add class attribute for capability attributes in entity base class (#76599) --- homeassistant/helpers/entity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index cb71cfd9edf..2f2588367b6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -274,6 +274,7 @@ class Entity(ABC): _attr_assumed_state: bool = False _attr_attribution: str | None = None _attr_available: bool = True + _attr_capability_attributes: Mapping[str, Any] | None = None _attr_context_recent_time: timedelta = timedelta(seconds=5) _attr_device_class: str | None _attr_device_info: DeviceInfo | None = None @@ -337,7 +338,7 @@ class Entity(ABC): Implemented by component base class. Convention for attribute names is lowercase snake_case. """ - return None + return self._attr_capability_attributes @property def state_attributes(self) -> dict[str, Any] | None: From 9919dd500dda5d5248dd42bac3c4c5ccd90d8be2 Mon Sep 17 00:00:00 2001 From: Antonino Piazza Date: Thu, 11 Aug 2022 11:03:12 +0200 Subject: [PATCH 300/903] Improve code quality in huawei_lte (#76583) Co-authored-by: Martin Hjelmare --- .../components/huawei_lte/__init__.py | 9 +- homeassistant/components/huawei_lte/switch.py | 4 +- tests/components/huawei_lte/test_switches.py | 182 ++++++++++-------- 3 files changed, 107 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index bace633f128..565286c4505 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -279,11 +279,12 @@ class Router: self._get_data( KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, lambda: next( - filter( - lambda ssid: ssid.get("wifiisguestnetwork") == "1", - self.client.wlan.multi_basic_settings() + ( + ssid + for ssid in self.client.wlan.multi_basic_settings() .get("Ssids", {}) - .get("Ssid", []), + .get("Ssid", []) + if isinstance(ssid, dict) and ssid.get("wifiisguestnetwork") == "1" ), {}, ), diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 78579d62698..261b77987cf 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -37,7 +37,7 @@ async def async_setup_entry( if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): switches.append(HuaweiLteMobileDataSwitch(router)) - if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH).get("WifiEnable"): + if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, {}).get("WifiEnable"): switches.append(HuaweiLteWifiGuestNetworkSwitch(router)) async_add_entities(switches, True) @@ -151,6 +151,6 @@ class HuaweiLteWifiGuestNetworkSwitch(HuaweiLteBaseSwitch): return "mdi:wifi" if self.is_on else "mdi:wifi-off" @property - def extra_state_attributes(self) -> dict[str, str]: + def extra_state_attributes(self) -> dict[str, str | None]: """Return the state attributes.""" return {"ssid": self.router.data[self.key].get("WifiSsid")} diff --git a/tests/components/huawei_lte/test_switches.py b/tests/components/huawei_lte/test_switches.py index 5bafed27e70..4b0b81a86cd 100644 --- a/tests/components/huawei_lte/test_switches.py +++ b/tests/components/huawei_lte/test_switches.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock, patch from huawei_lte_api.enums.cradle import ConnectionStatusEnum -from pytest import fixture from homeassistant.components.huawei_lte.const import DOMAIN from homeassistant.components.switch import ( @@ -20,94 +19,70 @@ from tests.common import MockConfigEntry SWITCH_WIFI_GUEST_NETWORK = "switch.lte_wifi_guest_network" -@fixture +def magic_client(multi_basic_settings_value: dict) -> MagicMock: + """Mock huawei_lte.Client.""" + information = MagicMock(return_value={"SerialNumber": "test-serial-number"}) + check_notifications = MagicMock(return_value={"SmsStorageFull": 0}) + status = MagicMock( + return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} + ) + multi_basic_settings = MagicMock(return_value=multi_basic_settings_value) + wifi_feature_switch = MagicMock(return_value={"wifi24g_switch_enable": 1}) + device = MagicMock(information=information) + monitoring = MagicMock(check_notifications=check_notifications, status=status) + wlan = MagicMock( + multi_basic_settings=multi_basic_settings, + wifi_feature_switch=wifi_feature_switch, + ) + return MagicMock(device=device, monitoring=monitoring, wlan=wlan) + + @patch("homeassistant.components.huawei_lte.Connection", MagicMock()) -@patch( - "homeassistant.components.huawei_lte.Client", - return_value=MagicMock( - device=MagicMock( - information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) - ), - monitoring=MagicMock( - check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), - status=MagicMock( - return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} - ), - ), - wlan=MagicMock( - multi_basic_settings=MagicMock( - return_value={ - "Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]} - } - ), - wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), - ), - ), -) -async def setup_component_with_wifi_guest_network( - client: MagicMock, hass: HomeAssistant -) -> None: - """Initialize huawei_lte components.""" - assert client - huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) - huawei_lte.add_to_hass(hass) - assert await hass.config_entries.async_setup(huawei_lte.entry_id) - await hass.async_block_till_done() - - -@fixture -@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) -@patch( - "homeassistant.components.huawei_lte.Client", - return_value=MagicMock( - device=MagicMock( - information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) - ), - monitoring=MagicMock( - check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), - status=MagicMock( - return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} - ), - ), - wlan=MagicMock( - multi_basic_settings=MagicMock(return_value={}), - wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), - ), - ), -) -async def setup_component_without_wifi_guest_network( - client: MagicMock, hass: HomeAssistant -) -> None: - """Initialize huawei_lte components.""" - assert client - huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) - huawei_lte.add_to_hass(hass) - assert await hass.config_entries.async_setup(huawei_lte.entry_id) - await hass.async_block_till_done() - - -def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_not_present( +@patch("homeassistant.components.huawei_lte.Client", return_value=magic_client({})) +async def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_not_present( + client, hass: HomeAssistant, - setup_component_without_wifi_guest_network, ) -> None: """Test switch wifi guest network config entry when network is not present.""" + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() entity_registry: EntityRegistry = er.async_get(hass) assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) -def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_present( +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=magic_client( + {"Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]}} + ), +) +async def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_present( + client, hass: HomeAssistant, - setup_component_with_wifi_guest_network, ) -> None: """Test switch wifi guest network config entry when network is present.""" + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() entity_registry: EntityRegistry = er.async_get(hass) assert entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) -async def test_turn_on_switch_wifi_guest_network( - hass: HomeAssistant, setup_component_with_wifi_guest_network -) -> None: +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch("homeassistant.components.huawei_lte.Client") +async def test_turn_on_switch_wifi_guest_network(client, hass: HomeAssistant) -> None: """Test switch wifi guest network turn on method.""" + client.return_value = magic_client( + {"Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]}} + ) + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -116,15 +91,20 @@ async def test_turn_on_switch_wifi_guest_network( ) await hass.async_block_till_done() assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_ON) - hass.data[DOMAIN].routers[ - "test-serial-number" - ].client.wlan.wifi_guest_network_switch.assert_called_once_with(True) + client.return_value.wlan.wifi_guest_network_switch.assert_called_once_with(True) -async def test_turn_off_switch_wifi_guest_network( - hass: HomeAssistant, setup_component_with_wifi_guest_network -) -> None: +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch("homeassistant.components.huawei_lte.Client") +async def test_turn_off_switch_wifi_guest_network(client, hass: HomeAssistant) -> None: """Test switch wifi guest network turn off method.""" + client.return_value = magic_client( + {"Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "1"}]}} + ) + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -133,6 +113,44 @@ async def test_turn_off_switch_wifi_guest_network( ) await hass.async_block_till_done() assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_OFF) - hass.data[DOMAIN].routers[ - "test-serial-number" - ].client.wlan.wifi_guest_network_switch.assert_called_with(False) + client.return_value.wlan.wifi_guest_network_switch.assert_called_with(False) + + +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=magic_client({"Ssids": {"Ssid": "str"}}), +) +async def test_huawei_lte_wifi_guest_network_config_entry_when_ssid_is_str( + client, hass: HomeAssistant +): + """Test switch wifi guest network config entry when ssid is a str. + + Issue #76244. Huawai models: H312-371, E5372 and E8372. + """ + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + entity_registry: EntityRegistry = er.async_get(hass) + assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) + + +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=magic_client({"Ssids": {"Ssid": None}}), +) +async def test_huawei_lte_wifi_guest_network_config_entry_when_ssid_is_none( + client, hass: HomeAssistant +): + """Test switch wifi guest network config entry when ssid is a None. + + Issue #76244. + """ + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + entity_registry: EntityRegistry = er.async_get(hass) + assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) From b5787fe8fa83c3ce2b1324542f6e14a0441ecc88 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 23:05:58 -1000 Subject: [PATCH 301/903] Add RSSI sensors to Yale Access Bluetooth (#76590) --- .coveragerc | 1 + .../components/yalexs_ble/__init__.py | 2 +- homeassistant/components/yalexs_ble/sensor.py | 44 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/yalexs_ble/sensor.py diff --git a/.coveragerc b/.coveragerc index 4e88d171d57..e326f5f44c8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1487,6 +1487,7 @@ omit = homeassistant/components/yalexs_ble/binary_sensor.py homeassistant/components/yalexs_ble/entity.py homeassistant/components/yalexs_ble/lock.py + homeassistant/components/yalexs_ble/sensor.py homeassistant/components/yalexs_ble/util.py homeassistant/components/yale_smart_alarm/__init__.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 265b10a502b..3b9481b6982 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -17,7 +17,7 @@ from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN from .models import YaleXSBLEData from .util import async_find_existing_service_info, bluetooth_callback_matcher -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR] class YaleXSBLEDiscovery(TypedDict): diff --git a/homeassistant/components/yalexs_ble/sensor.py b/homeassistant/components/yalexs_ble/sensor.py new file mode 100644 index 00000000000..19fee1924e6 --- /dev/null +++ b/homeassistant/components/yalexs_ble/sensor.py @@ -0,0 +1,44 @@ +"""Support for yalexs ble sensors.""" +from __future__ import annotations + +from yalexs_ble import ConnectionInfo, LockInfo, LockState + +from homeassistant import config_entries +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import YALEXSBLEEntity +from .models import YaleXSBLEData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YALE XS Bluetooth sensors.""" + data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities([YaleXSBLERSSISensor(data)]) + + +class YaleXSBLERSSISensor(YALEXSBLEEntity, SensorEntity): + """Yale XS Bluetooth RSSI sensor.""" + + _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_entity_registry_enabled_default = False + _attr_has_entity_name = True + _attr_name = "Signal strength" + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + + @callback + def _async_update_state( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Update the state.""" + self._attr_native_value = connection_info.rssi + super()._async_update_state(new_state, lock_info, connection_info) From eb0b6f3d75ee97dd50e33be84fc99f30d0c31413 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 23:08:03 -1000 Subject: [PATCH 302/903] Fix Govee 5181 with old firmware (#76600) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index e8df2af3abb..7c65fe35b5d 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -23,6 +23,10 @@ "manufacturer_id": 818, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "manufacturer_id": 59970, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" @@ -32,7 +36,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.14.0"], + "requirements": ["govee-ble==0.14.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 7f0696f1a76..2575be9aa69 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -51,6 +51,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 818, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 59970, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, { "domain": "govee_ble", "manufacturer_id": 14474, diff --git a/requirements_all.txt b/requirements_all.txt index 21fa1488f48..3054c425ecc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -757,7 +757,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.0 +govee-ble==0.14.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35f845c8046..8a16718a237 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.0 +govee-ble==0.14.1 # homeassistant.components.gree greeclimate==1.3.0 From 66b742f110025013e60ca8cac7aeb3247bac8f47 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 12:41:24 +0200 Subject: [PATCH 303/903] Improve type hints in yeelight lights (#76018) Co-authored-by: Franck Nijhof --- homeassistant/components/yeelight/light.py | 94 ++++++++++------------ 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 9bb0ed21989..af1c3de74e7 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -63,6 +63,7 @@ from .const import ( MODELS_WITH_DELAYED_ON_TRANSITION, POWER_STATE_CHANGE_TIME, ) +from .device import YeelightDevice from .entity import YeelightEntity _LOGGER = logging.getLogger(__name__) @@ -218,7 +219,7 @@ def _transitions_config_parser(transitions): @callback -def _parse_custom_effects(effects_config): +def _parse_custom_effects(effects_config) -> dict[str, dict[str, Any]]: effects = {} for config in effects_config: params = config[CONF_FLOW_PARAMS] @@ -414,18 +415,23 @@ class YeelightGenericLight(YeelightEntity, LightEntity): ) _attr_should_poll = False - def __init__(self, device, entry, custom_effects=None): + def __init__( + self, + device: YeelightDevice, + entry: ConfigEntry, + custom_effects: dict[str, dict[str, Any]] | None = None, + ) -> None: """Initialize the Yeelight light.""" super().__init__(device, entry) self.config = device.config - self._color_temp = None + self._color_temp: int | None = None self._effect = None model_specs = self._bulb.get_model_specs() - self._min_mireds = kelvin_to_mired(model_specs["color_temp"]["max"]) - self._max_mireds = kelvin_to_mired(model_specs["color_temp"]["min"]) + self._attr_min_mireds = kelvin_to_mired(model_specs["color_temp"]["max"]) + self._attr_max_mireds = kelvin_to_mired(model_specs["color_temp"]["min"]) self._light_type = LightType.Main @@ -437,7 +443,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self._unexpected_state_check = None @callback - def async_state_changed(self): + def async_state_changed(self) -> None: """Call when the device changes state.""" if not self._device.available: self._async_cancel_pending_state_check() @@ -455,12 +461,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity): await super().async_added_to_hass() @property - def effect_list(self): + def effect_list(self) -> list[str]: """Return the list of supported effects.""" return self._predefined_effects + self.custom_effects_names @property - def color_temp(self) -> int: + def color_temp(self) -> int | None: """Return the color temperature.""" if temp_in_k := self._get_property("ct"): self._color_temp = kelvin_to_mired(int(temp_in_k)) @@ -489,32 +495,22 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return round(255 * (int(brightness) / 100)) @property - def min_mireds(self): - """Return minimum supported color temperature.""" - return self._min_mireds - - @property - def max_mireds(self): - """Return maximum supported color temperature.""" - return self._max_mireds - - @property - def custom_effects(self): + def custom_effects(self) -> dict[str, dict[str, Any]]: """Return dict with custom effects.""" return self._custom_effects @property - def custom_effects_names(self): + def custom_effects_names(self) -> list[str]: """Return list with custom effects names.""" return list(self.custom_effects) @property - def light_type(self): + def light_type(self) -> LightType: """Return light type.""" return self._light_type @property - def hs_color(self) -> tuple[int, int] | None: + def hs_color(self) -> tuple[float, float] | None: """Return the color property.""" hue = self._get_property("hue") sat = self._get_property("sat") @@ -537,7 +533,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return (red, green, blue) @property - def effect(self): + def effect(self) -> str | None: """Return the current effect.""" return self._effect if self.device.is_color_flow_enabled else None @@ -549,27 +545,27 @@ class YeelightGenericLight(YeelightEntity, LightEntity): def _properties(self) -> dict: return self._bulb.last_properties if self._bulb else {} - def _get_property(self, prop, default=None): + def _get_property(self, prop: str, default=None): return self._properties.get(prop, default) @property - def _brightness_property(self): + def _brightness_property(self) -> str: return "bright" @property - def _power_property(self): + def _power_property(self) -> str: return "power" @property - def _turn_on_power_mode(self): + def _turn_on_power_mode(self) -> PowerMode: return PowerMode.LAST @property - def _predefined_effects(self): + def _predefined_effects(self) -> list[str]: return YEELIGHT_MONO_EFFECT_LIST @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" attributes = { "flowing": self.device.is_color_flow_enabled, @@ -582,7 +578,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return attributes @property - def device(self): + def device(self) -> YeelightDevice: """Return yeelight device.""" return self._device @@ -890,7 +886,7 @@ class YeelightColorLightSupport(YeelightGenericLight): return ColorMode.UNKNOWN @property - def _predefined_effects(self): + def _predefined_effects(self) -> list[str]: return YEELIGHT_COLOR_EFFECT_LIST @@ -901,7 +897,7 @@ class YeelightWhiteTempLightSupport(YeelightGenericLight): _attr_supported_color_modes = {ColorMode.COLOR_TEMP} @property - def _predefined_effects(self): + def _predefined_effects(self) -> list[str]: return YEELIGHT_TEMP_ONLY_EFFECT_LIST @@ -909,7 +905,7 @@ class YeelightNightLightSupport: """Representation of a Yeelight nightlight support.""" @property - def _turn_on_power_mode(self): + def _turn_on_power_mode(self) -> PowerMode: return PowerMode.NORMAL @@ -917,7 +913,7 @@ class YeelightWithoutNightlightSwitchMixIn(YeelightGenericLight): """A mix-in for yeelights without a nightlight switch.""" @property - def _brightness_property(self): + def _brightness_property(self) -> str: # If the nightlight is not active, we do not # want to "current_brightness" since it will check # "bg_power" and main light could still be on @@ -926,11 +922,11 @@ class YeelightWithoutNightlightSwitchMixIn(YeelightGenericLight): return super()._brightness_property @property - def color_temp(self) -> int: + def color_temp(self) -> int | None: """Return the color temperature.""" if self.device.is_nightlight_enabled: # Enabling the nightlight locks the colortemp to max - return self._max_mireds + return self.max_mireds return super().color_temp @@ -978,6 +974,7 @@ class YeelightNightLightMode(YeelightGenericLight): """Representation of a Yeelight when in nightlight mode.""" _attr_color_mode = ColorMode.BRIGHTNESS + _attr_icon = "mdi:weather-night" _attr_supported_color_modes = {ColorMode.BRIGHTNESS} @property @@ -991,26 +988,21 @@ class YeelightNightLightMode(YeelightGenericLight): """Return the name of the device if any.""" return f"{self.device.name} Nightlight" - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:weather-night" - @property def is_on(self) -> bool: """Return true if device is on.""" return super().is_on and self.device.is_nightlight_enabled @property - def _brightness_property(self): + def _brightness_property(self) -> str: return "nl_br" @property - def _turn_on_power_mode(self): + def _turn_on_power_mode(self) -> PowerMode: return PowerMode.MOONLIGHT @property - def supported_features(self): + def supported_features(self) -> int: """Flag no supported features.""" return 0 @@ -1019,7 +1011,7 @@ class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode): """Representation of a Yeelight, with ambient support, when in nightlight mode.""" @property - def _power_property(self): + def _power_property(self) -> str: return "main_power" @@ -1040,7 +1032,7 @@ class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwi """ @property - def _power_property(self): + def _power_property(self) -> str: return "main_power" @@ -1051,7 +1043,7 @@ class YeelightWithAmbientAndNightlight(YeelightWithNightLight): """ @property - def _power_property(self): + def _power_property(self) -> str: return "main_power" @@ -1063,8 +1055,8 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): def __init__(self, *args, **kwargs): """Initialize the Yeelight Ambient light.""" super().__init__(*args, **kwargs) - self._min_mireds = kelvin_to_mired(6500) - self._max_mireds = kelvin_to_mired(1700) + self._attr_min_mireds = kelvin_to_mired(6500) + self._attr_max_mireds = kelvin_to_mired(1700) self._light_type = LightType.Ambient @@ -1080,10 +1072,10 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): return f"{self.device.name} Ambilight" @property - def _brightness_property(self): + def _brightness_property(self) -> str: return "bright" - def _get_property(self, prop, default=None): + def _get_property(self, prop: str, default=None): if not (bg_prop := self.PROPERTIES_MAPPING.get(prop)): bg_prop = f"bg_{prop}" From 078a4974e143d14e307f50201d088f3fbbce6967 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 11 Aug 2022 14:53:40 +0200 Subject: [PATCH 304/903] Fix evohome preset modes (#76606) --- homeassistant/components/evohome/climate.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index c1a630d0d05..e9eab8c2ae3 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -128,26 +128,17 @@ class EvoClimateEntity(EvoDevice, ClimateEntity): _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, evo_broker, evo_device) -> None: - """Initialize a Climate device.""" - super().__init__(evo_broker, evo_device) - - self._preset_modes = None - @property def hvac_modes(self) -> list[str]: """Return a list of available hvac operation modes.""" return list(HA_HVAC_TO_TCS) - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes.""" - return self._preset_modes - class EvoZone(EvoChild, EvoClimateEntity): """Base for a Honeywell TCC Zone.""" + _attr_preset_modes = list(HA_PRESET_TO_EVO) + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Zone.""" super().__init__(evo_broker, evo_device) @@ -233,7 +224,7 @@ class EvoZone(EvoChild, EvoClimateEntity): """ return self._evo_device.setpointCapabilities["maxHeatSetpoint"] - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature.""" temperature = kwargs["temperature"] @@ -249,7 +240,7 @@ class EvoZone(EvoChild, EvoClimateEntity): self._evo_device.set_temperature(temperature, until=until) ) - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set a Zone to one of its native EVO_* operating modes. Zones inherit their _effective_ operating mode from their Controller. @@ -387,7 +378,7 @@ class EvoController(EvoClimateEntity): """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Raise exception as Controllers don't have a target temperature.""" raise NotImplementedError("Evohome Controllers don't have target temperatures.") From ebbff7b60e43f17d65ead811d314602b9daddfc4 Mon Sep 17 00:00:00 2001 From: Zach Berger Date: Thu, 11 Aug 2022 06:01:35 -0700 Subject: [PATCH 305/903] Add Awair Local API support (#75535) --- homeassistant/components/awair/__init__.py | 84 ++++-- homeassistant/components/awair/config_flow.py | 124 ++++++++- homeassistant/components/awair/const.py | 7 +- homeassistant/components/awair/manifest.json | 10 +- homeassistant/components/awair/sensor.py | 4 +- homeassistant/components/awair/strings.json | 32 ++- homeassistant/config_entries.py | 4 +- homeassistant/generated/zeroconf.py | 4 + tests/components/awair/conftest.py | 73 +++++ tests/components/awair/const.py | 31 ++- .../awair/fixtures/awair-local.json | 17 ++ .../{devices.json => cloud_devices.json} | 0 .../awair/fixtures/local_devices.json | 16 ++ tests/components/awair/test_config_flow.py | 251 ++++++++++++++---- tests/components/awair/test_sensor.py | 89 ++++--- 15 files changed, 603 insertions(+), 143 deletions(-) create mode 100644 tests/components/awair/conftest.py create mode 100644 tests/components/awair/fixtures/awair-local.json rename tests/components/awair/fixtures/{devices.json => cloud_devices.json} (100%) create mode 100644 tests/components/awair/fixtures/local_devices.json diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index ef39e488001..b0a5d39814c 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -2,20 +2,27 @@ from __future__ import annotations from asyncio import gather -from typing import Any from async_timeout import timeout -from python_awair import Awair -from python_awair.exceptions import AuthError +from python_awair import Awair, AwairLocal +from python_awair.devices import AwairBaseDevice, AwairLocalDevice +from python_awair.exceptions import AuthError, AwairError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, Platform +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import API_TIMEOUT, DOMAIN, LOGGER, UPDATE_INTERVAL, AwairResult +from .const import ( + API_TIMEOUT, + DOMAIN, + LOGGER, + UPDATE_INTERVAL_CLOUD, + UPDATE_INTERVAL_LOCAL, + AwairResult, +) PLATFORMS = [Platform.SENSOR] @@ -23,7 +30,13 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Awair integration from a config entry.""" session = async_get_clientsession(hass) - coordinator = AwairDataUpdateCoordinator(hass, config_entry, session) + + coordinator: AwairDataUpdateCoordinator + + if CONF_HOST in config_entry.data: + coordinator = AwairLocalDataUpdateCoordinator(hass, config_entry, session) + else: + coordinator = AwairCloudDataUpdateCoordinator(hass, config_entry, session) await coordinator.async_config_entry_first_refresh() @@ -50,15 +63,31 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class AwairDataUpdateCoordinator(DataUpdateCoordinator): """Define a wrapper class to update Awair data.""" - def __init__(self, hass, config_entry, session) -> None: + def __init__(self, hass, config_entry, update_interval) -> None: """Set up the AwairDataUpdateCoordinator class.""" - access_token = config_entry.data[CONF_ACCESS_TOKEN] - self._awair = Awair(access_token=access_token, session=session) self._config_entry = config_entry - super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=update_interval) - async def _async_update_data(self) -> Any | None: + async def _fetch_air_data(self, device: AwairBaseDevice): + """Fetch latest air quality data.""" + LOGGER.debug("Fetching data for %s", device.uuid) + air_data = await device.air_data_latest() + LOGGER.debug(air_data) + return AwairResult(device=device, air_data=air_data) + + +class AwairCloudDataUpdateCoordinator(AwairDataUpdateCoordinator): + """Define a wrapper class to update Awair data from Cloud API.""" + + def __init__(self, hass, config_entry, session) -> None: + """Set up the AwairCloudDataUpdateCoordinator class.""" + access_token = config_entry.data[CONF_ACCESS_TOKEN] + self._awair = Awair(access_token=access_token, session=session) + + super().__init__(hass, config_entry, UPDATE_INTERVAL_CLOUD) + + async def _async_update_data(self) -> dict[str, AwairResult] | None: """Update data via Awair client library.""" async with timeout(API_TIMEOUT): try: @@ -74,9 +103,30 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): except Exception as err: raise UpdateFailed(err) from err - async def _fetch_air_data(self, device): - """Fetch latest air quality data.""" - LOGGER.debug("Fetching data for %s", device.uuid) - air_data = await device.air_data_latest() - LOGGER.debug(air_data) - return AwairResult(device=device, air_data=air_data) + +class AwairLocalDataUpdateCoordinator(AwairDataUpdateCoordinator): + """Define a wrapper class to update Awair data from the local API.""" + + _device: AwairLocalDevice | None = None + + def __init__(self, hass, config_entry, session) -> None: + """Set up the AwairLocalDataUpdateCoordinator class.""" + self._awair = AwairLocal( + session=session, device_addrs=[config_entry.data[CONF_HOST]] + ) + + super().__init__(hass, config_entry, UPDATE_INTERVAL_LOCAL) + + async def _async_update_data(self) -> dict[str, AwairResult] | None: + """Update data via Awair client library.""" + async with timeout(API_TIMEOUT): + try: + if self._device is None: + LOGGER.debug("Fetching devices") + devices = await self._awair.devices() + self._device = devices[0] + result = await self._fetch_air_data(self._device) + return {result.device.uuid: result} + except AwairError as err: + LOGGER.error("Unexpected API error: %s", err) + raise UpdateFailed(err) from err diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 1e83144945d..3fb822ab4fe 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -4,12 +4,14 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from python_awair import Awair +from aiohttp.client_exceptions import ClientConnectorError +from python_awair import Awair, AwairLocal, AwairLocalDevice from python_awair.exceptions import AuthError, AwairError import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,20 +23,76 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 + _device: AwairLocalDevice + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + + host = discovery_info.host + LOGGER.debug("Discovered device: %s", host) + + self._device, _ = await self._check_local_connection(host) + + if self._device is not None: + await self.async_set_unique_id(self._device.mac_address) + self._abort_if_unique_id_configured(error="already_configured_device") + self.context.update( + { + "title_placeholders": { + "model": self._device.model, + "device_id": self._device.device_id, + } + } + ) + else: + return self.async_abort(reason="unreachable") + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + if user_input is not None: + title = f"{self._device.model} ({self._device.device_id})" + return self.async_create_entry( + title=title, + data={CONF_HOST: self._device.device_addr}, + ) + + self._set_confirm_only() + placeholders = { + "model": self._device.model, + "device_id": self._device.device_id, + } + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders=placeholders, + ) + async def async_step_user( self, user_input: dict[str, str] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" + + return self.async_show_menu(step_id="user", menu_options=["local", "cloud"]) + + async def async_step_cloud(self, user_input: Mapping[str, Any]) -> FlowResult: + """Handle collecting and verifying Awair Cloud API credentials.""" + errors = {} if user_input is not None: - user, error = await self._check_connection(user_input[CONF_ACCESS_TOKEN]) + user, error = await self._check_cloud_connection( + user_input[CONF_ACCESS_TOKEN] + ) if user is not None: await self.async_set_unique_id(user.email) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(error="already_configured_account") - title = f"{user.email} ({user.user_id})" + title = user.email return self.async_create_entry(title=title, data=user_input) if error != "invalid_access_token": @@ -43,8 +101,39 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors = {CONF_ACCESS_TOKEN: "invalid_access_token"} return self.async_show_form( - step_id="user", - data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}), + step_id="cloud", + data_schema=vol.Schema({vol.Optional(CONF_ACCESS_TOKEN): str}), + description_placeholders={ + "url": "https://developer.getawair.com/onboard/login" + }, + errors=errors, + ) + + async def async_step_local(self, user_input: Mapping[str, Any]) -> FlowResult: + """Handle collecting and verifying Awair Local API hosts.""" + + errors = {} + + if user_input is not None: + self._device, error = await self._check_local_connection( + user_input[CONF_HOST] + ) + + if self._device is not None: + await self.async_set_unique_id(self._device.mac_address) + self._abort_if_unique_id_configured(error="already_configured_device") + title = f"{self._device.model} ({self._device.device_id})" + return self.async_create_entry(title=title, data=user_input) + + if error is not None: + errors = {CONF_HOST: error} + + return self.async_show_form( + step_id="local", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + description_placeholders={ + "url": "https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Element-Local-API-Feature" + }, errors=errors, ) @@ -60,7 +149,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is not None: access_token = user_input[CONF_ACCESS_TOKEN] - _, error = await self._check_connection(access_token) + _, error = await self._check_cloud_connection(access_token) if error is None: entry = await self.async_set_unique_id(self.unique_id) @@ -79,7 +168,24 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _check_connection(self, access_token: str): + async def _check_local_connection(self, device_address: str): + """Check the access token is valid.""" + session = async_get_clientsession(self.hass) + awair = AwairLocal(session=session, device_addrs=[device_address]) + + try: + devices = await awair.devices() + return (devices[0], None) + + except ClientConnectorError as err: + LOGGER.error("Unable to connect error: %s", err) + return (None, "unreachable") + + except AwairError as err: + LOGGER.error("Unexpected API error: %s", err) + return (None, "unknown") + + async def _check_cloud_connection(self, access_token: str): """Check the access token is valid.""" session = async_get_clientsession(self.hass) awair = Awair(access_token=access_token, session=session) diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 6fcf63abb4d..133cf03fdbe 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging from python_awair.air_data import AirData -from python_awair.devices import AwairDevice +from python_awair.devices import AwairBaseDevice from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.const import ( @@ -39,7 +39,8 @@ DUST_ALIASES = [API_PM25, API_PM10] LOGGER = logging.getLogger(__package__) -UPDATE_INTERVAL = timedelta(minutes=5) +UPDATE_INTERVAL_CLOUD = timedelta(minutes=5) +UPDATE_INTERVAL_LOCAL = timedelta(seconds=30) @dataclass @@ -129,5 +130,5 @@ SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = ( class AwairResult: """Wrapper class to hold an awair device and set of air data.""" - device: AwairDevice + device: AwairBaseDevice air_data: AirData diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index 57b3c242620..cea5d01bfab 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -5,6 +5,12 @@ "requirements": ["python_awair==0.2.3"], "codeowners": ["@ahayworth", "@danielsjf"], "config_flow": true, - "iot_class": "cloud_polling", - "loggers": ["python_awair"] + "iot_class": "local_polling", + "loggers": ["python_awair"], + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "awair*" + } + ] } diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index ddf76c0e93d..cda7f31095e 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from python_awair.air_data import AirData -from python_awair.devices import AwairDevice +from python_awair.devices import AwairBaseDevice from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -76,7 +76,7 @@ class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): def __init__( self, - device: AwairDevice, + device: AwairBaseDevice, coordinator: AwairDataUpdateCoordinator, description: AwairSensorEntityDescription, ) -> None: diff --git a/homeassistant/components/awair/strings.json b/homeassistant/components/awair/strings.json index 5ed7c0e715e..fc95fc861f1 100644 --- a/homeassistant/components/awair/strings.json +++ b/homeassistant/components/awair/strings.json @@ -1,29 +1,49 @@ { "config": { "step": { - "user": { - "description": "You must register for an Awair developer access token at: https://developer.getawair.com/onboard/login", + "cloud": { + "description": "You must register for an Awair developer access token at: {url}", "data": { "access_token": "[%key:common::config_flow::data::access_token%]", "email": "[%key:common::config_flow::data::email%]" } }, + "local": { + "data": { + "host": "[%key:common::config_flow::data::ip%]" + }, + "description": "Awair Local API must be enabled following these steps: {url}" + }, "reauth_confirm": { "description": "Please re-enter your Awair developer access token.", "data": { "access_token": "[%key:common::config_flow::data::access_token%]", "email": "[%key:common::config_flow::data::email%]" } + }, + "discovery_confirm": { + "description": "Do you want to setup {model} ({device_id})?" + }, + "user": { + "menu_options": { + "cloud": "Connect via the cloud", + "local": "Connect locally (preferred)" + }, + "description": "Pick local for the best experience. Only use cloud if the device is not connected to the same network as Home Assistant, or if you have a legacy device." } }, "error": { "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "unreachable": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]", + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - } + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "unreachable": "[%key:common::config_flow::error::cannot_connect%]" + }, + "flow_title": "{model} ({device_id})" } } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7c2f1c84ff9..f37efb6c627 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1297,6 +1297,8 @@ class ConfigFlow(data_entry_flow.FlowHandler): self, updates: dict[str, Any] | None = None, reload_on_update: bool = True, + *, + error: str = "already_configured", ) -> None: """Abort if the unique ID is already configured.""" if self.unique_id is None: @@ -1332,7 +1334,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) - raise data_entry_flow.AbortFlow("already_configured") + raise data_entry_flow.AbortFlow(error) async def async_set_unique_id( self, unique_id: str | None = None, *, raise_on_progress: bool = True diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index d59d37f4579..b6237a36cd7 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -183,6 +183,10 @@ ZEROCONF = { } ], "_http._tcp.local.": [ + { + "domain": "awair", + "name": "awair*" + }, { "domain": "bosch_shc", "name": "bosch shc*" diff --git a/tests/components/awair/conftest.py b/tests/components/awair/conftest.py new file mode 100644 index 00000000000..ec15561cc05 --- /dev/null +++ b/tests/components/awair/conftest.py @@ -0,0 +1,73 @@ +"""Fixtures for testing Awair integration.""" + +import json + +import pytest + +from tests.common import load_fixture + + +@pytest.fixture(name="cloud_devices", scope="session") +def cloud_devices_fixture(): + """Fixture representing devices returned by Awair Cloud API.""" + return json.loads(load_fixture("awair/cloud_devices.json")) + + +@pytest.fixture(name="local_devices", scope="session") +def local_devices_fixture(): + """Fixture representing devices returned by Awair local API.""" + return json.loads(load_fixture("awair/local_devices.json")) + + +@pytest.fixture(name="gen1_data", scope="session") +def gen1_data_fixture(): + """Fixture representing data returned from Gen1 Awair device.""" + return json.loads(load_fixture("awair/awair.json")) + + +@pytest.fixture(name="gen2_data", scope="session") +def gen2_data_fixture(): + """Fixture representing data returned from Gen2 Awair device.""" + return json.loads(load_fixture("awair/awair-r2.json")) + + +@pytest.fixture(name="glow_data", scope="session") +def glow_data_fixture(): + """Fixture representing data returned from Awair glow device.""" + return json.loads(load_fixture("awair/glow.json")) + + +@pytest.fixture(name="mint_data", scope="session") +def mint_data_fixture(): + """Fixture representing data returned from Awair mint device.""" + return json.loads(load_fixture("awair/mint.json")) + + +@pytest.fixture(name="no_devices", scope="session") +def no_devicess_fixture(): + """Fixture representing when no devices are found in Awair's cloud API.""" + return json.loads(load_fixture("awair/no_devices.json")) + + +@pytest.fixture(name="awair_offline", scope="session") +def awair_offline_fixture(): + """Fixture representing when Awair devices are offline.""" + return json.loads(load_fixture("awair/awair-offline.json")) + + +@pytest.fixture(name="omni_data", scope="session") +def omni_data_fixture(): + """Fixture representing data returned from Awair omni device.""" + return json.loads(load_fixture("awair/omni.json")) + + +@pytest.fixture(name="user", scope="session") +def user_fixture(): + """Fixture representing the User object returned from Awair's Cloud API.""" + return json.loads(load_fixture("awair/user.json")) + + +@pytest.fixture(name="local_data", scope="session") +def local_data_fixture(): + """Fixture representing data returned from Awair local device.""" + return json.loads(load_fixture("awair/awair-local.json")) diff --git a/tests/components/awair/const.py b/tests/components/awair/const.py index 94c07e9e9fd..cead20d10af 100644 --- a/tests/components/awair/const.py +++ b/tests/components/awair/const.py @@ -1,20 +1,19 @@ """Constants used in Awair tests.""" -import json - -from homeassistant.const import CONF_ACCESS_TOKEN - -from tests.common import load_fixture +from homeassistant.components import zeroconf +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST AWAIR_UUID = "awair_24947" -CONFIG = {CONF_ACCESS_TOKEN: "12345"} -UNIQUE_ID = "foo@bar.com" -DEVICES_FIXTURE = json.loads(load_fixture("awair/devices.json")) -GEN1_DATA_FIXTURE = json.loads(load_fixture("awair/awair.json")) -GEN2_DATA_FIXTURE = json.loads(load_fixture("awair/awair-r2.json")) -GLOW_DATA_FIXTURE = json.loads(load_fixture("awair/glow.json")) -MINT_DATA_FIXTURE = json.loads(load_fixture("awair/mint.json")) -NO_DEVICES_FIXTURE = json.loads(load_fixture("awair/no_devices.json")) -OFFLINE_FIXTURE = json.loads(load_fixture("awair/awair-offline.json")) -OMNI_DATA_FIXTURE = json.loads(load_fixture("awair/omni.json")) -USER_FIXTURE = json.loads(load_fixture("awair/user.json")) +CLOUD_CONFIG = {CONF_ACCESS_TOKEN: "12345"} +LOCAL_CONFIG = {CONF_HOST: "192.0.2.5"} +CLOUD_UNIQUE_ID = "foo@bar.com" +LOCAL_UNIQUE_ID = "00:B0:D0:63:C2:26" +ZEROCONF_DISCOVERY = zeroconf.ZeroconfServiceInfo( + host="192.0.2.5", + addresses=["192.0.2.5"], + hostname="mock_hostname", + name="awair12345", + port=None, + type="_http._tcp.local.", + properties={}, +) diff --git a/tests/components/awair/fixtures/awair-local.json b/tests/components/awair/fixtures/awair-local.json new file mode 100644 index 00000000000..d793b8f4017 --- /dev/null +++ b/tests/components/awair/fixtures/awair-local.json @@ -0,0 +1,17 @@ +{ + "timestamp": "2022-08-11T05:04:12.108Z", + "score": 94, + "dew_point": 14.47, + "temp": 23.64, + "humid": 56.45, + "abs_humid": 12.0, + "co2": 426, + "co2_est": 489, + "co2_est_baseline": 37021, + "voc": 149, + "voc_baseline": 37783, + "voc_h2_raw": 26, + "voc_ethanol_raw": 37, + "pm25": 2, + "pm10_est": 3 +} diff --git a/tests/components/awair/fixtures/devices.json b/tests/components/awair/fixtures/cloud_devices.json similarity index 100% rename from tests/components/awair/fixtures/devices.json rename to tests/components/awair/fixtures/cloud_devices.json diff --git a/tests/components/awair/fixtures/local_devices.json b/tests/components/awair/fixtures/local_devices.json new file mode 100644 index 00000000000..d657020df96 --- /dev/null +++ b/tests/components/awair/fixtures/local_devices.json @@ -0,0 +1,16 @@ +{ + "device_uuid": "awair-element_24947", + "wifi_mac": "00:B0:D0:63:C2:26", + "ssid": "Internet of Things", + "ip": "192.0.2.5", + "netmask": "255.255.255.0", + "gateway": "none", + "fw_version": "1.2.8", + "timezone": "America/Los_Angeles", + "display": "score", + "led": { + "mode": "auto", + "brightness": 179 + }, + "voc_feature_set": 34 +} diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index b5bc5f23eaa..f6513321dfb 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -1,99 +1,143 @@ """Define tests for the Awair config flow.""" +from unittest.mock import Mock, patch -from unittest.mock import patch - +from aiohttp.client_exceptions import ClientConnectorError from python_awair.exceptions import AuthError, AwairError from homeassistant import data_entry_flow from homeassistant.components.awair.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant -from .const import CONFIG, DEVICES_FIXTURE, NO_DEVICES_FIXTURE, UNIQUE_ID, USER_FIXTURE +from .const import ( + CLOUD_CONFIG, + CLOUD_UNIQUE_ID, + LOCAL_CONFIG, + LOCAL_UNIQUE_ID, + ZEROCONF_DISCOVERY, +) from tests.common import MockConfigEntry -async def test_show_form(hass): +async def test_show_form(hass: HomeAssistant): """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == data_entry_flow.FlowResultType.MENU assert result["step_id"] == SOURCE_USER -async def test_invalid_access_token(hass): +async def test_invalid_access_token(hass: HomeAssistant): """Test that errors are shown when the access token is invalid.""" with patch("python_awair.AwairClient.query", side_effect=AuthError()): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, ) assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} -async def test_unexpected_api_error(hass): +async def test_unexpected_api_error(hass: HomeAssistant): """Test that we abort on generic errors.""" with patch("python_awair.AwairClient.query", side_effect=AwairError()): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG ) - assert result["type"] == "abort" + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" -async def test_duplicate_error(hass): +async def test_duplicate_error(hass: HomeAssistant, user, cloud_devices): """Test that errors are shown when adding a duplicate config.""" with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] - ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", - return_value=True, + "python_awair.AwairClient.query", + side_effect=[user, cloud_devices], ): - MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG).add_to_hass( - hass + MockConfigEntry( + domain=DOMAIN, unique_id=CLOUD_UNIQUE_ID, data=CLOUD_CONFIG + ).add_to_hass(hass) + + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured_account" -async def test_no_devices_error(hass): +async def test_no_devices_error(hass: HomeAssistant, user, no_devices): """Test that errors are shown when the API returns no devices.""" - with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, NO_DEVICES_FIXTURE] - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + with patch("python_awair.AwairClient.query", side_effect=[user, no_devices]): + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG ) - assert result["type"] == "abort" + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" -async def test_reauth(hass: HomeAssistant) -> None: +async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None: """Test reauth flow.""" mock_config = MockConfigEntry( - domain=DOMAIN, unique_id=UNIQUE_ID, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"} + domain=DOMAIN, + unique_id=CLOUD_UNIQUE_ID, + data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, - data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, + context={"source": SOURCE_REAUTH, "unique_id": CLOUD_UNIQUE_ID}, + data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -102,7 +146,7 @@ async def test_reauth(hass: HomeAssistant) -> None: with patch("python_awair.AwairClient.query", side_effect=AuthError()): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONFIG, + user_input=CLOUD_CONFIG, ) assert result["type"] == data_entry_flow.FlowResultType.FORM @@ -110,11 +154,12 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] + "python_awair.AwairClient.query", + side_effect=[user, cloud_devices], ), patch("homeassistant.components.awair.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONFIG, + user_input=CLOUD_CONFIG, ) assert result["type"] == data_entry_flow.FlowResultType.ABORT @@ -124,14 +169,16 @@ async def test_reauth(hass: HomeAssistant) -> None: async def test_reauth_error(hass: HomeAssistant) -> None: """Test reauth flow.""" mock_config = MockConfigEntry( - domain=DOMAIN, unique_id=UNIQUE_ID, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"} + domain=DOMAIN, + unique_id=CLOUD_UNIQUE_ID, + data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, - data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, + context={"source": SOURCE_REAUTH, "unique_id": CLOUD_UNIQUE_ID}, + data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -140,27 +187,127 @@ async def test_reauth_error(hass: HomeAssistant) -> None: with patch("python_awair.AwairClient.query", side_effect=AwairError()): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONFIG, + user_input=CLOUD_CONFIG, ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" -async def test_create_entry(hass): - """Test overall flow.""" +async def test_create_cloud_entry(hass: HomeAssistant, user, cloud_devices): + """Test overall flow when using cloud api.""" with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] + "python_awair.AwairClient.query", + side_effect=[user, cloud_devices], ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", + "homeassistant.components.awair.async_setup_entry", return_value=True, ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "foo@bar.com (32406)" - assert result["data"][CONF_ACCESS_TOKEN] == CONFIG[CONF_ACCESS_TOKEN] - assert result["result"].unique_id == UNIQUE_ID + assert result["title"] == "foo@bar.com" + assert result["data"][CONF_ACCESS_TOKEN] == CLOUD_CONFIG[CONF_ACCESS_TOKEN] + assert result["result"].unique_id == CLOUD_UNIQUE_ID + + +async def test_create_local_entry(hass: HomeAssistant, local_devices): + """Test overall flow when using local API.""" + + with patch("python_awair.AwairClient.query", side_effect=[local_devices]), patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ): + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=LOCAL_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "local"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + LOCAL_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Awair Element (24947)" + assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] + assert result["result"].unique_id == LOCAL_UNIQUE_ID + + +async def test_create_local_entry_awair_error(hass: HomeAssistant): + """Test overall flow when using local API and device is returns error.""" + + with patch( + "python_awair.AwairClient.query", + side_effect=AwairError(), + ): + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=LOCAL_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "local"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + LOCAL_CONFIG, + ) + + # User is returned to form to try again + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "local" + + +async def test_create_zeroconf_entry(hass: HomeAssistant, local_devices): + """Test overall flow when using discovery.""" + + with patch("python_awair.AwairClient.query", side_effect=[local_devices]), patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ): + confirm_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY + ) + + result = await hass.config_entries.flow.async_configure( + confirm_step["flow_id"], + {}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Awair Element (24947)" + assert result["data"][CONF_HOST] == ZEROCONF_DISCOVERY.host + assert result["result"].unique_id == LOCAL_UNIQUE_ID + + +async def test_unsuccessful_create_zeroconf_entry(hass: HomeAssistant): + """Test overall flow when using discovery and device is unreachable.""" + + with patch( + "python_awair.AwairClient.query", + side_effect=ClientConnectorError(Mock(), OSError()), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 07b2f9ba00f..87b931a3f7f 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -26,21 +26,16 @@ from homeassistant.const import ( STATE_UNAVAILABLE, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from .const import ( AWAIR_UUID, - CONFIG, - DEVICES_FIXTURE, - GEN1_DATA_FIXTURE, - GEN2_DATA_FIXTURE, - GLOW_DATA_FIXTURE, - MINT_DATA_FIXTURE, - OFFLINE_FIXTURE, - OMNI_DATA_FIXTURE, - UNIQUE_ID, - USER_FIXTURE, + CLOUD_CONFIG, + CLOUD_UNIQUE_ID, + LOCAL_CONFIG, + LOCAL_UNIQUE_ID, ) from tests.common import MockConfigEntry @@ -50,10 +45,10 @@ SENSOR_TYPES_MAP = { } -async def setup_awair(hass, fixtures): +async def setup_awair(hass: HomeAssistant, fixtures, unique_id, data): """Add Awair devices to hass, using specified fixtures for data.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG) + entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=data) with patch("python_awair.AwairClient.query", side_effect=fixtures): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -61,7 +56,12 @@ async def setup_awair(hass, fixtures): def assert_expected_properties( - hass, registry, name, unique_id, state_value, attributes + hass: HomeAssistant, + registry: er.RegistryEntry, + name, + unique_id, + state_value, + attributes: dict, ): """Assert expected properties from a dict.""" @@ -74,11 +74,11 @@ def assert_expected_properties( assert state.attributes.get(attr) == value -async def test_awair_gen1_sensors(hass): +async def test_awair_gen1_sensors(hass: HomeAssistant, user, cloud_devices, gen1_data): """Test expected sensors on a 1st gen Awair.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GEN1_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, gen1_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -166,11 +166,11 @@ async def test_awair_gen1_sensors(hass): assert hass.states.get("sensor.living_room_illuminance") is None -async def test_awair_gen2_sensors(hass): +async def test_awair_gen2_sensors(hass: HomeAssistant, user, cloud_devices, gen2_data): """Test expected sensors on a 2nd gen Awair.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GEN2_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, gen2_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -199,11 +199,28 @@ async def test_awair_gen2_sensors(hass): assert hass.states.get("sensor.living_room_pm10") is None -async def test_awair_mint_sensors(hass): +async def test_local_awair_sensors(hass: HomeAssistant, local_devices, local_data): + """Test expected sensors on a local Awair.""" + + fixtures = [local_devices, local_data] + await setup_awair(hass, fixtures, LOCAL_UNIQUE_ID, LOCAL_CONFIG) + registry = er.async_get(hass) + + assert_expected_properties( + hass, + registry, + "sensor.awair_score", + f"{local_devices['device_uuid']}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", + "94", + {}, + ) + + +async def test_awair_mint_sensors(hass: HomeAssistant, user, cloud_devices, mint_data): """Test expected sensors on an Awair mint.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, MINT_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, mint_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -240,11 +257,11 @@ async def test_awair_mint_sensors(hass): assert hass.states.get("sensor.living_room_carbon_dioxide") is None -async def test_awair_glow_sensors(hass): +async def test_awair_glow_sensors(hass: HomeAssistant, user, cloud_devices, glow_data): """Test expected sensors on an Awair glow.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GLOW_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, glow_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -260,11 +277,11 @@ async def test_awair_glow_sensors(hass): assert hass.states.get("sensor.living_room_pm2_5") is None -async def test_awair_omni_sensors(hass): +async def test_awair_omni_sensors(hass: HomeAssistant, user, cloud_devices, omni_data): """Test expected sensors on an Awair omni.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, OMNI_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, omni_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -295,11 +312,11 @@ async def test_awair_omni_sensors(hass): ) -async def test_awair_offline(hass): +async def test_awair_offline(hass: HomeAssistant, user, cloud_devices, awair_offline): """Test expected behavior when an Awair is offline.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, OFFLINE_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, awair_offline] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) # The expected behavior is that we won't have any sensors # if the device is not online when we set it up. python_awair @@ -313,11 +330,13 @@ async def test_awair_offline(hass): assert hass.states.get("sensor.living_room_awair_score") is None -async def test_awair_unavailable(hass): +async def test_awair_unavailable( + hass: HomeAssistant, user, cloud_devices, gen1_data, awair_offline +): """Test expected behavior when an Awair becomes offline later.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GEN1_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, gen1_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -329,7 +348,7 @@ async def test_awair_unavailable(hass): {}, ) - with patch("python_awair.AwairClient.query", side_effect=OFFLINE_FIXTURE): + with patch("python_awair.AwairClient.query", side_effect=awair_offline): await async_update_entity(hass, "sensor.living_room_awair_score") assert_expected_properties( hass, From f0827a20c3c0014de7e28dbeba76fc3f2e74fc70 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 16:14:01 +0200 Subject: [PATCH 306/903] Add schedule helper (#76566) Co-authored-by: Paulus Schoutsen --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/default_config/manifest.json | 1 + homeassistant/components/schedule/__init__.py | 314 +++++++++++++++ homeassistant/components/schedule/const.py | 37 ++ .../components/schedule/manifest.json | 8 + homeassistant/components/schedule/recorder.py | 16 + .../components/schedule/services.yaml | 3 + .../components/schedule/strings.json | 9 + .../components/schedule/translations/en.json | 9 + mypy.ini | 11 + script/hassfest/manifest.py | 1 + tests/components/schedule/__init__.py | 1 + tests/components/schedule/test_init.py | 376 ++++++++++++++++++ tests/components/schedule/test_recorder.py | 70 ++++ 15 files changed, 859 insertions(+) create mode 100644 homeassistant/components/schedule/__init__.py create mode 100644 homeassistant/components/schedule/const.py create mode 100644 homeassistant/components/schedule/manifest.json create mode 100644 homeassistant/components/schedule/recorder.py create mode 100644 homeassistant/components/schedule/services.yaml create mode 100644 homeassistant/components/schedule/strings.json create mode 100644 homeassistant/components/schedule/translations/en.json create mode 100644 tests/components/schedule/__init__.py create mode 100644 tests/components/schedule/test_init.py create mode 100644 tests/components/schedule/test_recorder.py diff --git a/.strict-typing b/.strict-typing index cc1af9926bd..1574ea09f2e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -213,6 +213,7 @@ homeassistant.components.rpi_power.* homeassistant.components.rtsp_to_webrtc.* homeassistant.components.samsungtv.* homeassistant.components.scene.* +homeassistant.components.schedule.* homeassistant.components.select.* homeassistant.components.sensibo.* homeassistant.components.sensor.* diff --git a/CODEOWNERS b/CODEOWNERS index 8de29fd5ede..5de44d30fb3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -921,6 +921,8 @@ build.json @home-assistant/supervisor /tests/components/samsungtv/ @chemelli74 @epenet /homeassistant/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core +/homeassistant/components/schedule/ @home-assistant/core +/tests/components/schedule/ @home-assistant/core /homeassistant/components/schluter/ @prairieapps /homeassistant/components/scrape/ @fabaff /tests/components/scrape/ @fabaff diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index f790292c27a..593ac26dbc9 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -27,6 +27,7 @@ "network", "person", "scene", + "schedule", "script", "ssdp", "sun", diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py new file mode 100644 index 00000000000..023cfef99e1 --- /dev/null +++ b/homeassistant/components/schedule/__init__.py @@ -0,0 +1,314 @@ +"""Support for schedules in Home Assistant.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import itertools +import logging +from typing import Literal + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_EDITABLE, + CONF_ICON, + CONF_ID, + CONF_NAME, + SERVICE_RELOAD, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers.collection import ( + IDManager, + StorageCollection, + StorageCollectionWebsocket, + YamlCollection, + sync_entity_lifecycle, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.integration_platform import ( + async_process_integration_platform_for_component, +) +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util + +from .const import ( + ATTR_NEXT_EVENT, + CONF_ALL_DAYS, + CONF_FROM, + CONF_TO, + DOMAIN, + LOGGER, + WEEKDAY_TO_CONF, +) + +STORAGE_VERSION = 1 +STORAGE_VERSION_MINOR = 1 + + +def valid_schedule(schedule: list[dict[str, str]]) -> list[dict[str, str]]: + """Validate the schedule of time ranges. + + Ensure they have no overlap and the end time is greater than the start time. + """ + # Emtpty schedule is valid + if not schedule: + return schedule + + # Sort the schedule by start times + schedule = sorted(schedule, key=lambda time_range: time_range[CONF_FROM]) + + # Check if the start time of the next event is before the end time of the previous event + previous_to = None + for time_range in schedule: + if time_range[CONF_FROM] >= time_range[CONF_TO]: + raise vol.Invalid( + f"Invalid time range, from {time_range[CONF_FROM]} is after {time_range[CONF_TO]}" + ) + + # Check if the from time of the event is after the to time of the previous event + if previous_to is not None and previous_to > time_range[CONF_FROM]: # type: ignore[unreachable] + raise vol.Invalid("Overlapping times found in schedule") + + previous_to = time_range[CONF_TO] + + return schedule + + +BASE_SCHEMA = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_ICON): cv.icon, +} + +TIME_RANGE_SCHEMA = { + vol.Required(CONF_FROM): cv.time, + vol.Required(CONF_TO): cv.time, +} +STORAGE_TIME_RANGE_SCHEMA = vol.Schema( + { + vol.Required(CONF_FROM): vol.All(cv.time, vol.Coerce(str)), + vol.Required(CONF_TO): vol.All(cv.time, vol.Coerce(str)), + } +) + +SCHEDULE_SCHEMA = { + vol.Optional(day, default=[]): vol.All( + cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule + ) + for day in CONF_ALL_DAYS +} +STORAGE_SCHEDULE_SCHEMA = { + vol.Optional(day, default=[]): vol.All( + cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule, [STORAGE_TIME_RANGE_SCHEMA] + ) + for day in CONF_ALL_DAYS +} + + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: cv.schema_with_slug_keys(vol.All(BASE_SCHEMA | SCHEDULE_SCHEMA))}, + extra=vol.ALLOW_EXTRA, +) +STORAGE_SCHEMA = vol.Schema( + {vol.Required(CONF_ID): cv.string} | BASE_SCHEMA | SCHEDULE_SCHEMA +) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up an input select.""" + component = EntityComponent(LOGGER, DOMAIN, hass) + + # Process integration platforms right away since + # we will create entities before firing EVENT_COMPONENT_LOADED + await async_process_integration_platform_for_component(hass, DOMAIN) + + id_manager = IDManager() + + yaml_collection = YamlCollection(LOGGER, id_manager) + sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, Schedule.from_yaml + ) + + storage_collection = ScheduleStorageCollection( + Store( + hass, + key=DOMAIN, + version=STORAGE_VERSION, + minor_version=STORAGE_VERSION_MINOR, + ), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + ) + sync_entity_lifecycle(hass, DOMAIN, DOMAIN, component, storage_collection, Schedule) + + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()] + ) + await storage_collection.async_load() + + StorageCollectionWebsocket( + storage_collection, + DOMAIN, + DOMAIN, + BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA, + BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA, + ).async_setup(hass) + + async def reload_service_handler(service_call: ServiceCall) -> None: + """Reload yaml entities.""" + conf = await component.async_prepare_reload(skip_reset=True) + if conf is None: + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()] + ) + + async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + ) + + return True + + +class ScheduleStorageCollection(StorageCollection): + """Schedules stored in storage.""" + + SCHEMA = vol.Schema(BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA) + + async def _process_create_data(self, data: dict) -> dict: + """Validate the config is valid.""" + self.SCHEMA(data) + return data + + @callback + def _get_suggested_id(self, info: dict) -> str: + """Suggest an ID based on the config.""" + name: str = info[CONF_NAME] + return name + + async def _update_data(self, data: dict, update_data: dict) -> dict: + """Return a new updated data object.""" + self.SCHEMA(update_data) + return data | update_data + + async def _async_load_data(self) -> dict | None: + """Load the data.""" + if data := await super()._async_load_data(): + data["items"] = [STORAGE_SCHEMA(item) for item in data["items"]] + return data + + +class Schedule(Entity): + """Schedule entity.""" + + _attr_has_entity_name = True + _attr_should_poll = False + _attr_state: Literal["on", "off"] + _config: ConfigType + _next: datetime + _unsub_update: Callable[[], None] | None = None + + def __init__(self, config: ConfigType, editable: bool = True) -> None: + """Initialize a schedule.""" + self._config = STORAGE_SCHEMA(config) + self._attr_capability_attributes = {ATTR_EDITABLE: editable} + self._attr_icon = self._config.get(CONF_ICON) + self._attr_name = self._config[CONF_NAME] + self._attr_unique_id = self._config[CONF_ID] + + @classmethod + def from_yaml(cls, config: ConfigType) -> Schedule: + """Return entity instance initialized from yaml storage.""" + schedule = cls(config, editable=False) + schedule.entity_id = f"{DOMAIN}.{config[CONF_ID]}" + return schedule + + async def async_update_config(self, config: ConfigType) -> None: + """Handle when the config is updated.""" + self._config = STORAGE_SCHEMA(config) + self._attr_icon = config.get(CONF_ICON) + self._attr_name = config[CONF_NAME] + self._clean_up_listener() + self._update() + + @callback + def _clean_up_listener(self) -> None: + """Remove the update timer.""" + if self._unsub_update is not None: + self._unsub_update() + self._unsub_update = None + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self.async_on_remove(self._clean_up_listener) + self._update() + + @callback + def _update(self, _: datetime | None = None) -> None: + """Update the states of the schedule.""" + now = dt_util.now() + todays_schedule = self._config.get(WEEKDAY_TO_CONF[now.weekday()], []) + + # Determine current schedule state + self._attr_state = next( + ( + STATE_ON + for time_range in todays_schedule + if time_range[CONF_FROM] <= now.time() <= time_range[CONF_TO] + ), + STATE_OFF, + ) + + # Find next event in the schedule, loop over each day (starting with + # the current day) until the next event has been found. + next_event = None + for day in range(7): + day_schedule = self._config.get( + WEEKDAY_TO_CONF[(now.weekday() + day) % 7], [] + ) + times = sorted( + itertools.chain( + *[ + [time_range[CONF_FROM], time_range[CONF_TO]] + for time_range in day_schedule + ] + ) + ) + + if next_event := next( + ( + possible_next_event + for time in times + if ( + possible_next_event := ( + datetime.combine(now.date(), time, tzinfo=now.tzinfo) + + timedelta(days=day) + ) + ) + > now + ), + None, + ): + # We have found the next event in this day, stop searching. + break + + self._attr_extra_state_attributes = { + ATTR_NEXT_EVENT: next_event, + } + self.async_write_ha_state() + + if next_event: + self._unsub_update = async_track_point_in_utc_time( + self.hass, + self._update, + next_event, + ) diff --git a/homeassistant/components/schedule/const.py b/homeassistant/components/schedule/const.py new file mode 100644 index 00000000000..e044a614e4d --- /dev/null +++ b/homeassistant/components/schedule/const.py @@ -0,0 +1,37 @@ +"""Constants for the schedule integration.""" +import logging +from typing import Final + +DOMAIN: Final = "schedule" +LOGGER = logging.getLogger(__package__) + +CONF_FRIDAY: Final = "friday" +CONF_FROM: Final = "from" +CONF_MONDAY: Final = "monday" +CONF_SATURDAY: Final = "saturday" +CONF_SUNDAY: Final = "sunday" +CONF_THURSDAY: Final = "thursday" +CONF_TO: Final = "to" +CONF_TUESDAY: Final = "tuesday" +CONF_WEDNESDAY: Final = "wednesday" +CONF_ALL_DAYS: Final = { + CONF_MONDAY, + CONF_TUESDAY, + CONF_WEDNESDAY, + CONF_THURSDAY, + CONF_FRIDAY, + CONF_SATURDAY, + CONF_SUNDAY, +} + +ATTR_NEXT_EVENT: Final = "next_event" + +WEEKDAY_TO_CONF: Final = { + 0: CONF_MONDAY, + 1: CONF_TUESDAY, + 2: CONF_WEDNESDAY, + 3: CONF_THURSDAY, + 4: CONF_FRIDAY, + 5: CONF_SATURDAY, + 6: CONF_SUNDAY, +} diff --git a/homeassistant/components/schedule/manifest.json b/homeassistant/components/schedule/manifest.json new file mode 100644 index 00000000000..f36185e7ba7 --- /dev/null +++ b/homeassistant/components/schedule/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "schedule", + "integration_type": "helper", + "name": "Schedule", + "documentation": "https://www.home-assistant.io/integrations/schedule", + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/schedule/recorder.py b/homeassistant/components/schedule/recorder.py new file mode 100644 index 00000000000..b9911e0544b --- /dev/null +++ b/homeassistant/components/schedule/recorder.py @@ -0,0 +1,16 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.const import ATTR_EDITABLE +from homeassistant.core import HomeAssistant, callback + +from .const import ATTR_NEXT_EVENT + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude configuration to be recorded in the database.""" + return { + ATTR_EDITABLE, + ATTR_NEXT_EVENT, + } diff --git a/homeassistant/components/schedule/services.yaml b/homeassistant/components/schedule/services.yaml new file mode 100644 index 00000000000..b34dd5e83da --- /dev/null +++ b/homeassistant/components/schedule/services.yaml @@ -0,0 +1,3 @@ +reload: + name: Reload + description: Reload the schedule configuration diff --git a/homeassistant/components/schedule/strings.json b/homeassistant/components/schedule/strings.json new file mode 100644 index 00000000000..fdcb8c4ffdc --- /dev/null +++ b/homeassistant/components/schedule/strings.json @@ -0,0 +1,9 @@ +{ + "title": "Schedule", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant/components/schedule/translations/en.json b/homeassistant/components/schedule/translations/en.json new file mode 100644 index 00000000000..7b161cddd18 --- /dev/null +++ b/homeassistant/components/schedule/translations/en.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Off", + "on": "On" + } + }, + "title": "Schedule" +} \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index 26f76c24a02..700d96a4982 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2066,6 +2066,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.schedule.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.select.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5ce67b59198..1e6bd03f457 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -83,6 +83,7 @@ NO_IOT_CLASS = [ "raspberry_pi", "repairs", "safe_mode", + "schedule", "script", "search", "system_health", diff --git a/tests/components/schedule/__init__.py b/tests/components/schedule/__init__.py new file mode 100644 index 00000000000..86e5cb9a27d --- /dev/null +++ b/tests/components/schedule/__init__.py @@ -0,0 +1 @@ +"""Tests for the schedule integration.""" diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py new file mode 100644 index 00000000000..d559dc27a9a --- /dev/null +++ b/tests/components/schedule/test_init.py @@ -0,0 +1,376 @@ +"""Test for the Schedule integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Coroutine +from typing import Any +from unittest.mock import patch + +from aiohttp import ClientWebSocketResponse +from freezegun import freeze_time +import pytest + +from homeassistant.components.schedule import STORAGE_VERSION, STORAGE_VERSION_MINOR +from homeassistant.components.schedule.const import ( + ATTR_NEXT_EVENT, + CONF_FRIDAY, + CONF_FROM, + CONF_MONDAY, + CONF_SATURDAY, + CONF_SUNDAY, + CONF_THURSDAY, + CONF_TO, + CONF_TUESDAY, + CONF_WEDNESDAY, + DOMAIN, +) +from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_NAME, + CONF_ICON, + CONF_ID, + CONF_NAME, + SERVICE_RELOAD, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockUser, async_fire_time_changed + + +@pytest.fixture +def schedule_setup( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> Callable[..., Coroutine[Any, Any, bool]]: + """Schedule setup.""" + + async def _schedule_setup( + items: dict[str, Any] | None = None, + config: dict[str, Any] | None = None, + ) -> bool: + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": STORAGE_VERSION, + "minor_version": STORAGE_VERSION_MINOR, + "data": { + "items": [ + { + CONF_ID: "from_storage", + CONF_NAME: "from storage", + CONF_ICON: "mdi:party-popper", + CONF_FRIDAY: [ + {CONF_FROM: "17:00:00", CONF_TO: "23:59:59"}, + ], + CONF_SATURDAY: [ + {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}, + ], + CONF_SUNDAY: [ + {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}, + ], + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "minor_version": STORAGE_VERSION_MINOR, + "data": {"items": items}, + } + if config is None: + config = { + DOMAIN: { + "from_yaml": { + CONF_NAME: "from yaml", + CONF_ICON: "mdi:party-pooper", + CONF_MONDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_TUESDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_WEDNESDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_THURSDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_FRIDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_SATURDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_SUNDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + } + } + } + return await async_setup_component(hass, DOMAIN, config) + + return _schedule_setup + + +async def test_invalid_config(hass: HomeAssistant) -> None: + """Test invalid configs.""" + invalid_configs = [ + None, + {}, + {"name with space": None}, + ] + + for cfg in invalid_configs: + assert not await async_setup_component(hass, DOMAIN, {DOMAIN: cfg}) + + +@pytest.mark.parametrize( + "schedule,error", + ( + ( + [ + {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}, + {CONF_FROM: "07:00:00", CONF_TO: "08:00:00"}, + ], + "Overlapping times found in schedule", + ), + ( + [ + {CONF_FROM: "07:00:00", CONF_TO: "08:00:00"}, + {CONF_FROM: "07:00:00", CONF_TO: "08:00:00"}, + ], + "Overlapping times found in schedule", + ), + ( + [ + {CONF_FROM: "07:59:00", CONF_TO: "09:00:00"}, + {CONF_FROM: "07:00:00", CONF_TO: "08:00:00"}, + ], + "Overlapping times found in schedule", + ), + ( + [ + {CONF_FROM: "06:00:00", CONF_TO: "07:00:00"}, + {CONF_FROM: "06:59:00", CONF_TO: "08:00:00"}, + ], + "Overlapping times found in schedule", + ), + ( + [ + {CONF_FROM: "06:00:00", CONF_TO: "05:00:00"}, + ], + "Invalid time range, from 06:00:00 is after 05:00:00", + ), + ), +) +async def test_invalid_schedules( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + caplog: pytest.LogCaptureFixture, + schedule: list[dict[str, str]], + error: str, +) -> None: + """Test overlapping time ranges invalidate.""" + assert not await schedule_setup( + config={ + DOMAIN: { + "from_yaml": { + CONF_NAME: "from yaml", + CONF_ICON: "mdi:party-pooper", + CONF_SUNDAY: schedule, + } + } + } + ) + assert error in caplog.text + + +async def test_setup_no_config(hass: HomeAssistant, hass_admin_user: MockUser) -> None: + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) + + +@pytest.mark.freeze_time("2022-08-10 20:10:00-07:00") +async def test_load( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test set up from storage and YAML.""" + assert await schedule_setup() + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" + assert state.attributes[ATTR_EDITABLE] is True + assert state.attributes[ATTR_ICON] == "mdi:party-popper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-12T17:00:00-07:00" + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_FRIENDLY_NAME] == "from yaml" + assert state.attributes[ATTR_EDITABLE] is False + assert state.attributes[ATTR_ICON] == "mdi:party-pooper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-10T23:59:59-07:00" + + +async def test_schedule_updates( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test the schedule updates when time changes.""" + with freeze_time("2022-08-10 20:10:00-07:00"): + assert await schedule_setup() + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-12T17:00:00-07:00" + + with freeze_time(state.attributes[ATTR_NEXT_EVENT]): + async_fire_time_changed(hass, state.attributes[ATTR_NEXT_EVENT]) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-12T23:59:59-07:00" + + +async def test_ws_list( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test listing via WS.""" + assert await schedule_setup() + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert result["from_storage"][ATTR_NAME] == "from storage" + assert "from_yaml" not in result + + +async def test_ws_delete( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test WS delete cleans up entity registry.""" + ent_reg = er.async_get(hass) + + assert await schedule_setup() + + state = hass.states.get("schedule.from_storage") + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "from_storage") is not None + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 1, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": "from_storage"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get("schedule.from_storage") + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "from_storage") is None + + +@pytest.mark.freeze_time("2022-08-10 20:10:00-07:00") +async def test_update( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test updating the schedule.""" + ent_reg = er.async_get(hass) + + assert await schedule_setup() + + state = hass.states.get("schedule.from_storage") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" + assert state.attributes[ATTR_ICON] == "mdi:party-popper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-12T17:00:00-07:00" + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "from_storage") is not None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": "from_storage", + CONF_NAME: "Party pooper", + CONF_ICON: "mdi:party-pooper", + CONF_MONDAY: [], + CONF_TUESDAY: [], + CONF_WEDNESDAY: [{CONF_FROM: "17:00:00", CONF_TO: "23:59:59"}], + CONF_THURSDAY: [], + CONF_FRIDAY: [], + CONF_SATURDAY: [], + CONF_SUNDAY: [], + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get("schedule.from_storage") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_FRIENDLY_NAME] == "Party pooper" + assert state.attributes[ATTR_ICON] == "mdi:party-pooper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-10T23:59:59-07:00" + + +@pytest.mark.freeze_time("2022-08-11 8:52:00-07:00") +async def test_ws_create( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test create WS.""" + ent_reg = er.async_get(hass) + + assert await schedule_setup(items=[]) + + state = hass.states.get("schedule.party_mode") + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "party_mode") is None + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": f"{DOMAIN}/create", + "name": "Party mode", + "icon": "mdi:party-popper", + "monday": [{"from": "12:00:00", "to": "14:00:00"}], + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get("schedule.party_mode") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == "Party mode" + assert state.attributes[ATTR_EDITABLE] is True + assert state.attributes[ATTR_ICON] == "mdi:party-popper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-15T12:00:00-07:00" diff --git a/tests/components/schedule/test_recorder.py b/tests/components/schedule/test_recorder.py new file mode 100644 index 00000000000..a105f388fc2 --- /dev/null +++ b/tests/components/schedule/test_recorder.py @@ -0,0 +1,70 @@ +"""The tests for recorder platform.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components.recorder.db_schema import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.components.schedule.const import ATTR_NEXT_EVENT, DOMAIN +from homeassistant.const import ATTR_EDITABLE, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.core import HomeAssistant, State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.recorder.common import async_wait_recording_done + + +async def test_exclude_attributes( + hass: HomeAssistant, + recorder_mock: None, + enable_custom_integrations: None, +) -> None: + """Test attributes to be excluded.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test": { + "name": "Party mode", + "icon": "mdi:party-popper", + "monday": [{"from": "1:00", "to": "2:00"}], + "tuesday": [{"from": "2:00", "to": "3:00"}], + "wednesday": [{"from": "3:00", "to": "4:00"}], + "thursday": [{"from": "5:00", "to": "6:00"}], + "friday": [{"from": "7:00", "to": "8:00"}], + "saturday": [{"from": "9:00", "to": "10:00"}], + "sunday": [{"from": "11:00", "to": "12:00"}], + } + } + }, + ) + + state = hass.states.get("schedule.test") + assert state + assert state.attributes[ATTR_EDITABLE] is False + assert state.attributes[ATTR_FRIENDLY_NAME] + assert state.attributes[ATTR_ICON] + assert state.attributes[ATTR_NEXT_EVENT] + + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) == 1 + assert ATTR_EDITABLE not in states[0].attributes + assert ATTR_FRIENDLY_NAME in states[0].attributes + assert ATTR_ICON in states[0].attributes + assert ATTR_NEXT_EVENT not in states[0].attributes From 51ca74b3d12aed9c6384cb6a5a9c3cc1d3ff6c7c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 11 Aug 2022 23:24:55 +0100 Subject: [PATCH 307/903] Fix titles for discoveries and device names in xiaomi_ble (#76618) --- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/xiaomi_ble/test_config_flow.py | 38 ++++++------- tests/components/xiaomi_ble/test_sensor.py | 56 +++++++++++-------- 5 files changed, 56 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index cdcac07b5c9..8c1d47ee423 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.8.1"], + "requirements": ["xiaomi-ble==0.8.2"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 3054c425ecc..0e4e9b2c021 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.1 +xiaomi-ble==0.8.2 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a16718a237..762cdd16472 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.1 +xiaomi-ble==0.8.2 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 32ba6be3322..1871b0ae387 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -39,7 +39,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "MMC_T201_1" + assert result2["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -65,7 +65,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "LYWSD02MMC" + assert result2["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" assert result2["data"] == {} assert result2["result"].unique_id == "A4:C1:38:56:53:84" @@ -123,7 +123,7 @@ async def test_async_step_bluetooth_during_onboarding(hass): ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "MMC_T201_1" + assert result["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" assert result["data"] == {} assert result["result"].unique_id == "00:81:F9:DD:6F:C1" assert len(mock_setup_entry.mock_calls) == 1 @@ -148,7 +148,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption(hass): user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -180,7 +180,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key(has user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -214,7 +214,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_len user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -238,7 +238,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -272,7 +272,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -306,7 +306,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -370,7 +370,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "58:2D:34:35:93:21"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "LYWSDCGQ" + assert result2["title"] == "Temperature/Humidity Sensor 359321 (LYWSDCGQ)" assert result2["data"] == {} assert result2["result"].unique_id == "58:2D:34:35:93:21" @@ -405,7 +405,7 @@ async def test_async_step_user_short_payload(hass): result["flow_id"], user_input={} ) assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "LYWSD02MMC" + assert result3["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" assert result3["data"] == {} assert result3["result"].unique_id == "A4:C1:38:56:53:84" @@ -453,7 +453,7 @@ async def test_async_step_user_short_payload_then_full(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "LYWSD02MMC" + assert result2["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} @@ -486,7 +486,7 @@ async def test_async_step_user_with_found_devices_v4_encryption(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -532,7 +532,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -580,7 +580,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -613,7 +613,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption(hass): user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -658,7 +658,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -703,7 +703,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -792,7 +792,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "00:81:F9:DD:6F:C1"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "MMC_T201_1" + assert result2["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 063e4a22c2d..b49d65f58ae 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -39,10 +39,13 @@ async def test_sensors(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 - temp_sensor = hass.states.get("sensor.mmc_t201_1_temperature") + temp_sensor = hass.states.get("sensor.baby_thermometer_dd6fc1_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "36.8719980616822" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "MMC_T201_1 Temperature" + assert ( + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] + == "Baby Thermometer DD6FC1 Temperature" + ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -86,10 +89,10 @@ async def test_xiaomi_formaldeyhde(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("sensor.test_device_formaldehyde") + sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_formaldehyde") sensor_attr = sensor.attributes assert sensor.state == "2.44" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Formaldehyde" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Formaldehyde" assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "mg/m³" assert sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -133,10 +136,10 @@ async def test_xiaomi_consumable(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("sensor.test_device_consumable") + sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_consumable") sensor_attr = sensor.attributes assert sensor.state == "96" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Consumable" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Consumable" assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" assert sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -180,17 +183,17 @@ async def test_xiaomi_battery_voltage(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 - volt_sensor = hass.states.get("sensor.test_device_voltage") + volt_sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_voltage") volt_sensor_attr = volt_sensor.attributes assert volt_sensor.state == "3.1" - assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Voltage" + assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Voltage" assert volt_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "V" assert volt_sensor_attr[ATTR_STATE_CLASS] == "measurement" - bat_sensor = hass.states.get("sensor.test_device_battery") + bat_sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_battery") bat_sensor_attr = bat_sensor.attributes assert bat_sensor.state == "100" - assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Battery" assert bat_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" assert bat_sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -248,38 +251,42 @@ async def test_xiaomi_HHCCJCY01(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 - illum_sensor = hass.states.get("sensor.test_device_illuminance") + illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") illum_sensor_attr = illum_sensor.attributes assert illum_sensor.state == "0" - assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Illuminance" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" - cond_sensor = hass.states.get("sensor.test_device_conductivity") + cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" - assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Conductivity" + assert ( + cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" + ) assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - moist_sensor = hass.states.get("sensor.test_device_moisture") + moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") moist_sensor_attribtes = moist_sensor.attributes assert moist_sensor.state == "64" - assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Moisture" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "24.4" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert ( + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" + ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - batt_sensor = hass.states.get("sensor.test_device_battery") + batt_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_battery") batt_sensor_attribtes = batt_sensor.attributes assert batt_sensor.state == "5" - assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Battery" assert batt_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert batt_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -321,10 +328,15 @@ async def test_xiaomi_CGDK2(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor = hass.states.get( + "sensor.temperature_humidity_sensor_122089_temperature" + ) temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "22.6" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert ( + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] + == "Temperature/Humidity Sensor 122089 Temperature" + ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From 4f064268b01cd7626596e711219515cf9241e665 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 13:55:52 -1000 Subject: [PATCH 308/903] Add missing _abort_if_unique_id_configured to ble integrations (#76624) --- .../components/govee_ble/config_flow.py | 1 + .../components/inkbird/config_flow.py | 1 + homeassistant/components/moat/config_flow.py | 1 + .../components/sensorpush/config_flow.py | 1 + .../components/xiaomi_ble/config_flow.py | 1 + .../components/govee_ble/test_config_flow.py | 30 +++++++++++++++++++ tests/components/inkbird/test_config_flow.py | 28 +++++++++++++++++ tests/components/moat/test_config_flow.py | 28 +++++++++++++++++ .../components/sensorpush/test_config_flow.py | 30 +++++++++++++++++++ .../components/xiaomi_ble/test_config_flow.py | 30 +++++++++++++++++++ 10 files changed, 151 insertions(+) diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index 1e3a5566bfd..47d73f2779a 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -67,6 +67,7 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py index 21ed85e117e..524471bbcc7 100644 --- a/homeassistant/components/inkbird/config_flow.py +++ b/homeassistant/components/inkbird/config_flow.py @@ -67,6 +67,7 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py index 6f51b62d110..0e1b4f89568 100644 --- a/homeassistant/components/moat/config_flow.py +++ b/homeassistant/components/moat/config_flow.py @@ -67,6 +67,7 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py index d10c2f481a6..63edd59a5b7 100644 --- a/homeassistant/components/sensorpush/config_flow.py +++ b/homeassistant/components/sensorpush/config_flow.py @@ -67,6 +67,7 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index a05e703db6a..4ec3b66d0f9 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -205,6 +205,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() discovery = self._discovered_devices[address] self.context["title_placeholders"] = {"name": discovery.title} diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index a1b9fed3cd7..188672cdf18 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -78,6 +78,36 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index c1f8b3ef545..fe210f75f4b 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -74,6 +74,34 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/moat/test_config_flow.py b/tests/components/moat/test_config_flow.py index 7ceeb2ad73f..6e92be703a2 100644 --- a/tests/components/moat/test_config_flow.py +++ b/tests/components/moat/test_config_flow.py @@ -74,6 +74,34 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py index 1c825640603..244787eecb9 100644 --- a/tests/components/sensorpush/test_config_flow.py +++ b/tests/components/sensorpush/test_config_flow.py @@ -78,6 +78,36 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 1871b0ae387..6a1c9c8e435 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -708,6 +708,36 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le assert result2["result"].unique_id == "F8:24:41:C5:98:8B" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:35:93:21", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "58:2D:34:35:93:21"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( From 7c81f790a71a26bf33d7fda4b8f0a76f26cb0b00 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 12 Aug 2022 00:23:47 +0000 Subject: [PATCH 309/903] [ci skip] Translation update --- .../components/adax/translations/es.json | 10 +++--- .../components/adguard/translations/es.json | 2 +- .../components/aemet/translations/es.json | 2 +- .../components/airthings/translations/es.json | 2 +- .../airvisual/translations/sensor.es.json | 4 +-- .../components/airzone/translations/es.json | 2 +- .../alarm_control_panel/translations/es.json | 8 ++--- .../components/almond/translations/es.json | 2 +- .../components/ambee/translations/es.json | 4 +-- .../amberelectric/translations/es.json | 2 +- .../android_ip_webcam/translations/id.json | 26 ++++++++++++++ .../android_ip_webcam/translations/pl.json | 26 ++++++++++++++ .../components/androidtv/translations/es.json | 26 +++++++------- .../components/apple_tv/translations/es.json | 14 ++++---- .../components/asuswrt/translations/es.json | 4 +-- .../components/august/translations/es.json | 8 ++--- .../aurora_abb_powerone/translations/es.json | 10 +++--- .../aussie_broadband/translations/es.json | 16 ++++----- .../components/auth/translations/es.json | 2 +- .../components/awair/translations/ca.json | 25 +++++++++++-- .../components/awair/translations/de.json | 31 ++++++++++++++-- .../components/awair/translations/en.json | 31 ++++++++++++++-- .../components/awair/translations/es.json | 31 ++++++++++++++-- .../components/awair/translations/et.json | 31 ++++++++++++++-- .../components/awair/translations/id.json | 31 ++++++++++++++-- .../components/awair/translations/pl.json | 31 ++++++++++++++-- .../azure_event_hub/translations/es.json | 14 ++++---- .../components/balboa/translations/es.json | 4 +-- .../binary_sensor/translations/es.json | 12 +++---- .../components/bosch_shc/translations/es.json | 10 +++--- .../components/brother/translations/es.json | 2 +- .../components/brunt/translations/es.json | 4 +-- .../components/bsblan/translations/es.json | 2 +- .../components/cast/translations/es.json | 8 ++--- .../components/climacell/translations/es.json | 6 ++-- .../climacell/translations/sensor.es.json | 10 +++--- .../cloudflare/translations/es.json | 2 +- .../components/co2signal/translations/es.json | 6 ++-- .../components/coinbase/translations/es.json | 6 ++-- .../components/cpuspeed/translations/es.json | 6 ++-- .../crownstone/translations/es.json | 34 +++++++++--------- .../components/daikin/translations/es.json | 2 +- .../components/deconz/translations/es.json | 2 +- .../components/deluge/translations/es.json | 6 ++-- .../components/demo/translations/pl.json | 13 ++++++- .../derivative/translations/es.json | 6 ++-- .../deutsche_bahn/translations/pl.json | 8 +++++ .../devolo_home_network/translations/es.json | 10 +++--- .../components/dlna_dmr/translations/es.json | 14 ++++---- .../components/dlna_dms/translations/es.json | 10 +++--- .../components/dnsip/translations/es.json | 12 +++---- .../components/dsmr/translations/es.json | 12 +++---- .../components/efergy/translations/es.json | 4 +-- .../components/elgato/translations/es.json | 2 +- .../components/elkm1/translations/es.json | 18 +++++----- .../components/elmax/translations/es.json | 8 ++--- .../components/emonitor/translations/es.json | 2 +- .../enphase_envoy/translations/es.json | 4 +-- .../environment_canada/translations/es.json | 4 +-- .../components/escea/translations/pl.json | 13 +++++++ .../components/esphome/translations/es.json | 10 +++--- .../components/ezviz/translations/es.json | 8 ++--- .../faa_delays/translations/es.json | 4 +-- .../components/fan/translations/es.json | 2 +- .../components/fibaro/translations/es.json | 4 +-- .../components/filesize/translations/es.json | 2 +- .../components/fivem/translations/es.json | 2 +- .../components/flipr/translations/es.json | 2 +- .../components/flume/translations/es.json | 2 +- .../flunearyou/translations/pl.json | 13 +++++++ .../components/flux_led/translations/es.json | 10 +++--- .../forecast_solar/translations/es.json | 10 +++--- .../freedompro/translations/es.json | 2 +- .../components/fritz/translations/es.json | 12 +++---- .../components/fronius/translations/es.json | 4 +-- .../garages_amsterdam/translations/es.json | 4 +-- .../components/geofency/translations/es.json | 2 +- .../components/github/translations/es.json | 6 ++-- .../components/goalzero/translations/es.json | 2 +- .../components/google/translations/es.json | 18 +++++----- .../google_travel_time/translations/es.json | 6 ++-- .../components/group/translations/es.json | 36 +++++++++---------- .../components/guardian/translations/de.json | 13 +++++++ .../components/guardian/translations/et.json | 13 +++++++ .../components/guardian/translations/hu.json | 13 +++++++ .../components/guardian/translations/id.json | 13 +++++++ .../components/guardian/translations/no.json | 13 +++++++ .../components/guardian/translations/pl.json | 13 +++++++ .../guardian/translations/pt-BR.json | 13 +++++++ .../guardian/translations/zh-Hant.json | 13 +++++++ .../components/habitica/translations/es.json | 2 +- .../hisense_aehw4a1/translations/es.json | 2 +- .../components/hive/translations/es.json | 16 ++++----- .../components/homekit/translations/es.json | 14 ++++---- .../homekit_controller/translations/es.json | 2 +- .../translations/select.es.json | 4 +-- .../homewizard/translations/es.json | 8 ++--- .../components/honeywell/translations/es.json | 6 ++-- .../huawei_lte/translations/es.json | 4 +-- .../components/hue/translations/es.json | 10 +++--- .../humidifier/translations/es.json | 2 +- .../translations/es.json | 2 +- .../components/hyperion/translations/es.json | 2 +- .../components/ifttt/translations/es.json | 2 +- .../integration/translations/es.json | 2 +- .../intellifire/translations/es.json | 6 ++-- .../components/iotawatt/translations/es.json | 4 +-- .../components/iss/translations/es.json | 2 +- .../justnimbus/translations/id.json | 19 ++++++++++ .../justnimbus/translations/pl.json | 19 ++++++++++ .../kaleidescape/translations/es.json | 6 ++-- .../keenetic_ndms2/translations/es.json | 16 ++++----- .../components/kmtronic/translations/es.json | 6 ++-- .../components/knx/translations/es.json | 20 +++++------ .../components/kodi/translations/es.json | 2 +- .../components/kraken/translations/es.json | 4 --- .../launch_library/translations/es.json | 2 +- .../components/lcn/translations/es.json | 2 +- .../components/life360/translations/es.json | 2 +- .../components/light/translations/es.json | 2 +- .../components/litejet/translations/es.json | 4 +-- .../litterrobot/translations/es.json | 8 ++--- .../media_player/translations/es.json | 2 +- .../met_eireann/translations/es.json | 2 +- .../components/mill/translations/es.json | 4 +-- .../components/min_max/translations/es.json | 2 +- .../components/mjpeg/translations/es.json | 24 ++++++------- .../modem_callerid/translations/es.json | 4 +-- .../modern_forms/translations/es.json | 4 +-- .../moehlenhoff_alpha2/translations/es.json | 2 +- .../components/moon/translations/es.json | 4 +-- .../motion_blinds/translations/es.json | 2 +- .../components/motioneye/translations/es.json | 10 +++--- .../components/mqtt/translations/es.json | 2 +- .../components/mullvad/translations/es.json | 2 +- .../components/mutesync/translations/es.json | 2 +- .../components/myq/translations/es.json | 2 +- .../components/mysensors/translations/pl.json | 9 +++++ .../components/nam/translations/es.json | 8 ++--- .../components/nanoleaf/translations/es.json | 4 +-- .../components/nest/translations/es.json | 10 +++--- .../components/netatmo/translations/es.json | 14 ++++---- .../components/netgear/translations/es.json | 8 ++--- .../nfandroidtv/translations/es.json | 2 +- .../components/nina/translations/es.json | 6 ++-- .../nmap_tracker/translations/es.json | 14 ++++---- .../components/notion/translations/es.json | 2 +- .../components/nuki/translations/es.json | 2 +- .../components/octoprint/translations/es.json | 6 ++-- .../components/omnilogic/translations/es.json | 2 +- .../components/oncue/translations/es.json | 2 +- .../components/onewire/translations/es.json | 10 +++--- .../open_meteo/translations/es.json | 2 +- .../openexchangerates/translations/pl.json | 33 +++++++++++++++++ .../opentherm_gw/translations/es.json | 2 +- .../components/overkiz/translations/es.json | 14 ++++---- .../overkiz/translations/sensor.es.json | 12 +++---- .../p1_monitor/translations/es.json | 2 +- .../philips_js/translations/es.json | 4 +-- .../components/picnic/translations/es.json | 2 +- .../components/plugwise/translations/es.json | 2 +- .../components/powerwall/translations/es.json | 18 +++++----- .../components/prosegur/translations/es.json | 2 +- .../pure_energie/translations/es.json | 10 +++--- .../components/pvoutput/translations/es.json | 6 ++-- .../radio_browser/translations/es.json | 4 +-- .../rainforest_eagle/translations/es.json | 2 +- .../components/remote/translations/es.json | 2 +- .../components/ridwell/translations/es.json | 6 ++-- .../translations/es.json | 4 +-- .../rtsp_to_webrtc/translations/es.json | 18 +++++----- .../components/samsungtv/translations/es.json | 14 ++++---- .../components/schedule/translations/ca.json | 9 +++++ .../components/schedule/translations/de.json | 9 +++++ .../components/schedule/translations/es.json | 9 +++++ .../components/schedule/translations/et.json | 9 +++++ .../components/schedule/translations/id.json | 9 +++++ .../components/schedule/translations/pl.json | 9 +++++ .../screenlogic/translations/es.json | 8 ++--- .../components/select/translations/es.json | 6 ++-- .../components/sense/translations/es.json | 10 +++--- .../components/senseme/translations/es.json | 6 ++-- .../components/sensibo/translations/es.json | 8 ++--- .../components/sensor/translations/es.json | 32 ++++++++--------- .../components/shelly/translations/es.json | 4 +-- .../components/sia/translations/es.json | 18 +++++----- .../simplepush/translations/pl.json | 4 +++ .../simplisafe/translations/es.json | 2 +- .../simplisafe/translations/pl.json | 3 +- .../components/sleepiq/translations/es.json | 12 +++---- .../components/smappee/translations/es.json | 2 +- .../components/smarttub/translations/es.json | 2 +- .../components/solax/translations/es.json | 2 +- .../components/steamist/translations/es.json | 10 +++--- .../components/subaru/translations/es.json | 6 ++-- .../components/sun/translations/es.json | 4 +-- .../components/switch/translations/es.json | 2 +- .../switch_as_x/translations/es.json | 2 +- .../components/switchbot/translations/de.json | 9 +++++ .../components/switchbot/translations/es.json | 2 +- .../components/switchbot/translations/et.json | 9 +++++ .../components/switchbot/translations/id.json | 9 +++++ .../components/switchbot/translations/no.json | 9 +++++ .../components/switchbot/translations/pl.json | 9 +++++ .../switchbot/translations/zh-Hant.json | 9 +++++ .../components/syncthing/translations/es.json | 2 +- .../synology_dsm/translations/es.json | 2 +- .../system_bridge/translations/es.json | 2 +- .../components/tailscale/translations/es.json | 8 ++--- .../tesla_wall_connector/translations/es.json | 4 +-- .../components/threshold/translations/es.json | 4 +-- .../components/tile/translations/es.json | 2 +- .../components/tod/translations/es.json | 12 +++---- .../components/tolo/translations/es.json | 2 +- .../tomorrowio/translations/es.json | 8 ++--- .../tomorrowio/translations/sensor.es.json | 8 ++--- .../totalconnect/translations/es.json | 4 +-- .../components/tplink/translations/es.json | 2 +- .../components/traccar/translations/es.json | 2 +- .../components/tractive/translations/es.json | 2 +- .../tractive/translations/sensor.es.json | 6 ++-- .../components/tradfri/translations/es.json | 2 +- .../translations/es.json | 4 +-- .../components/tuya/translations/es.json | 4 +-- .../tuya/translations/select.es.json | 16 ++++----- .../tuya/translations/sensor.es.json | 4 +-- .../unifiprotect/translations/es.json | 12 +++---- .../unifiprotect/translations/id.json | 1 + .../unifiprotect/translations/pl.json | 1 + .../components/uptime/translations/es.json | 4 +-- .../uptimerobot/translations/es.json | 12 +++---- .../uptimerobot/translations/sensor.es.json | 10 +++--- .../components/vallox/translations/es.json | 2 +- .../components/verisure/translations/es.json | 10 +++--- .../components/version/translations/es.json | 6 ++-- .../components/vicare/translations/es.json | 6 ++-- .../vlc_telnet/translations/es.json | 6 ++-- .../components/wallbox/translations/es.json | 2 +- .../components/watttime/translations/es.json | 14 ++++---- .../waze_travel_time/translations/es.json | 4 +-- .../components/webostv/translations/es.json | 14 ++++---- .../components/whois/translations/es.json | 2 +- .../components/wiz/translations/es.json | 16 ++++----- .../components/wled/translations/es.json | 4 +-- .../xiaomi_miio/translations/es.json | 12 +++---- .../yale_smart_alarm/translations/es.json | 12 +++---- .../yalexs_ble/translations/de.json | 30 ++++++++++++++++ .../yalexs_ble/translations/et.json | 30 ++++++++++++++++ .../yalexs_ble/translations/id.json | 30 ++++++++++++++++ .../yalexs_ble/translations/no.json | 30 ++++++++++++++++ .../yalexs_ble/translations/pl.json | 30 ++++++++++++++++ .../yalexs_ble/translations/zh-Hant.json | 30 ++++++++++++++++ .../yamaha_musiccast/translations/es.json | 2 +- .../components/zha/translations/es.json | 18 +++++----- .../components/zwave_js/translations/es.json | 34 +++++++++--------- .../components/zwave_me/translations/es.json | 2 +- 256 files changed, 1465 insertions(+), 724 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/translations/id.json create mode 100644 homeassistant/components/android_ip_webcam/translations/pl.json create mode 100644 homeassistant/components/deutsche_bahn/translations/pl.json create mode 100644 homeassistant/components/escea/translations/pl.json create mode 100644 homeassistant/components/justnimbus/translations/id.json create mode 100644 homeassistant/components/justnimbus/translations/pl.json create mode 100644 homeassistant/components/openexchangerates/translations/pl.json create mode 100644 homeassistant/components/schedule/translations/ca.json create mode 100644 homeassistant/components/schedule/translations/de.json create mode 100644 homeassistant/components/schedule/translations/es.json create mode 100644 homeassistant/components/schedule/translations/et.json create mode 100644 homeassistant/components/schedule/translations/id.json create mode 100644 homeassistant/components/schedule/translations/pl.json create mode 100644 homeassistant/components/yalexs_ble/translations/de.json create mode 100644 homeassistant/components/yalexs_ble/translations/et.json create mode 100644 homeassistant/components/yalexs_ble/translations/id.json create mode 100644 homeassistant/components/yalexs_ble/translations/no.json create mode 100644 homeassistant/components/yalexs_ble/translations/pl.json create mode 100644 homeassistant/components/yalexs_ble/translations/zh-Hant.json diff --git a/homeassistant/components/adax/translations/es.json b/homeassistant/components/adax/translations/es.json index ea350d97f4e..c2d224aacb1 100644 --- a/homeassistant/components/adax/translations/es.json +++ b/homeassistant/components/adax/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "heater_not_available": "Calentador no disponible. Intente restablecer el calentador pulsando + y OK durante algunos segundos.", - "heater_not_found": "No se encuentra el calefactor. Intente acercar el calefactor al ordenador del Asistente de Hogar.", + "heater_not_available": "Calefactor no disponible. Intenta reiniciar el calentador presionando + y OK durante unos segundos.", + "heater_not_found": "No se encontr\u00f3 el calentador. Intenta acercar el calentador al ordenador con Home Assistant.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "error": { @@ -21,13 +21,13 @@ "wifi_pswd": "Contrase\u00f1a Wi-Fi", "wifi_ssid": "SSID Wi-Fi" }, - "description": "Reinicie el calentador presionando + y OK hasta que la pantalla muestre 'Reiniciar'. Luego presione y mantenga presionado el bot\u00f3n OK en el calentador hasta que el LED azul comience a parpadear antes de presionar Enviar. La configuraci\u00f3n del calentador puede llevar algunos minutos." + "description": "Reinicia el calentador presionando + y OK hasta que la pantalla muestre 'Restablecer'. Luego mant\u00e9n presionado el bot\u00f3n OK en el calentador hasta que el led azul comience a parpadear antes de presionar Enviar. La configuraci\u00f3n del calentador puede tardar algunos minutos." }, "user": { "data": { - "connection_type": "Seleccione el tipo de conexi\u00f3n" + "connection_type": "Selecciona el tipo de conexi\u00f3n" }, - "description": "Seleccione el tipo de conexi\u00f3n. Local requiere calentadores con bluetooth" + "description": "Selecciona el tipo de conexi\u00f3n. Local requiere calefactores con bluetooth" } } } diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index f7bfb1203d7..96a4546f735 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Supervisor: {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a AdGuard Home proporcionado por el complemento: {addon} ?", "title": "AdGuard Home v\u00eda complemento de Home Assistant" }, "user": { diff --git a/homeassistant/components/aemet/translations/es.json b/homeassistant/components/aemet/translations/es.json index b46f7720789..be22291c270 100644 --- a/homeassistant/components/aemet/translations/es.json +++ b/homeassistant/components/aemet/translations/es.json @@ -14,7 +14,7 @@ "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" + "description": "Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/airthings/translations/es.json b/homeassistant/components/airthings/translations/es.json index 5feddd20875..c5b0d338a03 100644 --- a/homeassistant/components/airthings/translations/es.json +++ b/homeassistant/components/airthings/translations/es.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "description": "Inicie sesi\u00f3n en {url} para encontrar sus credenciales", + "description": "Inicia sesi\u00f3n en {url} para encontrar tus credenciales", "id": "ID", "secret": "Secreto" } diff --git a/homeassistant/components/airvisual/translations/sensor.es.json b/homeassistant/components/airvisual/translations/sensor.es.json index 113c17246ed..b5385066e52 100644 --- a/homeassistant/components/airvisual/translations/sensor.es.json +++ b/homeassistant/components/airvisual/translations/sensor.es.json @@ -12,8 +12,8 @@ "good": "Bueno", "hazardous": "Da\u00f1ino", "moderate": "Moderado", - "unhealthy": "Insalubre", - "unhealthy_sensitive": "Insalubre para grupos sensibles", + "unhealthy": "Poco saludable", + "unhealthy_sensitive": "Poco saludable para grupos sensibles", "very_unhealthy": "Muy poco saludable" } } diff --git a/homeassistant/components/airzone/translations/es.json b/homeassistant/components/airzone/translations/es.json index 1fb525c9851..20c05fa714f 100644 --- a/homeassistant/components/airzone/translations/es.json +++ b/homeassistant/components/airzone/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_system_id": "ID del sistema Airzone no v\u00e1lido" }, "step": { diff --git a/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant/components/alarm_control_panel/translations/es.json index 6255f8cc574..38c4c059b48 100644 --- a/homeassistant/components/alarm_control_panel/translations/es.json +++ b/homeassistant/components/alarm_control_panel/translations/es.json @@ -4,7 +4,7 @@ "arm_away": "Armar {entity_name} exterior", "arm_home": "Armar {entity_name} modo casa", "arm_night": "Armar {entity_name} por la noche", - "arm_vacation": "Armar las vacaciones de {entity_name}", + "arm_vacation": "Armar de vacaciones {entity_name}", "disarm": "Desarmar {entity_name}", "trigger": "Lanzar {entity_name}" }, @@ -12,7 +12,7 @@ "is_armed_away": "{entity_name} est\u00e1 armada ausente", "is_armed_home": "{entity_name} est\u00e1 armada en casa", "is_armed_night": "{entity_name} est\u00e1 armada noche", - "is_armed_vacation": "{entity_name} est\u00e1 armado de vacaciones", + "is_armed_vacation": "{entity_name} est\u00e1 armada de vacaciones", "is_disarmed": "{entity_name} est\u00e1 desarmada", "is_triggered": "{entity_name} est\u00e1 disparada" }, @@ -20,7 +20,7 @@ "armed_away": "{entity_name} armada ausente", "armed_home": "{entity_name} armada en casa", "armed_night": "{entity_name} armada noche", - "armed_vacation": "Vacaciones armadas de {entity_name}", + "armed_vacation": "{entity_name} en armada de vacaciones", "disarmed": "{entity_name} desarmada", "triggered": "{entity_name} activado" } @@ -32,7 +32,7 @@ "armed_custom_bypass": "Armada personalizada", "armed_home": "Armada en casa", "armed_night": "Armada noche", - "armed_vacation": "Vacaciones armadas", + "armed_vacation": "Armada de vacaciones", "arming": "Armando", "disarmed": "Desarmada", "disarming": "Desarmando", diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index 83e78317741..94c5e89be5d 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a Almond proporcionado por el complemento: {addon} ?", "title": "Almond a trav\u00e9s del complemento Supervisor" }, "pick_implementation": { diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json index 6484ac8c3a0..1bf2f5391ad 100644 --- a/homeassistant/components/ambee/translations/es.json +++ b/homeassistant/components/ambee/translations/es.json @@ -11,7 +11,7 @@ "reauth_confirm": { "data": { "api_key": "Clave API", - "description": "Vuelva a autenticarse con su cuenta de Ambee." + "description": "Vuelve a autenticarse con tu cuenta de Ambee." } }, "user": { @@ -21,7 +21,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Configure Ambee para que se integre con Home Assistant." + "description": "Configura Ambee para que se integre con Home Assistant." } } }, diff --git a/homeassistant/components/amberelectric/translations/es.json b/homeassistant/components/amberelectric/translations/es.json index 18af819d5d1..e8c40903e27 100644 --- a/homeassistant/components/amberelectric/translations/es.json +++ b/homeassistant/components/amberelectric/translations/es.json @@ -6,7 +6,7 @@ "site_name": "Nombre del sitio", "site_nmi": "Sitio NMI" }, - "description": "Seleccione el NMI del sitio que le gustar\u00eda agregar" + "description": "Selecciona el NMI del sitio que te gustar\u00eda a\u00f1adir" }, "user": { "data": { diff --git a/homeassistant/components/android_ip_webcam/translations/id.json b/homeassistant/components/android_ip_webcam/translations/id.json new file mode 100644 index 00000000000..430ebe3645f --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Android IP Webcam lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Android IP Webcam dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Android IP Webcam dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/pl.json b/homeassistant/components/android_ip_webcam/translations/pl.json new file mode 100644 index 00000000000..8698af51c4b --- /dev/null +++ b/homeassistant/components/android_ip_webcam/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" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Android IP Webcam przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Android IP Webcam zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/es.json b/homeassistant/components/androidtv/translations/es.json index 74c7484257c..b41827566ab 100644 --- a/homeassistant/components/androidtv/translations/es.json +++ b/homeassistant/components/androidtv/translations/es.json @@ -8,15 +8,15 @@ "adbkey_not_file": "No se ha encontrado el archivo de claves ADB", "cannot_connect": "No se pudo conectar", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", - "key_and_server": "S\u00f3lo proporciona la clave ADB o el servidor ADB", + "key_and_server": "Solo proporcione la clave ADB o el servidor ADB", "unknown": "Error inesperado" }, "step": { "user": { "data": { - "adb_server_ip": "Direcci\u00f3n IP del servidor ADB (dejar vac\u00edo para no utilizarlo)", + "adb_server_ip": "Direcci\u00f3n IP del servidor ADB (d\u00e9jalo vac\u00edo para no utilizarlo)", "adb_server_port": "Puerto del servidor ADB", - "adbkey": "Ruta de acceso a su archivo de clave ADB (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", + "adbkey": "Ruta a tu archivo de clave ADB (d\u00e9jalo en blanco para generarlo autom\u00e1ticamente)", "device_class": "Tipo de dispositivo", "host": "Host", "port": "Puerto" @@ -31,31 +31,31 @@ "step": { "apps": { "data": { - "app_delete": "Marque para eliminar esta aplicaci\u00f3n", - "app_id": "ID de la aplicaci\u00f3n", + "app_delete": "Marcar para eliminar esta aplicaci\u00f3n", + "app_id": "ID de aplicaci\u00f3n", "app_name": "Nombre de la aplicaci\u00f3n" }, - "description": "Configurar el ID de la aplicaci\u00f3n {app_id}", + "description": "Configurar el ID de aplicaci\u00f3n {app_id}", "title": "Configurar aplicaciones de Android TV" }, "init": { "data": { "apps": "Configurar la lista de aplicaciones", "exclude_unnamed_apps": "Excluir aplicaciones con nombre desconocido de la lista de fuentes", - "get_sources": "Recupere las aplicaciones en ejecuci\u00f3n como lista de fuentes", + "get_sources": "Recuperar las aplicaciones en ejecuci\u00f3n como la lista de fuentes", "screencap": "Usar captura de pantalla para la car\u00e1tula del \u00e1lbum", "state_detection_rules": "Configurar reglas de detecci\u00f3n de estado", - "turn_off_command": "Comando de apagado del shell de ADB (dejar vac\u00edo por defecto)", - "turn_on_command": "Comando de activaci\u00f3n del shell ADB (dejar vac\u00edo por defecto)" + "turn_off_command": "Comando de apagado de shell ADB (d\u00e9jalo vac\u00edo para usar el comando por defecto)", + "turn_on_command": "Comando de encendido de shell ADB (d\u00e9jalo vac\u00edo para usar el comando por defecto)" } }, "rules": { "data": { - "rule_delete": "Marque para eliminar esta regla", - "rule_id": "ID de la aplicaci\u00f3n", - "rule_values": "Lista de reglas de detecci\u00f3n de estados (ver documentaci\u00f3n)" + "rule_delete": "Marcar para eliminar esta regla", + "rule_id": "ID de aplicaci\u00f3n", + "rule_values": "Lista de reglas de detecci\u00f3n de estado (ver documentaci\u00f3n)" }, - "description": "Configurar la regla de detecci\u00f3n del ID {rule_id} de la aplicaci\u00f3n", + "description": "Configura la regla de detecci\u00f3n para la identificaci\u00f3n de la aplicaci\u00f3n {rule_id}", "title": "Configurar las reglas de detecci\u00f3n de estado de Android TV" } } diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 6b136463aa0..b73aa659ef6 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -5,12 +5,12 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", - "device_not_found": "No se ha encontrado el dispositivo durante la detecci\u00f3n, por favor, intente a\u00f1adirlo de nuevo.", - "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con el DNS de multidifusi\u00f3n (Zeroconf). Por favor, intente a\u00f1adir el dispositivo de nuevo.", - "ipv6_not_supported": "IPv6 no est\u00e1 soportado.", + "device_not_found": "No se encontr\u00f3 el dispositivo durante el descubrimiento, por favor intenta a\u00f1adirlo nuevamente.", + "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo nuevamente.", + "ipv6_not_supported": "IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", - "setup_failed": "No se ha podido configurar el dispositivo.", + "setup_failed": "No se pudo configurar el dispositivo.", "unknown": "Error inesperado" }, "error": { @@ -37,11 +37,11 @@ "title": "Emparejamiento" }, "password": { - "description": "Una contrase\u00f1a es requerida por '{protocolo}'. Esto a\u00fan no es compatible, deshabilite la contrase\u00f1a para continuar.", - "title": "Se requiere una contrase\u00f1a" + "description": "Se requiere una contrase\u00f1a por `{protocol}`. Esto a\u00fan no es compatible, por favor deshabilita la contrase\u00f1a para continuar.", + "title": "Se requiere contrase\u00f1a" }, "protocol_disabled": { - "description": "El emparejamiento es necesario para `{protocol}` pero est\u00e1 desactivado en el dispositivo. Revise las posibles restricciones de acceso (por ejemplo, permitir que se conecten todos los dispositivos de la red local) en el dispositivo.\n\nPuede continuar sin emparejar este protocolo, pero algunas funciones estar\u00e1n limitadas.", + "description": "Se requiere emparejamiento para `{protocol}` pero est\u00e1 deshabilitado en el dispositivo. Revisa las posibles restricciones de acceso (p. ej., permitir que todos los dispositivos de la red local se conecten) en el dispositivo. \n\nPuedes continuar sin emparejar este protocolo, pero algunas funciones estar\u00e1n limitadas.", "title": "No es posible el emparejamiento" }, "reconfigure": { diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index c714c26129a..f46bf499455 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -35,9 +35,9 @@ "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.)", + "interface": "La interfaz de la que quieres 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" + "track_unknown": "Seguimiento de dispositivos desconocidos/sin nombre" }, "title": "Opciones de AsusWRT" } diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index f2c7a10d0e0..b9334d7b473 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -14,8 +14,8 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Introduzca la contrase\u00f1a de {username}.", - "title": "Reautorizar una cuenta de August" + "description": "Introduce la contrase\u00f1a de {username}.", + "title": "Volver a autenticar una cuenta August" }, "user_validate": { "data": { @@ -23,8 +23,8 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es \"correo electr\u00f3nico\", el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es \"tel\u00e9fono\", el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato \"+NNNNNNN\".", - "title": "Configurar una cuenta de August" + "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es 'correo electr\u00f3nico', el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es 'tel\u00e9fono', el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato '+NNNNNNNNN'.", + "title": "Configurar una cuenta August" }, "validation": { "data": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/es.json b/homeassistant/components/aurora_abb_powerone/translations/es.json index 4e1d95a126d..537ee81a30e 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/es.json +++ b/homeassistant/components/aurora_abb_powerone/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "no_serial_ports": "No se han encontrado puertos. Necesita un dispositivo RS485 v\u00e1lido para comunicarse." + "no_serial_ports": "No se encontraron puertos de comunicaciones. Necesitas un dispositivo RS485 v\u00e1lido para comunicarse." }, "error": { - "cannot_connect": "No se puede conectar, por favor, compruebe el puerto serie, la direcci\u00f3n, la conexi\u00f3n el\u00e9ctrica y que el inversor est\u00e1 encendido", - "cannot_open_serial_port": "No se puede abrir el puerto serie, por favor, compruebe y vuelva a intentarlo", - "invalid_serial_port": "El puerto serie no es v\u00e1lido para este dispositivo o no se ha podido abrir" + "cannot_connect": "No se puede conectar, por favor, verifica el puerto serie, la direcci\u00f3n, la conexi\u00f3n el\u00e9ctrica y que el inversor est\u00e9 encendido (durante la luz del d\u00eda)", + "cannot_open_serial_port": "No se puede abrir el puerto serie, por favor, verif\u00edcalo e int\u00e9ntalo de nuevo", + "invalid_serial_port": "El puerto serie no es un dispositivo v\u00e1lido o no se pudo abrir" }, "step": { "user": { @@ -15,7 +15,7 @@ "address": "Direcci\u00f3n del inversor", "port": "Puerto adaptador RS485 o USB-RS485" }, - "description": "El inversor debe conectarse a trav\u00e9s de un adaptador RS485, por favor, seleccione el puerto serie y la direcci\u00f3n del inversor tal y como se configura en el panel LCD" + "description": "El inversor debe estar conectado a trav\u00e9s de un adaptador RS485, por favor, selecciona el puerto serie y la direcci\u00f3n del inversor seg\u00fan lo configurado en el panel LCD" } } } diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json index e1ae211ca8b..ce0f6022094 100644 --- a/homeassistant/components/aussie_broadband/translations/es.json +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "no_services_found": "No se han encontrado servicios para esta cuenta", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "no_services_found": "No se encontraron servicios para esta cuenta", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -15,14 +15,14 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Actualice la contrase\u00f1a para {username}", - "title": "Reautenticar Integraci\u00f3n" + "description": "Actualizar contrase\u00f1a para {username}", + "title": "Volver a autenticar la integraci\u00f3n" }, "service": { "data": { "services": "Servicios" }, - "title": "Seleccionar Servicios" + "title": "Seleccionar servicios" }, "user": { "data": { @@ -34,7 +34,7 @@ }, "options": { "abort": { - "cannot_connect": "Fallo en la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -43,7 +43,7 @@ "data": { "services": "Servicios" }, - "title": "Selecciona servicios" + "title": "Seleccionar servicios" } } } diff --git a/homeassistant/components/auth/translations/es.json b/homeassistant/components/auth/translations/es.json index 7495ffcfbc9..0279bca1dfa 100644 --- a/homeassistant/components/auth/translations/es.json +++ b/homeassistant/components/auth/translations/es.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos el [Autenticador de Google](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo **`{code}`**.", + "description": "Para activar la autenticaci\u00f3n de dos factores usando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos [Google Authenticator](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code}\n\nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo **`{code}`**.", "title": "Configurar la autenticaci\u00f3n de dos factores usando TOTP" } }, diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index 3510d3a3a8b..e52451bd108 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -2,14 +2,30 @@ "config": { "abort": { "already_configured": "El compte ja est\u00e0 configurat", + "already_configured_account": "El compte ja est\u00e0 configurat", + "already_configured_device": "El dispositiu ja est\u00e0 configurat", "no_devices_found": "No s'han trobat dispositius a la xarxa", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unreachable": "Ha fallat la connexi\u00f3" }, "error": { "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "unreachable": "Ha fallat la connexi\u00f3" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token d'acc\u00e9s", + "email": "Correu electr\u00f2nic" + } + }, + "local": { + "data": { + "host": "Adre\u00e7a IP" + } + }, "reauth": { "data": { "access_token": "Token d'acc\u00e9s", @@ -29,7 +45,10 @@ "access_token": "Token d'acc\u00e9s", "email": "Correu electr\u00f2nic" }, - "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a trav\u00e9s de l'enlla\u00e7 seg\u00fcent: https://developer.getawair.com/onboard/login" + "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a trav\u00e9s de l'enlla\u00e7 seg\u00fcent: https://developer.getawair.com/onboard/login", + "menu_options": { + "local": "Connecta't localment (preferit)" + } } } } diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index c28ee6bc016..43b387e8286 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Konto wurde bereits konfiguriert", + "already_configured_account": "Konto wurde bereits konfiguriert", + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "unreachable": "Verbindung fehlgeschlagen" }, "error": { "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "unreachable": "Verbindung fehlgeschlagen" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Zugangstoken", + "email": "E-Mail" + }, + "description": "Du musst dich f\u00fcr ein Awair-Entwicklerzugriffstoken registrieren unter: {url}" + }, + "discovery_confirm": { + "description": "M\u00f6chtest du {model} ({device_id}) einrichten?" + }, + "local": { + "data": { + "host": "IP-Adresse" + }, + "description": "Awair Local API muss wie folgt aktiviert werden: {url}" + }, "reauth": { "data": { "access_token": "Zugangstoken", @@ -29,7 +50,11 @@ "access_token": "Zugangstoken", "email": "E-Mail" }, - "description": "Du musst dich f\u00fcr ein Awair Entwickler-Zugangs-Token registrieren unter: https://developer.getawair.com/onboard/login" + "description": "W\u00e4hle lokal f\u00fcr die beste Erfahrung. Verwende die Cloud nur, wenn das Ger\u00e4t nicht mit demselben Netzwerk wie Home Assistant verbunden ist oder wenn du ein \u00e4lteres Ger\u00e4t hast.", + "menu_options": { + "cloud": "Verbindung \u00fcber die Cloud", + "local": "Lokal verbinden (bevorzugt)" + } } } } diff --git a/homeassistant/components/awair/translations/en.json b/homeassistant/components/awair/translations/en.json index caec592c527..ca572958c46 100644 --- a/homeassistant/components/awair/translations/en.json +++ b/homeassistant/components/awair/translations/en.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Account is already configured", + "already_configured_account": "Account is already configured", + "already_configured_device": "Device is already configured", "no_devices_found": "No devices found on the network", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "unreachable": "Failed to connect" }, "error": { "invalid_access_token": "Invalid access token", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unreachable": "Failed to connect" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Access Token", + "email": "Email" + }, + "description": "You must register for an Awair developer access token at: {url}" + }, + "discovery_confirm": { + "description": "Do you want to setup {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP Address" + }, + "description": "Awair Local API must be enabled following these steps: {url}" + }, "reauth": { "data": { "access_token": "Access Token", @@ -29,7 +50,11 @@ "access_token": "Access Token", "email": "Email" }, - "description": "You must register for an Awair developer access token at: https://developer.getawair.com/onboard/login" + "description": "Pick local for the best experience. Only use cloud if the device is not connected to the same network as Home Assistant, or if you have a legacy device.", + "menu_options": { + "cloud": "Connect via the cloud", + "local": "Connect locally (preferred)" + } } } } diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index 5b203948a7c..64d678003ac 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", + "already_configured_account": "La cuenta ya est\u00e1 configurada", + "already_configured_device": "El dispositivo ya est\u00e1 configurado", "no_devices_found": "No se encontraron dispositivos en la red", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "unreachable": "No se pudo conectar" }, "error": { "invalid_access_token": "Token de acceso no v\u00e1lido", - "unknown": "Error inesperado" + "unknown": "Error inesperado", + "unreachable": "No se pudo conectar" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token de acceso", + "email": "Correo electr\u00f3nico" + }, + "description": "Debes registrarte para obtener un token de acceso de desarrollador de Awair en: {url}" + }, + "discovery_confirm": { + "description": "\u00bfQuieres configurar {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Direcci\u00f3n IP" + }, + "description": "La API local de Awair debe estar habilitada siguiendo estos pasos: {url}" + }, "reauth": { "data": { "access_token": "Token de acceso", @@ -29,7 +50,11 @@ "access_token": "Token de acceso", "email": "Correo electr\u00f3nico" }, - "description": "Debes registrarte para obtener un token de acceso de desarrollador Awair en: https://developer.getawair.com/onboard/login" + "description": "Debes registrarte para obtener un token de acceso de desarrollador Awair en: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Conectar a trav\u00e9s de la nube", + "local": "Conectar localmente (preferido)" + } } } } diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json index 70632b292e4..b741f5f8722 100644 --- a/homeassistant/components/awair/translations/et.json +++ b/homeassistant/components/awair/translations/et.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Konto on juba seadistatud", + "already_configured_account": "Konto on juba seadistatud", + "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", "no_devices_found": "V\u00f5rgust ei leitud Awair seadmeid", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unreachable": "\u00dchendamine nurjus" }, "error": { "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", - "unknown": "Tundmatu viga" + "unknown": "Tundmatu viga", + "unreachable": "\u00dchendamine nurjus" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Juurdep\u00e4\u00e4su t\u00f5end", + "email": "E-posti aadress" + }, + "description": "Pead registreeruma Awairi arendaja juurdep\u00e4\u00e4suloa saamiseks aadressil: {url}" + }, + "discovery_confirm": { + "description": "Kas seadistada {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP aadress" + }, + "description": "Awairi kohalik API tuleb lubada j\u00e4rgmiste sammude abil: {url}" + }, "reauth": { "data": { "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", @@ -29,7 +50,11 @@ "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", "email": "E-post" }, - "description": "Pead registreerima Awair arendaja juurdep\u00e4\u00e4su loa aadressil: https://developer.getawair.com/onboard/login" + "description": "Pead registreerima Awair arendaja juurdep\u00e4\u00e4su loa aadressil: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Pilve\u00fchendus", + "local": "Kohalik \u00fchendus (eelistatud)" + } } } } diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 863b7982b2a..53c41584413 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Akun sudah dikonfigurasi", + "already_configured_account": "Akun sudah dikonfigurasi", + "already_configured_device": "Perangkat sudah dikonfigurasi", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "unreachable": "Gagal terhubung" }, "error": { "invalid_access_token": "Token akses tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" + "unknown": "Kesalahan yang tidak diharapkan", + "unreachable": "Gagal terhubung" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token Akses", + "email": "Email" + }, + "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: {url}" + }, + "discovery_confirm": { + "description": "Ingin menyiapkan {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Alamat IP" + }, + "description": "API Awair Local harus diaktifkan dengan mengikuti langkah-langkah berikut: {url}" + }, "reauth": { "data": { "access_token": "Token Akses", @@ -29,7 +50,11 @@ "access_token": "Token Akses", "email": "Email" }, - "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: https://developer.getawair.com/onboard/login" + "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Terhubung melalui cloud", + "local": "Terhubung secara lokal (lebih disukai)" + } } } } diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index b0ee9e85adf..840612eff40 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_configured_account": "Konto jest ju\u017c skonfigurowane", + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "unreachable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { "invalid_access_token": "Niepoprawny token dost\u0119pu", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unreachable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token dost\u0119pu", + "email": "Adres e-mail" + }, + "description": "Aby uzyska\u0107 token dost\u0119pu programisty Awair, musisz zarejestrowa\u0107 si\u0119 pod adresem: {url}" + }, + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Adres IP" + }, + "description": "Lokalny interfejs API Awair musi by\u0107 w\u0142\u0105czony, wykonuj\u0105c nast\u0119puj\u0105ce czynno\u015bci: {url}" + }, "reauth": { "data": { "access_token": "Token dost\u0119pu", @@ -29,7 +50,11 @@ "access_token": "Token dost\u0119pu", "email": "Adres e-mail" }, - "description": "Aby uzyska\u0107 token dost\u0119pu programisty Awair, nale\u017cy zarejestrowa\u0107 si\u0119 pod adresem: https://developer.getawair.com/onboard/login" + "description": "Wybierz lokalny, aby uzyska\u0107 najlepsze efekty. Korzystaj z chmury tylko wtedy, gdy urz\u0105dzenie nie jest pod\u0142\u0105czone do tej samej sieci co Home Assistant lub je\u015bli masz starsze urz\u0105dzenie.", + "menu_options": { + "cloud": "Po\u0142\u0105cz si\u0119 przez chmur\u0119", + "local": "Po\u0142\u0105cz lokalnie (preferowane)" + } } } } diff --git a/homeassistant/components/azure_event_hub/translations/es.json b/homeassistant/components/azure_event_hub/translations/es.json index 7d9792ef371..25d5cfde92e 100644 --- a/homeassistant/components/azure_event_hub/translations/es.json +++ b/homeassistant/components/azure_event_hub/translations/es.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "cannot_connect": "La conexi\u00f3n con las credenciales del configuration.yaml ha fallado, por favor elim\u00ednelo de yaml y utilice el flujo de configuraci\u00f3n", - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "unknown": "La conexi\u00f3n con las credenciales de configuration.yaml ha fallado con un error desconocido, por favor, elim\u00ednelo de yaml y utilice el flujo de configuraci\u00f3n." + "cannot_connect": "No se pudo conectar con las credenciales de configuration.yaml, por favor, elim\u00ednalo de yaml y usa el flujo de configuraci\u00f3n.", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", + "unknown": "La conexi\u00f3n con las credenciales de la configuraci\u00f3n.yaml fall\u00f3 con un error desconocido, por favor, elim\u00ednalo de yaml y usa el flujo de configuraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", @@ -15,7 +15,7 @@ "data": { "event_hub_connection_string": "Cadena de conexi\u00f3n de Event Hub" }, - "description": "Por favor, introduzca la cadena de conexi\u00f3n para: {event_hub_instance_name}", + "description": "Por favor, introduce la cadena de conexi\u00f3n para: {event_hub_instance_name}", "title": "M\u00e9todo de cadena de conexi\u00f3n" }, "sas": { @@ -24,7 +24,7 @@ "event_hub_sas_key": "Clave de SAS de Event Hub", "event_hub_sas_policy": "Directiva de SAS de Event Hub" }, - "description": "Por favor, introduzca las credenciales SAS (firma de acceso compartido) para: {event_hub_instance_name}", + "description": "Por favor, introduce las credenciales SAS (firma de acceso compartido) para: {event_hub_instance_name}", "title": "M\u00e9todo de credenciales SAS" }, "user": { @@ -32,7 +32,7 @@ "event_hub_instance_name": "Nombre de instancia de Event Hub", "use_connection_string": "Usar cadena de conexi\u00f3n" }, - "title": "Configure su integraci\u00f3n de Azure Event Hub" + "title": "Configura tu integraci\u00f3n Azure Event Hub" } } }, @@ -40,7 +40,7 @@ "step": { "options": { "data": { - "send_interval": "Intervalo entre el env\u00edo de lotes al hub." + "send_interval": "Intervalo entre el env\u00edo de lotes al concentrador." }, "title": "Opciones para Azure Event Hub." } diff --git a/homeassistant/components/balboa/translations/es.json b/homeassistant/components/balboa/translations/es.json index c71f279fd6c..33cee6c5196 100644 --- a/homeassistant/components/balboa/translations/es.json +++ b/homeassistant/components/balboa/translations/es.json @@ -12,7 +12,7 @@ "data": { "host": "Host" }, - "title": "Con\u00e9ctese al dispositivo Wi-Fi de Balboa" + "title": "Con\u00e9ctate al dispositivo Wi-Fi de Balboa" } } }, @@ -20,7 +20,7 @@ "step": { "init": { "data": { - "sync_time": "Mantenga sincronizada la hora de su cliente de Balboa Spa con Home Assistant" + "sync_time": "Mant\u00e9n sincronizada la hora de tu Cliente Balboa Spa con Home Assistant" } } } diff --git a/homeassistant/components/binary_sensor/translations/es.json b/homeassistant/components/binary_sensor/translations/es.json index 005f2669e50..4a30bed923d 100644 --- a/homeassistant/components/binary_sensor/translations/es.json +++ b/homeassistant/components/binary_sensor/translations/es.json @@ -86,7 +86,7 @@ "not_powered": "{entity_name} no est\u00e1 activado", "not_present": "{entity_name} no est\u00e1 presente", "not_running": "{entity_name} ya no se est\u00e1 ejecutando", - "not_tampered": "{entity_name} dej\u00f3 de detectar alteraciones", + "not_tampered": "{entity_name} dej\u00f3 de detectar manipulaci\u00f3n", "not_unsafe": "{entity_name} se volvi\u00f3 seguro", "occupied": "{entity_name} se convirti\u00f3 en ocupado", "opened": "{entity_name} abierto", @@ -97,7 +97,7 @@ "running": "{entity_name} comenz\u00f3 a ejecutarse", "smoke": "{entity_name} empez\u00f3 a detectar humo", "sound": "{entity_name} empez\u00f3 a detectar sonido", - "tampered": "{entity_name} comenz\u00f3 a detectar alteraciones", + "tampered": "{entity_name} comenz\u00f3 a detectar manipulaci\u00f3n", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado", "unsafe": "{entity_name} se volvi\u00f3 inseguro", @@ -112,9 +112,9 @@ "heat": "calor", "moisture": "humedad", "motion": "movimiento", - "occupancy": "ocupaci\u00f3n", + "occupancy": "presencia", "power": "energ\u00eda", - "problem": "Problema", + "problem": "problema", "smoke": "humo", "sound": "sonido", "vibration": "vibraci\u00f3n" @@ -133,7 +133,7 @@ "on": "Cargando" }, "carbon_monoxide": { - "off": "Libre", + "off": "No detectado", "on": "Detectado" }, "cold": { @@ -202,7 +202,7 @@ }, "running": { "off": "No se est\u00e1 ejecutando", - "on": "Corriendo" + "on": "Ejecutando" }, "safety": { "off": "Seguro", diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json index 0e3c536995d..7a019e277a4 100644 --- a/homeassistant/components/bosch_shc/translations/es.json +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -7,22 +7,22 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "pairing_failed": "El emparejamiento ha fallado; compruebe que el Bosch Smart Home Controller est\u00e1 en modo de emparejamiento (el LED parpadea) y que su contrase\u00f1a es correcta.", - "session_error": "Error de sesi\u00f3n: La API devuelve un resultado no correcto.", + "pairing_failed": "Emparejamiento fallido; Verifica que el Smart Home Controller de Bosch est\u00e9 en modo de emparejamiento (el LED parpadea) y que tu contrase\u00f1a sea correcta.", + "session_error": "Error de sesi\u00f3n: la API devuelve un resultado No-OK.", "unknown": "Error inesperado" }, "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Pulse el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?" + "description": "Pulsa el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?" }, "credentials": { "data": { - "password": "Contrase\u00f1a del controlador smart home" + "password": "Contrase\u00f1a del Smart Home Controller" } }, "reauth_confirm": { - "description": "La integraci\u00f3n bosch_shc necesita volver a autentificar su cuenta", + "description": "La integraci\u00f3n bosch_shc necesita volver a autentificar tu cuenta", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/brother/translations/es.json b/homeassistant/components/brother/translations/es.json index 9c327a2887a..921f2806f0e 100644 --- a/homeassistant/components/brother/translations/es.json +++ b/homeassistant/components/brother/translations/es.json @@ -21,7 +21,7 @@ "data": { "type": "Tipo de impresora" }, - "description": "\u00bfQuieres a\u00f1adir la Impresora Brother {model} con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir la impresora {model} con n\u00famero de serie `{serial_number}` a Home Assistant?", "title": "Impresora Brother encontrada" } } diff --git a/homeassistant/components/brunt/translations/es.json b/homeassistant/components/brunt/translations/es.json index 362ef22b838..7a912e267f9 100644 --- a/homeassistant/components/brunt/translations/es.json +++ b/homeassistant/components/brunt/translations/es.json @@ -14,7 +14,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Por favor, vuelva a introducir la contrase\u00f1a de: {username}", + "description": "Por favor, vuelve a introducir la contrase\u00f1a de: {username}", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { @@ -22,7 +22,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Configure su integraci\u00f3n con Brunt" + "title": "Configura tu integraci\u00f3n Brunt" } } } diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 4b36080dd7c..5f1e55c8d0d 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_connect": "Fallo en la conexi\u00f3n" + "cannot_connect": "No se pudo conectar" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 39e34975af8..9c0847b52ce 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -9,9 +9,9 @@ "step": { "config": { "data": { - "known_hosts": "Anfitriones conocidos" + "known_hosts": "Hosts conocidos" }, - "description": "Introduce la configuraci\u00f3n de Google Cast.", + "description": "Hosts conocidos: una lista separada por comas de nombres de host o direcciones IP de dispositivos de transmisi\u00f3n, se usa si el descubrimiento de mDNS no funciona.", "title": "Configuraci\u00f3n de Google Cast" }, "confirm": { @@ -27,9 +27,9 @@ "advanced_options": { "data": { "ignore_cec": "Ignorar CEC", - "uuid": "UUID permitidos" + "uuid": "UUIDs permitidos" }, - "description": "UUID permitidos: lista separada por comas de UUID de dispositivos Cast para a\u00f1adir a Home Assistant. \u00dasalo solo si no deseas a\u00f1adir todos los dispositivos Cast disponibles.\nIgnorar CEC: lista separada por comas de Chromecasts que deben ignorar los datos CEC para determinar la entrada activa. Esto se pasar\u00e1 a pychromecast.IGNORE_CEC.", + "description": "UUID permitidos: una lista separada por comas de UUID de dispositivos Cast para a\u00f1adir a Home Assistant. \u00dasalo solo si no deseas a\u00f1adir todos los dispositivos de transmisi\u00f3n disponibles.\nIgnorar CEC: una lista separada por comas de Chromecasts que deben ignorar los datos de CEC para determinar la entrada activa. Esto se pasar\u00e1 a pychromecast.IGNORE_CEC.", "title": "Configuraci\u00f3n avanzada de Google Cast" }, "basic_options": { diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json index 056d26b077d..270d72bd58c 100644 --- a/homeassistant/components/climacell/translations/es.json +++ b/homeassistant/components/climacell/translations/es.json @@ -3,10 +3,10 @@ "step": { "init": { "data": { - "timestep": "Min. Entre pron\u00f3sticos de NowCast" + "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": "Actualizaci\u00f3n de opciones de ClimaCell" + "description": "Si eliges habilitar la entidad de pron\u00f3stico `nowcast`, puedes configurar la cantidad de minutos entre cada pron\u00f3stico. La cantidad de pron\u00f3sticos proporcionados depende de la cantidad de minutos elegidos entre los pron\u00f3sticos.", + "title": "Actualizar opciones de ClimaCell" } } } diff --git a/homeassistant/components/climacell/translations/sensor.es.json b/homeassistant/components/climacell/translations/sensor.es.json index 4ce9ac05c1c..4cb1b34eb21 100644 --- a/homeassistant/components/climacell/translations/sensor.es.json +++ b/homeassistant/components/climacell/translations/sensor.es.json @@ -4,21 +4,21 @@ "good": "Bueno", "hazardous": "Peligroso", "moderate": "Moderado", - "unhealthy": "Insalubre", - "unhealthy_for_sensitive_groups": "Insalubre para grupos sensibles", + "unhealthy": "No saludable", + "unhealthy_for_sensitive_groups": "No es saludable para grupos sensibles", "very_unhealthy": "Muy poco saludable" }, "climacell__pollen_index": { "high": "Alto", "low": "Bajo", "medium": "Medio", - "none": "Ninguna", + "none": "Ninguno", "very_high": "Muy alto", "very_low": "Muy bajo" }, "climacell__precipitation_type": { - "freezing_rain": "Lluvia g\u00e9lida", - "ice_pellets": "Perdigones de hielo", + "freezing_rain": "Lluvia helada", + "ice_pellets": "Granizo", "none": "Ninguna", "rain": "Lluvia", "snow": "Nieve" diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index 0647609e4e8..eff36eefcfa 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -15,7 +15,7 @@ "reauth_confirm": { "data": { "api_token": "Token API", - "description": "Vuelva a autenticarse con su cuenta de Cloudflare." + "description": "Vuelve a autenticarte con tu cuenta de Cloudflare." } }, "records": { diff --git a/homeassistant/components/co2signal/translations/es.json b/homeassistant/components/co2signal/translations/es.json index 921dd22a76a..7029bd48e52 100644 --- a/homeassistant/components/co2signal/translations/es.json +++ b/homeassistant/components/co2signal/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "api_ratelimit": "Se ha superado el l\u00edmite de velocidad de la API", + "api_ratelimit": "Se excedi\u00f3 el l\u00edmite de tasa de API", "unknown": "Error inesperado" }, "error": { - "api_ratelimit": "Excedida tasa l\u00edmite del API", + "api_ratelimit": "Se excedi\u00f3 el l\u00edmite de tasa de API", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -27,7 +27,7 @@ "api_key": "Token de acceso", "location": "Obtener datos para" }, - "description": "Visite https://co2signal.com/ para solicitar un token." + "description": "Visita https://co2signal.com/ para solicitar un token." } } } diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json index 32eb2af4d5c..8d89b9b546b 100644 --- a/homeassistant/components/coinbase/translations/es.json +++ b/homeassistant/components/coinbase/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_auth_key": "Credenciales de API rechazadas por Coinbase debido a una clave de API no v\u00e1lida.", - "invalid_auth_secret": "Credenciales de la API rechazadas por Coinbase debido a un secreto de API no v\u00e1lido.", + "invalid_auth_secret": "Credenciales de API rechazadas por Coinbase debido a un secreto de API no v\u00e1lido.", "unknown": "Error inesperado" }, "step": { @@ -23,8 +23,8 @@ }, "options": { "error": { - "currency_unavailable": "La API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", - "exchange_rate_unavailable": "El API de Coinbase no proporciona alguno/s de los tipos de cambio que has solicitado.", + "currency_unavailable": "Tu API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", + "exchange_rate_unavailable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados.", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/cpuspeed/translations/es.json b/homeassistant/components/cpuspeed/translations/es.json index 87ff7b0c783..f345f62a3d4 100644 --- a/homeassistant/components/cpuspeed/translations/es.json +++ b/homeassistant/components/cpuspeed/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "not_compatible": "No se puede obtener informaci\u00f3n de la CPU, esta integraci\u00f3n no es compatible con su sistema" + "already_configured": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", + "not_compatible": "No se puede obtener informaci\u00f3n de la CPU, esta integraci\u00f3n no es compatible con tu sistema" }, "step": { "user": { - "description": "\u00bfQuieres empezar a configurar?", + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Velocidad de la CPU" } } diff --git a/homeassistant/components/crownstone/translations/es.json b/homeassistant/components/crownstone/translations/es.json index b71c69db33c..dcb0e1af6e3 100644 --- a/homeassistant/components/crownstone/translations/es.json +++ b/homeassistant/components/crownstone/translations/es.json @@ -6,7 +6,7 @@ "usb_setup_unsuccessful": "La configuraci\u00f3n del USB de Crownstone no tuvo \u00e9xito." }, "error": { - "account_not_verified": "Cuenta no verificada. Por favor, active su cuenta a trav\u00e9s del correo electr\u00f3nico de activaci\u00f3n de Crownstone.", + "account_not_verified": "Cuenta no verificada. Por favor, activa tu cuenta a trav\u00e9s del correo electr\u00f3nico de activaci\u00f3n de Crownstone.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -15,22 +15,22 @@ "data": { "usb_path": "Ruta del dispositivo USB" }, - "description": "Selecciona el puerto serie del dongle USB de Crownstone, o selecciona \"No usar USB\" si no quieres configurar un dongle USB.\n\nBusca un dispositivo con VID 10C4 y PID EA60.", - "title": "Configuraci\u00f3n del dispositivo USB Crownstone" + "description": "Selecciona el puerto serie del dongle USB de Crownstone o selecciona 'No usar USB' si no deseas configurar un dongle USB. \n\nBusca un dispositivo con VID 10C4 y PID EA60.", + "title": "Configuraci\u00f3n del dongle USB Crownstone" }, "usb_manual_config": { "data": { "usb_manual_path": "Ruta del dispositivo USB" }, - "description": "Introduzca manualmente la ruta de un dispositivo USB Crownstone.", - "title": "Ruta manual del dispositivo USB Crownstone" + "description": "Introduce manualmente la ruta de un dongle USB de Crownstone.", + "title": "Ruta manual del dongle USB Crownstone" }, "usb_sphere_config": { "data": { - "usb_sphere": "Esfera Crownstone" + "usb_sphere": "Crownstone Sphere" }, - "description": "Selecciona una Esfera Crownstone donde se encuentra el USB.", - "title": "USB de Esfera Crownstone" + "description": "Selecciona una Crownstone Sphere donde se encuentra el USB.", + "title": "USB de Crownstone Sphere" }, "user": { "data": { @@ -45,30 +45,30 @@ "step": { "init": { "data": { - "usb_sphere_option": "Esfera de Crownstone donde se encuentra el USB", - "use_usb_option": "Utilice una llave USB Crownstone para la transmisi\u00f3n de datos local" + "usb_sphere_option": "Crownstone Sphere donde se encuentra el USB", + "use_usb_option": "Utiliza un dongle USB de Crownstone para la transmisi\u00f3n local de datos" } }, "usb_config": { "data": { "usb_path": "Ruta del dispositivo USB" }, - "description": "Seleccione el puerto serie del dispositivo USB Crownstone.\n\nBusque un dispositivo con VID 10C4 y PID EA60.", - "title": "Configuraci\u00f3n del dispositivo USB Crownstone" + "description": "Selecciona el puerto serie del dongle USB Crownstone. \n\nBusca un dispositivo con VID 10C4 y PID EA60.", + "title": "Configuraci\u00f3n del dongle USB Crownstone" }, "usb_manual_config": { "data": { "usb_manual_path": "Ruta del dispositivo USB" }, - "description": "Introduzca manualmente la ruta de un dispositivo USB Crownstone.", - "title": "Ruta manual del dispositivo USB Crownstone" + "description": "Introduce manualmente la ruta de un dongle USB de Crownstone.", + "title": "Ruta manual del dongle USB Crownstone" }, "usb_sphere_config": { "data": { - "usb_sphere": "Esfera Crownstone" + "usb_sphere": "Crownstone Sphere" }, - "description": "Selecciona una Esfera Crownstone donde se encuentra el USB.", - "title": "USB de Esfera Crownstone" + "description": "Selecciona una Crownstone Sphere donde se encuentra el USB.", + "title": "USB de Crownstone Sphere" } } } diff --git a/homeassistant/components/daikin/translations/es.json b/homeassistant/components/daikin/translations/es.json index 6908e0af5f7..ce72440bf7d 100644 --- a/homeassistant/components/daikin/translations/es.json +++ b/homeassistant/components/daikin/translations/es.json @@ -5,7 +5,7 @@ "cannot_connect": "No se pudo conectar" }, "error": { - "api_password": "Autenticaci\u00f3n no v\u00e1lida, utilice la clave de la API o la contrase\u00f1a.", + "api_password": "Autenticaci\u00f3n no v\u00e1lida, utiliza la clave API o la contrase\u00f1a.", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 0974be9ff9d..3c4d8a89134 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -14,7 +14,7 @@ "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para que se conecte al gateway de deCONZ proporcionado por el add-on {addon} de Supervisor?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento {addon} ?", "title": "Pasarela de enlace de CONZ Zigbee v\u00eda complemento de Home Assistant" }, "link": { diff --git a/homeassistant/components/deluge/translations/es.json b/homeassistant/components/deluge/translations/es.json index c09a1ef5ef5..73029783168 100644 --- a/homeassistant/components/deluge/translations/es.json +++ b/homeassistant/components/deluge/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { @@ -14,9 +14,9 @@ "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario", - "web_port": "Puerto web (para el servicio de visita)" + "web_port": "Puerto web (para visitar el servicio)" }, - "description": "Para poder usar esta integraci\u00f3n, debe habilitar la siguiente opci\u00f3n en la configuraci\u00f3n de diluvio: Daemon > Permitir controles remotos" + "description": "Para poder usar esta integraci\u00f3n, debes habilitar la siguiente opci\u00f3n en la configuraci\u00f3n de Deluge: Daemon > Allow Remote Connections" } } } diff --git a/homeassistant/components/demo/translations/pl.json b/homeassistant/components/demo/translations/pl.json index c57a1e4f619..6fa5f2509c4 100644 --- a/homeassistant/components/demo/translations/pl.json +++ b/homeassistant/components/demo/translations/pl.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Naci\u015bnij ZATWIERD\u0179, aby potwierdzi\u0107, \u017ce zasilacz zosta\u0142 wymieniony", + "title": "Zasilacz wymaga wymiany" + } + } + }, + "title": "Zasilacz nie jest stabilny" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Naci\u015bnij OK po uzupe\u0142nieniu p\u0142ynu \u015bwiate\u0142ka", + "description": "Naci\u015bnij ZATWIERD\u0179 po uzupe\u0142nieniu p\u0142ynu \u015bwiate\u0142ka", "title": "P\u0142yn \u015bwiate\u0142ka nale\u017cy uzupe\u0142ni\u0107" } } diff --git a/homeassistant/components/derivative/translations/es.json b/homeassistant/components/derivative/translations/es.json index d0722b70400..8462540f9dd 100644 --- a/homeassistant/components/derivative/translations/es.json +++ b/homeassistant/components/derivative/translations/es.json @@ -15,8 +15,8 @@ "time_window": "Si se establece, el valor del sensor es un promedio m\u00f3vil ponderado en el tiempo de las derivadas dentro de esta ventana.", "unit_prefix": "La salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico seleccionado y la unidad de tiempo de la derivada." }, - "description": "Crea un sensor que ama la derivada de otro sensor.", - "title": "A\u00f1ade sensor derivativo" + "description": "Crea un sensor que estima la derivada de un sensor.", + "title": "A\u00f1adir sensor de derivaci\u00f3n" } } }, @@ -39,5 +39,5 @@ } } }, - "title": "Sensor derivado" + "title": "Sensor de derivaci\u00f3n" } \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/pl.json b/homeassistant/components/deutsche_bahn/translations/pl.json new file mode 100644 index 00000000000..85cf97d0d17 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja Deutsche Bahn oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.11. \n\nIntegracja jest usuwana, poniewa\u017c opiera si\u0119 na webscrapingu, co jest niedozwolone. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Deutsche Bahn zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/es.json b/homeassistant/components/devolo_home_network/translations/es.json index df2435df665..645f0347554 100644 --- a/homeassistant/components/devolo_home_network/translations/es.json +++ b/homeassistant/components/devolo_home_network/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "home_control": "La unidad central de devolo Home Control no funciona con esta integraci\u00f3n." + "home_control": "La unidad central Home Control de devolo no funciona con esta integraci\u00f3n." }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{product} ({name})", @@ -14,11 +14,11 @@ "data": { "ip_address": "Direcci\u00f3n IP" }, - "description": "\u00bfDeseas iniciar la configuraci\u00f3n?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, "zeroconf_confirm": { - "description": "\u00bfDesea a\u00f1adir el dispositivo de red dom\u00e9stica de devolo con el nombre de host `{host_name}` a Home Assistant?", - "title": "Dispositivo de red dom\u00e9stico devolo descubierto" + "description": "\u00bfQuieres a\u00f1adir el dispositivo de red dom\u00e9stica devolo con el nombre de host `{host_name}` a Home Assistant?", + "title": "Dispositivo de red dom\u00e9stica devolo descubierto" } } } diff --git a/homeassistant/components/dlna_dmr/translations/es.json b/homeassistant/components/dlna_dmr/translations/es.json index 6d16766de03..3459adca00a 100644 --- a/homeassistant/components/dlna_dmr/translations/es.json +++ b/homeassistant/components/dlna_dmr/translations/es.json @@ -2,16 +2,16 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "alternative_integration": "Dispositivo compatible con otra integraci\u00f3n", + "alternative_integration": "El dispositivo es m\u00e1s compatible con otra integraci\u00f3n", "cannot_connect": "No se pudo conectar", "discovery_error": "No se ha podido descubrir un dispositivo DLNA coincidente", - "incomplete_config": "A la configuraci\u00f3n le falta una variable necesaria", + "incomplete_config": "A la configuraci\u00f3n le falta una variable requerida", "non_unique_id": "Se han encontrado varios dispositivos con el mismo ID \u00fanico", - "not_dmr": "El dispositivo no es un procesador de medios digitales compatible" + "not_dmr": "El dispositivo no es un Digital Media Renderer compatible" }, "error": { "cannot_connect": "No se pudo conectar", - "not_dmr": "El dispositivo no es un procesador de medios digitales compatible" + "not_dmr": "El dispositivo no es un Digital Media Renderer compatible" }, "flow_title": "{name}", "step": { @@ -19,13 +19,13 @@ "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, "import_turn_on": { - "description": "Por favor, encienda el dispositivo y haga clic en enviar para continuar la migraci\u00f3n" + "description": "Por favor, enciende el dispositivo y haz clic en enviar para continuar la migraci\u00f3n" }, "manual": { "data": { "url": "URL" }, - "description": "URL de un archivo XML de descripci\u00f3n del dispositivo", + "description": "URL a un archivo XML de descripci\u00f3n de dispositivo", "title": "Conexi\u00f3n manual del dispositivo DLNA DMR" }, "user": { @@ -46,7 +46,7 @@ "data": { "browse_unfiltered": "Mostrar medios incompatibles al navegar", "callback_url_override": "URL de devoluci\u00f3n de llamada del detector de eventos", - "listen_port": "Puerto de escucha de eventos (aleatorio si no se establece)", + "listen_port": "Puerto de escucha de eventos (aleatorio si no est\u00e1 configurado)", "poll_availability": "Sondeo para la disponibilidad del dispositivo" }, "title": "Configuraci\u00f3n de DLNA Digital Media Renderer" diff --git a/homeassistant/components/dlna_dms/translations/es.json b/homeassistant/components/dlna_dms/translations/es.json index f9d08f90451..d93988479aa 100644 --- a/homeassistant/components/dlna_dms/translations/es.json +++ b/homeassistant/components/dlna_dms/translations/es.json @@ -2,21 +2,21 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El proceso de configuraci\u00f3n ya est\u00e1 en curso", - "bad_ssdp": "Falta un valor necesario en los datos SSDP", - "no_devices_found": "No se han encontrado dispositivos en la red", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "bad_ssdp": "Falta un valor requerido en los datos SSDP", + "no_devices_found": "No se encontraron dispositivos en la red", "not_dms": "El dispositivo no es un servidor multimedia compatible" }, "flow_title": "{name}", "step": { "confirm": { - "description": "\u00bfQuiere empezar a configurar?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, "user": { "data": { "host": "Host" }, - "description": "Escoge un dispositivo a configurar", + "description": "Elige un dispositivo para configurar", "title": "Dispositivos DLNA DMA descubiertos" } } diff --git a/homeassistant/components/dnsip/translations/es.json b/homeassistant/components/dnsip/translations/es.json index a5a51b76746..de168346526 100644 --- a/homeassistant/components/dnsip/translations/es.json +++ b/homeassistant/components/dnsip/translations/es.json @@ -1,14 +1,14 @@ { "config": { "error": { - "invalid_hostname": "Nombre de host inv\u00e1lido" + "invalid_hostname": "Nombre de host no v\u00e1lido" }, "step": { "user": { "data": { - "hostname": "El nombre de host para el que se realiza la consulta DNS", - "resolver": "Conversor para la b\u00fasqueda de IPV4", - "resolver_ipv6": "Conversor para la b\u00fasqueda de IPV6" + "hostname": "El nombre de host para el que realizar la consulta de DNS", + "resolver": "Resolver para la b\u00fasqueda IPv4", + "resolver_ipv6": "Resolver para la b\u00fasqueda IPv6" } } } @@ -20,8 +20,8 @@ "step": { "init": { "data": { - "resolver": "Resolver para la b\u00fasqueda de IPV4", - "resolver_ipv6": "Resolver para la b\u00fasqueda de IPV6" + "resolver": "Resolver para la b\u00fasqueda IPv4", + "resolver_ipv6": "Resolver para la b\u00fasqueda IPv6" } } } diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json index 880f378c7c3..91ee7c58952 100644 --- a/homeassistant/components/dsmr/translations/es.json +++ b/homeassistant/components/dsmr/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_communicate": "No se ha podido comunicar", + "cannot_communicate": "No se pudo comunicar", "cannot_connect": "No se pudo conectar" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_communicate": "No se ha podido comunicar", + "cannot_communicate": "No se pudo comunicar", "cannot_connect": "No se pudo conectar" }, "step": { @@ -19,12 +19,12 @@ "host": "Host", "port": "Puerto" }, - "title": "Seleccione la direcci\u00f3n de la conexi\u00f3n" + "title": "Selecciona la direcci\u00f3n de conexi\u00f3n" }, "setup_serial": { "data": { - "dsmr_version": "Seleccione la versi\u00f3n de DSMR", - "port": "Seleccione el dispositivo" + "dsmr_version": "Selecciona la versi\u00f3n de DSMR", + "port": "Selecciona el dispositivo" }, "title": "Dispositivo" }, @@ -38,7 +38,7 @@ "data": { "type": "Tipo de conexi\u00f3n" }, - "title": "Seleccione el tipo de conexi\u00f3n" + "title": "Selecciona el tipo de conexi\u00f3n" } } }, diff --git a/homeassistant/components/efergy/translations/es.json b/homeassistant/components/efergy/translations/es.json index ce25d3f3184..a93f2f42235 100644 --- a/homeassistant/components/efergy/translations/es.json +++ b/homeassistant/components/efergy/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/elgato/translations/es.json b/homeassistant/components/elgato/translations/es.json index 2cdbd7e6ae1..a41872115fa 100644 --- a/homeassistant/components/elgato/translations/es.json +++ b/homeassistant/components/elgato/translations/es.json @@ -17,7 +17,7 @@ "description": "Configura la integraci\u00f3n de Elgato Light con Home Assistant." }, "zeroconf_confirm": { - "description": "\u00bfDesea a\u00f1adir Elgato Key Light con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el Key Light de Elgato con n\u00famero de serie `{serial_number}` a Home Assistant?", "title": "Descubierto dispositivo Elgato Key Light" } } diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index 9df8e1f4ddf..25c676caafc 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -3,8 +3,8 @@ "abort": { "address_already_configured": "Ya est\u00e1 configurado un Elk-M1 con esta direcci\u00f3n", "already_configured": "Ya est\u00e1 configurado un Elk-M1 con este prefijo", - "already_in_progress": "La configuraci\u00f3n ya se encuentra en proceso", - "cannot_connect": "Error al conectar", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -19,22 +19,22 @@ "data": { "password": "Contrase\u00f1a", "protocol": "Protocolo", - "temperature_unit": "La unidad de temperatura que el ElkM1 usa.", - "username": "Usuario" + "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", + "username": "Nombre de usuario" }, - "description": "Con\u00e9ctate al sistema descubierto: {mac_address} ({host})", + "description": "Conectar al sistema descubierto: {mac_address} ({host})", "title": "Conectar con Control Elk-M1" }, "manual_connection": { "data": { - "address": "La direcci\u00f3n IP o el dominio o el puerto serie si se conecta a trav\u00e9s de serie.", + "address": "La direcci\u00f3n IP o dominio o puerto serie si se conecta a trav\u00e9s de serie.", "password": "Contrase\u00f1a", - "prefix": "Un prefijo \u00fanico (dejar en blanco si solo tiene un ElkM1).", + "prefix": "Un prefijo \u00fanico (d\u00e9jalo en blanco si solo tienes un ElkM1).", "protocol": "Protocolo", "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", - "username": "Usuario" + "username": "Nombre de usuario" }, - "description": "Conecte un M\u00f3dulo de Interfaz Universal Powerline Bus Powerline (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n [: puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serie, la direcci\u00f3n debe estar en la forma 'tty [: baudios]'. El baud es opcional y el valor predeterminado es 4800. Ejemplo: '/ dev / ttyS1'.", + "description": "La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no seguro' y 2601 para 'seguro'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baud]'. Ejemplo: '/dev/ttyS1'. El baudio es opcional y el valor predeterminado es 115200.", "title": "Conectar con Control Elk-M1" }, "user": { diff --git a/homeassistant/components/elmax/translations/es.json b/homeassistant/components/elmax/translations/es.json index a8130c70d7e..8d6bccd03ab 100644 --- a/homeassistant/components/elmax/translations/es.json +++ b/homeassistant/components/elmax/translations/es.json @@ -6,8 +6,8 @@ "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_pin": "El pin proporcionado no es v\u00e1lido", - "network_error": "Se ha producido un error de red", - "no_panel_online": "No se encontr\u00f3 ning\u00fan panel de control de Elmax en l\u00ednea.", + "network_error": "Ocurri\u00f3 un error de red", + "no_panel_online": "No se encontr\u00f3 ning\u00fan panel de control Elmax en l\u00ednea.", "unknown": "Error inesperado" }, "step": { @@ -17,14 +17,14 @@ "panel_name": "Nombre del panel", "panel_pin": "C\u00f3digo PIN" }, - "description": "Seleccione el panel que desea controlar con esta integraci\u00f3n. Tenga en cuenta que el panel debe estar activado para poder configurarlo." + "description": "Selecciona qu\u00e9 panel te gustar\u00eda controlar con esta integraci\u00f3n. Ten en cuenta que el panel debe estar ENCENDIDO para poder configurarlo." }, "user": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Inicie sesi\u00f3n en Elmax Cloud con sus credenciales" + "description": "Por favor, inicia sesi\u00f3n en la nube de Elmax con tus credenciales" } } } diff --git a/homeassistant/components/emonitor/translations/es.json b/homeassistant/components/emonitor/translations/es.json index bef4b3b2329..8438338bbf9 100644 --- a/homeassistant/components/emonitor/translations/es.json +++ b/homeassistant/components/emonitor/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u00bfQuieres configurar {name} ({host})?", diff --git a/homeassistant/components/enphase_envoy/translations/es.json b/homeassistant/components/enphase_envoy/translations/es.json index 24a63fa6c73..ab385d0a282 100644 --- a/homeassistant/components/enphase_envoy/translations/es.json +++ b/homeassistant/components/enphase_envoy/translations/es.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { @@ -17,7 +17,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Para los modelos m\u00e1s nuevos, introduzca el nombre de usuario `envoy` sin contrase\u00f1a. Para los modelos m\u00e1s antiguos, introduzca el nombre de usuario `installer` sin contrase\u00f1a. Para todos los dem\u00e1s modelos, introduzca un nombre de usuario y una contrase\u00f1a v\u00e1lidos." + "description": "Para los modelos m\u00e1s nuevos, introduce el nombre de usuario `envoy` sin contrase\u00f1a. Para modelos m\u00e1s antiguos, introduce el nombre de usuario `installer` sin contrase\u00f1a. Para todos los dem\u00e1s modelos, introduce un nombre de usuario y contrase\u00f1a v\u00e1lidos." } } } diff --git a/homeassistant/components/environment_canada/translations/es.json b/homeassistant/components/environment_canada/translations/es.json index 7cbfb8caed9..5b491979072 100644 --- a/homeassistant/components/environment_canada/translations/es.json +++ b/homeassistant/components/environment_canada/translations/es.json @@ -1,10 +1,10 @@ { "config": { "error": { - "bad_station_id": "El ID de la estaci\u00f3n no es v\u00e1lido, falta o no se encuentra en la base de datos de ID de la estaci\u00f3n", + "bad_station_id": "El ID de la estaci\u00f3n no es v\u00e1lida, falta o no se encuentra en la base de datos de IDs de estaci\u00f3n", "cannot_connect": "No se pudo conectar", "error_response": "Error de respuesta de Environment Canada", - "too_many_attempts": "Las conexiones con Environment Canada son limitadas; Int\u00e9ntalo de nuevo en 60 segundos", + "too_many_attempts": "Las conexiones con Environment Canada tienen una frecuencia limitada; Int\u00e9ntalo de nuevo en 60 segundos", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/escea/translations/pl.json b/homeassistant/components/escea/translations/pl.json new file mode 100644 index 00000000000..cd9489661c0 --- /dev/null +++ b/homeassistant/components/escea/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 kominek Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index 68e53160322..f3e73d97a52 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "ESP ya est\u00e1 configurado", "already_in_progress": "La configuraci\u00f3n del ESP ya est\u00e1 en marcha", - "reauth_successful": "La re-autenticaci\u00f3n ha funcionado" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "connection_error": "No se puede conectar a ESP. Aseg\u00farate de que tu archivo YAML contenga una l\u00ednea 'api:'.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_psk": "La clave de transporte cifrado no es v\u00e1lida. Por favor, aseg\u00farese de que coincide con la que tiene en su configuraci\u00f3n", + "invalid_psk": "La clave de cifrado de transporte no es v\u00e1lida. Por favor, aseg\u00farate de que coincida con lo que tienes en tu configuraci\u00f3n", "resolve_error": "No se puede resolver la direcci\u00f3n del ESP. Si este error persiste, por favor, configura una direcci\u00f3n IP est\u00e1tica" }, "flow_title": "{name}", @@ -20,20 +20,20 @@ "description": "Escribe la contrase\u00f1a que hayas puesto en la configuraci\u00f3n para {name}." }, "discovery_confirm": { - "description": "\u00bfQuieres a\u00f1adir el nodo `{name}` de ESPHome a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el nodo de ESPHome `{name}` a Home Assistant?", "title": "Nodo ESPHome descubierto" }, "encryption_key": { "data": { "noise_psk": "Clave de cifrado" }, - "description": "Por favor, introduzca la clave de cifrado que estableci\u00f3 en su configuraci\u00f3n para {name}." + "description": "Por favor, introduce la clave de cifrado que estableciste en tu configuraci\u00f3n para {name}." }, "reauth_confirm": { "data": { "noise_psk": "Clave de cifrado" }, - "description": "El dispositivo ESPHome {name} ha activado el transporte cifrado o ha cambiado la clave de cifrado. Por favor, introduzca la clave actualizada." + "description": "El dispositivo ESPHome {name} habilit\u00f3 el cifrado de transporte o cambi\u00f3 la clave de cifrado. Introduce la clave actualizada." }, "user": { "data": { diff --git a/homeassistant/components/ezviz/translations/es.json b/homeassistant/components/ezviz/translations/es.json index f8a80614fb0..a69cb8d5d24 100644 --- a/homeassistant/components/ezviz/translations/es.json +++ b/homeassistant/components/ezviz/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "La cuenta ya est\u00e1 configurada", - "ezviz_cloud_account_missing": "Falta la cuenta de Ezviz Cloud. Por favor, reconfigura la cuenta de Ezviz Cloud", + "ezviz_cloud_account_missing": "Falta la cuenta de Ezviz Cloud. Por favor, vuelve a configurar la cuenta de Ezviz Cloud", "unknown": "Error inesperado" }, "error": { @@ -35,7 +35,7 @@ "username": "Nombre de usuario" }, "description": "Especificar manualmente la URL de tu regi\u00f3n", - "title": "Conectar con la URL personalizada de Ezviz" + "title": "Conectar a la URL personalizada de Ezviz" } } }, @@ -43,8 +43,8 @@ "step": { "init": { "data": { - "ffmpeg_arguments": "Par\u00e1metros pasados a ffmpeg para c\u00e1maras", - "timeout": "Tiempo de espera de la solicitud (segundos)" + "ffmpeg_arguments": "Argumentos pasados a ffmpeg para las c\u00e1maras", + "timeout": "Tiempo de espera de solicitud (segundos)" } } } diff --git a/homeassistant/components/faa_delays/translations/es.json b/homeassistant/components/faa_delays/translations/es.json index 71f7fecef41..3108f3d30fe 100644 --- a/homeassistant/components/faa_delays/translations/es.json +++ b/homeassistant/components/faa_delays/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "Este aeropuerto ya est\u00e1 configurado." }, "error": { - "cannot_connect": "Fallo al conectar", + "cannot_connect": "No se pudo conectar", "invalid_airport": "El c\u00f3digo del aeropuerto no es v\u00e1lido", "unknown": "Error inesperado" }, @@ -13,7 +13,7 @@ "data": { "id": "Aeropuerto" }, - "description": "Introduzca un c\u00f3digo de aeropuerto estadounidense en formato IATA", + "description": "Introduce un c\u00f3digo de aeropuerto de EE.UU. en formato IATA", "title": "Retrasos de la FAA" } } diff --git a/homeassistant/components/fan/translations/es.json b/homeassistant/components/fan/translations/es.json index e2bc0b1611d..15486b23a48 100644 --- a/homeassistant/components/fan/translations/es.json +++ b/homeassistant/components/fan/translations/es.json @@ -10,7 +10,7 @@ "is_on": "{entity_name} est\u00e1 activado" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" } diff --git a/homeassistant/components/fibaro/translations/es.json b/homeassistant/components/fibaro/translations/es.json index 567ae7e59a2..0bf8f1aaa76 100644 --- a/homeassistant/components/fibaro/translations/es.json +++ b/homeassistant/components/fibaro/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -13,7 +13,7 @@ "data": { "import_plugins": "\u00bfImportar entidades desde los plugins de fibaro?", "password": "Contrase\u00f1a", - "url": "URL en el format http://HOST/api/", + "url": "URL en el formato http://HOST/api/", "username": "Nombre de usuario" } } diff --git a/homeassistant/components/filesize/translations/es.json b/homeassistant/components/filesize/translations/es.json index ee030e86cf9..c78a497b9d6 100644 --- a/homeassistant/components/filesize/translations/es.json +++ b/homeassistant/components/filesize/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "not_allowed": "Ruta no permitida", + "not_allowed": "La ruta no est\u00e1 permitida", "not_valid": "La ruta no es v\u00e1lida" }, "step": { diff --git a/homeassistant/components/fivem/translations/es.json b/homeassistant/components/fivem/translations/es.json index 273435a4e5e..3264888e71e 100644 --- a/homeassistant/components/fivem/translations/es.json +++ b/homeassistant/components/fivem/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectarse. Compruebe el host y el puerto e int\u00e9ntelo de nuevo. Aseg\u00farese tambi\u00e9n de que est\u00e1 ejecutando el servidor FiveM m\u00e1s reciente.", + "cannot_connect": "No se pudo conectar. Por favor, verifica el host y el puerto y vuelve a intentarlo. Tambi\u00e9n aseg\u00farate de estar ejecutando el servidor FiveM m\u00e1s reciente.", "invalid_game_name": "La API del juego al que intentas conectarte no es un juego de FiveM.", "unknown_error": "Error inesperado" }, diff --git a/homeassistant/components/flipr/translations/es.json b/homeassistant/components/flipr/translations/es.json index 0a066451b84..02af1fc3121 100644 --- a/homeassistant/components/flipr/translations/es.json +++ b/homeassistant/components/flipr/translations/es.json @@ -22,7 +22,7 @@ "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" }, - "description": "Con\u00e9ctese usando su cuenta Flipr.", + "description": "Con\u00e9ctate usando tu cuenta Flipr.", "title": "Conectarse a Flipr" } } diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index 5789a2eccc0..b2ffbe162a8 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -15,7 +15,7 @@ "password": "Contrase\u00f1a" }, "description": "La contrase\u00f1a de {username} ya no es v\u00e1lida.", - "title": "Reautenticar tu cuenta de Flume" + "title": "Volver a autenticar tu cuenta Flume" }, "user": { "data": { diff --git a/homeassistant/components/flunearyou/translations/pl.json b/homeassistant/components/flunearyou/translations/pl.json index b250d220cf3..71d390992e4 100644 --- a/homeassistant/components/flunearyou/translations/pl.json +++ b/homeassistant/components/flunearyou/translations/pl.json @@ -16,5 +16,18 @@ "title": "Konfiguracja Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zewn\u0119trzne \u017ar\u00f3d\u0142o danych dla integracji Flu Near You nie jest ju\u017c dost\u0119pne; tym sposobem integracja ju\u017c nie dzia\u0142a. \n\nNaci\u015bnij ZATWIERD\u0179, aby usun\u0105\u0107 Flu Near You z Home Assistanta.", + "title": "Usu\u0144 Flu Near You" + } + } + }, + "title": "Flu Near You nie jest ju\u017c dost\u0119pna" + } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/es.json b/homeassistant/components/flux_led/translations/es.json index 54fef29c371..89a4e02319e 100644 --- a/homeassistant/components/flux_led/translations/es.json +++ b/homeassistant/components/flux_led/translations/es.json @@ -11,13 +11,13 @@ "flow_title": "{model} {id} ({ipaddr})", "step": { "discovery_confirm": { - "description": "\u00bfQuieres configurar {model} {id} ( {ipaddr} )?" + "description": "\u00bfQuieres configurar {model} {id} ({ipaddr})?" }, "user": { "data": { "host": "Host" }, - "description": "Si deja el host vac\u00edo, la detecci\u00f3n se utilizar\u00e1 para buscar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } }, @@ -25,10 +25,10 @@ "step": { "init": { "data": { - "custom_effect_colors": "Efecto personalizado: Lista de 1 a 16 colores [R, G, B]. Ejemplo: [255,0,255],[60,128,0]", + "custom_effect_colors": "Efecto personalizado: Lista de 1 a 16 colores [R,G,B]. Ejemplo: [255,0,255],[60,128,0]", "custom_effect_speed_pct": "Efecto personalizado: Velocidad en porcentajes para el efecto que cambia de color.", - "custom_effect_transition": "Efecto personalizado: Tipo de transici\u00f3n entre colores.", - "mode": "Modo de brillo elegido." + "custom_effect_transition": "Efecto personalizado: Tipo de transici\u00f3n entre los colores.", + "mode": "El modo de brillo elegido." } } } diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index d82bd944202..5567fa63ee2 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -3,14 +3,14 @@ "step": { "user": { "data": { - "azimuth": "Acimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", + "azimuth": "Azimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", "latitude": "Latitud", "longitude": "Longitud", - "modules power": "Potencia total en vatios pico de sus m\u00f3dulos solares", + "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares", "name": "Nombre" }, - "description": "Rellene los datos de sus paneles solares. Consulte la documentaci\u00f3n si alg\u00fan campo no est\u00e1 claro." + "description": "Rellena los datos de tus placas solares. Consulta la documentaci\u00f3n si un campo no est\u00e1 claro." } } }, @@ -22,10 +22,10 @@ "azimuth": "Azimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", "damping": "Factor de amortiguaci\u00f3n: ajusta los resultados por la ma\u00f1ana y por la noche", "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", - "inverter_size": "Potencia del inversor (Watts)", + "inverter_size": "Tama\u00f1o del inversor (vatios)", "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares" }, - "description": "Estos valores permiten ajustar el resultado de Solar.Forecast. Consulte la documentaci\u00f3n si un campo no est\u00e1 claro." + "description": "Estos valores permiten modificar el resultado de Solar.Forecast. Por favor, consulta la documentaci\u00f3n si un campo no est\u00e1 claro." } } } diff --git a/homeassistant/components/freedompro/translations/es.json b/homeassistant/components/freedompro/translations/es.json index c08c30d64dc..1657aaa908e 100644 --- a/homeassistant/components/freedompro/translations/es.json +++ b/homeassistant/components/freedompro/translations/es.json @@ -12,7 +12,7 @@ "data": { "api_key": "Clave API" }, - "description": "Ingresa la clave API obtenida de https://home.freedompro.eu", + "description": "Por favor, introduce la clave API obtenida de https://home.freedompro.eu", "title": "Clave API de Freedompro" } } diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index 21dafaec367..a8f5168902f 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -13,14 +13,14 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "upnp_not_configured": "Falta la configuraci\u00f3n de UPnP en el dispositivo." }, - "flow_title": "FRITZ!Box Tools: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Descubierto FRITZ!Box: {name}\n\nConfigurar FRITZ!Box Tools para controlar tu {name}", + "description": "Descubierto FRITZ!Box: {name}\n\nConfigura FRITZ!Box Tools para controlar tu {name}", "title": "Configurar FRITZ!Box Tools" }, "reauth_confirm": { @@ -28,7 +28,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Actualizar credenciales de FRITZ!Box Tools para: {host}.\n\n FRITZ!Box Tools no puede iniciar sesi\u00f3n en tu FRITZ!Box.", + "description": "Actualiza las credenciales de FRITZ!Box Tools para: {host}. \n\nFRITZ!Box Tools no puede iniciar sesi\u00f3n en tu FRITZ!Box.", "title": "Actualizando FRITZ!Box Tools - credenciales" }, "user": { @@ -38,8 +38,8 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Configure las herramientas de FRITZ! Box para controlar su FRITZ! Box.\n M\u00ednimo necesario: nombre de usuario, contrase\u00f1a.", - "title": "Configurar las herramientas de FRITZ! Box" + "description": "Configura las herramientas de FRITZ!Box para controlar tu FRITZ!Box.\nM\u00ednimo necesario: nombre de usuario, contrase\u00f1a.", + "title": "Configurar las herramientas de FRITZ!Box" } } }, @@ -48,7 +48,7 @@ "init": { "data": { "consider_home": "Segundos para considerar un dispositivo en 'casa'", - "old_discovery": "Habilitar m\u00e9todo de descubrimiento antiguo" + "old_discovery": "Habilitar el m\u00e9todo de descubrimiento antiguo" } } } diff --git a/homeassistant/components/fronius/translations/es.json b/homeassistant/components/fronius/translations/es.json index e8df3b81a6e..98fdc7e1ebd 100644 --- a/homeassistant/components/fronius/translations/es.json +++ b/homeassistant/components/fronius/translations/es.json @@ -11,13 +11,13 @@ "flow_title": "{device}", "step": { "confirm_discovery": { - "description": "\u00bfQuieres agregar {device} a Home Assistant?" + "description": "\u00bfQuieres a\u00f1adir {device} a Home Assistant?" }, "user": { "data": { "host": "Host" }, - "description": "Configure la direcci\u00f3n IP o el nombre de host local de su dispositivo Fronius.", + "description": "Configura la direcci\u00f3n IP o el nombre de host local de tu dispositivo Fronius.", "title": "Fronius SolarNet" } } diff --git a/homeassistant/components/garages_amsterdam/translations/es.json b/homeassistant/components/garages_amsterdam/translations/es.json index 79433b6b854..dc4b9aa9c96 100644 --- a/homeassistant/components/garages_amsterdam/translations/es.json +++ b/homeassistant/components/garages_amsterdam/translations/es.json @@ -10,9 +10,9 @@ "data": { "garage_name": "Nombre del garaje" }, - "title": "Elige un garaje para vigilar" + "title": "Elige un garaje para supervisar" } } }, - "title": "Garajes Amsterdam" + "title": "Garages Amsterdam" } \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/es.json b/homeassistant/components/geofency/translations/es.json index 6726a4d2e3b..8e3c806f708 100644 --- a/homeassistant/components/geofency/translations/es.json +++ b/homeassistant/components/geofency/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "No est\u00e1 conectado a Home Assistant Cloud.", + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "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." }, diff --git a/homeassistant/components/github/translations/es.json b/homeassistant/components/github/translations/es.json index 37bf1d4689e..bf1dcf64ae4 100644 --- a/homeassistant/components/github/translations/es.json +++ b/homeassistant/components/github/translations/es.json @@ -5,14 +5,14 @@ "could_not_register": "No se pudo registrar la integraci\u00f3n con GitHub" }, "progress": { - "wait_for_device": "1. Abra {url}\n 2.Pegue la siguiente clave para autorizar la integraci\u00f3n:\n ```\n {code}\n ```\n" + "wait_for_device": "1. Abre {url}\n2.Pega la siguiente clave para autorizar la integraci\u00f3n:\n```\n{code}\n```\n" }, "step": { "repositories": { "data": { - "repositories": "Seleccione los repositorios a seguir." + "repositories": "Selecciona los repositorios a seguir." }, - "title": "Configuraci\u00f3n de repositorios" + "title": "Configura los repositorios" } } } diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json index e02b06d32f5..9772f3f6d91 100644 --- a/homeassistant/components/goalzero/translations/es.json +++ b/homeassistant/components/goalzero/translations/es.json @@ -12,7 +12,7 @@ }, "step": { "confirm_discovery": { - "description": "Se recomienda reservar el DHCP en el router. Si no se configura, el dispositivo puede dejar de estar disponible hasta que el Home Assistant detecte la nueva direcci\u00f3n ip. Consulte el manual de usuario de su router." + "description": "Se recomienda la reserva de DHCP en tu router. Si no se configura, es posible que el dispositivo no est\u00e9 disponible hasta que Home Assistant detecte la nueva direcci\u00f3n IP. Consulta el manual de usuario de tu router." }, "user": { "data": { diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 5bd628e1f6e..eaf84a11931 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -5,20 +5,20 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "already_in_progress": "El proceso de configuraci\u00f3n ya est\u00e1 en curso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", - "code_expired": "El c\u00f3digo de autenticaci\u00f3n caduc\u00f3 o la configuraci\u00f3n de la credencial no es v\u00e1lida, int\u00e9ntelo de nuevo.", + "code_expired": "El c\u00f3digo de autenticaci\u00f3n caduc\u00f3 o la configuraci\u00f3n de la credencial no es v\u00e1lida, por favor, int\u00e9ntalo de nuevo.", "invalid_access_token": "Token de acceso no v\u00e1lido", - "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "oauth_error": "Se han recibido datos token inv\u00e1lidos.", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "create_entry": { - "default": "Autenticaci\u00f3n exitosa" + "default": "Autenticado correctamente" }, "progress": { - "exchange": "Para vincular su cuenta de Google, visite [ {url} ]( {url} ) e ingrese el c\u00f3digo: \n\n {user_code}" + "exchange": "Para vincular tu cuenta de Google, visita la [{url}]({url}) e introduce el c\u00f3digo:\n\n{user_code}" }, "step": { "auth": { @@ -28,8 +28,8 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { - "description": "La integraci\u00f3n de Google Calendar necesita volver a autenticar su cuenta", - "title": "Integraci\u00f3n de la reautenticaci\u00f3n" + "description": "La integraci\u00f3n Google Calendar necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" } } }, diff --git a/homeassistant/components/google_travel_time/translations/es.json b/homeassistant/components/google_travel_time/translations/es.json index cecfc620e47..44d227554cb 100644 --- a/homeassistant/components/google_travel_time/translations/es.json +++ b/homeassistant/components/google_travel_time/translations/es.json @@ -14,7 +14,7 @@ "name": "Nombre", "origin": "Origen" }, - "description": "Al especificar el origen y el destino, puedes proporcionar una o m\u00e1s ubicaciones separadas por el car\u00e1cter de barra vertical, en forma de una direcci\u00f3n, coordenadas de latitud/longitud o un ID de lugar de Google. Al especificar la ubicaci\u00f3n utilizando un ID de lugar de Google, el ID debe tener el prefijo `place_id:`." + "description": "Al especificar el origen y el destino, puedes proporcionar una o m\u00e1s ubicaciones separadas por el car\u00e1cter de barra vertical, en forma de direcci\u00f3n, coordenadas de latitud/longitud o un ID de lugar de Google. Al especificar la ubicaci\u00f3n mediante un ID de lugar de Google, el ID debe tener el prefijo `place_id:`." } } }, @@ -28,10 +28,10 @@ "time": "Hora", "time_type": "Tipo de tiempo", "transit_mode": "Modo de tr\u00e1nsito", - "transit_routing_preference": "Preferencia de enrutamiento de tr\u00e1nsito", + "transit_routing_preference": "Preferencia de ruta de tr\u00e1nsito", "units": "Unidades" }, - "description": "Opcionalmente, puedes especificar una hora de salida o una hora de llegada. Si especifica una hora de salida, puedes introducir `ahora`, una marca de tiempo Unix o una cadena de tiempo 24 horas como `08:00:00`. Si especifica una hora de llegada, puede usar una marca de tiempo Unix o una cadena de tiempo 24 horas como `08:00:00`" + "description": "Opcionalmente, puedes especificar una Hora de salida o una Hora de llegada. Si especificas una hora de salida, puedes introducir \"ahora\", una marca de tiempo de Unix o una cadena de tiempo de 24 horas como \"08:00:00\". Si especificas una hora de llegada, puedes usar una marca de tiempo de Unix o una cadena de tiempo de 24 horas como `08:00:00`" } } }, diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index bea5a398f8c..c5fe0ede852 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -8,32 +8,32 @@ "hide_members": "Ocultar miembros", "name": "Nombre" }, - "description": "Si \"todas las entidades\" est\u00e1n habilitadas, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1n deshabilitadas, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado.", - "title": "Agregar grupo" + "description": "Si \"todas las entidades\" est\u00e1 habilitado, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1 deshabilitado, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado.", + "title": "A\u00f1adir grupo" }, "cover": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", - "name": "Nombre del Grupo" + "hide_members": "Ocultar miembros", + "name": "Nombre" }, "title": "A\u00f1adir grupo" }, "fan": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, - "title": "Agregar grupo" + "title": "A\u00f1adir grupo" }, "light": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, - "title": "Agregar grupo" + "title": "A\u00f1adir grupo" }, "lock": { "data": { @@ -46,10 +46,10 @@ "media_player": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, - "title": "Agregar grupo" + "title": "A\u00f1adir grupo" }, "switch": { "data": { @@ -60,12 +60,12 @@ "title": "A\u00f1adir grupo" }, "user": { - "description": "Los grupos permiten crear una nueva entidad que representa a varias entidades del mismo tipo.", + "description": "Los grupos te permiten crear una nueva entidad que representa varias entidades del mismo tipo.", "menu_options": { "binary_sensor": "Grupo de sensores binarios", - "cover": "Grupo de cubiertas", + "cover": "Grupo de persianas/cortinas", "fan": "Grupo de ventiladores", - "light": "Grupo de luz", + "light": "Grupo de luces", "lock": "Bloquear el grupo", "media_player": "Grupo de reproductores multimedia", "switch": "Grupo de conmutadores" @@ -80,27 +80,27 @@ "data": { "all": "Todas las entidades", "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" }, "description": "Si \"todas las entidades\" est\u00e1 habilitado, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1 deshabilitado, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado." }, "cover": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" } }, "fan": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" } }, "light": { "data": { "all": "Todas las entidades", "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" }, "description": "Si \"todas las entidades\" est\u00e1 habilitado, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1 deshabilitado, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado." }, @@ -113,7 +113,7 @@ "media_player": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" } }, "switch": { diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 2078df1cae9..9b042a7a1c4 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -17,5 +17,18 @@ "description": "Konfiguriere ein lokales Elexa Guardian Ger\u00e4t." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Zielentit\u00e4ts-ID von `{alternate_target}` zu verwenden. Klicke dann unten auf SUBMIT, um dieses Problem als behoben zu markieren.", + "title": "Der Dienst {deprecated_service} wird entfernt" + } + } + }, + "title": "Der Dienst {deprecated_service} wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/et.json b/homeassistant/components/guardian/translations/et.json index 22ee0bf1300..49172263d9c 100644 --- a/homeassistant/components/guardian/translations/et.json +++ b/homeassistant/components/guardian/translations/et.json @@ -17,5 +17,18 @@ "description": "Seadista kohalik Elexa Guardiani seade." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Uuenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte, et need kasutaksid selle asemel teenust `{alternate_service}}, mille siht\u00fcksuse ID on `{alternate_target}}. Seej\u00e4rel kl\u00f5psa allpool nuppu ESITA, et m\u00e4rkida see probleem lahendatuks.", + "title": "Teenus {deprecated_service} eemaldatakse" + } + } + }, + "title": "Teenus {deprecated_service} eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 1f8f9039707..25a0abb5526 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -17,5 +17,18 @@ "description": "Konfigur\u00e1lja a helyi Elexa Guardian eszk\u00f6zt." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", + "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } + }, + "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json index 77c46e95afc..0d06eb61729 100644 --- a/homeassistant/components/guardian/translations/id.json +++ b/homeassistant/components/guardian/translations/id.json @@ -17,5 +17,18 @@ "description": "Konfigurasikan perangkat Elexa Guardian lokal." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`. Kemudian, klik KIRIM di bawah ini untuk menandai masalah ini sebagai terselesaikan.", + "title": "Layanan {deprecated_service} dalam proses penghapusan" + } + } + }, + "title": "Layanan {deprecated_service} dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json index a61b43dcfea..9c2669fdeb2 100644 --- a/homeassistant/components/guardian/translations/no.json +++ b/homeassistant/components/guardian/translations/no.json @@ -17,5 +17,18 @@ "description": "Konfigurer en lokal Elexa Guardian-enhet." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten til i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `. Klikk deretter SEND nedenfor for \u00e5 merke dette problemet som l\u00f8st.", + "title": "{deprecated_service} -tjenesten blir fjernet" + } + } + }, + "title": "{deprecated_service} -tjenesten blir fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json index 5e5b2d143e2..c86f98b3b8e 100644 --- a/homeassistant/components/guardian/translations/pl.json +++ b/homeassistant/components/guardian/translations/pl.json @@ -17,5 +17,18 @@ "description": "Skonfiguruj lokalne urz\u0105dzenie Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi `{alternate_service}` z encj\u0105 docelow\u0105 `{alternate_target}`. Nast\u0119pnie kliknij ZATWIERD\u0179 poni\u017cej, aby oznaczy\u0107 ten problem jako rozwi\u0105zany.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } + } + }, + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pt-BR.json b/homeassistant/components/guardian/translations/pt-BR.json index f4d4273b4ab..2a4514f4968 100644 --- a/homeassistant/components/guardian/translations/pt-BR.json +++ b/homeassistant/components/guardian/translations/pt-BR.json @@ -17,5 +17,18 @@ "description": "Configure um dispositivo local Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } + } + }, + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index 40a7de81170..baa0e477b4c 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -17,5 +17,18 @@ "description": "\u8a2d\u5b9a\u5340\u57df Elexa Guardian \u88dd\u7f6e\u3002" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\uff0c\u7136\u5f8c\u9ede\u9078\u50b3\u9001\u4ee5\u6a19\u793a\u554f\u984c\u5df2\u89e3\u6c7a\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + } + }, + "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/es.json b/homeassistant/components/habitica/translations/es.json index 55cf8eb7642..681336e553f 100644 --- a/homeassistant/components/habitica/translations/es.json +++ b/homeassistant/components/habitica/translations/es.json @@ -12,7 +12,7 @@ "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" + "description": "Conecta tu perfil de Habitica para permitir el seguimiento 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" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/es.json b/homeassistant/components/hisense_aehw4a1/translations/es.json index 8eb81391ec4..3f6e58b792e 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/es.json +++ b/homeassistant/components/hisense_aehw4a1/translations/es.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar Hisense AEH-W4A1?" + "description": "\u00bfQuieres configurar Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index ccca2b3ea4d..ffba8558cac 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -6,9 +6,9 @@ "unknown_entry": "No se puede encontrar una entrada existente." }, "error": { - "invalid_code": "No se ha podido iniciar la sesi\u00f3n en Hive. Tu c\u00f3digo de autenticaci\u00f3n de dos factores era incorrecto.", - "invalid_password": "No se ha podido iniciar la sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, int\u00e9ntelo de nuevo.", - "invalid_username": "No se ha podido iniciar la sesi\u00f3n en Hive. No se reconoce su direcci\u00f3n de correo electr\u00f3nico.", + "invalid_code": "Error al iniciar sesi\u00f3n en Hive. Tu c\u00f3digo de autenticaci\u00f3n de dos factores era incorrecto.", + "invalid_password": "Error al iniciar sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, prueba de nuevo.", + "invalid_username": "Error al iniciar sesi\u00f3n en Hive. No se reconoce tu direcci\u00f3n de correo electr\u00f3nico.", "no_internet_available": "Se requiere una conexi\u00f3n a Internet para conectarse a Hive.", "unknown": "Error inesperado" }, @@ -17,7 +17,7 @@ "data": { "2fa": "C\u00f3digo de dos factores" }, - "description": "Introduzca su c\u00f3digo de autentificaci\u00f3n Hive. \n \n Introduzca el c\u00f3digo 0000 para solicitar otro c\u00f3digo.", + "description": "introduce tu c\u00f3digo de autenticaci\u00f3n de Hive. \n\nPor favor, introduce el c\u00f3digo 0000 para solicitar otro c\u00f3digo.", "title": "Autenticaci\u00f3n de dos factores de Hive." }, "configuration": { @@ -32,13 +32,13 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Vuelva a introducir sus datos de acceso a Hive.", + "description": "Vuelve a introducir tus datos de acceso a Hive.", "title": "Inicio de sesi\u00f3n en Hive" }, "user": { "data": { "password": "Contrase\u00f1a", - "scan_interval": "Intervalo de exploraci\u00f3n (segundos)", + "scan_interval": "Intervalo de escaneo (segundos)", "username": "Nombre de usuario" }, "description": "Introduce tus datos de acceso a Hive.", @@ -50,9 +50,9 @@ "step": { "user": { "data": { - "scan_interval": "Intervalo de exploraci\u00f3n (segundos)" + "scan_interval": "Intervalo de escaneo (segundos)" }, - "description": "Actualice el intervalo de escaneo para buscar datos m\u00e1s a menudo.", + "description": "Actualiza el intervalo de escaneo para buscar datos con m\u00e1s frecuencia.", "title": "Opciones para Hive" } } diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 5076e7b8800..170df5c4a13 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -23,11 +23,11 @@ "data": { "entities": "Entidad" }, - "title": "Seleccione la entidad para el accesorio" + "title": "Selecciona la entidad para el accesorio" }, "advanced": { "data": { - "devices": "Dispositivos (disparadores)" + "devices": "Dispositivos (Disparadores)" }, "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", "title": "Configuraci\u00f3n avanzada" @@ -37,26 +37,26 @@ "camera_audio": "C\u00e1maras que admiten audio", "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" }, - "description": "Verifique todas las c\u00e1maras que admiten transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", + "description": "Verifica todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." }, "exclude": { "data": { "entities": "Entidades" }, - "description": "Se incluir\u00e1n todas las entidades de \" {domains} \" excepto las entidades excluidas y las entidades categorizadas.", + "description": "Se incluir\u00e1n todas las entidades de \"{domains}\" excepto las entidades excluidas y las entidades categorizadas.", "title": "Selecciona las entidades a excluir" }, "include": { "data": { "entities": "Entidades" }, - "description": "Se incluir\u00e1n todas las entidades de \" {domains} \" a menos que se seleccionen entidades espec\u00edficas.", - "title": "Seleccione las entidades a incluir" + "description": "Se incluir\u00e1n todas las entidades de \"{domains}\" a menos que se seleccionen entidades espec\u00edficas.", + "title": "Selecciona las entidades a incluir" }, "init": { "data": { - "domains": "Dominios a incluir", + "domains": "Dominios para incluir", "include_exclude_mode": "Modo de inclusi\u00f3n", "mode": "Mode de HomeKit" }, diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index 54dd93f8a55..48015d0418d 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -12,7 +12,7 @@ }, "error": { "authentication_error": "C\u00f3digo HomeKit incorrecto. Por favor, compru\u00e9belo e int\u00e9ntelo de nuevo.", - "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado es inseguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", + "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado no es seguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", "max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.", "pairing_failed": "Se ha producido un error no controlado al intentar emparejarse con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no est\u00e9 admitido en este momento.", "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", diff --git a/homeassistant/components/homekit_controller/translations/select.es.json b/homeassistant/components/homekit_controller/translations/select.es.json index 13c45f8e538..8ef5ae09895 100644 --- a/homeassistant/components/homekit_controller/translations/select.es.json +++ b/homeassistant/components/homekit_controller/translations/select.es.json @@ -1,9 +1,9 @@ { "state": { "homekit_controller__ecobee_mode": { - "away": "Afuera", + "away": "Ausente", "home": "Inicio", - "sleep": "Durmiendo" + "sleep": "Dormir" } } } \ No newline at end of file diff --git a/homeassistant/components/homewizard/translations/es.json b/homeassistant/components/homewizard/translations/es.json index 898d37fed09..c2fe80926da 100644 --- a/homeassistant/components/homewizard/translations/es.json +++ b/homeassistant/components/homewizard/translations/es.json @@ -2,21 +2,21 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "api_not_enabled": "La API no est\u00e1 habilitada. Habilite la API en la aplicaci\u00f3n HomeWizard Energy en configuraci\u00f3n", + "api_not_enabled": "La API no est\u00e1 habilitada. Habilita la API en la aplicaci\u00f3n HomeWizard Energy dentro de Configuraci\u00f3n", "device_not_supported": "Este dispositivo no es compatible", - "invalid_discovery_parameters": "Versi\u00f3n de API no compatible detectada", + "invalid_discovery_parameters": "Se ha detectado una versi\u00f3n de API no compatible", "unknown_error": "Error inesperado" }, "step": { "discovery_confirm": { - "description": "\u00bfDesea configurar {product_type} ({serial}) en {ip_address} ?", + "description": "\u00bfQuieres configurar {product_type} ({serial}) en {ip_address} ?", "title": "Confirmar" }, "user": { "data": { "ip_address": "Direcci\u00f3n IP" }, - "description": "Ingrese la direcci\u00f3n IP de su dispositivo HomeWizard Energy para integrarlo con Home Assistant.", + "description": "Introduce la direcci\u00f3n IP de tu dispositivo HomeWizard Energy para integrarlo con Home Assistant.", "title": "Configurar dispositivo" } } diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index c08c02c3632..c97e5aa3ddf 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -9,7 +9,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." + "description": "Por favor, introduce las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." } } }, @@ -17,8 +17,8 @@ "step": { "init": { "data": { - "away_cool_temperature": "Temperatura fria, modo fuera", - "away_heat_temperature": "Temperatura del calor exterior" + "away_cool_temperature": "Temperatura en modo fr\u00edo cuando ausente", + "away_heat_temperature": "Temperatura en modo calor cuando ausente" }, "description": "Opciones de configuraci\u00f3n adicionales de Honeywell. Las temperaturas se establecen en Fahrenheit." } diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 485d2e16f69..4d1ffba4608 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -32,8 +32,8 @@ "data": { "name": "Nombre del servicio de notificaci\u00f3n (el cambio requiere reiniciar)", "recipient": "Destinatarios de notificaciones por SMS", - "track_wired_clients": "Seguir clientes de red cableados", - "unauthenticated_mode": "Modo no autenticado (el cambio requiere recarga)" + "track_wired_clients": "Rastrear clientes de red cableados", + "unauthenticated_mode": "Modo no autenticado (el cambio requiere recargar)" } } } diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index 6b72df25f65..db5a72c7fe2 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -53,15 +53,15 @@ }, "trigger_type": { "double_short_release": "Ambos \"{subtype}\" soltados", - "initial_press": "Bot\u00f3n \"{subtype}\" pulsado inicialmente", - "long_release": "Bot\u00f3n \"{subtype}\" liberado tras una pulsaci\u00f3n larga", + "initial_press": "Bot\u00f3n \"{subtype}\" presionado inicialmente", + "long_release": "Bot\u00f3n \"{subtype}\" soltado tras una pulsaci\u00f3n larga", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", "remote_double_button_long_press": "Ambos \"{subtype}\" soltados despu\u00e9s de pulsaci\u00f3n larga", "remote_double_button_short_press": "Ambos \"{subtype}\" soltados", - "repeat": "Bot\u00f3n \"{subtype}\" pulsado", - "short_release": "Bot\u00f3n \"{subtype}\" liberado tras una breve pulsaci\u00f3n" + "repeat": "Bot\u00f3n \"{subtype}\" presionado", + "short_release": "Bot\u00f3n \"{subtype}\" soltado tras una breve pulsaci\u00f3n" } }, "options": { @@ -71,7 +71,7 @@ "allow_hue_groups": "Permitir grupos de Hue", "allow_hue_scenes": "Permitir escenas Hue", "allow_unreachable": "Permitir que las bombillas inalcanzables informen su estado correctamente", - "ignore_availability": "Ignorar el estado de conectividad de los dispositivos dados" + "ignore_availability": "Ignorar el estado de conectividad de los siguientes dispositivos" } } } diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json index 8506445b25c..944361ef35d 100644 --- a/homeassistant/components/humidifier/translations/es.json +++ b/homeassistant/components/humidifier/translations/es.json @@ -13,7 +13,7 @@ "is_on": "{entity_name} est\u00e1 activado" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "target_humidity_changed": "La humedad objetivo ha cambiado en {entity_name}", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json index a5cf5303000..924edd5394f 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/es.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", "unknown": "Error inesperado" }, - "flow_title": "{name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "link": { "description": "\u00bfQuieres configurar {name} ({host})?", diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index d96584ee532..45e41859c81 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -23,7 +23,7 @@ "description": "Configurar autorizaci\u00f3n a tu servidor Hyperion Ambilight" }, "confirm": { - "description": "\u00bfQuieres a\u00f1adir el siguiente Hyperion Ambilight en Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**Identificaci\u00f3n**: {id}", + "description": "\u00bfQuieres a\u00f1adir el siguiente Ambilight de Hyperion a Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**ID**: {id}", "title": "Confirmar la adici\u00f3n del servicio Hyperion Ambilight" }, "create_token": { diff --git a/homeassistant/components/ifttt/translations/es.json b/homeassistant/components/ifttt/translations/es.json index e65f12b6295..a296af6a6d6 100644 --- a/homeassistant/components/ifttt/translations/es.json +++ b/homeassistant/components/ifttt/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "No est\u00e1 conectado a Home Assistant Cloud.", + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "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." }, diff --git a/homeassistant/components/integration/translations/es.json b/homeassistant/components/integration/translations/es.json index 7fee4f392ce..59e2dacb2c7 100644 --- a/homeassistant/components/integration/translations/es.json +++ b/homeassistant/components/integration/translations/es.json @@ -15,7 +15,7 @@ "unit_prefix": "La salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico seleccionado.", "unit_time": "La salida se escalar\u00e1 seg\u00fan la unidad de tiempo seleccionada." }, - "description": "Cree un sensor que calcule una suma de Riemann para estimar la integral de un sensor.", + "description": "Crea un sensor que calcule una suma de Riemann para estimar la integral de un sensor.", "title": "A\u00f1adir sensor integral de suma de Riemann" } } diff --git a/homeassistant/components/intellifire/translations/es.json b/homeassistant/components/intellifire/translations/es.json index c44475be1a1..3cf2cfc6938 100644 --- a/homeassistant/components/intellifire/translations/es.json +++ b/homeassistant/components/intellifire/translations/es.json @@ -7,7 +7,7 @@ }, "error": { "api_error": "Error de inicio de sesi\u00f3n", - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "iftapi_connect": "Se ha producido un error al conectar a iftapi.net" }, "flow_title": "{serial} ({host})", @@ -19,11 +19,11 @@ } }, "dhcp_confirm": { - "description": "\u00bfQuieres configurar {host} \nSerie: {serial}?" + "description": "\u00bfQuieres configurar {host} \nN\u00ba serie: {serial}?" }, "manual_device_entry": { "data": { - "host": "Host (direcci\u00f3n IP)" + "host": "Host (Direcci\u00f3n IP)" }, "description": "Configuraci\u00f3n local" }, diff --git a/homeassistant/components/iotawatt/translations/es.json b/homeassistant/components/iotawatt/translations/es.json index 4a8b29a183f..4370bfbe181 100644 --- a/homeassistant/components/iotawatt/translations/es.json +++ b/homeassistant/components/iotawatt/translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "La conexi\u00f3n ha fallado", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -11,7 +11,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "El dispositivo IoTawatt requiere autenticaci\u00f3n. Introduce el nombre de usuario y la contrase\u00f1a y haz clic en el bot\u00f3n Enviar." + "description": "El dispositivo IoTawatt requiere autenticaci\u00f3n. Por favor, introduce el nombre de usuario y la contrase\u00f1a y haz clic en el bot\u00f3n Enviar." }, "user": { "data": { diff --git a/homeassistant/components/iss/translations/es.json b/homeassistant/components/iss/translations/es.json index 02a03ee5995..aa50ad5ac0f 100644 --- a/homeassistant/components/iss/translations/es.json +++ b/homeassistant/components/iss/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "latitude_longitude_not_defined": "La latitud y la longitud no est\u00e1n definidas en Home Assistant.", - "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { diff --git a/homeassistant/components/justnimbus/translations/id.json b/homeassistant/components/justnimbus/translations/id.json new file mode 100644 index 00000000000..74d2a8adb7c --- /dev/null +++ b/homeassistant/components/justnimbus/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "client_id": "ID Klien" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/pl.json b/homeassistant/components/justnimbus/translations/pl.json new file mode 100644 index 00000000000..bbc8bab8392 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/pl.json @@ -0,0 +1,19 @@ +{ + "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": { + "client_id": "Identyfikator klienta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/es.json b/homeassistant/components/kaleidescape/translations/es.json index 5cb7047f4f5..333a44fb123 100644 --- a/homeassistant/components/kaleidescape/translations/es.json +++ b/homeassistant/components/kaleidescape/translations/es.json @@ -4,16 +4,16 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "unknown": "Error inesperado", - "unsupported": "Dispositiu no compatible" + "unsupported": "Dispositivo no compatible" }, "error": { - "cannot_connect": "Fallo en la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "unsupported": "Dispositivo no compatible" }, "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "\u00bfQuieres configurar el reproductor {name} modelo {model}?" + "description": "\u00bfQuieres configurar el reproductor {model} llamado {name}?" }, "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json index 190caf9947b..84e39aed3c5 100644 --- a/homeassistant/components/keenetic_ndms2/translations/es.json +++ b/homeassistant/components/keenetic_ndms2/translations/es.json @@ -3,19 +3,19 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "no_udn": "La informaci\u00f3n de descubrimiento SSDP no tiene UDN", - "not_keenetic_ndms2": "El art\u00edculo descubierto no es un router Keenetic" + "not_keenetic_ndms2": "El elemento descubierto no es un router Keenetic" }, "error": { - "cannot_connect": "Fallo de conexi\u00f3n" + "cannot_connect": "No se pudo conectar" }, - "flow_title": "{name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Configurar el router Keenetic NDMS2" } @@ -26,11 +26,11 @@ "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", + "include_arp": "Usar datos ARP (se ignora si se usan datos de puntos de acceso)", + "include_associated": "Usar datos de asociaciones de puntos de acceso WiFi (se ignoran si se usan datos de puntos de acceso)", + "interfaces": "Elige las interfaces para escanear", "scan_interval": "Intervalo de escaneo", - "try_hotspot": "Utilizar datos de 'punto de acceso ip' (m\u00e1s precisos)" + "try_hotspot": "Usar los datos de 'ip hotspot' (m\u00e1s precisos)" } } } diff --git a/homeassistant/components/kmtronic/translations/es.json b/homeassistant/components/kmtronic/translations/es.json index 822a37649fd..f8df877d1b5 100644 --- a/homeassistant/components/kmtronic/translations/es.json +++ b/homeassistant/components/kmtronic/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fallo al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -13,7 +13,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "reverse": "L\u00f3gica de conmutaci\u00f3n inversa (utilizar NC)" + "reverse": "L\u00f3gica de interruptor inverso (usar NC)" } } } diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index 627c26ab72a..cad83301d83 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "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": "Error al conectar", + "cannot_connect": "No se pudo conectar", "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", @@ -24,20 +24,20 @@ "local_ip": "D\u00e9jalo en blanco para utilizar el descubrimiento autom\u00e1tico.", "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." }, - "description": "Introduzca la informaci\u00f3n de conexi\u00f3n de su dispositivo de tunelizaci\u00f3n." + "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu dispositivo de t\u00fanel." }, "routing": { "data": { "individual_address": "Direcci\u00f3n individual", "local_ip": "IP local de Home Assistant", - "multicast_group": "El grupo de multidifusi\u00f3n utilizado para el enrutamiento", - "multicast_port": "El puerto de multidifusi\u00f3n utilizado para el enrutamiento" + "multicast_group": "Grupo multicast", + "multicast_port": "Puerto multicast" }, "data_description": { "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico." }, - "description": "Por favor, configure las opciones de enrutamiento." + "description": "Por favor, configura las opciones de enrutamiento." }, "secure_knxkeys": { "data": { @@ -74,13 +74,13 @@ "data": { "gateway": "Conexi\u00f3n de t\u00fanel KNX" }, - "description": "Seleccione una puerta de enlace de la lista." + "description": "Selecciona una puerta de enlace de la lista." }, "type": { "data": { "connection_type": "Tipo de conexi\u00f3n KNX" }, - "description": "Por favor, introduzca el tipo de conexi\u00f3n que debemos utilizar para su conexi\u00f3n KNX. \n AUTOM\u00c1TICO - La integraci\u00f3n se encarga de la conectividad a su bus KNX realizando una exploraci\u00f3n de la pasarela. \n TUNNELING - La integraci\u00f3n se conectar\u00e1 a su bus KNX mediante tunneling. \n ROUTING - La integraci\u00f3n se conectar\u00e1 a su bus KNX mediante routing." + "description": "Por favor, introduce el tipo de conexi\u00f3n que debemos usar para tu conexi\u00f3n KNX.\n AUTOM\u00c1TICO: la integraci\u00f3n se encarga de la conectividad con tu bus KNX mediante la realizaci\u00f3n de un escaneo de la puerta de enlace.\n T\u00daNELES: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s de t\u00faneles.\n ENRUTAMIENTO: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s del enrutamiento." } } }, @@ -91,8 +91,8 @@ "connection_type": "Tipo de conexi\u00f3n KNX", "individual_address": "Direcci\u00f3n individual predeterminada", "local_ip": "IP local de Home Assistant", - "multicast_group": "Grupo multidifusi\u00f3n", - "multicast_port": "Puerto multidifusi\u00f3n", + "multicast_group": "Grupo multicast", + "multicast_port": "Puerto multicast", "rate_limit": "Frecuencia m\u00e1xima", "state_updater": "Actualizador de estado" }, diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json index b19d09a92d0..290d7aa53ee 100644 --- a/homeassistant/components/kodi/translations/es.json +++ b/homeassistant/components/kodi/translations/es.json @@ -22,7 +22,7 @@ "description": "Por favor, introduzca su nombre de usuario y contrase\u00f1a de Kodi. Estos se pueden encontrar en Sistema/Configuraci\u00f3n/Red/Servicios." }, "discovery_confirm": { - "description": "\u00bfQuieres agregar Kodi (`{name}`) a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir Kodi (`{name}`) a Home Assistant?", "title": "Descubierto Kodi" }, "user": { diff --git a/homeassistant/components/kraken/translations/es.json b/homeassistant/components/kraken/translations/es.json index 86df8397c15..2a473d78080 100644 --- a/homeassistant/components/kraken/translations/es.json +++ b/homeassistant/components/kraken/translations/es.json @@ -5,10 +5,6 @@ }, "step": { "user": { - "data": { - "one": "", - "other": "Otros" - }, "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } diff --git a/homeassistant/components/launch_library/translations/es.json b/homeassistant/components/launch_library/translations/es.json index c9dc0a00b92..0ca93e34e5e 100644 --- a/homeassistant/components/launch_library/translations/es.json +++ b/homeassistant/components/launch_library/translations/es.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "\u00bfDesea configurar la biblioteca de lanzamiento?" + "description": "\u00bfQuieres configurar Launch Library?" } } } diff --git a/homeassistant/components/lcn/translations/es.json b/homeassistant/components/lcn/translations/es.json index 045f87a1927..a5d0be12c8e 100644 --- a/homeassistant/components/lcn/translations/es.json +++ b/homeassistant/components/lcn/translations/es.json @@ -4,7 +4,7 @@ "codelock": "c\u00f3digo de bloqueo de c\u00f3digo recibido", "fingerprint": "c\u00f3digo de huella dactilar recibido", "send_keys": "enviar claves recibidas", - "transmitter": "c\u00f3digo de transmisor recibido", + "transmitter": "c\u00f3digo del transmisor recibido", "transponder": "c\u00f3digo de transpondedor recibido" } } diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index b8495b5e916..4645b9c88b9 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -28,7 +28,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Para configurar las opciones avanzadas, revisa la [documentaci\u00f3n de Life360]({docs_url}).\nDeber\u00edas hacerlo antes de a\u00f1adir alguna cuenta.", + "description": "Para configurar las opciones avanzadas, consulta la [documentaci\u00f3n de Life360]({docs_url}).\nEs posible que quieras hacerlo antes de a\u00f1adir cuentas.", "title": "Configurar la cuenta de Life360" } } diff --git a/homeassistant/components/light/translations/es.json b/homeassistant/components/light/translations/es.json index 53cf50215aa..94e28719702 100644 --- a/homeassistant/components/light/translations/es.json +++ b/homeassistant/components/light/translations/es.json @@ -13,7 +13,7 @@ "is_on": "{entity_name} est\u00e1 encendida" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "turned_off": "{entity_name} apagada", "turned_on": "{entity_name} encendida" } diff --git a/homeassistant/components/litejet/translations/es.json b/homeassistant/components/litejet/translations/es.json index 41875da9e69..95e552560ec 100644 --- a/homeassistant/components/litejet/translations/es.json +++ b/homeassistant/components/litejet/translations/es.json @@ -11,8 +11,8 @@ "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" + "description": "Conecta el puerto RS232-2 del LiteJet a tu ordenador e introduce la ruta al dispositivo del puerto serie. \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": "Conectar a LiteJet" } } }, diff --git a/homeassistant/components/litterrobot/translations/es.json b/homeassistant/components/litterrobot/translations/es.json index 12a48f17c32..f92417d76a0 100644 --- a/homeassistant/components/litterrobot/translations/es.json +++ b/homeassistant/components/litterrobot/translations/es.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { - "cannot_connect": "Fallo al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index ca2a633eb21..0924f212179 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -10,7 +10,7 @@ }, "trigger_type": { "buffering": "{entity_name} comienza a almacenar en b\u00fafer", - "changed_states": "{entity_name} ha cambiado de estado", + "changed_states": "{entity_name} cambi\u00f3 de estado", "idle": "{entity_name} est\u00e1 inactivo", "paused": "{entity_name} est\u00e1 en pausa", "playing": "{entity_name} comienza a reproducirse", diff --git a/homeassistant/components/met_eireann/translations/es.json b/homeassistant/components/met_eireann/translations/es.json index 97b6518862c..5448f05a9bb 100644 --- a/homeassistant/components/met_eireann/translations/es.json +++ b/homeassistant/components/met_eireann/translations/es.json @@ -11,7 +11,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Introduce tu ubicaci\u00f3n para utilizar los datos meteorol\u00f3gicos de la API p\u00fablica de previsi\u00f3n meteorol\u00f3gica de Met \u00c9ireann", + "description": "Introduce tu ubicaci\u00f3n para utilizar los datos meteorol\u00f3gicos de la API del pron\u00f3stico meteorol\u00f3gico p\u00fablico de Met \u00c9ireann", "title": "Ubicaci\u00f3n" } } diff --git a/homeassistant/components/mill/translations/es.json b/homeassistant/components/mill/translations/es.json index 280d4ad4ba9..8b56b8be89d 100644 --- a/homeassistant/components/mill/translations/es.json +++ b/homeassistant/components/mill/translations/es.json @@ -21,9 +21,9 @@ }, "user": { "data": { - "connection_type": "Seleccione el tipo de conexi\u00f3n" + "connection_type": "Selecciona el tipo de conexi\u00f3n" }, - "description": "Seleccione el tipo de conexi\u00f3n. Local requiere calentadores de generaci\u00f3n 3" + "description": "Selecciona el tipo de conexi\u00f3n. Local requiere calentadores de generaci\u00f3n 3" } } } diff --git a/homeassistant/components/min_max/translations/es.json b/homeassistant/components/min_max/translations/es.json index 2be7203d0d4..149f3f030d3 100644 --- a/homeassistant/components/min_max/translations/es.json +++ b/homeassistant/components/min_max/translations/es.json @@ -11,7 +11,7 @@ "data_description": { "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media o mediana." }, - "description": "Cree un sensor que calcule un valor m\u00ednimo, m\u00e1ximo, medio o mediano a partir de una lista de sensores de entrada.", + "description": "Crea un sensor que calcula el valor m\u00ednimo, m\u00e1ximo, medio o mediano a partir de una lista de sensores de entrada.", "title": "A\u00f1adir sensor m\u00edn / m\u00e1x / media / mediana" } } diff --git a/homeassistant/components/mjpeg/translations/es.json b/homeassistant/components/mjpeg/translations/es.json index 113193e3832..72644dad83a 100644 --- a/homeassistant/components/mjpeg/translations/es.json +++ b/homeassistant/components/mjpeg/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya se encuentra configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "user": { @@ -13,18 +13,18 @@ "mjpeg_url": "URL MJPEG", "name": "Nombre", "password": "Contrase\u00f1a", - "still_image_url": "URL de imagen est\u00e1tica", - "username": "Usuario", - "verify_ssl": "Verifique el certificado SSL" + "still_image_url": "URL de imagen fija", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" } } } }, "options": { "error": { - "already_configured": "El dispositivo ya se encuentra configurado", - "cannot_connect": "Error al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "init": { @@ -32,9 +32,9 @@ "mjpeg_url": "URL MJPEG", "name": "Nombre", "password": "Contrase\u00f1a", - "still_image_url": "URL de imagen est\u00e1tica", - "username": "Nombre de Usuario", - "verify_ssl": "Verifique el certificado SSL" + "still_image_url": "URL de imagen fija", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/modem_callerid/translations/es.json b/homeassistant/components/modem_callerid/translations/es.json index bc1b20cbbe0..c9d551f2d7e 100644 --- a/homeassistant/components/modem_callerid/translations/es.json +++ b/homeassistant/components/modem_callerid/translations/es.json @@ -10,14 +10,14 @@ }, "step": { "usb_confirm": { - "description": "Se trata de una integraci\u00f3n para llamadas a tel\u00e9fonos fijos que utilizan un m\u00f3dem de voz CX93001. Puede recuperar la informaci\u00f3n del identificador de llamadas con una opci\u00f3n para rechazar una llamada entrante." + "description": "Esta es una integraci\u00f3n para llamadas de l\u00ednea fija usando un m\u00f3dem de voz CX93001. Esto puede recuperar informaci\u00f3n de identificaci\u00f3n de llamadas con una opci\u00f3n para rechazar una llamada entrante." }, "user": { "data": { "name": "Nombre", "port": "Puerto" }, - "description": "Se trata de una integraci\u00f3n para llamadas a tel\u00e9fonos fijos que utilizan un m\u00f3dem de voz CX93001. Puede recuperar la informaci\u00f3n del identificador de llamadas con una opci\u00f3n para rechazar una llamada entrante." + "description": "Esta es una integraci\u00f3n para llamadas de l\u00ednea fija usando un m\u00f3dem de voz CX93001. Esto puede recuperar informaci\u00f3n de identificaci\u00f3n de llamadas con una opci\u00f3n para rechazar una llamada entrante." } } } diff --git a/homeassistant/components/modern_forms/translations/es.json b/homeassistant/components/modern_forms/translations/es.json index 29b51c03cf1..3b6e1148e13 100644 --- a/homeassistant/components/modern_forms/translations/es.json +++ b/homeassistant/components/modern_forms/translations/es.json @@ -16,8 +16,8 @@ "description": "Configura tu ventilador de Modern Forms para que se integre con Home Assistant." }, "zeroconf_confirm": { - "description": "\u00bfQuieres a\u00f1adir el ventilador de Modern Forms llamado `{name}` a Home Assistant?", - "title": "Dispositivo de ventilador de Modern Forms descubierto" + "description": "\u00bfQuieres a\u00f1adir el ventilador de Modern Forms `{name}` a Home Assistant?", + "title": "Dispositivo ventilador de Modern Forms descubierto" } } } diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/es.json b/homeassistant/components/moehlenhoff_alpha2/translations/es.json index 12821ae906d..5a62e2517d2 100644 --- a/homeassistant/components/moehlenhoff_alpha2/translations/es.json +++ b/homeassistant/components/moehlenhoff_alpha2/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/moon/translations/es.json b/homeassistant/components/moon/translations/es.json index d23683ceb5d..4bd878ba9f3 100644 --- a/homeassistant/components/moon/translations/es.json +++ b/homeassistant/components/moon/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. Solo una configuraci\u00f3n es posible." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { - "description": "\u00bfQuieres empezar la configuraci\u00f3n?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index 11e8c2a2e71..e316f882712 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -35,7 +35,7 @@ "step": { "init": { "data": { - "wait_for_push": "Espere a que se realice la actualizaci\u00f3n de multidifusi\u00f3n" + "wait_for_push": "Esperar a que se active la actualizaci\u00f3n de multicast" } } } diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json index 881972cf5c4..d7dc7f1a083 100644 --- a/homeassistant/components/motioneye/translations/es.json +++ b/homeassistant/components/motioneye/translations/es.json @@ -12,15 +12,15 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para que se conecte al servicio motionEye proporcionado por el complemento: {addon}?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse al servicio motionEye proporcionado por el complemento: {addon}?", "title": "motionEye a trav\u00e9s del complemento Home Assistant" }, "user": { "data": { - "admin_password": "Contrase\u00f1a administrador", + "admin_password": "Contrase\u00f1a de administrador", "admin_username": "Nombre de usuario administrador", - "surveillance_password": "Contrase\u00f1a vigilancia", - "surveillance_username": "Nombre de usuario vigilancia", + "surveillance_password": "Contrase\u00f1a de vigilancia", + "surveillance_username": "Nombre de usuario de vigilancia", "url": "URL" } } @@ -31,7 +31,7 @@ "init": { "data": { "stream_url_template": "Plantilla de URL de transmisi\u00f3n", - "webhook_set": "Configure los webhooks de motionEye para informar eventos a Home Assistant", + "webhook_set": "Configura los webhooks de motionEye para informar eventos a Home Assistant", "webhook_set_overwrite": "Sobrescribir webhooks no reconocidos" } } diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 93fe4b53933..915af215a26 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -22,7 +22,7 @@ "data": { "discovery": "Habilitar descubrimiento" }, - "description": "\u00bfDesea configurar Home Assistant para conectar con el br\u00f3ker de MQTT proporcionado por el complemento {addon}?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse al agente MQTT proporcionado por el complemento {addon} ?", "title": "Br\u00f3ker MQTT a trav\u00e9s de complemento de Home Assistant" } } diff --git a/homeassistant/components/mullvad/translations/es.json b/homeassistant/components/mullvad/translations/es.json index f7ad856ea91..2f6395282d3 100644 --- a/homeassistant/components/mullvad/translations/es.json +++ b/homeassistant/components/mullvad/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fallo al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/mutesync/translations/es.json b/homeassistant/components/mutesync/translations/es.json index fb32193010e..f3a4eb12460 100644 --- a/homeassistant/components/mutesync/translations/es.json +++ b/homeassistant/components/mutesync/translations/es.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Activar la autenticaci\u00f3n en las Preferencias de m\u00fctesync > Autenticaci\u00f3n", + "invalid_auth": "Habilita la autenticaci\u00f3n en Preferencias de m\u00fctesync > Autenticaci\u00f3n", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index d8520cd2b6f..a6b81fbcbbc 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -15,7 +15,7 @@ "password": "Contrase\u00f1a" }, "description": "La contrase\u00f1a de {username} ya no es v\u00e1lida.", - "title": "Reautenticar tu cuenta MyQ" + "title": "Volver a autenticar tu cuenta MyQ" }, "user": { "data": { diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json index 3c4bed1ee86..ef473a6aff5 100644 --- a/homeassistant/components/mysensors/translations/pl.json +++ b/homeassistant/components/mysensors/translations/pl.json @@ -14,6 +14,7 @@ "invalid_serial": "Nieprawid\u0142owy port szeregowy", "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"", "invalid_version": "Nieprawid\u0142owa wersja MySensors", + "mqtt_required": "Integracja MQTT nie jest skonfigurowana", "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", @@ -68,6 +69,14 @@ }, "description": "Konfiguracja bramki LAN" }, + "select_gateway_type": { + "description": "Wybierz bramk\u0119 do skonfigurowania.", + "menu_options": { + "gw_mqtt": "Skonfiguruj bramk\u0119 MQTT", + "gw_serial": "Skonfiguruj bramk\u0119 szeregow\u0105", + "gw_tcp": "Skonfiguruj bramk\u0119 TCP" + } + }, "user": { "data": { "gateway_type": "Typ bramki" diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index b6494327077..0ba92e457ca 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "device_unsupported": "El dispositivo no es compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", - "reauth_unsuccessful": "La reautenticaci\u00f3n no se realiz\u00f3 correctamente, elimine la integraci\u00f3n y vuelva a configurarla." + "reauth_unsuccessful": "No se pudo volver a autenticar, elimina la integraci\u00f3n y vuelve a configurarla." }, "error": { "cannot_connect": "No se pudo conectar", @@ -21,20 +21,20 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a." + "description": "Por favor, introduce el nombre de usuario y la contrase\u00f1a." }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a correctos para el host: {host}" + "description": "Por favor, introduce el nombre de usuario y la contrase\u00f1a correctos para el host: {host}" }, "user": { "data": { "host": "Host" }, - "description": "Configurar la integraci\u00f3n de Nettigo Air Monitor." + "description": "Configurar la integraci\u00f3n Nettigo Air Monitor." } } } diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 9899d30d36f..00ba28474ac 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -15,8 +15,8 @@ "flow_title": "{name}", "step": { "link": { - "description": "Mantenga presionado el bot\u00f3n de encendido en su Nanoleaf durante 5 segundos hasta que los LED de los botones comiencen a parpadear, luego haga clic en ** ENVIAR ** dentro de los 30 segundos.", - "title": "Link Nanoleaf" + "description": "Mant\u00e9n presionado el bot\u00f3n de encendido de tu Nanoleaf durante 5 segundos hasta que los LED del bot\u00f3n comiencen a parpadear, luego haz clic en **ENVIAR** en los siguientes 30 segundos.", + "title": "Enlazar Nanoleaf" }, "user": { "data": { diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index a04dc0baab6..36b5d64e253 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -17,20 +17,20 @@ "default": "Autenticado con \u00e9xito" }, "error": { - "bad_project_id": "Por favor, introduzca un ID de proyecto Cloud v\u00e1lido (compruebe la consola de la nube)", + "bad_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (verifica la Cloud Console)", "internal_error": "Error interno validando el c\u00f3digo", "invalid_pin": "C\u00f3digo PIN no v\u00e1lido", - "subscriber_error": "Error de abonado desconocido, ver registros", + "subscriber_error": "Error de suscriptor desconocido, mira los registros", "timeout": "Tiempo de espera agotado validando el c\u00f3digo", "unknown": "Error inesperado", - "wrong_project_id": "Por favor, introduzca un ID de proyecto Cloud v\u00e1lido (encontr\u00f3 el ID de proyecto de acceso al dispositivo)" + "wrong_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (era el mismo que el ID del proyecto de acceso al dispositivo)" }, "step": { "auth": { "data": { "code": "Token de acceso" }, - "description": "Para vincular tu cuenta de Google, [autoriza tu cuenta]({url}).\n\nDespu\u00e9s de la autorizaci\u00f3n, copie y pegue el c\u00f3digo Auth Token proporcionado a continuaci\u00f3n.", + "description": "Para vincular tu cuenta de Google, [autoriza tu cuenta]({url}).\n\nDespu\u00e9s de la autorizaci\u00f3n, copia y pega el c\u00f3digo Auth Token proporcionado a continuaci\u00f3n.", "title": "Vincular cuenta de Google" }, "auth_upgrade": { @@ -80,7 +80,7 @@ "data": { "cloud_project_id": "ID de proyecto de Google Cloud" }, - "description": "Visite [Cloud Console] ({url}) para encontrar su ID de proyecto de Google Cloud.", + "description": "Visita [Cloud Console]({url}) para encontrar tu ID de proyecto de Google Cloud.", "title": "Configurar Google Cloud" }, "reauth_confirm": { diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index dd9fcf18d60..c658c0046a1 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -15,16 +15,16 @@ "title": "Selecciona un m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { - "description": "La integraci\u00f3n de Netatmo necesita volver a autentificar su cuenta", + "description": "La integraci\u00f3n Netatmo necesita volver a autenticar tu cuenta", "title": "Volver a autenticar la integraci\u00f3n" } } }, "device_automation": { "trigger_subtype": { - "away": "fuera", - "hg": "protector contra las heladas", - "schedule": "Horario" + "away": "ausente", + "hg": "protector contra heladas", + "schedule": "programaci\u00f3n" }, "trigger_type": { "alarm_started": "{entity_name} ha detectado una alarma", @@ -35,10 +35,10 @@ "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", + "set_point": "Temperatura objetivo de {entity_name} configurada manualmente", "therm_mode": "{entity_name} cambi\u00f3 a \" {subtype} \"", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido", "vehicle": "{entity_name} ha detectado un veh\u00edculo" } }, diff --git a/homeassistant/components/netgear/translations/es.json b/homeassistant/components/netgear/translations/es.json index 69bea9a5de6..2ad6d0e6d16 100644 --- a/homeassistant/components/netgear/translations/es.json +++ b/homeassistant/components/netgear/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "config": "Error de conexi\u00f3n o de inicio de sesi\u00f3n: compruebe su configuraci\u00f3n" + "config": "Error de conexi\u00f3n o de inicio de sesi\u00f3n: verifica tu configuraci\u00f3n" }, "step": { "user": { @@ -13,7 +13,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario (Opcional)" }, - "description": "Host predeterminado: {host} \nNombre de usuario predeterminado: {username}" + "description": "Host por defecto: {host}\nNombre de usuario por defecto: {username}" } } }, @@ -21,9 +21,9 @@ "step": { "init": { "data": { - "consider_home": "Considere el tiempo en casa (segundos)" + "consider_home": "Considerar la hora local (segundos)" }, - "description": "Especifica los ajustes opcionales" + "description": "Especificar configuraciones opcionales" } } } diff --git a/homeassistant/components/nfandroidtv/translations/es.json b/homeassistant/components/nfandroidtv/translations/es.json index e382855479f..4522f36cb12 100644 --- a/homeassistant/components/nfandroidtv/translations/es.json +++ b/homeassistant/components/nfandroidtv/translations/es.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Nombre" }, - "description": "Esta integraci\u00f3n requiere la aplicaci\u00f3n de Notificaciones para Android TV.\n\nPara Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPara Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nDebes configurar una reserva DHCP en su router (consulta el manual de usuario de tu router) o una direcci\u00f3n IP est\u00e1tica en el dispositivo. Si no, el dispositivo acabar\u00e1 por no estar disponible." + "description": "Por favor, consulta la documentaci\u00f3n para asegurarte de que se cumplen todos los requisitos." } } } diff --git a/homeassistant/components/nina/translations/es.json b/homeassistant/components/nina/translations/es.json index a605bfb7a9a..a4cabb669d8 100644 --- a/homeassistant/components/nina/translations/es.json +++ b/homeassistant/components/nina/translations/es.json @@ -1,11 +1,11 @@ { "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", - "no_selection": "Por favor, seleccione al menos una ciudad/condado", + "no_selection": "Por favor selecciona al menos una ciudad/condado", "unknown": "Error inesperado" }, "step": { @@ -20,7 +20,7 @@ "corona_filter": "Eliminar las advertencias de Corona", "slots": "Advertencias m\u00e1ximas por ciudad/condado" }, - "title": "Seleccionar ciudad/pa\u00eds" + "title": "Seleccionar ciudad/condado" } } }, diff --git a/homeassistant/components/nmap_tracker/translations/es.json b/homeassistant/components/nmap_tracker/translations/es.json index 9b943edb310..f978f0d18ad 100644 --- a/homeassistant/components/nmap_tracker/translations/es.json +++ b/homeassistant/components/nmap_tracker/translations/es.json @@ -10,11 +10,11 @@ "user": { "data": { "exclude": "Direcciones de red (separadas por comas) para excluir del escaneo", - "home_interval": "N\u00famero m\u00ednimo de minutos entre los escaneos de los dispositivos activos (preservar la bater\u00eda)", + "home_interval": "N\u00famero m\u00ednimo de minutos entre escaneos de dispositivos activos (conservar bater\u00eda)", "hosts": "Direcciones de red a escanear (separadas por comas)", - "scan_options": "Opciones de escaneo configurables sin procesar para Nmap" + "scan_options": "Opciones de escaneo configurables sin formato para Nmap" }, - "description": "Configure los hosts que ser\u00e1n escaneados por Nmap. Las direcciones de red y los excluidos pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos IP (192.168.1.0-32)." + "description": "Configura los hosts para que sean escaneados por Nmap. La direcci\u00f3n de red y las exclusiones pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos de IP (192.168.1.0-32)." } } }, @@ -25,14 +25,14 @@ "step": { "init": { "data": { - "consider_home": "Segundos de espera hasta que se marca un dispositivo de seguimiento como no en casa despu\u00e9s de no ser visto.", + "consider_home": "Segundos de espera hasta marcar un rastreador de dispositivo como ausente despu\u00e9s de no ser visto.", "exclude": "Direcciones de red (separadas por comas) para excluir del escaneo", - "home_interval": "N\u00famero m\u00ednimo de minutos entre los escaneos de los dispositivos activos (preservar la bater\u00eda)", + "home_interval": "N\u00famero m\u00ednimo de minutos entre escaneos de dispositivos activos (conservar bater\u00eda)", "hosts": "Direcciones de red a escanear (separadas por comas)", "interval_seconds": "Intervalo de exploraci\u00f3n", - "scan_options": "Opciones de escaneo configurables sin procesar para Nmap" + "scan_options": "Opciones de escaneo configurables sin formato para Nmap" }, - "description": "Configure los hosts que ser\u00e1n escaneados por Nmap. Las direcciones de red y los excluidos pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos IP (192.168.1.0-32)." + "description": "Configura los hosts para que sean escaneados por Nmap. La direcci\u00f3n de red y las exclusiones pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos de IP (192.168.1.0-32)." } } }, diff --git a/homeassistant/components/notion/translations/es.json b/homeassistant/components/notion/translations/es.json index 3eb17fa606d..f7a7a01f4d8 100644 --- a/homeassistant/components/notion/translations/es.json +++ b/homeassistant/components/notion/translations/es.json @@ -13,7 +13,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Por favor, vuelva a introducir la contrase\u00f1a de {username}.", + "description": "Por favor, vuelve a introducir la contrase\u00f1a de {username}.", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/nuki/translations/es.json b/homeassistant/components/nuki/translations/es.json index 33fe3f462df..d21ef9dfdb6 100644 --- a/homeassistant/components/nuki/translations/es.json +++ b/homeassistant/components/nuki/translations/es.json @@ -13,7 +13,7 @@ "data": { "token": "Token de acceso" }, - "description": "La integraci\u00f3n de Nuki debe volver a autenticarse con tu bridge.", + "description": "La integraci\u00f3n Nuki debe volver a autenticarse con tu bridge.", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/octoprint/translations/es.json b/homeassistant/components/octoprint/translations/es.json index e9135b25be8..827e29e0fde 100644 --- a/homeassistant/components/octoprint/translations/es.json +++ b/homeassistant/components/octoprint/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "auth_failed": "Error al recuperar la clave de API de la aplicaci\u00f3n", + "auth_failed": "No se pudo recuperar la clave API de la aplicaci\u00f3n", "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, @@ -12,7 +12,7 @@ }, "flow_title": "Impresora OctoPrint: {host}", "progress": { - "get_api_key": "Abra la interfaz de usuario de OctoPrint y haga clic en 'Permitir' en la solicitud de acceso para 'Home Assistant'." + "get_api_key": "Abre la interfaz de usuario de OctoPrint y haz clic en 'Permitir' en la solicitud de acceso para 'Home Assistant'." }, "step": { "user": { @@ -22,7 +22,7 @@ "port": "N\u00famero de puerto", "ssl": "Usar SSL", "username": "Nombre de usuario", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/omnilogic/translations/es.json b/homeassistant/components/omnilogic/translations/es.json index 1755942e5df..a47b982d94f 100644 --- a/homeassistant/components/omnilogic/translations/es.json +++ b/homeassistant/components/omnilogic/translations/es.json @@ -21,7 +21,7 @@ "step": { "init": { "data": { - "ph_offset": "Desplazamiento del pH (positivo o negativo)", + "ph_offset": "Compensaci\u00f3n del pH (positivo o negativo)", "polling_interval": "Intervalo de sondeo (en segundos)" } } diff --git a/homeassistant/components/oncue/translations/es.json b/homeassistant/components/oncue/translations/es.json index 13f2eb38bef..f92417d76a0 100644 --- a/homeassistant/components/oncue/translations/es.json +++ b/homeassistant/components/oncue/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/onewire/translations/es.json b/homeassistant/components/onewire/translations/es.json index 3617f4c8fd4..986a36a30d4 100644 --- a/homeassistant/components/onewire/translations/es.json +++ b/homeassistant/components/onewire/translations/es.json @@ -18,22 +18,22 @@ }, "options": { "error": { - "device_not_selected": "Seleccionar los dispositivos a configurar" + "device_not_selected": "Selecciona los dispositivos para configurar" }, "step": { "configure_device": { "data": { "precision": "Precisi\u00f3n del sensor" }, - "description": "Selecciona la precisi\u00f3n del sensor {sensor_id}", + "description": "Selecciona la precisi\u00f3n del sensor para {sensor_id}", "title": "Precisi\u00f3n del sensor OneWire" }, "device_selection": { "data": { - "clear_device_options": "Borra todas las configuraciones de dispositivo", - "device_selection": "Seleccionar los dispositivos a configurar" + "clear_device_options": "Borrar todas las configuraciones de dispositivo", + "device_selection": "Selecciona los dispositivos para configurar" }, - "description": "Seleccione los pasos de configuraci\u00f3n a procesar", + "description": "Selecciona qu\u00e9 pasos de configuraci\u00f3n procesar", "title": "Opciones de dispositivo OneWire" } } diff --git a/homeassistant/components/open_meteo/translations/es.json b/homeassistant/components/open_meteo/translations/es.json index 68e31e61ccd..87bc3b879be 100644 --- a/homeassistant/components/open_meteo/translations/es.json +++ b/homeassistant/components/open_meteo/translations/es.json @@ -5,7 +5,7 @@ "data": { "zone": "Zona" }, - "description": "Seleccionar la ubicaci\u00f3n que se utilizar\u00e1 para la previsi\u00f3n meteorol\u00f3gica" + "description": "Selecciona la ubicaci\u00f3n que se usar\u00e1 para el pron\u00f3stico del tiempo" } } } diff --git a/homeassistant/components/openexchangerates/translations/pl.json b/homeassistant/components/openexchangerates/translations/pl.json new file mode 100644 index 00000000000..e7de6c30cec --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "base": "Waluta bazowa" + }, + "data_description": { + "base": "Korzystanie z innej waluty bazowej ni\u017c USD wymaga [p\u0142atnego abonamentu]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Open Exchange Rates przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Open Exchange Rates zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index b464549ed71..0310cdd8236 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -23,7 +23,7 @@ "floor_temperature": "Temperatura del suelo", "read_precision": "Leer precisi\u00f3n", "set_precision": "Establecer precisi\u00f3n", - "temporary_override_mode": "Modo de anulaci\u00f3n temporal del punto de ajuste" + "temporary_override_mode": "Modo de anulaci\u00f3n temporal de la consigna" } } } diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index ac9d3f72ebc..5503525ac58 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente", - "reauth_wrong_account": "S\u00f3lo puedes volver a autenticar esta entrada con la misma cuenta y hub de Overkiz" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_wrong_account": "Solo puedes volver a autenticar esta entrada con la misma cuenta y concentrador de Overkiz" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "server_in_maintenance": "El servidor est\u00e1 inactivo por mantenimiento", + "server_in_maintenance": "El servidor est\u00e1 fuera de servicio por mantenimiento", "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", - "too_many_requests": "Demasiadas solicitudes, int\u00e9ntalo de nuevo m\u00e1s tarde.", + "too_many_requests": "Demasiadas solicitudes, vuelve a intentarlo m\u00e1s tarde", "unknown": "Error inesperado" }, "flow_title": "Puerta de enlace: {gateway_id}", @@ -18,11 +18,11 @@ "user": { "data": { "host": "Host", - "hub": "Hub", + "hub": "Concentrador", "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "La plataforma Overkiz es utilizada por varios proveedores como Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) y Atlantic (Cozytouch). Introduce las credenciales de tu aplicaci\u00f3n y selecciona tu hub." + "description": "La plataforma Overkiz es utilizada por varios proveedores como Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) y Atlantic (Cozytouch). Introduce las credenciales de tu aplicaci\u00f3n y selecciona tu concentrador." } } } diff --git a/homeassistant/components/overkiz/translations/sensor.es.json b/homeassistant/components/overkiz/translations/sensor.es.json index 523ddf7fe7b..794730c5c86 100644 --- a/homeassistant/components/overkiz/translations/sensor.es.json +++ b/homeassistant/components/overkiz/translations/sensor.es.json @@ -1,10 +1,10 @@ { "state": { "overkiz__battery": { - "full": "Completo", - "low": "Bajo", + "full": "Completa", + "low": "Baja", "normal": "Normal", - "verylow": "Muy bajo" + "verylow": "Muy baja" }, "overkiz__discrete_rssi_level": { "good": "Bien", @@ -30,11 +30,11 @@ "overkiz__sensor_defect": { "dead": "Muerto", "low_battery": "Bater\u00eda baja", - "maintenance_required": "Mantenimiento necesario", - "no_defect": "Ning\u00fan defecto" + "maintenance_required": "Requiere mantenimiento", + "no_defect": "Sin defectos" }, "overkiz__sensor_room": { - "clean": "Limpiar", + "clean": "Limpio", "dirty": "Sucio" }, "overkiz__three_way_handle_direction": { diff --git a/homeassistant/components/p1_monitor/translations/es.json b/homeassistant/components/p1_monitor/translations/es.json index 31976986ec4..28dcb186505 100644 --- a/homeassistant/components/p1_monitor/translations/es.json +++ b/homeassistant/components/p1_monitor/translations/es.json @@ -9,7 +9,7 @@ "host": "Host", "name": "Nombre" }, - "description": "Configurar el monitor P1 para que se integre con el asistente dom\u00e9stico." + "description": "Configura P1 Monitor para integrarlo con Home Assistant." } } } diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index 153512cb83b..a2842917b87 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_pin": "PIN no v\u00e1lido", - "pairing_failure": "No se ha podido emparejar: {error_id}", + "pairing_failure": "No se puede emparejar: {error_id}", "unknown": "Error inesperado" }, "step": { @@ -14,7 +14,7 @@ "data": { "pin": "C\u00f3digo PIN" }, - "description": "Introduzca el PIN que se muestra en el televisor", + "description": "Introduce el PIN que se muestra en tu TV", "title": "Par" }, "user": { diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json index 7ecfc37d97d..93024c5611c 100644 --- a/homeassistant/components/picnic/translations/es.json +++ b/homeassistant/components/picnic/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "Reauntenticaci\u00f3n exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 284bfe97943..1a0ae166aaa 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_setup": "Agregue su Adam en lugar de su Anna, consulte la documentaci\u00f3n de integraci\u00f3n de Home Assistant Plugwise para obtener m\u00e1s informaci\u00f3n", + "invalid_setup": "A\u00f1ade tu Adam en lugar de tu Anna, consulta la documentaci\u00f3n de la integraci\u00f3n Home Assistant Plugwise para m\u00e1s informaci\u00f3n", "unknown": "Error inesperado" }, "flow_title": "{name}", diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index ae578b37e50..baf6fbda838 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "El powerwall ya est\u00e1 configurado", - "cannot_connect": "Fallo en la conexi\u00f3n", + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "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." + "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Por favor, considera actualizar o informar este problema para que pueda resolverse." }, - "flow_title": "Powerwall de Tesla ({ip_address})", + "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { "description": "\u00bfQuieres configurar {name} ({ip_address})?", @@ -21,16 +21,16 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie de Backup Gateway y se puede encontrar en la aplicaci\u00f3n de Tesla o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentra dentro de la puerta de Backup Gateway 2.", - "title": "Reautorizar la powerwall" + "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 Tesla o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.", + "title": "Volver a autenticar el powerwall" }, "user": { "data": { "ip_address": "Direcci\u00f3n IP", "password": "Contrase\u00f1a" }, - "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" + "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 Tesla o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.", + "title": "Conectar al powerwall" } } } diff --git a/homeassistant/components/prosegur/translations/es.json b/homeassistant/components/prosegur/translations/es.json index 1cbb1e25dc9..4447bbbc2e4 100644 --- a/homeassistant/components/prosegur/translations/es.json +++ b/homeassistant/components/prosegur/translations/es.json @@ -12,7 +12,7 @@ "step": { "reauth_confirm": { "data": { - "description": "Vuelva a autenticarse con su cuenta Prosegur.", + "description": "Vuelve a autenticarte con tu cuenta Prosegur.", "password": "Contrase\u00f1a", "username": "Nombre de usuario" } diff --git a/homeassistant/components/pure_energie/translations/es.json b/homeassistant/components/pure_energie/translations/es.json index 9725448be66..58d02574211 100644 --- a/homeassistant/components/pure_energie/translations/es.json +++ b/homeassistant/components/pure_energie/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya se encuentra configurado", - "cannot_connect": "Error al conectar" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar" }, "error": { - "cannot_connect": "Error al conectar" + "cannot_connect": "No se pudo conectar" }, "flow_title": "{model} ({host})", "step": { @@ -15,8 +15,8 @@ } }, "zeroconf_confirm": { - "description": "\u00bfQuieres a\u00f1adir el Medidor Pure Energie (`{name}`) a Home Assistant?", - "title": "Medidor Pure Energie encontrado" + "description": "\u00bfQuieres a\u00f1adir el medidor Pure Energie (`{model}`) a Home Assistant?", + "title": "Dispositivo medidor Pure Energie descubierto" } } } diff --git a/homeassistant/components/pvoutput/translations/es.json b/homeassistant/components/pvoutput/translations/es.json index 6ddf1dc9cfa..188a7e8d293 100644 --- a/homeassistant/components/pvoutput/translations/es.json +++ b/homeassistant/components/pvoutput/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" }, "error": { "cannot_connect": "No se pudo conectar", @@ -12,14 +12,14 @@ "data": { "api_key": "Clave API" }, - "description": "Para volver a autenticarse con PVOutput, deber\u00e1 obtener la clave API en {account_url} ." + "description": "Para volver a autenticarte con PVOutput, deber\u00e1s obtener la clave API en {account_url}." }, "user": { "data": { "api_key": "Clave API", "system_id": "ID del sistema" }, - "description": "Para autenticarse con PVOutput, deber\u00e1 obtener la clave API en {account_url} . \n\n Los ID de los sistemas registrados se enumeran en esa misma p\u00e1gina." + "description": "Para autenticarte con PVOutput, deber\u00e1s obtener la clave API en {account_url}. \n\nLos ID de sistema de los sistemas registrados se enumeran en esa misma p\u00e1gina." } } } diff --git a/homeassistant/components/radio_browser/translations/es.json b/homeassistant/components/radio_browser/translations/es.json index aa8e6336550..1a797d16af3 100644 --- a/homeassistant/components/radio_browser/translations/es.json +++ b/homeassistant/components/radio_browser/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Ya est\u00e1 configurado. Solamente una configuraci\u00f3n es posible." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { - "description": "\u00bfQuieres a\u00f1adir el navegador radio a Home Assistant?" + "description": "\u00bfQuieres a\u00f1adir Radio Browser a Home Assistant?" } } } diff --git a/homeassistant/components/rainforest_eagle/translations/es.json b/homeassistant/components/rainforest_eagle/translations/es.json index da6a6fd6936..d8501c80c89 100644 --- a/homeassistant/components/rainforest_eagle/translations/es.json +++ b/homeassistant/components/rainforest_eagle/translations/es.json @@ -13,7 +13,7 @@ "data": { "cloud_id": "ID de Cloud", "host": "Host", - "install_code": "Codigo de instalacion" + "install_code": "C\u00f3digo de instalaci\u00f3n" } } } diff --git a/homeassistant/components/remote/translations/es.json b/homeassistant/components/remote/translations/es.json index 31e68384b8b..b2c8aea25cc 100644 --- a/homeassistant/components/remote/translations/es.json +++ b/homeassistant/components/remote/translations/es.json @@ -10,7 +10,7 @@ "is_on": "{entity_name} est\u00e1 activado" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" } diff --git a/homeassistant/components/ridwell/translations/es.json b/homeassistant/components/ridwell/translations/es.json index 74171cb98f0..07037e942cf 100644 --- a/homeassistant/components/ridwell/translations/es.json +++ b/homeassistant/components/ridwell/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", @@ -13,7 +13,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Por favor, vuelva a introducir la contrase\u00f1a de: {username}", + "description": "Por favor, vuelve a introducir la contrase\u00f1a de {username}:", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { @@ -21,7 +21,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Introduzca su nombre de usuario y contrase\u00f1a:" + "description": "Introduce tu nombre de usuario y contrase\u00f1a:" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/es.json b/homeassistant/components/rituals_perfume_genie/translations/es.json index bdb1933eaf7..6be6c9d1e04 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/es.json +++ b/homeassistant/components/rituals_perfume_genie/translations/es.json @@ -11,10 +11,10 @@ "step": { "user": { "data": { - "email": "email", + "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" }, - "title": "Con\u00e9ctese a su cuenta de Rituals" + "title": "Con\u00e9ctate a tu cuenta Rituals" } } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/es.json b/homeassistant/components/rtsp_to_webrtc/translations/es.json index c74f0b6b34d..5a066d6645e 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/es.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/es.json @@ -1,25 +1,25 @@ { "config": { "abort": { - "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", - "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", + "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "invalid_url": "Debe ser una URL de servidor RTSPtoWebRTC v\u00e1lida, por ejemplo, https://ejemplo.com", - "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", - "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulte los registros para obtener m\u00e1s informaci\u00f3n." + "invalid_url": "Debe ser una URL de servidor RTSPtoWebRTC v\u00e1lida, por ejemplo, https://example.com", + "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", + "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulta los registros para obtener m\u00e1s informaci\u00f3n." }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse al servidor RTSPtoWebRTC proporcionado por el complemento: {complemento}?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse al servidor RTSPtoWebRTC proporcionado por el complemento: {addon}?", "title": "RTSPtoWebRTC a trav\u00e9s del complemento Home Assistant" }, "user": { "data": { - "server_url": "URL del servidor RTSPtoWebRTC, por ejemplo, https://ejemplo.com" + "server_url": "URL del servidor RTSPtoWebRTC, por ejemplo, https://example.com" }, - "description": "La integraci\u00f3n RTSPtoWebRTC requiere un servidor para traducir las transmisiones RTSP a WebRTC. Ingrese la URL del servidor RTSPtoWebRTC.", + "description": "La integraci\u00f3n RTSPtoWebRTC requiere un servidor para traducir flujos RTSP a WebRTC. Introduce la URL del servidor RTSPtoWebRTC.", "title": "Configurar RTSPtoWebRTC" } } diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index ebcaa398949..70bd9f30b9b 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.", + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible.", @@ -11,8 +11,8 @@ "unknown": "Error inesperado" }, "error": { - "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.", - "invalid_pin": "El PIN es inv\u00e1lido; vuelve a intentarlo." + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", + "invalid_pin": "El PIN no es v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "flow_title": "{device}", "step": { @@ -20,16 +20,16 @@ "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." }, "encrypted_pairing": { - "description": "Introduce el PIN que se muestra en {device}." + "description": "Por favor, introduce el PIN que aparece en {device}." }, "pairing": { - "description": "\u00bfQuiere configurar {device}? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor solicitando autorizaci\u00f3n." + "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." }, "reauth_confirm": { - "description": "Despu\u00e9s de enviarlo, acepte la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos." + "description": "Despu\u00e9s de enviar, acepta la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos o introduce el PIN." }, "reauth_confirm_encrypted": { - "description": "Introduce el PIN que se muestra en {device}." + "description": "Por favor, introduce el PIN que aparece en {device}." }, "user": { "data": { diff --git a/homeassistant/components/schedule/translations/ca.json b/homeassistant/components/schedule/translations/ca.json new file mode 100644 index 00000000000..323bb583601 --- /dev/null +++ b/homeassistant/components/schedule/translations/ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "OFF", + "on": "ON" + } + }, + "title": "Horari" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/de.json b/homeassistant/components/schedule/translations/de.json new file mode 100644 index 00000000000..33e07d1c2ca --- /dev/null +++ b/homeassistant/components/schedule/translations/de.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Aus", + "on": "An" + } + }, + "title": "Zeitplan" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/es.json b/homeassistant/components/schedule/translations/es.json new file mode 100644 index 00000000000..b41c3ed8281 --- /dev/null +++ b/homeassistant/components/schedule/translations/es.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Apagado", + "on": "Encendido" + } + }, + "title": "Programaci\u00f3n" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/et.json b/homeassistant/components/schedule/translations/et.json new file mode 100644 index 00000000000..de5b11e6af7 --- /dev/null +++ b/homeassistant/components/schedule/translations/et.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "V\u00e4ljas", + "on": "Sees" + } + }, + "title": "Ajakava" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/id.json b/homeassistant/components/schedule/translations/id.json new file mode 100644 index 00000000000..f179781c3f0 --- /dev/null +++ b/homeassistant/components/schedule/translations/id.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Mati", + "on": "Nyala" + } + }, + "title": "Jadwal" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/pl.json b/homeassistant/components/schedule/translations/pl.json new file mode 100644 index 00000000000..9f02b8a0c51 --- /dev/null +++ b/homeassistant/components/schedule/translations/pl.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "wy\u0142.", + "on": "w\u0142." + } + }, + "title": "Harmonogram" +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/es.json b/homeassistant/components/screenlogic/translations/es.json index c890d3bf10c..c5309c928db 100644 --- a/homeassistant/components/screenlogic/translations/es.json +++ b/homeassistant/components/screenlogic/translations/es.json @@ -6,21 +6,21 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { "ip_address": "Direcci\u00f3n IP", "port": "Puerto" }, - "description": "Introduzca la informaci\u00f3n de su ScreenLogic Gateway.", + "description": "Introduce la informaci\u00f3n de tu puerta de enlace ScreenLogic.", "title": "ScreenLogic" }, "gateway_select": { "data": { "selected_gateway": "Puerta de enlace" }, - "description": "Se han descubierto las siguientes puertas de enlace ScreenLogic. Seleccione una para configurarla o elija configurar manualmente una puerta de enlace ScreenLogic.", + "description": "Se han descubierto las siguientes puertas de enlace ScreenLogic. Selecciona una para configurarla o elige configurar manualmente una puerta de enlace ScreenLogic.", "title": "ScreenLogic" } } @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "scan_interval": "Segundos entre exploraciones" + "scan_interval": "Segundos entre escaneos" }, "description": "Especificar la configuraci\u00f3n de {gateway_name}", "title": "ScreenLogic" diff --git a/homeassistant/components/select/translations/es.json b/homeassistant/components/select/translations/es.json index b6db30f0711..b2066b4606d 100644 --- a/homeassistant/components/select/translations/es.json +++ b/homeassistant/components/select/translations/es.json @@ -1,13 +1,13 @@ { "device_automation": { "action_type": { - "select_option": "Cambiar la opci\u00f3n {entity_name}" + "select_option": "Cambiar opci\u00f3n de {entity_name}" }, "condition_type": { - "selected_option": "Opci\u00f3n actual {entity_name} seleccionada" + "selected_option": "Opci\u00f3n de {entity_name} seleccionada actualmente" }, "trigger_type": { - "current_option_changed": "Opci\u00f3n {entity_name} cambiada" + "current_option_changed": "Opci\u00f3n de {entity_name} cambiada" } }, "title": "Seleccionar" diff --git a/homeassistant/components/sense/translations/es.json b/homeassistant/components/sense/translations/es.json index 3593b08f17c..6790c0841e3 100644 --- a/homeassistant/components/sense/translations/es.json +++ b/homeassistant/components/sense/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -14,14 +14,14 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "La integraci\u00f3n Sense debe volver a autenticar la cuenta {email}.", - "title": "Reautenticar la integraci\u00f3n" + "description": "La integraci\u00f3n Sense necesita volver a autenticar tu cuenta {email}.", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a", - "timeout": "Timeout" + "timeout": "Tiempo de espera" }, "title": "Conectar a tu Sense Energy Monitor" }, @@ -29,7 +29,7 @@ "data": { "code": "C\u00f3digo de verificaci\u00f3n" }, - "title": "Detecci\u00f3n de autenticaci\u00f3n multifactor" + "title": "Autenticaci\u00f3n multifactor de Sense" } } } diff --git a/homeassistant/components/senseme/translations/es.json b/homeassistant/components/senseme/translations/es.json index a10e4422a82..33349bbbc69 100644 --- a/homeassistant/components/senseme/translations/es.json +++ b/homeassistant/components/senseme/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_connect": "Fallo en la conexi\u00f3n" + "cannot_connect": "No se pudo conectar" }, "error": { "cannot_connect": "No se pudo conectar", @@ -17,13 +17,13 @@ "data": { "host": "Host" }, - "description": "Introduzca una direcci\u00f3n IP." + "description": "Introduce una direcci\u00f3n IP." }, "user": { "data": { "device": "Dispositivo" }, - "description": "Seleccione un dispositivo o elija \"Direcci\u00f3n IP\" para introducir manualmente una direcci\u00f3n IP." + "description": "Selecciona un dispositivo o elige 'Direcci\u00f3n IP' para introducir manualmente una direcci\u00f3n IP." } } } diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json index 43d9acc2645..beb6ed575a2 100644 --- a/homeassistant/components/sensibo/translations/es.json +++ b/homeassistant/components/sensibo/translations/es.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", - "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", - "no_devices": "No se ha descubierto ning\u00fan dispositivo", + "incorrect_api_key": "Clave API no v\u00e1lida para la cuenta seleccionada", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_devices": "No se han descubierto dispositivos", "no_username": "No se pudo obtener el nombre de usuario" }, "step": { diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index 4190680944b..a99b59dceae 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -3,11 +3,11 @@ "condition_type": { "is_apparent_power": "Potencia aparente actual de {entity_name}", "is_battery_level": "Nivel de bater\u00eda actual de {entity_name}", - "is_carbon_dioxide": "Nivel actual de concentraci\u00f3n de di\u00f3xido de carbono {entity_name}", - "is_carbon_monoxide": "Nivel actual de concentraci\u00f3n de mon\u00f3xido de carbono {entity_name}", + "is_carbon_dioxide": "Nivel actual en {entity_name} de concentraci\u00f3n de di\u00f3xido de carbono", + "is_carbon_monoxide": "Nivel actual en {entity_name} de concentraci\u00f3n de mon\u00f3xido de carbono", "is_current": "Corriente actual de {entity_name}", "is_energy": "Energ\u00eda actual de {entity_name}", - "is_frequency": "Frecuencia actual de {entity_name}", + "is_frequency": "Frecuencia de {entity_name} actual", "is_gas": "Gas actual de {entity_name}", "is_humidity": "Humedad actual de {entity_name}", "is_illuminance": "Luminosidad actual de {entity_name}", @@ -30,32 +30,32 @@ "is_voltage": "Voltaje actual de {entity_name}" }, "trigger_type": { - "apparent_power": "Cambios de potencia aparente de {entity_name}", + "apparent_power": "{entity_name} ha cambiado de potencia aparente", "battery_level": "Cambios de nivel de bater\u00eda de {entity_name}", "carbon_dioxide": "{entity_name} cambios en la concentraci\u00f3n de di\u00f3xido de carbono", "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono", "current": "Cambio de corriente en {entity_name}", "energy": "Cambio de energ\u00eda en {entity_name}", - "frequency": "Cambios de frecuencia de {entity_name}", - "gas": "Cambio de gas de {entity_name}", + "frequency": "{entity_name} ha cambiado de frecuencia", + "gas": "{entity_name} ha hambiado gas", "humidity": "Cambios de humedad de {entity_name}", "illuminance": "Cambios de luminosidad de {entity_name}", - "nitrogen_dioxide": "Cambios en la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno de {entity_name}", - "nitrogen_monoxide": "Cambios en la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno de {entity_name}", - "nitrous_oxide": "Cambios en la concentraci\u00f3n de \u00f3xido nitroso de {entity_name}", - "ozone": "Cambios en la concentraci\u00f3n de ozono de {entity_name}", - "pm1": "Cambios en la concentraci\u00f3n de PM1 de {entity_name}", - "pm10": "Cambios en la concentraci\u00f3n de PM10 de {entity_name}", - "pm25": "Cambios en la concentraci\u00f3n de PM2.5 de {entity_name}", + "nitrogen_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno", + "nitrogen_monoxide": "{entity_name} ha cambiado en la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno", + "nitrous_oxide": "{entity_name} ha cambiado en la concentraci\u00f3n de \u00f3xido nitroso", + "ozone": "{entity_name} ha cambiado en la concentraci\u00f3n de ozono", + "pm1": "{entity_name} ha cambiado en la concentraci\u00f3n de PM1", + "pm10": "{entity_name} ha cambiado en la concentraci\u00f3n de PM10", + "pm25": "{entity_name} ha cambiado en la concentraci\u00f3n de PM2.5", "power": "Cambios de potencia de {entity_name}", "power_factor": "Cambio de factor de potencia en {entity_name}", "pressure": "Cambios de presi\u00f3n de {entity_name}", - "reactive_power": "Cambios de potencia reactiva de {entity_name}", + "reactive_power": "{entity_name} ha cambiado de potencia reactiva", "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name}", - "sulphur_dioxide": "Cambios en la concentraci\u00f3n de di\u00f3xido de azufre de {entity_name}", + "sulphur_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de azufre", "temperature": "{entity_name} cambios de temperatura", "value": "Cambios de valor de la {entity_name}", - "volatile_organic_compounds": "Cambios en la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles de {entity_name}", + "volatile_organic_compounds": "{entity_name} ha cambiado la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles", "voltage": "Cambio de voltaje en {entity_name}" } }, diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 876d64093a0..85394365c1a 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -13,7 +13,7 @@ "flow_title": "Shelly: {name}", "step": { "confirm_discovery": { - "description": "\u00bfQuieres configurar el {model} en {host}?\n\nAntes de configurarlo, el dispositivo que funciona con pilas debe ser despertado pulsando el bot\u00f3n situado en el dispositivo." + "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte, puedes activar manualmente el dispositivo ahora con un bot\u00f3n o esperar la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." }, "credentials": { "data": { @@ -43,7 +43,7 @@ "double": "Pulsaci\u00f3n doble de {subtype}", "double_push": "Pulsaci\u00f3n doble de {subtype}", "long": "Pulsaci\u00f3n larga de {subtype}", - "long_push": "{subtype} pulsado durante un rato", + "long_push": "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", diff --git a/homeassistant/components/sia/translations/es.json b/homeassistant/components/sia/translations/es.json index 8b8b7e97f1f..60ad0a84f92 100644 --- a/homeassistant/components/sia/translations/es.json +++ b/homeassistant/components/sia/translations/es.json @@ -1,10 +1,10 @@ { "config": { "error": { - "invalid_account_format": "La cuenta no es un valor hexadecimal, por favor utilice s\u00f3lo 0-9 y A-F.", + "invalid_account_format": "La cuenta no es un valor hexadecimal, por favor, utiliza solo 0-9 y A-F.", "invalid_account_length": "La cuenta no tiene la longitud adecuada, tiene que tener entre 3 y 16 caracteres.", - "invalid_key_format": "La clave no es un valor hexadecimal, por favor utilice s\u00f3lo 0-9 y A-F.", - "invalid_key_length": "La clave no tiene la longitud correcta, tiene que ser de 16, 24 o 32 caracteres hexadecimales.", + "invalid_key_format": "La clave no es un valor hexadecimal, por favor, utiliza solo 0-9 y A-F.", + "invalid_key_length": "La clave no tiene la longitud correcta, debe tener 16, 24 o 32 caracteres hexadecimales.", "invalid_ping": "El intervalo de ping debe estar entre 1 y 1440 minutos.", "invalid_zones": "Tiene que haber al menos 1 zona.", "unknown": "Error inesperado" @@ -14,23 +14,23 @@ "data": { "account": "ID de la cuenta", "additional_account": "Cuentas adicionales", - "encryption_key": "Clave de encriptaci\u00f3n", + "encryption_key": "Clave de cifrado", "ping_interval": "Intervalo de ping (min)", "zones": "N\u00famero de zonas de la cuenta" }, - "title": "Agrega otra cuenta al puerto actual." + "title": "A\u00f1adir otra cuenta al puerto actual." }, "user": { "data": { "account": "ID de la cuenta", "additional_account": "Cuentas adicionales", - "encryption_key": "Clave de encriptaci\u00f3n", + "encryption_key": "Clave de cifrado", "ping_interval": "Intervalo de ping (min)", "port": "Puerto", "protocol": "Protocolo", "zones": "N\u00famero de zonas de la cuenta" }, - "title": "Cree una conexi\u00f3n para sistemas de alarma basados en SIA." + "title": "Crear una conexi\u00f3n para sistemas de alarma basados en SIA." } } }, @@ -38,10 +38,10 @@ "step": { "options": { "data": { - "ignore_timestamps": "Ignore la verificaci\u00f3n de la marca de tiempo de los eventos SIA", + "ignore_timestamps": "Ignorar la verificaci\u00f3n de marca de tiempo de los eventos SIA", "zones": "N\u00famero de zonas de la cuenta" }, - "description": "Configure las opciones para la cuenta: {account}", + "description": "Configura las opciones para la cuenta: {account}", "title": "Opciones para la configuraci\u00f3n de SIA." } } diff --git a/homeassistant/components/simplepush/translations/pl.json b/homeassistant/components/simplepush/translations/pl.json index 10f83b3401c..a3269fda96c 100644 --- a/homeassistant/components/simplepush/translations/pl.json +++ b/homeassistant/components/simplepush/translations/pl.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Konfiguracja Simplepush przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", "title": "Konfiguracja YAML dla Simplepush zostanie usuni\u0119ta" + }, + "removed_yaml": { + "description": "Konfiguracja Simplepush za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Simplepush zosta\u0142a usuni\u0119ta" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index db73c6ce042..b47b3870d46 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -35,7 +35,7 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\n Cuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." + "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\nCuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 04f6e8dd44b..c3eb5433359 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_auth_code_length": "Kody autoryzacji SimpliSafe maj\u0105 d\u0142ugo\u015b\u0107 45 znak\u00f3w", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "progress": { @@ -34,7 +35,7 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "SimpliSafe uwierzytelnia u\u017cytkownik\u00f3w za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) przed rozpocz\u0119ciem. \n\nGdy b\u0119dziesz ju\u017c gotowy, kliknij [tutaj]({url}), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Po zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i wprowad\u017a kod autoryzacji z adresu URL aplikacji internetowej SimpliSafe." + "description": "SimpliSafe uwierzytelnia u\u017cytkownik\u00f3w za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) przed rozpocz\u0119ciem. \n\nGdy b\u0119dziesz ju\u017c gotowy, kliknij [tutaj]({url}), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Je\u015bli ju\u017c zalogowa\u0142e\u015b si\u0119 do Simplisafe w swojej przegl\u0105darce, mo\u017cesz otworzy\u0107 now\u0105 kart\u0119, a nast\u0119pnie skopiowa\u0107/wklei\u0107 powy\u017cszy adres URL.\n\nPo zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i wprowad\u017a kod autoryzacji z adresu URL 'com.simplisafe.mobile'." } } }, diff --git a/homeassistant/components/sleepiq/translations/es.json b/homeassistant/components/sleepiq/translations/es.json index 033941b21d2..cdea85e7360 100644 --- a/homeassistant/components/sleepiq/translations/es.json +++ b/homeassistant/components/sleepiq/translations/es.json @@ -2,24 +2,24 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "reauth_confirm": { "data": { "password": "Contrase\u00f1a" }, - "description": "La integraci\u00f3n de SleepIQ necesita volver a autenticar su cuenta {username} .", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "description": "La integraci\u00f3n SleepIQ necesita volver a autenticar tu cuenta {username}.", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index 891e9642d53..808eeffbbcd 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -27,7 +27,7 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "zeroconf_confirm": { - "description": "\u00bfDesea agregar el dispositivo Smappee con el n\u00famero de serie `{serialnumber}` a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el dispositivo Smappee con n\u00famero de serie `{serialnumber}` a Home Assistant?", "title": "Dispositivo Smappee descubierto" } } diff --git a/homeassistant/components/smarttub/translations/es.json b/homeassistant/components/smarttub/translations/es.json index 8f2eb153cb7..fa2b9d91e9b 100644 --- a/homeassistant/components/smarttub/translations/es.json +++ b/homeassistant/components/smarttub/translations/es.json @@ -17,7 +17,7 @@ "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" }, - "description": "Introduzca su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a de SmartTub para iniciar sesi\u00f3n", + "description": "Introduce tu direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a de SmartTub para iniciar sesi\u00f3n", "title": "Inicio de sesi\u00f3n" } } diff --git a/homeassistant/components/solax/translations/es.json b/homeassistant/components/solax/translations/es.json index da4765b897d..c8a47a84150 100644 --- a/homeassistant/components/solax/translations/es.json +++ b/homeassistant/components/solax/translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/steamist/translations/es.json b/homeassistant/components/steamist/translations/es.json index 95592668ad8..d937ed49112 100644 --- a/homeassistant/components/steamist/translations/es.json +++ b/homeassistant/components/steamist/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n se encuentra en progreso", - "cannot_connect": "Error en la conexi\u00f3n", - "no_devices_found": "No se han encontrado dispositivos en la red", - "not_steamist_device": "No es un dispositivo de vapor" + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_steamist_device": "No es un dispositivo de Steamist" }, "error": { "cannot_connect": "No se pudo conectar", @@ -25,7 +25,7 @@ "data": { "host": "Host" }, - "description": "Si deja el host vac\u00edo, la detecci\u00f3n se utilizar\u00e1 para buscar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index fe02d508241..be4121cf747 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -18,7 +18,7 @@ "data": { "pin": "PIN" }, - "description": "Por favor, introduzca su PIN de MySubaru\nNOTA: Todos los veh\u00edculos de la cuenta deben tener el mismo PIN", + "description": "Por favor, introduce tu PIN de MySubaru\nNOTA: Todos los veh\u00edculos en la cuenta deben tener el mismo PIN", "title": "Configuraci\u00f3n de Subaru Starlink" }, "two_factor": { @@ -41,7 +41,7 @@ "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", + "description": "Por favor, introduce tus credenciales de MySubaru\nNOTA: La configuraci\u00f3n inicial puede tardar hasta 30 segundos", "title": "Configuraci\u00f3n de Subaru Starlink" } } @@ -52,7 +52,7 @@ "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).", + "description": "Cuando est\u00e1 habilitado, el sondeo de veh\u00edculos enviar\u00e1 un comando remoto a tu 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" } } diff --git a/homeassistant/components/sun/translations/es.json b/homeassistant/components/sun/translations/es.json index 68db12462b1..9b30ae8a012 100644 --- a/homeassistant/components/sun/translations/es.json +++ b/homeassistant/components/sun/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { - "description": "\u00bfQuiere empezar a configurar?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, diff --git a/homeassistant/components/switch/translations/es.json b/homeassistant/components/switch/translations/es.json index e6190f32c8b..a605ceb238b 100644 --- a/homeassistant/components/switch/translations/es.json +++ b/homeassistant/components/switch/translations/es.json @@ -10,7 +10,7 @@ "is_on": "{entity_name} est\u00e1 encendida" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido" } diff --git a/homeassistant/components/switch_as_x/translations/es.json b/homeassistant/components/switch_as_x/translations/es.json index 0228d86d0fe..eeeae5ede55 100644 --- a/homeassistant/components/switch_as_x/translations/es.json +++ b/homeassistant/components/switch_as_x/translations/es.json @@ -10,5 +10,5 @@ } } }, - "title": "Cambia el tipo de dispositivo de un conmutador" + "title": "Cambiar el tipo de dispositivo de un interruptor" } \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index ca306d6fa2d..2b4fd5ba8cf 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "password": { + "data": { + "password": "Passwort" + }, + "description": "F\u00fcr das Ger\u00e4t {name} ist ein Kennwort erforderlich." + }, "user": { "data": { "address": "Ger\u00e4teadresse", diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index d0f908df646..a4cca573f7d 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -35,7 +35,7 @@ "data": { "retry_count": "Recuento de reintentos", "retry_timeout": "Tiempo de espera entre reintentos", - "scan_timeout": "Cu\u00e1nto tiempo se debe buscar datos de anuncio", + "scan_timeout": "Cu\u00e1nto tiempo escanear en busca de datos de anuncio", "update_time": "Tiempo entre actualizaciones (segundos)" } } diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index a46ec376a96..0f5998dee99 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Kas seadistada {name} ?" + }, + "password": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Seade {name} n\u00f5uab salas\u00f5na" + }, "user": { "data": { "address": "Seadme aadress", diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index f7baed8c8db..d9d4ade13b1 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "password": { + "data": { + "password": "Kata Sandi" + }, + "description": "Perangkat {name} memerlukan kata sandi" + }, "user": { "data": { "address": "Alamat perangkat", diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 6c8d877551f..53767627d2d 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ( {address} )", "step": { + "confirm": { + "description": "Vil du konfigurere {name}?" + }, + "password": { + "data": { + "password": "Passord" + }, + "description": "{name} -enheten krever et passord" + }, "user": { "data": { "address": "Enhetsadresse", diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index dc43a7f610d..5dbc87c07af 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -15,6 +15,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "password": { + "data": { + "password": "Has\u0142o" + }, + "description": "Urz\u0105dzenie {name} wymaga has\u0142a" + }, "user": { "data": { "address": "Adres urz\u0105dzenia", diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 6d14e05aff7..082ad32f84c 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "password": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u88dd\u7f6e {name} \u9700\u8981\u8f38\u5165\u5bc6\u78bc" + }, "user": { "data": { "address": "\u88dd\u7f6e\u4f4d\u5740", diff --git a/homeassistant/components/syncthing/translations/es.json b/homeassistant/components/syncthing/translations/es.json index dd156f7edb4..34e3a391e86 100644 --- a/homeassistant/components/syncthing/translations/es.json +++ b/homeassistant/components/syncthing/translations/es.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "title": "Configurar integraci\u00f3n de Syncthing", + "title": "Configurar la integraci\u00f3n Syncthing", "token": "Token", "url": "URL", "verify_ssl": "Verificar el certificado SSL" diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index d7f1f16197c..41d3d369115 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -54,7 +54,7 @@ "init": { "data": { "scan_interval": "Minutos entre escaneos", - "snap_profile_type": "Calidad de las fotos de la c\u00e1mara (0:alta, 1:media, 2:baja)", + "snap_profile_type": "Nivel de calidad de las instant\u00e1neas de la c\u00e1mara (0:alta 1:media 2:baja)", "timeout": "Tiempo de espera (segundos)" } } diff --git a/homeassistant/components/system_bridge/translations/es.json b/homeassistant/components/system_bridge/translations/es.json index d7fba5e7c32..b6df8d6f57c 100644 --- a/homeassistant/components/system_bridge/translations/es.json +++ b/homeassistant/components/system_bridge/translations/es.json @@ -16,7 +16,7 @@ "data": { "api_key": "Clave API" }, - "description": "Escribe la clave API que estableciste en la configuraci\u00f3n para {name}." + "description": "Por favor, escribe la clave API que estableciste en la configuraci\u00f3n para {name}." }, "user": { "data": { diff --git a/homeassistant/components/tailscale/translations/es.json b/homeassistant/components/tailscale/translations/es.json index 5e87412da4f..62f803025e8 100644 --- a/homeassistant/components/tailscale/translations/es.json +++ b/homeassistant/components/tailscale/translations/es.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { @@ -12,14 +12,14 @@ "data": { "api_key": "Clave API" }, - "description": "Los tokens de la API de Tailscale son v\u00e1lidos durante 90 d\u00edas. Puedes crear una nueva clave de API de Tailscale en https://login.tailscale.com/admin/settings/authkeys." + "description": "Los tokens de la API Tailscale son v\u00e1lidos durante 90 d\u00edas. Puedes crear una nueva clave API de Tailscale en https://login.tailscale.com/admin/settings/authkeys." }, "user": { "data": { "api_key": "Clave API", "tailnet": "Tailnet" }, - "description": "Para autenticarse con Tailscale, deber\u00e1 crear una clave API en https://login.tailscale.com/admin/settings/authkeys. \n\nTailnet es el nombre de su red Tailscale. Puede encontrarlo en la esquina superior izquierda en el Panel de administraci\u00f3n de Tailscale (al lado del logotipo de Tailscale)." + "description": "Esta integraci\u00f3n supervisa tu red Tailscale, **NO** hace que tu instancia de Home Assistant sea accesible a trav\u00e9s de Tailscale VPN. \n\nPara autenticarse con Tailscale, deber\u00e1s crear una clave de API en {authkeys_url}. \n\nTailnet es el nombre de tu red Tailscale. Puedes encontrarlo en la esquina superior izquierda del Panel de administraci\u00f3n de Tailscale (junto al logotipo de Tailscale)." } } } diff --git a/homeassistant/components/tesla_wall_connector/translations/es.json b/homeassistant/components/tesla_wall_connector/translations/es.json index 34cdf425528..c324ad42733 100644 --- a/homeassistant/components/tesla_wall_connector/translations/es.json +++ b/homeassistant/components/tesla_wall_connector/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{serial_number} ({host})", @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "title": "Configurar el conector de pared Tesla" + "title": "Configurar el Tesla Wall Connector" } } } diff --git a/homeassistant/components/threshold/translations/es.json b/homeassistant/components/threshold/translations/es.json index 585a690108f..e35d539d2d2 100644 --- a/homeassistant/components/threshold/translations/es.json +++ b/homeassistant/components/threshold/translations/es.json @@ -12,7 +12,7 @@ "name": "Nombre", "upper": "L\u00edmite superior" }, - "description": "Cree un sensor binario que se encienda y apague dependiendo del valor de un sensor \n\n Solo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\n Solo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\n Ambos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior].", + "description": "Crea un sensor binario que se enciende y apaga dependiendo del valor de un sensor \n\nSolo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\nSolo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\nAmbos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior].", "title": "A\u00f1adir sensor de umbral" } } @@ -30,7 +30,7 @@ "name": "Nombre", "upper": "L\u00edmite superior" }, - "description": "Solo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\n Solo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\n Ambos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior]." + "description": "Solo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\nSolo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\nAmbos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior]." } } }, diff --git a/homeassistant/components/tile/translations/es.json b/homeassistant/components/tile/translations/es.json index c13488183d3..0b0979991bb 100644 --- a/homeassistant/components/tile/translations/es.json +++ b/homeassistant/components/tile/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/tod/translations/es.json b/homeassistant/components/tod/translations/es.json index a5451faf145..55b5c495f18 100644 --- a/homeassistant/components/tod/translations/es.json +++ b/homeassistant/components/tod/translations/es.json @@ -3,12 +3,12 @@ "step": { "user": { "data": { - "after_time": "Tiempo de activaci\u00f3n", - "before_time": "Tiempo de desactivaci\u00f3n", + "after_time": "Hora de encendido", + "before_time": "Hora de apagado", "name": "Nombre" }, - "description": "Crea un sensor binario que se activa o desactiva en funci\u00f3n de la hora.", - "title": "A\u00f1ade sensor tiempo del d\u00eda" + "description": "Crea un sensor binario que se enciende o se apaga dependiendo de la hora.", + "title": "A\u00f1adir sensor Horas del d\u00eda" } } }, @@ -16,8 +16,8 @@ "step": { "init": { "data": { - "after_time": "Tiempo de activaci\u00f3n", - "before_time": "Tiempo apagado" + "after_time": "Hora de encendido", + "before_time": "Hora de apagado" } } } diff --git a/homeassistant/components/tolo/translations/es.json b/homeassistant/components/tolo/translations/es.json index 76cb6c73275..9f08b482d3e 100644 --- a/homeassistant/components/tolo/translations/es.json +++ b/homeassistant/components/tolo/translations/es.json @@ -15,7 +15,7 @@ "data": { "host": "Host" }, - "description": "Introduzca el nombre de host o la direcci\u00f3n IP de su dispositivo TOLO Sauna." + "description": "Introduce el nombre de host o la direcci\u00f3n IP de tu dispositivo TOLO Sauna." } } } diff --git a/homeassistant/components/tomorrowio/translations/es.json b/homeassistant/components/tomorrowio/translations/es.json index bacc07bcdfc..48bac23d2ba 100644 --- a/homeassistant/components/tomorrowio/translations/es.json +++ b/homeassistant/components/tomorrowio/translations/es.json @@ -1,9 +1,9 @@ { "config": { "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_api_key": "Clave API inv\u00e1lida", - "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde.", + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida", + "rate_limited": "Actualmente el ritmo de consultas est\u00e1 limitado, por favor int\u00e9ntalo m\u00e1s tarde.", "unknown": "Error inesperado" }, "step": { @@ -23,7 +23,7 @@ "data": { "timestep": "Min. entre previsiones 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.", + "description": "Si eliges habilitar la entidad de pron\u00f3stico del tiempo `nowcast`, puedes configurar la cantidad de minutos entre cada pron\u00f3stico. La cantidad de pron\u00f3sticos proporcionados depende de la cantidad de minutos elegidos entre los pron\u00f3sticos.", "title": "Actualizar las opciones de Tomorrow.io" } } diff --git a/homeassistant/components/tomorrowio/translations/sensor.es.json b/homeassistant/components/tomorrowio/translations/sensor.es.json index 3967aeba2e2..8be15843389 100644 --- a/homeassistant/components/tomorrowio/translations/sensor.es.json +++ b/homeassistant/components/tomorrowio/translations/sensor.es.json @@ -6,19 +6,19 @@ "moderate": "Moderado", "unhealthy": "Poco saludable", "unhealthy_for_sensitive_groups": "No saludable para grupos sensibles", - "very_unhealthy": "Nada saludable" + "very_unhealthy": "Muy poco saludable" }, "tomorrowio__pollen_index": { "high": "Alto", "low": "Bajo", "medium": "Medio", - "none": "Ninguna", + "none": "Ninguno", "very_high": "Muy alto", "very_low": "Muy bajo" }, "tomorrowio__precipitation_type": { - "freezing_rain": "Lluvia g\u00e9lida", - "ice_pellets": "Perdigones de hielo", + "freezing_rain": "Lluvia helada", + "ice_pellets": "Granizo", "none": "Ninguna", "rain": "Lluvia", "snow": "Nieve" diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index a25a204eec4..61930b53fb0 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "no_locations": "No hay ubicaciones disponibles para este usuario, compruebe la configuraci\u00f3n de TotalConnect", + "no_locations": "No hay ubicaciones disponibles para este usuario, verifica la configuraci\u00f3n de TotalConnect", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { @@ -12,7 +12,7 @@ "step": { "locations": { "data": { - "usercode": "Codigo de usuario" + "usercode": "C\u00f3digo de usuario" }, "description": "Introduce el c\u00f3digo de usuario para este usuario en la ubicaci\u00f3n {location_id}", "title": "C\u00f3digos de usuario de ubicaci\u00f3n" diff --git a/homeassistant/components/tplink/translations/es.json b/homeassistant/components/tplink/translations/es.json index e083acf61ed..964777e876e 100644 --- a/homeassistant/components/tplink/translations/es.json +++ b/homeassistant/components/tplink/translations/es.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Si dejas el host vac\u00edo, se usar\u00e1 descubrimiento para encontrar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/traccar/translations/es.json b/homeassistant/components/traccar/translations/es.json index d23aa678816..21e90275d17 100644 --- a/homeassistant/components/traccar/translations/es.json +++ b/homeassistant/components/traccar/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "No est\u00e1 conectado a Home Assistant Cloud.", + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "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." }, diff --git a/homeassistant/components/tractive/translations/es.json b/homeassistant/components/tractive/translations/es.json index d1f10a1b293..0e242f535ee 100644 --- a/homeassistant/components/tractive/translations/es.json +++ b/homeassistant/components/tractive/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimine la integraci\u00f3n y config\u00farela nuevamente.", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimina la integraci\u00f3n y vuelve a configurarla.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/tractive/translations/sensor.es.json b/homeassistant/components/tractive/translations/sensor.es.json index 21a642c14d8..e36f70c9bf2 100644 --- a/homeassistant/components/tractive/translations/sensor.es.json +++ b/homeassistant/components/tractive/translations/sensor.es.json @@ -1,9 +1,9 @@ { "state": { "tractive__tracker_state": { - "not_reporting": "No reportando", - "operational": "Operacional", - "system_shutdown_user": "Usuario de cierre del sistema", + "not_reporting": "No informando", + "operational": "Operativo", + "system_shutdown_user": "Usuario de apagado del sistema", "system_startup": "Inicio del sistema" } } diff --git a/homeassistant/components/tradfri/translations/es.json b/homeassistant/components/tradfri/translations/es.json index caadd9b7903..500b357319c 100644 --- a/homeassistant/components/tradfri/translations/es.json +++ b/homeassistant/components/tradfri/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso" }, "error": { - "cannot_authenticate": "No se puede autenticar, \u00bfGateway est\u00e1 emparejado con otro servidor como, por ejemplo, Homekit?", + "cannot_authenticate": "No se puede autenticar, \u00bfest\u00e1 la puerta de enlace emparejada con otro servidor como, por ejemplo, Homekit?", "cannot_connect": "No se puede conectar a la puerta de enlace.", "invalid_key": "No se ha podido registrar con la clave proporcionada. Si esto sigue ocurriendo, intenta reiniciar el gateway.", "timeout": "Tiempo de espera agotado validando el c\u00f3digo." diff --git a/homeassistant/components/trafikverket_weatherstation/translations/es.json b/homeassistant/components/trafikverket_weatherstation/translations/es.json index 9513ae65221..f32acf866f4 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/es.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/es.json @@ -6,8 +6,8 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_station": "No se ha podido encontrar una estaci\u00f3n meteorol\u00f3gica con el nombre especificado", - "more_stations": "Se han encontrado varias estaciones meteorol\u00f3gicas con el nombre especificado" + "invalid_station": "No se pudo encontrar una estaci\u00f3n meteorol\u00f3gica con el nombre especificado", + "more_stations": "Se encontraron varias estaciones meteorol\u00f3gicas con el nombre especificado" }, "step": { "user": { diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index d51024c5640..ca46860a208 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -7,8 +7,8 @@ "step": { "user": { "data": { - "access_id": "ID de acceso de Tuya IoT", - "access_secret": "Tuya IoT Access Secret", + "access_id": "ID de acceso a Tuya IoT", + "access_secret": "Secreto de acceso a Tuya IoT", "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", "password": "Contrase\u00f1a", "username": "Nombre de usuario" diff --git a/homeassistant/components/tuya/translations/select.es.json b/homeassistant/components/tuya/translations/select.es.json index b6ab1f8ce1b..0e64ced084e 100644 --- a/homeassistant/components/tuya/translations/select.es.json +++ b/homeassistant/components/tuya/translations/select.es.json @@ -55,8 +55,8 @@ "tuya__humidifier_moodlighting": { "1": "Estado de \u00e1nimo 1", "2": "Estado de \u00e1nimo 2", - "3": "Estado 3", - "4": "Estado 4", + "3": "Estado de \u00e1nimo 3", + "4": "Estado de \u00e1nimo 4", "5": "Estado de \u00e1nimo 5" }, "tuya__humidifier_spray_mode": { @@ -68,7 +68,7 @@ }, "tuya__ipc_work_mode": { "0": "Modo de bajo consumo", - "1": "Modo de trabajo continuo" + "1": "Modo de funcionamiento continuo" }, "tuya__led_type": { "halogen": "Hal\u00f3geno", @@ -78,7 +78,7 @@ "tuya__light_mode": { "none": "Apagado", "pos": "Indicar la ubicaci\u00f3n del interruptor", - "relay": "Indica el estado de encendido / apagado" + "relay": "Indicar el estado de encendido/apagado del interruptor" }, "tuya__motion_sensitivity": { "0": "Sensibilidad baja", @@ -90,15 +90,15 @@ "2": "Grabaci\u00f3n continua" }, "tuya__relay_status": { - "last": "Recuerda el \u00faltimo estado", - "memory": "Recuerda el \u00faltimo estado", + "last": "Recordar el \u00faltimo estado", + "memory": "Recordar el \u00faltimo estado", "off": "Apagado", "on": "Encendido", "power_off": "Apagado", "power_on": "Encendido" }, "tuya__vacuum_cistern": { - "closed": "Cerrado", + "closed": "Cerrada", "high": "Alto", "low": "Bajo", "middle": "Medio" @@ -125,7 +125,7 @@ "single": "\u00danico", "smart": "Inteligente", "spiral": "Espiral", - "standby": "Standby", + "standby": "En espera", "wall_follow": "Seguir Muro", "zone": "Zona" } diff --git a/homeassistant/components/tuya/translations/sensor.es.json b/homeassistant/components/tuya/translations/sensor.es.json index 7dad02bdf7d..da317023bb0 100644 --- a/homeassistant/components/tuya/translations/sensor.es.json +++ b/homeassistant/components/tuya/translations/sensor.es.json @@ -8,13 +8,13 @@ }, "tuya__status": { "boiling_temp": "Temperatura de ebullici\u00f3n", - "cooling": "Enfriamiento", + "cooling": "Refrigeraci\u00f3n", "heating": "Calefacci\u00f3n", "heating_temp": "Temperatura de calentamiento", "reserve_1": "Reserva 1", "reserve_2": "Reserva 2", "reserve_3": "Reserva 3", - "standby": "Standby", + "standby": "En espera", "warm": "Conservaci\u00f3n del calor" } } diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index 8a1cdd9afcd..684ee3f817f 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "discovery_started": "Se ha iniciado el descubrimiento" + "discovery_started": "Descubrimiento iniciado" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Actualice UniFi Protect y vuelva a intentarlo." + "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Actualiza UniFi Protect y vuelve a intentarlo." }, "flow_title": "{name} ({ip_address})", "step": { @@ -16,7 +16,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "\u00bfQuieres configurar {name} ({ip_address})? Necesitar\u00e1 un usuario local creado en su consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", + "description": "\u00bfQuieres configurar {name} ({ip_address})? Necesitar\u00e1s un usuario local creado en tu consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", "title": "UniFi Protect descubierto" }, "reauth_confirm": { @@ -36,7 +36,7 @@ "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" }, - "description": "Necesitar\u00e1 un usuario local creado en su consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", + "description": "Necesitar\u00e1s un usuario local creado en tu consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", "title": "Configuraci\u00f3n de UniFi Protect" } } @@ -45,12 +45,12 @@ "step": { "init": { "data": { - "all_updates": "M\u00e9tricas en tiempo real (ADVERTENCIA: Aumenta en gran medida el uso de la CPU)", + "all_updates": "M\u00e9tricas en tiempo real (ADVERTENCIA: aumenta considerablemente el uso de la CPU)", "disable_rtsp": "Deshabilitar la transmisi\u00f3n RTSP", "max_media": "N\u00famero m\u00e1ximo de eventos a cargar para el Navegador de Medios (aumenta el uso de RAM)", "override_connection_host": "Anular la conexi\u00f3n del host" }, - "description": "La opci\u00f3n de m\u00e9tricas en tiempo real s\u00f3lo debe estar activada si ha habilitado los sensores de diagn\u00f3stico y quiere que se actualicen en tiempo real. Si no est\u00e1 activada, s\u00f3lo se actualizar\u00e1n una vez cada 15 minutos.", + "description": "La opci\u00f3n de m\u00e9tricas en tiempo real solo debe habilitarse si has habilitado los sensores de diagn\u00f3stico y deseas que se actualicen en tiempo real. Si no est\u00e1n habilitados, solo se actualizar\u00e1n una vez cada 15 minutos.", "title": "Opciones de UniFi Protect" } } diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index a0a3b9751d3..38f5a87cb05 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Metrik waktu nyata (PERINGATAN: Meningkatkan penggunaan CPU)", "disable_rtsp": "Nonaktifkan aliran RTSP", + "max_media": "Jumlah maksimum peristiwa yang akan dimuat untuk Browser Media (meningkatkan penggunaan RAM)", "override_connection_host": "Timpa Host Koneksi" }, "description": "Opsi metrik waktu nyata hanya boleh diaktifkan jika Anda telah mengaktifkan sensor diagnostik dan ingin memperbaruinya secara waktu nyata. Jika tidak diaktifkan, metrik hanya akan memperbarui setiap 15 menit sekali.", diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index 82aa3c91ee3..1752e40ac3c 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Metryki w czasie rzeczywistym (UWAGA: Znacznie zwi\u0119ksza u\u017cycie CPU)", "disable_rtsp": "Wy\u0142\u0105cz strumie\u0144 RTSP", + "max_media": "Maksymalna liczba zdarze\u0144 do za\u0142adowania dla przegl\u0105darki medi\u00f3w (zwi\u0119ksza u\u017cycie pami\u0119ci RAM)", "override_connection_host": "Zast\u0105p host po\u0142\u0105czenia" }, "description": "Opcja metryk w czasie rzeczywistym powinna by\u0107 w\u0142\u0105czona tylko wtedy, gdy w\u0142\u0105czono sensory diagnostyczne i chcesz je aktualizowa\u0107 w czasie rzeczywistym. Je\u015bli nie s\u0105 w\u0142\u0105czone, b\u0119d\u0105 aktualizowane tylko raz na 15 minut.", diff --git a/homeassistant/components/uptime/translations/es.json b/homeassistant/components/uptime/translations/es.json index 84e840f453f..e04b528f153 100644 --- a/homeassistant/components/uptime/translations/es.json +++ b/homeassistant/components/uptime/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { - "description": "\u00bfQuiere empezar a configurar?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json index e78a90b4176..e1ede36a55a 100644 --- a/homeassistant/components/uptimerobot/translations/es.json +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -2,30 +2,30 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimine la integraci\u00f3n y config\u00farela nuevamente.", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, eliminq la integraci\u00f3n y vuelve a configurarla.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_api_key": "Clave API no v\u00e1lida", - "not_main_key": "Se ha detectado un tipo de clave API incorrecta, utiliza la clave API 'principal'", - "reauth_failed_matching_account": "La clave de API que has proporcionado no coincide con el ID de cuenta para la configuraci\u00f3n existente.", + "not_main_key": "Se detect\u00f3 un tipo de clave API incorrecto, utiliza la clave API 'principal'", + "reauth_failed_matching_account": "La clave de API que proporcionaste no coincide con el ID de cuenta para la configuraci\u00f3n existente.", "unknown": "Error inesperado" }, "step": { "reauth_confirm": { "data": { - "api_key": "API Key" + "api_key": "Clave API" }, - "description": "Debes proporcionar una nueva clave API de solo lectura de Uptime Robot", + "description": "Debes proporcionar una nueva clave API 'principal' de UptimeRobot", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { "api_key": "Clave API" }, - "description": "Debes proporcionar una clave API de solo lectura de robot de tiempo de actividad/funcionamiento" + "description": "Debes proporcionar la clave API 'principal' de UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/sensor.es.json b/homeassistant/components/uptimerobot/translations/sensor.es.json index 2adb0ff18c5..5397e6eb54c 100644 --- a/homeassistant/components/uptimerobot/translations/sensor.es.json +++ b/homeassistant/components/uptimerobot/translations/sensor.es.json @@ -1,11 +1,11 @@ { "state": { "uptimerobot__monitor_status": { - "down": "No disponible", - "not_checked_yet": "No comprobado", - "pause": "En pausa", - "seems_down": "Parece no disponible", - "up": "Funcionante" + "down": "Ca\u00eddo", + "not_checked_yet": "A\u00fan no se ha comprobado", + "pause": "Pausa", + "seems_down": "Parece ca\u00eddo", + "up": "Arriba" } } } \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/es.json b/homeassistant/components/vallox/translations/es.json index 373ae333ec3..088257647ad 100644 --- a/homeassistant/components/vallox/translations/es.json +++ b/homeassistant/components/vallox/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El servicio ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "invalid_host": "Nombre del host o direcci\u00f3n IP no v\u00e1lidos", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/verisure/translations/es.json b/homeassistant/components/verisure/translations/es.json index c7ea0af6f41..ed0d1328e79 100644 --- a/homeassistant/components/verisure/translations/es.json +++ b/homeassistant/components/verisure/translations/es.json @@ -14,7 +14,7 @@ "data": { "giid": "Instalaci\u00f3n" }, - "description": "Home Assistant encontr\u00f3 varias instalaciones de Verisure en su cuenta de Mis p\u00e1ginas. Por favor, seleccione la instalaci\u00f3n para agregar a Home Assistant." + "description": "Home Assistant encontr\u00f3 varias instalaciones Verisure en tu cuenta My Pages. Por favor, selecciona la instalaci\u00f3n a a\u00f1adir a Home Assistant." }, "mfa": { "data": { @@ -24,7 +24,7 @@ }, "reauth_confirm": { "data": { - "description": "Vuelva a autenticarse con su cuenta Verisure My Pages.", + "description": "Vuelva a autenticarse con tu cuenta Verisure My Pages.", "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" } @@ -46,13 +46,13 @@ }, "options": { "error": { - "code_format_mismatch": "El c\u00f3digo PIN predeterminado no coincide con el n\u00famero necesario de d\u00edgitos" + "code_format_mismatch": "El c\u00f3digo PIN predeterminado no coincide con el n\u00famero requerido de d\u00edgitos" }, "step": { "init": { "data": { - "lock_code_digits": "N\u00famero de d\u00edgitos del c\u00f3digo PIN de las cerraduras", - "lock_default_code": "C\u00f3digo PIN por defecto para las cerraduras, utilizado si no se indica ninguno" + "lock_code_digits": "N\u00famero de d\u00edgitos en el c\u00f3digo PIN para cerraduras", + "lock_default_code": "C\u00f3digo PIN predeterminado para cerraduras, se usa si no se proporciona ninguno" } } } diff --git a/homeassistant/components/version/translations/es.json b/homeassistant/components/version/translations/es.json index e2cc5937f8d..cbcd4bcd511 100644 --- a/homeassistant/components/version/translations/es.json +++ b/homeassistant/components/version/translations/es.json @@ -8,8 +8,8 @@ "data": { "version_source": "Fuente de la versi\u00f3n" }, - "description": "Seleccione la fuente desde la que desea realizar el seguimiento de las versiones", - "title": "Seleccione el tipo de instalaci\u00f3n" + "description": "Selecciona la fuente de la que deseas realizar un seguimiento de las versiones", + "title": "Selecciona el tipo de instalaci\u00f3n" }, "version_source": { "data": { @@ -18,7 +18,7 @@ "channel": "Qu\u00e9 canal debe ser rastreado", "image": "Qu\u00e9 imagen debe ser rastreada" }, - "description": "Configurar el seguimiento de la versi\u00f3n {version_source}", + "description": "Configurar el seguimiento de versiones de {version_source}", "title": "Configurar" } } diff --git a/homeassistant/components/vicare/translations/es.json b/homeassistant/components/vicare/translations/es.json index 6653fa3198e..26f32b1e720 100644 --- a/homeassistant/components/vicare/translations/es.json +++ b/homeassistant/components/vicare/translations/es.json @@ -1,7 +1,7 @@ { "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.", "unknown": "Error inesperado" }, "error": { @@ -14,9 +14,9 @@ "client_id": "Clave API", "heating_type": "Tipo de calefacci\u00f3n", "password": "Contrase\u00f1a", - "username": "Email" + "username": "Correo electr\u00f3nico" }, - "description": "Configure la integraci\u00f3n de ViCare. Para generar la clave API, vaya a https://developer.viessmann.com" + "description": "Configura la integraci\u00f3n de ViCare. Para generar la clave API, ve a https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vlc_telnet/translations/es.json b/homeassistant/components/vlc_telnet/translations/es.json index 64f3b72d223..4cdc7cb2154 100644 --- a/homeassistant/components/vlc_telnet/translations/es.json +++ b/homeassistant/components/vlc_telnet/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" @@ -15,13 +15,13 @@ "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "\u00bfDesea conectarse al complemento {addon}?" + "description": "\u00bfQuieres conectarte al complemento {addon}?" }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a" }, - "description": "Por favor, introduzca la contrase\u00f1a correcta para el host: {host}" + "description": "Por favor, introduce la contrase\u00f1a correcta para el host: {host}" }, "user": { "data": { diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json index 451e55ed41e..e93a47ce3b3 100644 --- a/homeassistant/components/wallbox/translations/es.json +++ b/homeassistant/components/wallbox/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "reauth_invalid": "Fallo en la reautenticaci\u00f3n; el n\u00famero de serie no coincide con el original", + "reauth_invalid": "La reautenticaci\u00f3n fall\u00f3; El n\u00famero de serie no coincide con el original", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/watttime/translations/es.json b/homeassistant/components/watttime/translations/es.json index 189ea8b70cb..9267314d960 100644 --- a/homeassistant/components/watttime/translations/es.json +++ b/homeassistant/components/watttime/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya se ha configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado", - "unknown_coordinates": "No hay datos para esa latitud/longitud" + "unknown_coordinates": "No hay datos para latitud/longitud" }, "step": { "coordinates": { @@ -15,19 +15,19 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "Introduzca la latitud y longitud a monitorizar:" + "description": "Introduce la latitud y longitud a supervisar:" }, "location": { "data": { "location_type": "Ubicaci\u00f3n" }, - "description": "Escoja una ubicaci\u00f3n para monitorizar:" + "description": "Elige una ubicaci\u00f3n para supervisar:" }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a" }, - "description": "Vuelva a ingresar la contrase\u00f1a de {username} :", + "description": "Por favor, vuelve a introducir la contrase\u00f1a de {username}:", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { @@ -35,7 +35,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Introduzca su nombre de usuario y contrase\u00f1a:" + "description": "Introduce tu nombre de usuario y contrase\u00f1a:" } } }, @@ -43,7 +43,7 @@ "step": { "init": { "data": { - "show_on_map": "Mostrar la ubicaci\u00f3n en el mapa" + "show_on_map": "Mostrar la ubicaci\u00f3n supervisada en el mapa" }, "title": "Configurar WattTime" } diff --git a/homeassistant/components/waze_travel_time/translations/es.json b/homeassistant/components/waze_travel_time/translations/es.json index 62325233cab..118bf583d56 100644 --- a/homeassistant/components/waze_travel_time/translations/es.json +++ b/homeassistant/components/waze_travel_time/translations/es.json @@ -14,7 +14,7 @@ "origin": "Origen", "region": "Regi\u00f3n" }, - "description": "En Origen y Destino, introduce la direcci\u00f3n de las coordenadas GPS de la ubicaci\u00f3n (las coordenadas GPS deben estar separadas por una coma). Tambi\u00e9n puedes escribir un id de entidad que proporcione esta informaci\u00f3n en su estado, un id de entidad con atributos de latitud y longitud o un nombre descriptivo de zona." + "description": "Para Origen y Destino, introduce la direcci\u00f3n o las coordenadas GPS de la ubicaci\u00f3n (las coordenadas GPS deben estar separadas por una coma). Tambi\u00e9n puedes introducir un id de entidad que proporcione esta informaci\u00f3n en su estado, un id de entidad con atributos de latitud y longitud, o un nombre descriptivo de zona." } } }, @@ -31,7 +31,7 @@ "units": "Unidades", "vehicle_type": "Tipo de veh\u00edculo" }, - "description": "Las entradas `subcadena` te permitir\u00e1n forzar a la integraci\u00f3n a utilizar una ruta concreta o a evitar una ruta concreta en el c\u00e1lculo del tiempo del recorrido." + "description": "Las entradas `subcadena` te permitir\u00e1n forzar la integraci\u00f3n para usar una ruta en particular o evitar una ruta en particular en su c\u00e1lculo del tiempo de viaje." } } }, diff --git a/homeassistant/components/webostv/translations/es.json b/homeassistant/components/webostv/translations/es.json index db23caa048b..d4b3d2eecdc 100644 --- a/homeassistant/components/webostv/translations/es.json +++ b/homeassistant/components/webostv/translations/es.json @@ -6,32 +6,32 @@ "error_pairing": "Conectado a LG webOS TV pero no emparejado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, encienda el televisor o compruebe la direcci\u00f3n IP" + "cannot_connect": "No se pudo conectar, por favor, enciende tu televisor o verifica la direcci\u00f3n IP" }, "flow_title": "LG webOS Smart TV", "step": { "pairing": { - "description": "Haz clic en enviar y acepta la solicitud de emparejamiento en tu televisor.\n\n![Image](/static/images/config_webos.png)", + "description": "Haz clic en enviar y acepta la solicitud de emparejamiento en tu televisor. \n\n ![Image](/static/images/config_webos.png)", "title": "Emparejamiento de webOS TV" }, "user": { "data": { - "host": "Anfitri\u00f3n", + "host": "Host", "name": "Nombre" }, - "description": "Encienda la televisi\u00f3n, rellene los siguientes campos y haga clic en enviar", + "description": "Enciende la TV, completa los siguientes campos y haz clic en enviar", "title": "Conectarse a webOS TV" } } }, "device_automation": { "trigger_type": { - "webostv.turn_on": "Se solicita el encendido del dispositivo" + "webostv.turn_on": "Se solicita que el dispositivo se encienda" } }, "options": { "error": { - "cannot_retrieve": "No se puede recuperar la lista de fuentes. Aseg\u00farese de que el dispositivo est\u00e1 encendido", + "cannot_retrieve": "No se puede recuperar la lista de fuentes. Aseg\u00farate de que el dispositivo est\u00e9 encendido", "script_not_found": "Script no encontrado" }, "step": { @@ -39,7 +39,7 @@ "data": { "sources": "Lista de fuentes" }, - "description": "Seleccionar fuentes habilitadas", + "description": "Selecciona las fuentes habilitadas", "title": "Opciones para webOS Smart TV" } } diff --git a/homeassistant/components/whois/translations/es.json b/homeassistant/components/whois/translations/es.json index e2803c251d6..ff7506bf0c5 100644 --- a/homeassistant/components/whois/translations/es.json +++ b/homeassistant/components/whois/translations/es.json @@ -7,7 +7,7 @@ "unexpected_response": "Respuesta inesperada del servidor whois", "unknown_date_format": "Formato de fecha desconocido en la respuesta del servidor whois", "unknown_tld": "El TLD dado es desconocido o no est\u00e1 disponible para esta integraci\u00f3n", - "whois_command_failed": "El comando whois ha fallado: no se pudo obtener la informaci\u00f3n whois" + "whois_command_failed": "Error en el comando whois: no se pudo recuperar la informaci\u00f3n whois" }, "step": { "user": { diff --git a/homeassistant/components/wiz/translations/es.json b/homeassistant/components/wiz/translations/es.json index b86f60649a2..d3d0469cd0b 100644 --- a/homeassistant/components/wiz/translations/es.json +++ b/homeassistant/components/wiz/translations/es.json @@ -1,21 +1,21 @@ { "config": { "abort": { - "already_configured": "Dispositivo ya configurado", - "cannot_connect": "Error al conectar", - "no_devices_found": "Ning\u00fan dispositivo encontrado en la red." + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { - "bulb_time_out": "No se puede conectar a la bombilla. Tal vez la bombilla est\u00e1 desconectada o se ingres\u00f3 una IP incorrecta. \u00a1Por favor encienda la luz y vuelve a intentarlo!", - "cannot_connect": "Error al conectar", + "bulb_time_out": "No se puede conectar a la bombilla. Tal vez la bombilla est\u00e1 desconectada o se ha introducido una IP incorrecta. \u00a1Por favor, enciende la luz y vuelve a intentarlo!", + "cannot_connect": "No se pudo conectar", "no_ip": "No es una direcci\u00f3n IP v\u00e1lida.", - "no_wiz_light": "La bombilla no se puede conectar a trav\u00e9s de la integraci\u00f3n de WiZ Platform.", + "no_wiz_light": "La bombilla no se puede conectar a trav\u00e9s de la integraci\u00f3n WiZ Platform.", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", "step": { "discovery_confirm": { - "description": "\u00bfDesea configurar {name} ({host})?" + "description": "\u00bfQuieres configurar {name} ({host})?" }, "pick_device": { "data": { @@ -26,7 +26,7 @@ "data": { "host": "Direcci\u00f3n IP" }, - "description": "Si deja la direcci\u00f3n IP vac\u00eda, la detecci\u00f3n se utilizar\u00e1 para buscar dispositivos." + "description": "Si dejas la direcci\u00f3n IP vac\u00eda, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index 07ebd6c2e02..7a428960d62 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Este dispositivo WLED ya est\u00e1 configurado.", "cannot_connect": "No se pudo conectar", - "cct_unsupported": "Este dispositivo WLED utiliza canales CCT, que no son compatibles con esta integraci\u00f3n." + "cct_unsupported": "Este dispositivo WLED utiliza canales CCT, que no son compatibles con esta integraci\u00f3n" }, "error": { "cannot_connect": "No se pudo conectar" @@ -26,7 +26,7 @@ "step": { "init": { "data": { - "keep_master_light": "Mantenga la luz principal, incluso con 1 segmento de LED." + "keep_master_light": "Mantener la luz principal, incluso con 1 segmento LED." } } } diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 6152da62e47..8f692a87b2c 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -3,16 +3,16 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se ha suministrado ning\u00fan host o token.", + "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se proporcion\u00f3 host ni token.", "not_xiaomi_miio": "El dispositivo no es (todav\u00eda) compatible con Xiaomi Miio.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", - "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellene el nombre de usuario, la contrase\u00f1a y el pa\u00eds", - "cloud_login_error": "No se ha podido iniciar sesi\u00f3n en Xiaomi Miio Cloud, comprueba las credenciales.", - "cloud_no_devices": "No se han encontrado dispositivos en esta cuenta de Xiaomi Miio.", - "unknown_device": "No se conoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n.", + "cloud_credentials_incomplete": "Credenciales de la nube incompletas, por favor, completa el nombre de usuario, la contrase\u00f1a y el pa\u00eds", + "cloud_login_error": "No se pudo iniciar sesi\u00f3n en Xiaomi Miio Cloud, verifica las credenciales.", + "cloud_no_devices": "No se encontraron dispositivos en esta cuenta en la nube de Xiaomi Miio.", + "unknown_device": "Se desconoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n.", "wrong_token": "Error de suma de comprobaci\u00f3n, token err\u00f3neo" }, "flow_title": "{name}", @@ -24,7 +24,7 @@ "cloud_username": "Nombre de usuario de la nube", "manual": "Configurar manualmente (no recomendado)" }, - "description": "Inicie sesi\u00f3n en la nube de Xiaomi Miio, consulte https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debe utilizar." + "description": "Inicia sesi\u00f3n en la nube de Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debes utilizar." }, "connect": { "data": { diff --git a/homeassistant/components/yale_smart_alarm/translations/es.json b/homeassistant/components/yale_smart_alarm/translations/es.json index 59824bfdb3b..94f54854126 100644 --- a/homeassistant/components/yale_smart_alarm/translations/es.json +++ b/homeassistant/components/yale_smart_alarm/translations/es.json @@ -2,16 +2,16 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "reauth_confirm": { "data": { - "area_id": "ID de \u00e1rea", + "area_id": "ID de \u00c1rea", "name": "Nombre", "password": "Contrase\u00f1a", "username": "Nombre de usuario" @@ -29,13 +29,13 @@ }, "options": { "error": { - "code_format_mismatch": "El c\u00f3digo no coincide con el n\u00famero de d\u00edgitos requerido" + "code_format_mismatch": "El c\u00f3digo no coincide con el n\u00famero requerido de d\u00edgitos" }, "step": { "init": { "data": { - "code": "C\u00f3digo predeterminado para cerraduras, utilizado si no se proporciona ninguno", - "lock_code_digits": "N\u00famero de d\u00edgitos del c\u00f3digo PIN de las cerraduras" + "code": "C\u00f3digo predeterminado para cerraduras, se usa si no se proporciona ninguno", + "lock_code_digits": "N\u00famero de d\u00edgitos en el c\u00f3digo PIN para cerraduras" } } } diff --git a/homeassistant/components/yalexs_ble/translations/de.json b/homeassistant/components/yalexs_ble/translations/de.json new file mode 100644 index 00000000000..cfd368aadef --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_key_format": "Der Offline-Schl\u00fcssel muss eine 32-Byte-Hex-Zeichenfolge sein.", + "invalid_key_index": "Der Offline-Schl\u00fcssel-Slot muss eine ganze Zahl zwischen 0 und 255 sein.", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "M\u00f6chtest du {name} \u00fcber Bluetooth mit der Adresse {address} einrichten?" + }, + "user": { + "data": { + "address": "Bluetooth-Adresse", + "key": "Offline-Schl\u00fcssel (32-Byte-Hex-String)", + "slot": "Offline-Schl\u00fcssel-Slot (Ganzzahl zwischen 0 und 255)" + }, + "description": "Lies in der Dokumentation unter {docs_url} nach, wie du den Offline-Schl\u00fcssel finden kannst." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/et.json b/homeassistant/components/yalexs_ble/translations/et.json new file mode 100644 index 00000000000..564b6c32dd5 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "no_unconfigured_devices": "H\u00e4\u00e4lestamata seadmeid ei leitud." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_key_format": "V\u00f5rgu\u00fchenduseta v\u00f5ti peab olema 32-baidine kuueteistk\u00fcmnebaidine string.", + "invalid_key_index": "V\u00f5rgu\u00fchenduseta v\u00f5tmepesa peab olema t\u00e4isarv vahemikus 0 kuni 255.", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Kas seadistada {name} Bluetoothi kaudu aadressiga {address} ?" + }, + "user": { + "data": { + "address": "Bluetoothi aadress", + "key": "V\u00f5rgu\u00fchenduseta v\u00f5ti (32-baidine kuueteistk\u00fcmnebaidine string)", + "slot": "V\u00f5rgu\u00fchenduseta v\u00f5tmepesa (t\u00e4isarv vahemikus 0 kuni 255)" + }, + "description": "V\u00f5rgu\u00fchenduseta v\u00f5tme leidmise kohta vaata dokumentatsiooni aadressil {docs_url} ." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/id.json b/homeassistant/components/yalexs_ble/translations/id.json new file mode 100644 index 00000000000..fda1e243e0c --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "no_unconfigured_devices": "Tidak ditemukan perangkat yang tidak dikonfigurasi." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_key_format": "Kunci offline harus berupa string heksadesimal 32 byte.", + "invalid_key_index": "Slot kunci offline harus berupa bilangan bulat antara 0 dan 255.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Ingin menyiapkan {name} melalui Bluetooth dengan alamat {address} ?" + }, + "user": { + "data": { + "address": "Alamat Bluetooth", + "key": "Kunci Offline (string heksadesimal 32 byte)", + "slot": "Slot Kunci Offline (Bilangan Bulat antara 0 dan 255)" + }, + "description": "Lihat dokumentasi di {docs_url} untuk mengetahui cara menemukan kunci offline." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/no.json b/homeassistant/components/yalexs_ble/translations/no.json new file mode 100644 index 00000000000..6d2be535c73 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "no_unconfigured_devices": "Fant ingen ukonfigurerte enheter." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_key_format": "Den frakoblede n\u00f8kkelen m\u00e5 v\u00e6re en 32-byte sekskantstreng.", + "invalid_key_index": "Frakoblet n\u00f8kkelspor m\u00e5 v\u00e6re et heltall mellom 0 og 255.", + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Vil du konfigurere {name} over Bluetooth med adressen {address} ?" + }, + "user": { + "data": { + "address": "Bluetooth-adresse", + "key": "Frakoblet n\u00f8kkel (32-byte sekskantstreng)", + "slot": "Frakoblet n\u00f8kkelspor (heltall mellom 0 og 255)" + }, + "description": "Se dokumentasjonen p\u00e5 {docs_url} for hvordan du finner frakoblet n\u00f8kkel." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/pl.json b/homeassistant/components/yalexs_ble/translations/pl.json new file mode 100644 index 00000000000..b178db81e8d --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "no_unconfigured_devices": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_key_format": "Klucz offline musi by\u0107 32-bajtowym ci\u0105giem szesnastkowym.", + "invalid_key_index": "Slot klucza offline musi by\u0107 liczb\u0105 ca\u0142kowit\u0105 z zakresu od 0 do 255.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} przez Bluetooth z adresem {address}?" + }, + "user": { + "data": { + "address": "Adres Bluetooth", + "key": "Klucz offline (32-bajtowy ci\u0105g szesnastkowy)", + "slot": "Slot klucza offline (liczba ca\u0142kowita od 0 do 255)" + }, + "description": "Sprawd\u017a dokumentacj\u0119 na {docs_url}, aby dowiedzie\u0107 si\u0119, jak znale\u017a\u0107 klucz offline." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/zh-Hant.json b/homeassistant/components/yalexs_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..f16fdcb0d07 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_key_format": "\u96e2\u7dda\u91d1\u9470\u5fc5\u9808\u70ba 32 \u4f4d\u5143\u5341\u516d\u9032\u4f4d\u5b57\u4e32\u3002", + "invalid_key_index": "\u96e2\u7dda\u91d1\u9470\u5fc5\u9808\u70ba 0 \u81f3 255 \u4e4b\u9593\u7684\u6574\u6578\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u5740\u70ba {address}\u3001\u540d\u7a31\u70ba {name} \u4e4b\u85cd\u7259\u88dd\u7f6e\uff1f" + }, + "user": { + "data": { + "address": "\u85cd\u7259\u4f4d\u5740", + "key": "\u96e2\u7dda\u91d1\u9470\uff0832 \u4f4d\u5143 16 \u9032\u4f4d\u5b57\u4e32\uff09", + "slot": "\u96e2\u7dda\u91d1\u9470\uff080 \u81f3 255 \u9593\u7684\u6574\u6578\uff09" + }, + "description": "\u8acb\u53c3\u8003\u6587\u4ef6 {docs_url} \u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/es.json b/homeassistant/components/yamaha_musiccast/translations/es.json index a83d97db703..e5c24d474b8 100644 --- a/homeassistant/components/yamaha_musiccast/translations/es.json +++ b/homeassistant/components/yamaha_musiccast/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "yxc_control_url_missing": "La URL de control no se proporciona en la descripci\u00f3n del ssdp." + "yxc_control_url_missing": "La URL de control no se proporciona en la descripci\u00f3n de ssdp." }, "error": { "no_musiccast_device": "Este dispositivo no parece ser un dispositivo MusicCast." diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 9056708fd96..0378ee6cdd0 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -3,7 +3,7 @@ "abort": { "not_zha_device": "Este dispositivo no es un dispositivo zha", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "usb_probe_failed": "No se ha podido sondear el dispositivo usb" + "usb_probe_failed": "Error al sondear el dispositivo USB" }, "error": { "cannot_connect": "No se pudo conectar" @@ -40,17 +40,17 @@ }, "config_panel": { "zha_alarm_options": { - "alarm_arm_requires_code": "C\u00f3digo requerido para las acciones de armado", - "alarm_failed_tries": "El n\u00famero de entradas de c\u00f3digo fallidas consecutivas para activar una alarma", - "alarm_master_code": "C\u00f3digo maestro de la(s) central(es) de alarma", - "title": "Opciones del panel de control de la alarma" + "alarm_arm_requires_code": "C\u00f3digo requerido para acciones de armado", + "alarm_failed_tries": "El n\u00famero de entradas consecutivas de c\u00f3digos fallidos para disparar una alarma", + "alarm_master_code": "C\u00f3digo maestro para el(los) panel(es) de control de alarma", + "title": "Opciones del panel de control de alarma" }, "zha_options": { "always_prefer_xy_color_mode": "Preferir siempre el modo de color XY", - "consider_unavailable_battery": "Considere que los dispositivos alimentados por bater\u00eda no est\u00e1n disponibles despu\u00e9s de (segundos)", - "consider_unavailable_mains": "Considere que los dispositivos alimentados por la red el\u00e9ctrica no est\u00e1n disponibles despu\u00e9s de (segundos)", - "default_light_transition": "Tiempo de transici\u00f3n de la luz por defecto (segundos)", - "enable_identify_on_join": "Activar el efecto de identificaci\u00f3n cuando los dispositivos se unen a la red", + "consider_unavailable_battery": "Considerar que los dispositivos alimentados por bater\u00eda no est\u00e1n disponibles despu\u00e9s de (segundos)", + "consider_unavailable_mains": "Considerar que los dispositivos alimentados por la red no est\u00e1n disponibles despu\u00e9s de (segundos)", + "default_light_transition": "Tiempo de transici\u00f3n de luz predeterminado (segundos)", + "enable_identify_on_join": "Habilitar el efecto de identificaci\u00f3n cuando los dispositivos se unan a la red", "enhanced_light_transition": "Habilitar la transici\u00f3n mejorada de color de luz/temperatura desde un estado apagado", "light_transitioning_flag": "Habilitar el control deslizante de brillo mejorado durante la transici\u00f3n de luz", "title": "Opciones globales" diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 3101d804b43..9e3a2f154bc 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -5,7 +5,7 @@ "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.", - "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", + "addon_start_failed": "No se pudo 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 curso", "cannot_connect": "No se pudo conectar", @@ -21,7 +21,7 @@ "flow_title": "{name}", "progress": { "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." + "start_addon": "Por favor, espera mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos." }, "step": { "configure_addon": { @@ -54,7 +54,7 @@ "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { - "title": "Se est\u00e1 iniciando el complemento Z-Wave JS." + "title": "El complemento Z-Wave JS se est\u00e1 iniciando." }, "usb_confirm": { "description": "\u00bfQuieres configurar {name} con el complemento Z-Wave JS?" @@ -68,12 +68,12 @@ "device_automation": { "action_type": { "clear_lock_usercode": "Borrar c\u00f3digo de usuario en {entity_name}", - "ping": "Ping del dispositivo", - "refresh_value": "Actualizar los valores de {entity_name}", + "ping": "Ping al dispositivo", + "refresh_value": "Actualizar el/los valor(es) para {entity_name}", "reset_meter": "Restablecer contadores en {subtype}", "set_config_parameter": "Establecer el valor del par\u00e1metro de configuraci\u00f3n {subtype}", "set_lock_usercode": "Establecer un c\u00f3digo de usuario en {entity_name}", - "set_value": "Establecer valor de un valor Z-Wave" + "set_value": "Establecer el valor de un valor de Z-Wave" }, "condition_type": { "config_parameter": "Valor del par\u00e1metro de configuraci\u00f3n {subtype}", @@ -88,19 +88,19 @@ "event.value_notification.scene_activation": "Activaci\u00f3n de escena en {subtype}", "state.node_status": "El estado del nodo ha cambiado", "zwave_js.value_updated.config_parameter": "Cambio de valor en el par\u00e1metro de configuraci\u00f3n {subtype}", - "zwave_js.value_updated.value": "Cambio de valor en un valor JS de Z-Wave" + "zwave_js.value_updated.value": "Cambio de valor en un valor Z-Wave JS" } }, "options": { "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": "Fallo en la obtenci\u00f3n de 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.", - "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", + "addon_get_discovery_info_failed": "No se pudo obtener la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", + "addon_info_failed": "No se pudo obtener la informaci\u00f3n adicional de Z-Wave JS.", + "addon_install_failed": "No se pudo instalar el complemento Z-Wave JS.", + "addon_set_config_failed": "No se pudo establecer la configuraci\u00f3n de Z-Wave JS.", + "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "different_device": "El dispositivo USB conectado no es el mismo que el configurado anteriormente para esta entrada de configuraci\u00f3n. Por favor, crea una nueva entrada de configuraci\u00f3n para el nuevo dispositivo." + "different_device": "El dispositivo USB conectado no es el mismo que se configur\u00f3 previamente para esta entrada de configuraci\u00f3n. Por favor, en su lugar crea una nueva entrada de configuraci\u00f3n para el nuevo dispositivo." }, "error": { "cannot_connect": "No se pudo conectar", @@ -108,8 +108,8 @@ "unknown": "Error inesperado" }, "progress": { - "install_addon": "Por favor, espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Esto puede tardar varios minutos.", - "start_addon": "Por favor, espera mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar algunos segundos." + "install_addon": "Por favor, espera mientras finaliza la instalaci\u00f3n del complemento Z-Wave JS. Esto puede tardar varios minutos.", + "start_addon": "Por favor, espera mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos." }, "step": { "configure_addon": { @@ -135,9 +135,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usar el complemento Z-Wave JS Supervisor" + "use_addon": "Usar el complemento Supervisor Z-Wave JS" }, - "description": "\u00bfQuieres utilizar el complemento Z-Wave JS Supervisor?", + "description": "\u00bfQuieres utilizar el complemento Supervisor Z-Wave JS ?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { diff --git a/homeassistant/components/zwave_me/translations/es.json b/homeassistant/components/zwave_me/translations/es.json index 2443547e59d..ff067220022 100644 --- a/homeassistant/components/zwave_me/translations/es.json +++ b/homeassistant/components/zwave_me/translations/es.json @@ -13,7 +13,7 @@ "token": "Token API", "url": "URL" }, - "description": "Direcci\u00f3n IP de entrada del servidor Z-Way y token de acceso Z-Way. La direcci\u00f3n IP se puede prefijar con wss:// si se debe usar HTTPS en lugar de HTTP. Para obtener el token, vaya a la interfaz de usuario de Z-Way > Configuraci\u00f3n de > de men\u00fa > token de API de > de usuario. Se sugiere crear un nuevo usuario para Home Assistant y conceder acceso a los dispositivos que necesita controlar desde Home Assistant. Tambi\u00e9n es posible utilizar el acceso remoto a trav\u00e9s de find.z-wave.me para conectar un Z-Way remoto. Ingrese wss://find.z-wave.me en el campo IP y copie el token con alcance global (inicie sesi\u00f3n en Z-Way a trav\u00e9s de find.z-wave.me para esto)." + "description": "Introduce la direcci\u00f3n IP con el puerto y el token de acceso del servidor Z-Way. Para obtener el token, ve a la interfaz de usuario de Z-Way Smart Home UI > Menu > Settings > Users > Administrator > API token. \n\nEjemplo de conexi\u00f3n a Z-Way en la red local:\nURL: {local_url}\nFicha: {local_token} \n\nEjemplo de conexi\u00f3n a Z-Way mediante acceso remoto find.z-wave.me:\nURL: {find_url}\nFicha: {find_token} \n\nEjemplo de conexi\u00f3n a Z-Way con una direcci\u00f3n IP p\u00fablica est\u00e1tica:\nURL: {remote_url}\nFicha: {local_token} \n\nAl conectarte a trav\u00e9s de find.z-wave.me, debes usar un token con un alcance global (inicia sesi\u00f3n en Z-Way a trav\u00e9s de find.z-wave.me para esto)." } } } From ae9ab48d05caaaacbd2962755f4506a314241f8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 15:09:27 -1000 Subject: [PATCH 310/903] Downgrade bluetooth_le_tracker timeout message to debug (#76639) Fixes #76558 --- homeassistant/components/bluetooth_le_tracker/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index b416fe3e070..7e2c484e1a6 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -144,7 +144,7 @@ async def async_setup_scanner( # noqa: C901 bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) battery = ord(bat_char) except asyncio.TimeoutError: - _LOGGER.warning( + _LOGGER.debug( "Timeout when trying to get battery status for %s", service_info.name ) # Bleak currently has a few places where checking dbus attributes From 14e6c84104857e25b112c4a71e47d06111ce70f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 15:10:14 -1000 Subject: [PATCH 311/903] Bump yalexs-ble to 1.2.0 (#76631) Implements service caching Speeds up lock and unlock operations Changelog: https://github.com/bdraco/yalexs-ble/compare/v1.1.3...v1.2.0 --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 0bf861e4d44..78abb0fce30 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.1.3"], + "requirements": ["yalexs-ble==1.2.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 0e4e9b2c021..72fcba303be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.1.3 +yalexs-ble==1.2.0 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 762cdd16472..a8ad45e3251 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1695,7 +1695,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.1.3 +yalexs-ble==1.2.0 # homeassistant.components.august yalexs==1.2.1 From 4a5a0399846f18043a9fb75c685f4509a6c4adc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 15:10:59 -1000 Subject: [PATCH 312/903] Use async_timeout instead of asyncio.wait_for in switchbot (#76630) --- homeassistant/components/switchbot/coordinator.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index ad9aff8c53b..e4e7c25dc70 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -2,9 +2,11 @@ from __future__ import annotations import asyncio +import contextlib import logging from typing import TYPE_CHECKING, Any +import async_timeout import switchbot from homeassistant.components import bluetooth @@ -71,8 +73,8 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): async def async_wait_ready(self) -> bool: """Wait for the device to be ready.""" - try: - await asyncio.wait_for(self._ready_event.wait(), timeout=55) - except asyncio.TimeoutError: - return False - return True + with contextlib.suppress(asyncio.TimeoutError): + async with async_timeout.timeout(55): + await self._ready_event.wait() + return True + return False From 75ca80428d0eb7c86e61a5ea9fd6d5a476ea49b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 15:12:25 -1000 Subject: [PATCH 313/903] Add support for August locks to Yale Access Bluetooth (#76625) --- homeassistant/components/yalexs_ble/manifest.json | 5 ++++- homeassistant/components/yalexs_ble/util.py | 10 +++++++++- homeassistant/generated/supported_brands.py | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 78abb0fce30..c4f5b139c86 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -7,5 +7,8 @@ "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], - "iot_class": "local_push" + "iot_class": "local_push", + "supported_brands": { + "august_ble": "August Bluetooth" + } } diff --git a/homeassistant/components/yalexs_ble/util.py b/homeassistant/components/yalexs_ble/util.py index a1c6fdf7d32..465f4487c0b 100644 --- a/homeassistant/components/yalexs_ble/util.py +++ b/homeassistant/components/yalexs_ble/util.py @@ -1,6 +1,8 @@ """The yalexs_ble integration models.""" from __future__ import annotations +import platform + from yalexs_ble import local_name_is_unique from homeassistant.components.bluetooth import ( @@ -23,8 +25,14 @@ def bluetooth_callback_matcher( local_name: str, address: str ) -> BluetoothCallbackMatcher: """Return a BluetoothCallbackMatcher for the given local_name and address.""" - if local_name_is_unique(local_name): + # On MacOS, coreblueooth uses UUIDs for addresses so we must + # have a unique local_name to match since the system + # hides the address from us. + if local_name_is_unique(local_name) and platform.system() == "Darwin": return BluetoothCallbackMatcher({LOCAL_NAME: local_name}) + # On every other platform we actually get the mac address + # which is needed for the older August locks that use the + # older version of the underlying protocol. return BluetoothCallbackMatcher({ADDRESS: address}) diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 4e151f5578d..b4eaa9d8a06 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -12,5 +12,6 @@ HAS_SUPPORTED_BRANDS = ( "overkiz", "renault", "wemo", + "yalexs_ble", "zwave_js" ) From 3937ac2ca3cb408b67f78519fd6f93adc78de53e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 11 Aug 2022 21:13:27 -0400 Subject: [PATCH 314/903] Track code coverage for ZHA sensor entities (#76617) * Track code coverage for ZHA sensor entities * remove correct entry --- .coveragerc | 1 - homeassistant/components/zha/sensor.py | 6 +++--- tests/components/zha/test_sensor.py | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index e326f5f44c8..283adf0d3fc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1538,7 +1538,6 @@ omit = homeassistant/components/zha/core/registries.py homeassistant/components/zha/entity.py homeassistant/components/zha/light.py - homeassistant/components/zha/sensor.py homeassistant/components/zhong_hong/climate.py homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index f42e88041ef..e0f5bb958cd 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -183,7 +183,7 @@ class Sensor(ZhaEntity, SensorEntity): """Handle state update from channel.""" self.async_write_ha_state() - def formatter(self, value: int) -> int | float: + def formatter(self, value: int) -> int | float | None: """Numeric pass-through formatter.""" if self._decimals > 0: return round( @@ -236,11 +236,11 @@ class Battery(Sensor): return cls(unique_id, zha_device, channels, **kwargs) @staticmethod - def formatter(value: int) -> int: # pylint: disable=arguments-differ + def formatter(value: int) -> int | None: # pylint: disable=arguments-differ """Return the state of the entity.""" # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ if not isinstance(value, numbers.Number) or value == -1: - return value + return None value = round(value / 2) return value diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index d2fc7c3ca73..0698c07db9e 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -255,6 +255,17 @@ async def async_test_powerconfiguration(hass, cluster, entity_id): assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.0 +async def async_test_powerconfiguration2(hass, cluster, entity_id): + """Test powerconfiguration/battery sensor.""" + await send_attributes_report(hass, cluster, {33: -1}) + assert_state(hass, entity_id, STATE_UNKNOWN, "%") + assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.9 + assert hass.states.get(entity_id).attributes["battery_quantity"] == 3 + assert hass.states.get(entity_id).attributes["battery_size"] == "AAA" + await send_attributes_report(hass, cluster, {32: 20}) + assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.0 + + async def async_test_device_temperature(hass, cluster, entity_id): """Test temperature sensor.""" await send_attributes_report(hass, cluster, {0: 2900}) @@ -370,6 +381,18 @@ async def async_test_device_temperature(hass, cluster, entity_id): }, None, ), + ( + general.PowerConfiguration.cluster_id, + "battery", + async_test_powerconfiguration2, + 2, + { + "battery_size": 4, # AAA + "battery_voltage": 29, + "battery_quantity": 3, + }, + None, + ), ( general.DeviceTemperature.cluster_id, "device_temperature", From 828b97f46098c8d55ab46701b5c9d685ad7419ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 16:03:31 -1000 Subject: [PATCH 315/903] Bump pySwitchbot to 0.18.5 (#76640) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b413b44d605..dd04829de15 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.4"], + "requirements": ["PySwitchbot==0.18.5"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 72fcba303be..b4d5fe6374a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.4 +PySwitchbot==0.18.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8ad45e3251..8eb0066b32c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.4 +PySwitchbot==0.18.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 86b968bf79ffd0392b69fd937227f26444fe117d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 12 Aug 2022 10:50:27 +0300 Subject: [PATCH 316/903] Migrate Glances to new entity naming style (#76651) * Migrate Glances to new entity naming style * minor fixes --- homeassistant/components/glances/__init__.py | 2 +- .../components/glances/config_flow.py | 12 +- homeassistant/components/glances/const.py | 206 --------------- homeassistant/components/glances/sensor.py | 235 +++++++++++++++++- homeassistant/components/glances/strings.json | 2 - .../components/glances/translations/en.json | 4 +- tests/components/glances/test_config_flow.py | 11 +- 7 files changed, 240 insertions(+), 232 deletions(-) diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 2b52cdeef8b..0747db89cd2 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -129,7 +129,7 @@ class GlancesData: def get_api(hass, entry): """Return the api from glances_api.""" params = entry.copy() - params.pop(CONF_NAME) + params.pop(CONF_NAME, None) verify_ssl = params.pop(CONF_VERIFY_SSL, True) httpx_client = get_async_client(hass, verify_ssl=verify_ssl) return Glances(httpx_client=httpx_client, **params) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index a4a345116eb..16c33182c25 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -1,13 +1,14 @@ """Config flow for Glances.""" from __future__ import annotations +from typing import Any + import glances_api import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import ( CONF_HOST, - CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, @@ -18,10 +19,10 @@ from homeassistant.const import ( from homeassistant.core import callback from . import get_api +from ...data_entry_flow import FlowResult from .const import ( CONF_VERSION, DEFAULT_HOST, - DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_VERSION, @@ -31,7 +32,6 @@ from .const import ( DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_HOST, default=DEFAULT_HOST): str, vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, @@ -67,7 +67,7 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return GlancesOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -75,7 +75,7 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await validate_input(self.hass, user_input) return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input + title=user_input[CONF_HOST], data=user_input ) except CannotConnect: errors["base"] = "cannot_connect" @@ -94,7 +94,7 @@ class GlancesOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Glances options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: """Manage the Glances options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index d28c7395a43..92fe8ba91f6 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -1,21 +1,11 @@ """Constants for Glances component.""" -from __future__ import annotations -from dataclasses import dataclass import sys -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import DATA_GIBIBYTES, DATA_MEBIBYTES, PERCENTAGE, TEMP_CELSIUS - DOMAIN = "glances" CONF_VERSION = "version" DEFAULT_HOST = "localhost" -DEFAULT_NAME = "Glances" DEFAULT_PORT = 61208 DEFAULT_VERSION = 3 DEFAULT_SCAN_INTERVAL = 60 @@ -27,199 +17,3 @@ if sys.maxsize > 2**32: CPU_ICON = "mdi:cpu-64-bit" else: CPU_ICON = "mdi:cpu-32-bit" - - -@dataclass -class GlancesSensorEntityDescription(SensorEntityDescription): - """Describe Glances sensor entity.""" - - type: str | None = None - name_suffix: str | None = None - - -SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( - GlancesSensorEntityDescription( - key="disk_use_percent", - type="fs", - name_suffix="used percent", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="disk_use", - type="fs", - name_suffix="used", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="disk_free", - type="fs", - name_suffix="free", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="memory_use_percent", - type="mem", - name_suffix="RAM used percent", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="memory_use", - type="mem", - name_suffix="RAM used", - native_unit_of_measurement=DATA_MEBIBYTES, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="memory_free", - type="mem", - name_suffix="RAM free", - native_unit_of_measurement=DATA_MEBIBYTES, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="swap_use_percent", - type="memswap", - name_suffix="Swap used percent", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="swap_use", - type="memswap", - name_suffix="Swap used", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="swap_free", - type="memswap", - name_suffix="Swap free", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="processor_load", - type="load", - name_suffix="CPU load", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="process_running", - type="processcount", - name_suffix="Running", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="process_total", - type="processcount", - name_suffix="Total", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="process_thread", - type="processcount", - name_suffix="Thread", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="process_sleeping", - type="processcount", - name_suffix="Sleeping", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="cpu_use_percent", - type="cpu", - name_suffix="CPU used", - native_unit_of_measurement=PERCENTAGE, - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="temperature_core", - type="sensors", - name_suffix="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="temperature_hdd", - type="sensors", - name_suffix="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="fan_speed", - type="sensors", - name_suffix="Fan speed", - native_unit_of_measurement="RPM", - icon="mdi:fan", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="battery", - type="sensors", - name_suffix="Charge", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:battery", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="docker_active", - type="docker", - name_suffix="Containers active", - icon="mdi:docker", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="docker_cpu_use", - type="docker", - name_suffix="Containers CPU used", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:docker", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="docker_memory_use", - type="docker", - name_suffix="Containers RAM used", - native_unit_of_measurement=DATA_MEBIBYTES, - icon="mdi:docker", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="used", - type="raid", - name_suffix="Raid used", - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="available", - type="raid", - name_suffix="Raid available", - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), -) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index e37cfaca211..af6f307ef3a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,7 +1,25 @@ """Support gathering system information of hosts which are running glances.""" -from homeassistant.components.sensor import SensorEntity +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, Platform +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + DATA_GIBIBYTES, + DATA_MEBIBYTES, + PERCENTAGE, + STATE_UNAVAILABLE, + TEMP_CELSIUS, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -9,7 +27,203 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import GlancesData -from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES, GlancesSensorEntityDescription +from .const import CPU_ICON, DATA_UPDATED, DOMAIN + + +@dataclass +class GlancesSensorEntityDescription(SensorEntityDescription): + """Describe Glances sensor entity.""" + + type: str | None = None + name_suffix: str | None = None + + +SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( + GlancesSensorEntityDescription( + key="disk_use_percent", + type="fs", + name_suffix="used percent", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="disk_use", + type="fs", + name_suffix="used", + native_unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="disk_free", + type="fs", + name_suffix="free", + native_unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="memory_use_percent", + type="mem", + name_suffix="RAM used percent", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="memory_use", + type="mem", + name_suffix="RAM used", + native_unit_of_measurement=DATA_MEBIBYTES, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="memory_free", + type="mem", + name_suffix="RAM free", + native_unit_of_measurement=DATA_MEBIBYTES, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="swap_use_percent", + type="memswap", + name_suffix="Swap used percent", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="swap_use", + type="memswap", + name_suffix="Swap used", + native_unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="swap_free", + type="memswap", + name_suffix="Swap free", + native_unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="processor_load", + type="load", + name_suffix="CPU load", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="process_running", + type="processcount", + name_suffix="Running", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="process_total", + type="processcount", + name_suffix="Total", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="process_thread", + type="processcount", + name_suffix="Thread", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="process_sleeping", + type="processcount", + name_suffix="Sleeping", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="cpu_use_percent", + type="cpu", + name_suffix="CPU used", + native_unit_of_measurement=PERCENTAGE, + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="temperature_core", + type="sensors", + name_suffix="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="temperature_hdd", + type="sensors", + name_suffix="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="fan_speed", + type="sensors", + name_suffix="Fan speed", + native_unit_of_measurement="RPM", + icon="mdi:fan", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="battery", + type="sensors", + name_suffix="Charge", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:battery", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="docker_active", + type="docker", + name_suffix="Containers active", + icon="mdi:docker", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="docker_cpu_use", + type="docker", + name_suffix="Containers CPU used", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:docker", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="docker_memory_use", + type="docker", + name_suffix="Containers RAM used", + native_unit_of_measurement=DATA_MEBIBYTES, + icon="mdi:docker", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="used", + type="raid", + name_suffix="Raid used", + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="available", + type="raid", + name_suffix="Raid available", + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), +) async def async_setup_entry( @@ -20,7 +234,7 @@ async def async_setup_entry( """Set up the Glances sensors.""" client: GlancesData = hass.data[DOMAIN][config_entry.entry_id] - name = config_entry.data[CONF_NAME] + name = config_entry.data.get(CONF_NAME) dev = [] @callback @@ -102,11 +316,13 @@ class GlancesSensor(SensorEntity): """Implementation of a Glances sensor.""" entity_description: GlancesSensorEntityDescription + _attr_has_entity_name = True + _attr_should_poll = False def __init__( self, glances_data: GlancesData, - name: str, + name: str | None, sensor_name_prefix: str, description: GlancesSensorEntityDescription, ) -> None: @@ -117,11 +333,11 @@ class GlancesSensor(SensorEntity): self.unsub_update = None self.entity_description = description - self._attr_name = f"{name} {sensor_name_prefix} {description.name_suffix}" + self._attr_name = f"{sensor_name_prefix} {description.name_suffix}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, glances_data.config_entry.entry_id)}, manufacturer="Glances", - name=name, + name=name or glances_data.config_entry.data[CONF_HOST], ) self._attr_unique_id = f"{self.glances_data.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" @@ -135,11 +351,6 @@ class GlancesSensor(SensorEntity): """Return the state of the resources.""" return self._state - @property - def should_poll(self): - """Return the polling requirement for this sensor.""" - return False - async def async_added_to_hass(self): """Handle entity which will be added.""" self.unsub_update = async_dispatcher_connect( diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index 5d96b1ae57e..11c9792f364 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -2,9 +2,7 @@ "config": { "step": { "user": { - "title": "Setup Glances", "data": { - "name": "[%key:common::config_flow::data::name%]", "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", diff --git a/homeassistant/components/glances/translations/en.json b/homeassistant/components/glances/translations/en.json index 87c53c3cf48..aa7005bddea 100644 --- a/homeassistant/components/glances/translations/en.json +++ b/homeassistant/components/glances/translations/en.json @@ -11,15 +11,13 @@ "user": { "data": { "host": "Host", - "name": "Name", "password": "Password", "port": "Port", "ssl": "Uses an SSL certificate", "username": "Username", "verify_ssl": "Verify SSL certificate", "version": "Glances API Version (2 or 3)" - }, - "title": "Setup Glances" + } } } }, diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index d996c3af533..8ee669ae84e 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from glances_api import exceptions +import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import glances @@ -18,7 +19,6 @@ VERSION = 3 SCAN_INTERVAL = 10 DEMO_USER_INPUT = { - "name": NAME, "host": HOST, "username": USERNAME, "password": PASSWORD, @@ -29,6 +29,13 @@ DEMO_USER_INPUT = { } +@pytest.fixture(autouse=True) +def glances_setup_fixture(): + """Mock transmission entry setup.""" + with patch("homeassistant.components.glances.async_setup_entry", return_value=True): + yield + + async def test_form(hass): """Test config entry configured successfully.""" @@ -45,7 +52,7 @@ async def test_form(hass): ) assert result["type"] == "create_entry" - assert result["title"] == NAME + assert result["title"] == HOST assert result["data"] == DEMO_USER_INPUT From 46369b274b0309518e2dd555bf075379e97cd633 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 12 Aug 2022 09:25:24 +0100 Subject: [PATCH 317/903] Initial binary_sensor support for Xiaomi BLE (#76635) --- .../components/xiaomi_ble/__init__.py | 2 +- .../components/xiaomi_ble/binary_sensor.py | 99 +++++++++++++++++++ homeassistant/components/xiaomi_ble/device.py | 31 ++++++ .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 37 ++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../xiaomi_ble/test_binary_sensor.py | 54 ++++++++++ 8 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/xiaomi_ble/binary_sensor.py create mode 100644 homeassistant/components/xiaomi_ble/device.py create mode 100644 tests/components/xiaomi_ble/test_binary_sensor.py diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 031490d6d68..626b7325014 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py new file mode 100644 index 00000000000..448b1f176e5 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -0,0 +1,99 @@ +"""Support for Xiaomi binary sensors.""" +from __future__ import annotations + +from typing import Optional + +from xiaomi_ble.parser import ( + BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, + SensorUpdate, +) + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +BINARY_SENSOR_DESCRIPTIONS = { + XiaomiBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + XiaomiBinarySensorDeviceClass.LIGHT: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.LIGHT, + device_class=BinarySensorDeviceClass.LIGHT, + ), + XiaomiBinarySensorDeviceClass.SMOKE: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[ + description.device_class + ] + for device_key, description in sensor_update.binary_entity_descriptions.items() + if description.device_class + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Xiaomi BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + XiaomiBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class XiaomiBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + BinarySensorEntity, +): + """Representation of a Xiaomi binary sensor.""" + + @property + def is_on(self) -> bool | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/xiaomi_ble/device.py b/homeassistant/components/xiaomi_ble/device.py new file mode 100644 index 00000000000..4ddfc31ae51 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/device.py @@ -0,0 +1,31 @@ +"""Support for Xioami BLE devices.""" +from __future__ import annotations + +from xiaomi_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 8c1d47ee423..e93dace95c6 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.8.2"], + "requirements": ["xiaomi-ble==0.9.0"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index d22ed46dd83..1aa1b7f8f72 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -3,13 +3,12 @@ from __future__ import annotations from typing import Optional, Union -from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from xiaomi_ble import DeviceClass, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothEntityKey, PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) @@ -20,9 +19,6 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONDUCTIVITY, ELECTRIC_POTENTIAL_VOLT, @@ -33,10 +29,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass SENSOR_DESCRIPTIONS = { (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( @@ -108,49 +104,28 @@ SENSOR_DESCRIPTIONS = { } -def _device_key_to_bluetooth_entity_key( - device_key: DeviceKey, -) -> PassiveBluetoothEntityKey: - """Convert a device key to an entity key.""" - return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def _sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: _sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ - _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() if description.native_unit_of_measurement }, entity_data={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value for device_key, sensor_values in sensor_update.entity_values.items() }, entity_names={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + device_key_to_bluetooth_entity_key(device_key): sensor_values.name for device_key, sensor_values in sensor_update.entity_values.items() }, ) diff --git a/requirements_all.txt b/requirements_all.txt index b4d5fe6374a..a356cec0af6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.2 +xiaomi-ble==0.9.0 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eb0066b32c..7cbef52b7ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.2 +xiaomi-ble==0.9.0 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py new file mode 100644 index 00000000000..03e3d52d783 --- /dev/null +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -0,0 +1,54 @@ +"""Test Xiaomi binary sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME + +from . import make_advertisement + +from tests.common import MockConfigEntry + + +async def test_smoke_sensor(hass): + """Test setting up a smoke sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + data={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + smoke_sensor = hass.states.get("binary_sensor.thermometer_e39cbc_smoke") + smoke_sensor_attribtes = smoke_sensor.attributes + assert smoke_sensor.state == "on" + assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer E39CBC Smoke" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From cafc6ca89511bd0a576b618216415f8068c1133e Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 12 Aug 2022 12:42:42 +0300 Subject: [PATCH 318/903] Fix typing in `glances` config flow (#76654) fix typing in config_flow --- homeassistant/components/glances/config_flow.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index 16c33182c25..568586f177b 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -17,9 +17,9 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from . import get_api -from ...data_entry_flow import FlowResult from .const import ( CONF_VERSION, DEFAULT_HOST, @@ -67,7 +67,9 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return GlancesOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -94,7 +96,9 @@ class GlancesOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Glances options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Glances options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) From 795e7f570791c329383ce5a4e752ba9d7545da2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 02:58:48 -1000 Subject: [PATCH 319/903] Bump pySwitchbot to 0.18.6 to fix disconnect race (#76656) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index dd04829de15..e26100108c9 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.5"], + "requirements": ["PySwitchbot==0.18.6"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a356cec0af6..0332c7c06e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.5 +PySwitchbot==0.18.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7cbef52b7ed..a20e82bffce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.5 +PySwitchbot==0.18.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 563e899d2e2bc8c97444396a55bd7f7329e480bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 02:58:59 -1000 Subject: [PATCH 320/903] Bump yalexs_ble to 1.3.1 to fix disconnect race (#76657) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index c4f5b139c86..aa3cdcd592b 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.2.0"], + "requirements": ["yalexs-ble==1.3.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 0332c7c06e9..35a231a8cd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.2.0 +yalexs-ble==1.3.1 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a20e82bffce..011c93291e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1695,7 +1695,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.2.0 +yalexs-ble==1.3.1 # homeassistant.components.august yalexs==1.2.1 From a86397cc10212a9bf0acac442736909076acf8e3 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:19:16 +0200 Subject: [PATCH 321/903] Fix non-awaited coroutine in BMW notify (#76664) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/notify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 14f6c94dff6..b48441ae5fe 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -56,7 +56,7 @@ class BMWNotificationService(BaseNotificationService): """Set up the notification service.""" self.targets: dict[str, MyBMWVehicle] = targets - def send_message(self, message: str = "", **kwargs: Any) -> None: + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message or POI to the car.""" for vehicle in kwargs[ATTR_TARGET]: vehicle = cast(MyBMWVehicle, vehicle) @@ -81,6 +81,6 @@ class BMWNotificationService(BaseNotificationService): } ) - vehicle.remote_services.trigger_send_poi(location_dict) + await vehicle.remote_services.trigger_send_poi(location_dict) else: raise ValueError(f"'data.{ATTR_LOCATION}' is required.") From eeb9a9f0584c99eb74cc34dd7fe6fd666cc7f16f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 03:25:23 -1000 Subject: [PATCH 322/903] Make sure all discovery flows are using the helper (#76641) --- .../components/bluetooth/__init__.py | 11 ++++---- .../components/discovery/__init__.py | 4 ++- homeassistant/components/elkm1/discovery.py | 13 +++++----- homeassistant/components/ezviz/camera.py | 23 +++++++++-------- .../components/flux_led/discovery.py | 13 +++++----- homeassistant/components/hassio/discovery.py | 4 ++- homeassistant/components/lifx/discovery.py | 12 ++++----- homeassistant/components/mqtt/__init__.py | 18 +++++++------ homeassistant/components/plex/config_flow.py | 4 ++- homeassistant/components/senseme/discovery.py | 12 ++++----- .../components/squeezebox/media_player.py | 25 +++++++++++-------- .../components/steamist/discovery.py | 23 ++++++++--------- homeassistant/components/tplink/__init__.py | 21 ++++++++-------- .../components/unifiprotect/discovery.py | 12 ++++----- homeassistant/components/wiz/discovery.py | 12 ++++----- .../components/yalexs_ble/__init__.py | 12 ++++----- homeassistant/components/yeelight/scanner.py | 22 ++++++++-------- tests/components/hassio/test_discovery.py | 7 ++++-- tests/components/plex/test_config_flow.py | 1 + 19 files changed, 132 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 9492642e0e0..a5204d50b68 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -249,12 +249,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) elif await _async_has_bluetooth_adapter(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, ) return True diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index cc104cc2110..75016c28048 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -13,6 +13,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_discover, async_load_platform from homeassistant.helpers.event import async_track_point_in_utc_time @@ -173,7 +174,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: already_discovered.add(discovery_hash) if service in CONFIG_ENTRY_HANDLERS: - await hass.config_entries.flow.async_init( + discovery_flow.async_create_flow( + hass, CONFIG_ENTRY_HANDLERS[service], context={"source": config_entries.SOURCE_DISCOVERY}, data=info, diff --git a/homeassistant/components/elkm1/discovery.py b/homeassistant/components/elkm1/discovery.py index 326698c3686..50db2840753 100644 --- a/homeassistant/components/elkm1/discovery.py +++ b/homeassistant/components/elkm1/discovery.py @@ -10,7 +10,7 @@ from elkm1_lib.discovery import AIOELKDiscovery, ElkSystem from homeassistant import config_entries from homeassistant.components import network from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery_flow from .const import DISCOVER_SCAN_TIMEOUT, DOMAIN @@ -87,10 +87,9 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for device in discovered_devices: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data=asdict(device), - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=asdict(device), ) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 91bab5f83af..307e1fac185 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -17,7 +17,11 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import ( + config_validation as cv, + discovery_flow, + entity_platform, +) from .const import ( ATTR_DIRECTION, @@ -93,15 +97,14 @@ async def async_setup_entry( else: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_INTEGRATION_DISCOVERY}, - data={ - ATTR_SERIAL: camera, - CONF_IP_ADDRESS: value["local_ip"], - }, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data={ + ATTR_SERIAL: camera, + CONF_IP_ADDRESS: value["local_ip"], + }, ) _LOGGER.warning( diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index 67dbbc74e2e..ef0c131993e 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -27,7 +27,7 @@ from homeassistant.components import network from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.util.network import is_ip_address from .const import ( @@ -221,10 +221,9 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for device in discovered_devices: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={**device}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={**device}, ) diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 587457f2ca2..e8cbbfc6bf5 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -14,6 +14,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ATTR_NAME, ATTR_SERVICE, EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo +from homeassistant.helpers import discovery_flow from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID from .handler import HassioAPIError @@ -99,7 +100,8 @@ class HassIODiscovery(HomeAssistantView): config_data[ATTR_ADDON] = addon_info[ATTR_NAME] # Use config flow - await self.hass.config_entries.flow.async_init( + discovery_flow.async_create_flow( + self.hass, service, context={"source": config_entries.SOURCE_HASSIO}, data=HassioServiceInfo(config=config_data), diff --git a/homeassistant/components/lifx/discovery.py b/homeassistant/components/lifx/discovery.py index 1c6e9ab3060..6e1507c92ca 100644 --- a/homeassistant/components/lifx/discovery.py +++ b/homeassistant/components/lifx/discovery.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components import network from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow from .const import CONF_SERIAL, DOMAIN @@ -38,12 +39,11 @@ async def async_discover_devices(hass: HomeAssistant) -> Iterable[Light]: @callback def async_init_discovery_flow(hass: HomeAssistant, host: str, serial: str) -> None: """Start discovery of devices.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={CONF_HOST: host, CONF_SERIAL: serial}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_HOST: host, CONF_SERIAL: serial}, ) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 2bea1a593d1..1121377a30e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -22,7 +22,12 @@ from homeassistant.const import ( ) from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import TemplateError, Unauthorized -from homeassistant.helpers import config_validation as cv, event, template +from homeassistant.helpers import ( + config_validation as cv, + discovery_flow, + event, + template, +) from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import ( @@ -178,12 +183,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Create an import flow if the user has yaml configured entities etc. # but no broker configuration. Note: The intention is not for this to # import broker configuration from YAML because that has been deprecated. - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, ) hass.data[DATA_MQTT_RELOAD_NEEDED] = True elif mqtt_entry_status is False: diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 42d227154a6..e79b7e7ee04 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -29,6 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -76,7 +77,8 @@ async def async_discover(hass): gdm = GDM() await hass.async_add_executor_job(gdm.scan) for server_data in gdm.entries: - await hass.config_entries.flow.async_init( + discovery_flow.async_create_flow( + hass, DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=server_data, diff --git a/homeassistant/components/senseme/discovery.py b/homeassistant/components/senseme/discovery.py index 624b18a8761..d3924ef16c0 100644 --- a/homeassistant/components/senseme/discovery.py +++ b/homeassistant/components/senseme/discovery.py @@ -8,6 +8,7 @@ from aiosenseme import SensemeDevice, SensemeDiscovery from homeassistant import config_entries from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow from .const import DISCOVERY, DOMAIN @@ -55,10 +56,9 @@ def async_trigger_discovery( """Trigger config flows for discovered devices.""" for device in discovered_devices: if device.uuid: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={CONF_ID: device.uuid}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ID: device.uuid}, ) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index cd628a639c5..260228e4fdf 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -39,7 +39,11 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import ( + config_validation as cv, + discovery_flow, + entity_platform, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import ( @@ -99,16 +103,15 @@ async def start_server_discovery(hass): """Start a server discovery task.""" def _discovered_server(server): - asyncio.create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: server.host, - CONF_PORT: int(server.port), - "uuid": server.uuid, - }, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: server.host, + CONF_PORT: int(server.port), + "uuid": server.uuid, + }, ) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/steamist/discovery.py b/homeassistant/components/steamist/discovery.py index 7600503658f..cff97692979 100644 --- a/homeassistant/components/steamist/discovery.py +++ b/homeassistant/components/steamist/discovery.py @@ -11,7 +11,7 @@ from homeassistant import config_entries from homeassistant.components import network from homeassistant.const import CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.util.network import is_ip_address from .const import DISCOVER_SCAN_TIMEOUT, DISCOVERY, DOMAIN @@ -122,15 +122,14 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for device in discovered_devices: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - "ipaddress": device.ipaddress, - "name": device.name, - "mac": device.mac, - "hostname": device.hostname, - }, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "ipaddress": device.ipaddress, + "name": device.name, + "mac": device.mac, + "hostname": device.hostname, + }, ) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 50c18000baa..9606dc29a44 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -36,16 +36,15 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for formatted_mac, device in discovered_devices.items(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_NAME: device.alias, - CONF_HOST: device.host, - CONF_MAC: formatted_mac, - }, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_NAME: device.alias, + CONF_HOST: device.host, + CONF_MAC: formatted_mac, + }, ) diff --git a/homeassistant/components/unifiprotect/discovery.py b/homeassistant/components/unifiprotect/discovery.py index 537e2fa1121..d58cad4e40a 100644 --- a/homeassistant/components/unifiprotect/discovery.py +++ b/homeassistant/components/unifiprotect/discovery.py @@ -11,6 +11,7 @@ from unifi_discovery import AIOUnifiScanner, UnifiDevice, UnifiService from homeassistant import config_entries from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from .const import DOMAIN @@ -54,10 +55,9 @@ def async_trigger_discovery( """Trigger config flows for discovered devices.""" for device in discovered_devices: if device.services[UnifiService.Protect] and device.hw_addr: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data=asdict(device), - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=asdict(device), ) diff --git a/homeassistant/components/wiz/discovery.py b/homeassistant/components/wiz/discovery.py index 0b7015643ff..0f4be1d873e 100644 --- a/homeassistant/components/wiz/discovery.py +++ b/homeassistant/components/wiz/discovery.py @@ -10,6 +10,7 @@ from pywizlight.discovery import DiscoveredBulb, find_wizlights from homeassistant import config_entries from homeassistant.components import network from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow from .const import DOMAIN @@ -46,10 +47,9 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for device in discovered_devices: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data=asdict(device), - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=asdict(device), ) diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 3b9481b6982..6073bf7a032 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEnt from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery_flow from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN from .models import YaleXSBLEData @@ -33,12 +34,11 @@ class YaleXSBLEDiscovery(TypedDict): @callback def async_discovery(hass: HomeAssistant, discovery: YaleXSBLEDiscovery) -> None: """Update keys for the yalexs-ble integration if available.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - "yalexs_ble", - context={"source": SOURCE_INTEGRATION_DISCOVERY}, - data=discovery, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data=discovery, ) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 088071244b3..0b33512e315 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -15,6 +15,7 @@ from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries from homeassistant.components import network, ssdp from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_call_later, async_track_time_interval from .const import ( @@ -161,17 +162,16 @@ class YeelightScanner: def _async_discovered_by_ssdp(self, response: CaseInsensitiveDict) -> None: @callback def _async_start_flow(*_) -> None: - asyncio.create_task( - self._hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="", - ssdp_st=SSDP_ST, - ssdp_headers=response, - upnp={}, - ), - ) + discovery_flow.async_create_flow( + self._hass, + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn="", + ssdp_st=SSDP_ST, + ssdp_headers=response, + upnp={}, + ), ) # Delay starting the flow in case the discovery is the result diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 71f5f8acf96..30013c34f21 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED from homeassistant.setup import async_setup_component @@ -45,7 +45,8 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): ) as mock_mqtt: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() assert aioclient_mock.call_count == 2 assert mock_mqtt.called mock_mqtt.assert_called_with( @@ -159,6 +160,8 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): json={"addon": "mosquitto", "service": "mqtt", "uuid": "testuuid"}, ) await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() assert resp.status == HTTPStatus.OK assert aioclient_mock.call_count == 2 diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index f02abd834d7..fb5a0f06724 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -703,6 +703,7 @@ async def test_integration_discovery(hass): with patch("homeassistant.components.plex.config_flow.GDM", return_value=mock_gdm): await config_flow.async_discover(hass) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() From 0293db343fd364ed64d7b6c31d1683f7bdcc8087 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:45:28 +0200 Subject: [PATCH 323/903] Allow only known attrs for BMW binary sensors (#76663) Co-authored-by: Paulus Schoutsen Co-authored-by: rikroe --- .../bmw_connected_drive/binary_sensor.py | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 94d8902fe23..96e8a1fc0e4 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -28,11 +28,32 @@ from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +ALLOWED_CONDITION_BASED_SERVICE_KEYS = { + "BRAKE_FLUID", + "ENGINE_OIL", + "OIL", + "TIRE_WEAR_FRONT", + "TIRE_WEAR_REAR", + "VEHICLE_CHECK", + "VEHICLE_TUV", +} + +ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {"ENGINE_OIL", "TIRE_PRESSURE"} + + def _condition_based_services( vehicle: MyBMWVehicle, unit_system: UnitSystem ) -> dict[str, Any]: extra_attributes = {} for report in vehicle.condition_based_services.messages: + if report.service_type not in ALLOWED_CONDITION_BASED_SERVICE_KEYS: + _LOGGER.warning( + "'%s' not an allowed condition based service (%s)", + report.service_type, + report, + ) + continue + extra_attributes.update(_format_cbs_report(report, unit_system)) return extra_attributes @@ -40,7 +61,17 @@ def _condition_based_services( def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]: extra_attributes: dict[str, Any] = {} for message in vehicle.check_control_messages.messages: - extra_attributes.update({message.description_short: message.state.value}) + if message.description_short not in ALLOWED_CHECK_CONTROL_MESSAGE_KEYS: + _LOGGER.warning( + "'%s' not an allowed check control message (%s)", + message.description_short, + message, + ) + continue + + extra_attributes.update( + {message.description_short.lower(): message.state.value} + ) return extra_attributes @@ -48,10 +79,10 @@ def _format_cbs_report( report: ConditionBasedService, unit_system: UnitSystem ) -> dict[str, Any]: result: dict[str, Any] = {} - service_type = report.service_type.lower().replace("_", " ") - result[f"{service_type} status"] = report.state.value + service_type = report.service_type.lower() + result[service_type] = report.state.value if report.due_date is not None: - result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") + result[f"{service_type}_date"] = report.due_date.strftime("%Y-%m-%d") if report.due_distance.value and report.due_distance.unit: distance = round( unit_system.length( @@ -59,7 +90,7 @@ def _format_cbs_report( UNIT_MAP.get(report.due_distance.unit, report.due_distance.unit), ) ) - result[f"{service_type} distance"] = f"{distance} {unit_system.length_unit}" + result[f"{service_type}_distance"] = f"{distance} {unit_system.length_unit}" return result From e033c8b38037438a98f4a6cbc07fb6efbccbb0ba Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 12 Aug 2022 13:14:41 -0400 Subject: [PATCH 324/903] Migrate Abode to new entity naming style (#76673) --- homeassistant/components/abode/__init__.py | 2 +- homeassistant/components/abode/sensor.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index bd66aa66c9c..092f9d36071 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -248,6 +248,7 @@ class AbodeEntity(entity.Entity): """Representation of an Abode entity.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__(self, data: AbodeSystem) -> None: """Initialize Abode entity.""" @@ -283,7 +284,6 @@ class AbodeDevice(AbodeEntity): """Initialize Abode device.""" super().__init__(data) self._device = device - self._attr_name = device.name self._attr_unique_id = device.device_uuid async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index da854153293..7fd3a0280a1 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -64,7 +64,6 @@ class AbodeSensor(AbodeDevice, SensorEntity): """Initialize a sensor for an Abode device.""" super().__init__(data, device) self.entity_description = description - self._attr_name = f"{device.name} {description.name}" self._attr_unique_id = f"{device.device_uuid}-{description.key}" if description.key == CONST.TEMP_STATUS_KEY: self._attr_native_unit_of_measurement = device.temp_unit From 2e40fc72883ec366fdb17504e486aef8ea2ff563 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 13 Aug 2022 01:39:12 +0200 Subject: [PATCH 325/903] Bump motionblinds to 0.6.12 (#76665) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 3499932c1d8..90ad330bd40 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.11"], + "requirements": ["motionblinds==0.6.12"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index 35a231a8cd4..9ec008d1a45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1055,7 +1055,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.11 +motionblinds==0.6.12 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 011c93291e8..09d654157e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -751,7 +751,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.11 +motionblinds==0.6.12 # homeassistant.components.motioneye motioneye-client==0.3.12 From 2f29f38ec6f5620412442dabdf4ce7fdfbf498bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Aug 2022 19:42:41 -0400 Subject: [PATCH 326/903] Streamline discovery flow callback (#76666) --- homeassistant/helpers/discovery_flow.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/discovery_flow.py b/homeassistant/helpers/discovery_flow.py index 5bb0da2dc05..863fb58625c 100644 --- a/homeassistant/helpers/discovery_flow.py +++ b/homeassistant/helpers/discovery_flow.py @@ -60,17 +60,14 @@ class FlowDispatcher: @callback def async_setup(self) -> None: """Set up the flow disptcher.""" - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self.async_start) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self._async_start) - @callback - def async_start(self, event: Event) -> None: + async def _async_start(self, event: Event) -> None: """Start processing pending flows.""" self.hass.data.pop(DISCOVERY_FLOW_DISPATCHER) - self.hass.async_create_task(self._async_process_pending_flows()) - async def _async_process_pending_flows(self) -> None: - """Process any pending discovery flows.""" init_coros = [_async_init_flow(self.hass, *flow) for flow in self.pending_flows] + await gather_with_concurrency( FLOW_INIT_LIMIT, *[init_coro for init_coro in init_coros if init_coro is not None], From 6e03b12a93a9287b60bb8a52c0c590226637e3d1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 13 Aug 2022 00:25:00 +0000 Subject: [PATCH 327/903] [ci skip] Translation update --- .../accuweather/translations/es.json | 2 +- .../components/airly/translations/es.json | 2 +- .../components/airnow/translations/es.json | 2 +- .../components/airvisual/translations/es.json | 2 +- .../components/almond/translations/es.json | 2 +- .../ambiclimate/translations/es.json | 4 +-- .../components/apple_tv/translations/es.json | 6 ++-- .../components/arcam_fmj/translations/es.json | 2 +- .../components/asuswrt/translations/es.json | 8 ++--- .../components/atag/translations/es.json | 2 +- .../components/auth/translations/es.json | 6 ++-- .../automation/translations/es.json | 2 +- .../components/awair/translations/ca.json | 3 ++ .../components/awair/translations/el.json | 16 +++++++++- .../components/awair/translations/es.json | 2 +- .../components/awair/translations/et.json | 2 +- .../components/awair/translations/fr.json | 31 +++++++++++++++++-- .../components/awair/translations/hu.json | 31 +++++++++++++++++-- .../components/awair/translations/pt-BR.json | 31 +++++++++++++++++-- .../awair/translations/zh-Hant.json | 31 +++++++++++++++++-- .../components/axis/translations/es.json | 6 ++-- .../azure_devops/translations/es.json | 2 +- .../components/blink/translations/es.json | 4 +-- .../components/braviatv/translations/es.json | 2 +- .../components/broadlink/translations/es.json | 8 ++--- .../components/bsblan/translations/es.json | 2 +- .../cloudflare/translations/es.json | 2 +- .../components/daikin/translations/es.json | 2 +- .../components/deconz/translations/es.json | 8 ++--- .../components/denonavr/translations/es.json | 2 +- .../dialogflow/translations/es.json | 2 +- .../components/directv/translations/es.json | 2 +- .../components/doorbird/translations/es.json | 2 +- .../components/dsmr/translations/es.json | 2 -- .../components/dunehd/translations/es.json | 2 +- .../components/ecobee/translations/es.json | 2 +- .../components/elgato/translations/es.json | 2 +- .../components/elkm1/translations/es.json | 2 +- .../emulated_roku/translations/es.json | 4 +-- .../components/esphome/translations/es.json | 6 ++-- .../fireservicerota/translations/es.json | 2 +- .../components/flume/translations/es.json | 2 +- .../flunearyou/translations/es.json | 2 +- .../forked_daapd/translations/es.json | 2 +- .../components/freebox/translations/es.json | 4 +-- .../components/fritz/translations/es.json | 2 +- .../components/fritzbox/translations/es.json | 6 ++-- .../components/geofency/translations/es.json | 2 +- .../components/glances/translations/en.json | 4 ++- .../components/goalzero/translations/es.json | 2 +- .../components/gogogate2/translations/es.json | 2 +- .../components/gpslogger/translations/es.json | 2 +- .../components/guardian/translations/el.json | 13 ++++++++ .../components/guardian/translations/es.json | 2 +- .../components/hangouts/translations/es.json | 10 +++--- .../components/heos/translations/es.json | 2 +- .../components/homekit/translations/es.json | 12 +++---- .../homekit_controller/translations/es.json | 18 +++++------ .../homematicip_cloud/translations/es.json | 14 ++++----- .../huawei_lte/translations/es.json | 2 +- .../components/hue/translations/es.json | 6 ++-- .../translations/es.json | 2 +- .../components/iaqualink/translations/es.json | 2 +- .../components/icloud/translations/es.json | 2 +- .../components/ifttt/translations/es.json | 4 +-- .../components/insteon/translations/es.json | 4 +-- .../components/ipp/translations/es.json | 4 +-- .../components/iqvia/translations/es.json | 2 +- .../components/isy994/translations/es.json | 4 +-- .../components/kodi/translations/es.json | 4 +-- .../components/konnected/translations/es.json | 26 ++++++++-------- .../components/locative/translations/es.json | 4 +-- .../logi_circle/translations/es.json | 8 ++--- .../lutron_caseta/translations/es.json | 2 +- .../components/mailgun/translations/es.json | 2 +- .../components/melcloud/translations/es.json | 2 +- .../meteo_france/translations/es.json | 2 +- .../components/mikrotik/translations/es.json | 2 +- .../minecraft_server/translations/es.json | 2 +- .../mobile_app/translations/es.json | 2 +- .../motion_blinds/translations/es.json | 2 +- .../components/mqtt/translations/es.json | 8 ++--- .../components/myq/translations/es.json | 4 +-- .../components/nanoleaf/translations/es.json | 2 +- .../components/nest/translations/es.json | 4 +-- .../components/netatmo/translations/es.json | 8 ++--- .../components/nexia/translations/es.json | 4 +-- .../nightscout/translations/es.json | 2 +- .../components/nws/translations/es.json | 2 +- .../components/onewire/translations/es.json | 2 +- .../components/onvif/translations/es.json | 4 +-- .../components/owntracks/translations/es.json | 2 +- .../components/pi_hole/translations/es.json | 4 +-- .../components/plaato/translations/es.json | 2 +- .../components/plant/translations/es.json | 2 +- .../components/plex/translations/es.json | 4 +-- .../components/plugwise/translations/es.json | 6 ++-- .../components/point/translations/es.json | 4 +-- .../components/profiler/translations/es.json | 2 +- .../components/ps4/translations/es.json | 18 +++++------ .../components/rachio/translations/es.json | 2 +- .../components/renault/translations/es.json | 2 +- .../components/rfxtrx/translations/es.json | 2 +- .../components/risco/translations/es.json | 2 +- .../components/roomba/translations/es.json | 10 +++--- .../components/samsungtv/translations/es.json | 2 +- .../components/schedule/translations/el.json | 3 ++ .../components/schedule/translations/fr.json | 9 ++++++ .../components/schedule/translations/hu.json | 9 ++++++ .../schedule/translations/pt-BR.json | 9 ++++++ .../schedule/translations/zh-Hant.json | 9 ++++++ .../components/sentry/translations/es.json | 2 +- .../components/shelly/translations/es.json | 6 ++-- .../simplisafe/translations/es.json | 2 +- .../components/smappee/translations/es.json | 2 +- .../smartthings/translations/es.json | 4 +-- .../components/soma/translations/es.json | 4 +-- .../somfy_mylink/translations/es.json | 4 +-- .../components/sonarr/translations/es.json | 4 +-- .../components/songpal/translations/es.json | 4 +-- .../speedtestdotnet/translations/es.json | 2 +- .../components/spotify/translations/es.json | 4 +-- .../components/sql/translations/es.json | 4 +-- .../components/starline/translations/es.json | 2 +- .../components/switchbot/translations/el.json | 6 ++++ .../synology_dsm/translations/es.json | 6 ++-- .../components/tado/translations/es.json | 4 +-- .../tellduslive/translations/es.json | 7 ++--- .../components/tibber/translations/es.json | 2 +- .../components/tradfri/translations/es.json | 8 ++--- .../transmission/translations/es.json | 4 +-- .../components/tuya/translations/es.json | 4 +-- .../components/twilio/translations/es.json | 4 +-- .../components/unifi/translations/es.json | 16 +++++----- .../components/upnp/translations/es.json | 6 +--- .../components/vera/translations/es.json | 2 +- .../components/vizio/translations/es.json | 10 +++--- .../components/withings/translations/es.json | 4 +-- .../components/wled/translations/es.json | 2 +- .../components/wolflink/translations/es.json | 4 +-- .../wolflink/translations/sensor.es.json | 6 ++-- .../xiaomi_aqara/translations/es.json | 6 ++-- .../yalexs_ble/translations/el.json | 24 ++++++++++++++ .../components/yeelight/translations/es.json | 2 +- 144 files changed, 475 insertions(+), 283 deletions(-) create mode 100644 homeassistant/components/schedule/translations/el.json create mode 100644 homeassistant/components/schedule/translations/fr.json create mode 100644 homeassistant/components/schedule/translations/hu.json create mode 100644 homeassistant/components/schedule/translations/pt-BR.json create mode 100644 homeassistant/components/schedule/translations/zh-Hant.json create mode 100644 homeassistant/components/yalexs_ble/translations/el.json diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index 36ccc3f0cca..df5b9c21494 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -28,7 +28,7 @@ "data": { "forecast": "Pron\u00f3stico del tiempo" }, - "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos." + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 80 minutos en lugar de cada 40 minutos." } } }, diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index 9d0b254713b..1a4197b758b 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register" + "description": "Para generar la clave API, ve a https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airnow/translations/es.json b/homeassistant/components/airnow/translations/es.json index b5b915910c5..94ed5e64123 100644 --- a/homeassistant/components/airnow/translations/es.json +++ b/homeassistant/components/airnow/translations/es.json @@ -17,7 +17,7 @@ "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/" + "description": "Para generar la clave API, ve a https://docs.airnowapi.org/account/request/" } } } diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index b51d3035fe5..5c38176bf22 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "general_error": "Se ha producido un error desconocido.", + "general_error": "Error inesperado", "invalid_api_key": "Clave API no v\u00e1lida", "location_not_found": "Ubicaci\u00f3n no encontrada" }, diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index 94c5e89be5d..a3383cd4b5f 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -9,7 +9,7 @@ "step": { "hassio_confirm": { "description": "\u00bfQuieres configurar Home Assistant para conectarse a Almond proporcionado por el complemento: {addon} ?", - "title": "Almond a trav\u00e9s del complemento Supervisor" + "title": "Almond a trav\u00e9s del complemento Home Assistant" }, "pick_implementation": { "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" diff --git a/homeassistant/components/ambiclimate/translations/es.json b/homeassistant/components/ambiclimate/translations/es.json index 521234c972a..1ac7a371f82 100644 --- a/homeassistant/components/ambiclimate/translations/es.json +++ b/homeassistant/components/ambiclimate/translations/es.json @@ -6,7 +6,7 @@ "missing_configuration": "El componente no est\u00e1 configurado. Siga la documentaci\u00f3n." }, "create_entry": { - "default": "Autenticado correctamente con Ambiclimate" + "default": "Autenticado correctamente" }, "error": { "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Accede al siguiente [enlace]({authorization_url}) y permite el acceso a tu cuenta de Ambiclimate, despu\u00e9s vuelve y pulsa en enviar a continuaci\u00f3n.\n(Aseg\u00farate que la url de devoluci\u00f3n de llamada es {cb_url})", + "description": "Por favor, sigue este [enlace]({authorization_url}) y **Permite** el acceso a tu cuenta Ambiclimate, luego regresa y presiona **Enviar** a continuaci\u00f3n.\n(Aseg\u00farate de que la URL de devoluci\u00f3n de llamada especificada sea {cb_url})", "title": "Autenticaci\u00f3n de Ambiclimate" } } diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index b73aa659ef6..918a95de976 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", + "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), intenta de nuevo m\u00e1s tarde.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", "device_not_found": "No se encontr\u00f3 el dispositivo durante el descubrimiento, por favor intenta a\u00f1adirlo nuevamente.", "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo nuevamente.", @@ -26,7 +26,7 @@ "title": "Confirma la adici\u00f3n del Apple TV" }, "pair_no_pin": { - "description": "El emparejamiento es necesario para el servicio `{protocol}`. Introduce el PIN en tu Apple TV para continuar.", + "description": "Se requiere emparejamiento para el servicio `{protocol}`. Por favor, introduce el PIN {pin} en tu dispositivo para continuar.", "title": "Emparejamiento" }, "pair_with_pin": { @@ -56,7 +56,7 @@ "data": { "device_input": "Dispositivo" }, - "description": "Empieza introduciendo el nombre del dispositivo (eje. Cocina o Dormitorio) o la direcci\u00f3n IP del Apple TV que quieres a\u00f1adir. Si se han econtrado dispositivos en tu red, se mostrar\u00e1n a continuaci\u00f3n.\n\nSi no puedes ver el dispositivo o experimentas alg\u00fan problema, intente especificar la direcci\u00f3n IP del dispositivo.\n\n{devices}", + "description": "Comienza introduciendo el nombre del dispositivo (por ejemplo, cocina o dormitorio) o la direcci\u00f3n IP del Apple TV que deseas a\u00f1adir. \n\n Si no puedes ver tu dispositivo o experimentas alg\u00fan problema, intenta especificar la direcci\u00f3n IP del dispositivo.", "title": "Configurar un nuevo Apple TV" } } diff --git a/homeassistant/components/arcam_fmj/translations/es.json b/homeassistant/components/arcam_fmj/translations/es.json index 33c0558a4ce..035749be792 100644 --- a/homeassistant/components/arcam_fmj/translations/es.json +++ b/homeassistant/components/arcam_fmj/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar" }, - "flow_title": "Arcam FMJ en {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "\u00bfQuieres a\u00f1adir el Arcam FMJ en `{host}` a Home Assistant?" diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index f46bf499455..f20f0faa3eb 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -19,12 +19,12 @@ "mode": "Modo", "name": "Nombre", "password": "Contrase\u00f1a", - "port": "Puerto (d\u00e9jalo vac\u00edo por el predeterminado del protocolo)", + "port": "Puerto (dejar vac\u00edo para el predeterminado del protocolo)", "protocol": "Protocolo de comunicaci\u00f3n a utilizar", - "ssh_key": "Ruta de acceso a su archivo de clave SSH (en lugar de la contrase\u00f1a)", + "ssh_key": "Ruta a tu archivo de clave SSH (en lugar de contrase\u00f1a)", "username": "Nombre de usuario" }, - "description": "Establezca los par\u00e1metros necesarios para conectarse a su router", + "description": "Establece el par\u00e1metro requerido para conectarte a tu router", "title": "AsusWRT" } } @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "consider_home": "Segundos de espera antes de considerar un dispositivo ausente", + "consider_home": "Segundos de espera antes de considerar un dispositivo como ausente", "dnsmasq": "La ubicaci\u00f3n en el router de los archivos dnsmasq.leases", "interface": "La interfaz de la que quieres estad\u00edsticas (por ejemplo, eth0, eth1, etc.)", "require_ip": "Los dispositivos deben tener IP (para el modo de punto de acceso)", diff --git a/homeassistant/components/atag/translations/es.json b/homeassistant/components/atag/translations/es.json index ed89c8c385c..c2da8173e22 100644 --- a/homeassistant/components/atag/translations/es.json +++ b/homeassistant/components/atag/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Este dispositivo ya ha sido a\u00f1adido a HomeAssistant" + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/auth/translations/es.json b/homeassistant/components/auth/translations/es.json index 0279bca1dfa..85f412f0814 100644 --- a/homeassistant/components/auth/translations/es.json +++ b/homeassistant/components/auth/translations/es.json @@ -5,7 +5,7 @@ "no_available_service": "No hay servicios de notificaci\u00f3n disponibles." }, "error": { - "invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntelo de nuevo." + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, vuelve a intentarlo." }, "step": { "init": { @@ -13,7 +13,7 @@ "title": "Configurar una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" }, "setup": { - "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notify.{notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:", + "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de **notify.{notify_service}**. Por favor, introd\u00facela a continuaci\u00f3n:", "title": "Verificar la configuraci\u00f3n" } }, @@ -21,7 +21,7 @@ }, "totp": { "error": { - "invalid_code": "C\u00f3digo inv\u00e1lido, int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, aseg\u00farate de que el reloj de tu sistema Home Assistant es correcto." + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, vuelve a intentarlo. Si recibes este error constantemente, aseg\u00farate de que el reloj de tu sistema Home Assistant sea exacto." }, "step": { "init": { diff --git a/homeassistant/components/automation/translations/es.json b/homeassistant/components/automation/translations/es.json index c20f1be7d1d..08d1cc7df07 100644 --- a/homeassistant/components/automation/translations/es.json +++ b/homeassistant/components/automation/translations/es.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Apagado", + "off": "Apagada", "on": "Encendida" } }, diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index e52451bd108..eaf255a0532 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -21,6 +21,9 @@ "email": "Correu electr\u00f2nic" } }, + "discovery_confirm": { + "description": "Vols configurar {model} ({device_id})?" + }, "local": { "data": { "host": "Adre\u00e7a IP" diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index e878c370d93..4cd1ac93d0a 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -9,7 +9,17 @@ "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: {url}" + }, + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} ({device_id});" + }, + "local": { + "description": "\u03a4\u03bf Awair Local API \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1: {url}" + }, "reauth": { "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", @@ -29,7 +39,11 @@ "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "email": "Email" }, - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://developer.getawair.com/onboard/login" + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 cloud", + "local": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ac (\u03c0\u03c1\u03bf\u03c4\u03b9\u03bc\u03ac\u03c4\u03b1\u03b9)" + } } } } diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index 64d678003ac..df5667ff1f9 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -50,7 +50,7 @@ "access_token": "Token de acceso", "email": "Correo electr\u00f3nico" }, - "description": "Debes registrarte para obtener un token de acceso de desarrollador Awair en: https://developer.getawair.com/onboard/login", + "description": "Elige local para la mejor experiencia. Usa solo la nube si el dispositivo no est\u00e1 conectado a la misma red que Home Assistant, o si tienes un dispositivo heredado.", "menu_options": { "cloud": "Conectar a trav\u00e9s de la nube", "local": "Conectar localmente (preferido)" diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json index b741f5f8722..c40f4be1f1c 100644 --- a/homeassistant/components/awair/translations/et.json +++ b/homeassistant/components/awair/translations/et.json @@ -50,7 +50,7 @@ "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", "email": "E-post" }, - "description": "Pead registreerima Awair arendaja juurdep\u00e4\u00e4su loa aadressil: https://developer.getawair.com/onboard/login", + "description": "Parima kogemuse saamiseks vali kohalik \u00fchendus. Kasuta pilve ainult siis, kui seade ei ole \u00fchendatud samasse v\u00f5rku kui Home Assistant v\u00f5i kui on vanem seade.", "menu_options": { "cloud": "Pilve\u00fchendus", "local": "Kohalik \u00fchendus (eelistatud)" diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index fd915507762..2b4d572609d 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_configured_account": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "unreachable": "\u00c9chec de connexion" }, "error": { "invalid_access_token": "Jeton d'acc\u00e8s non valide", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "unreachable": "\u00c9chec de connexion" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Jeton d'acc\u00e8s", + "email": "Courriel" + }, + "description": "Vous devez r\u00e9aliser une demande de jeton d'acc\u00e8s de d\u00e9veloppeur Awair \u00e0 l'adresse suivante\u00a0: {url}" + }, + "discovery_confirm": { + "description": "Voulez-vous configurer {model} ({device_id})\u00a0?" + }, + "local": { + "data": { + "host": "Adresse IP" + }, + "description": "L'API locale Awair doit \u00eatre activ\u00e9e en suivant ces \u00e9tapes\u00a0: {url}" + }, "reauth": { "data": { "access_token": "Jeton d'acc\u00e8s", @@ -29,7 +50,11 @@ "access_token": "Jeton d'acc\u00e8s", "email": "Courriel" }, - "description": "Vous devez vous inscrire pour un jeton d'acc\u00e8s d\u00e9veloppeur Awair sur: https://developer.getawair.com/onboard/login" + "description": "S\u00e9lectionnez le mode local pour une exp\u00e9rience optimale. S\u00e9lectionnez le mode cloud uniquement si l'appareil n'est pas connect\u00e9 au m\u00eame r\u00e9seau que Home Assistant ou si vous disposez d'un appareil ancien.", + "menu_options": { + "cloud": "Connexion cloud", + "local": "Connexion locale (recommand\u00e9e)" + } } } } diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 2e81b31f187..7ce394ccb4b 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "unreachable": "Sikertelen csatlakoz\u00e1s" }, "error": { "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unreachable": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "email": "E-mail" + }, + "description": "Regisztr\u00e1lnia kell egy Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si token\u00e9rt a k\u00f6vetkez\u0151 c\u00edmen: {url}" + }, + "discovery_confirm": { + "description": "Be\u00e1ll\u00edtja a k\u00f6vetkez\u0151t: {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP c\u00edm" + }, + "description": "Az Awair lok\u00e1lis API-t az al\u00e1bbi l\u00e9p\u00e9sekkel kell enged\u00e9lyezni: {url}" + }, "reauth": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", @@ -29,7 +50,11 @@ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "email": "E-mail" }, - "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login" + "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Csatlakoz\u00e1s a felh\u0151n kereszt\u00fcl", + "local": "Lok\u00e1lis csatlakoz\u00e1s (aj\u00e1nlott)" + } } } } diff --git a/homeassistant/components/awair/translations/pt-BR.json b/homeassistant/components/awair/translations/pt-BR.json index 7406bdf3ee0..e3c3e24f738 100644 --- a/homeassistant/components/awair/translations/pt-BR.json +++ b/homeassistant/components/awair/translations/pt-BR.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "A conta j\u00e1 foi configurada", + "already_configured_account": "A conta j\u00e1 est\u00e1 configurada", + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", "no_devices_found": "Nenhum dispositivo encontrado na rede", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unreachable": "Falhou ao conectar" }, "error": { "invalid_access_token": "Token de acesso inv\u00e1lido", - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unreachable": "Falhou ao conectar" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token de acesso", + "email": "E-mail" + }, + "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: {url}" + }, + "discovery_confirm": { + "description": "Deseja configurar {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Endere\u00e7o IP" + }, + "description": "Awair Local API deve ser ativada seguindo estas etapas: {url}" + }, "reauth": { "data": { "access_token": "Token de acesso", @@ -29,7 +50,11 @@ "access_token": "Token de acesso", "email": "Email" }, - "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: https://developer.getawair.com/onboard/login" + "description": "Escolha local para a melhor experi\u00eancia. Use a nuvem apenas se o dispositivo n\u00e3o estiver conectado \u00e0 mesma rede que o Home Assistant ou se voc\u00ea tiver um dispositivo legado.", + "menu_options": { + "cloud": "Conecte-se pela nuvem", + "local": "Conecte-se localmente (preferencial)" + } } } } diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index f14acef8550..e7953517823 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unreachable": "\u9023\u7dda\u5931\u6557" }, "error": { "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unreachable": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "\u5b58\u53d6\u6b0a\u6756", + "email": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u5fc5\u9808\u5148\u8a3b\u518a\u53d6\u5f97 Awair \u958b\u767c\u8005\u5e33\u865f\u6b0a\u6756\uff1a{url}" + }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {model} ({device_id})\uff1f" + }, + "local": { + "data": { + "host": "IP \u4f4d\u5740" + }, + "description": "\u5fc5\u9808\u900f\u904e\u4ee5\u4e0b\u6b65\u9a5f\u4ee5\u555f\u7528 Awair \u672c\u5730\u7aef API\uff1a{url}" + }, "reauth": { "data": { "access_token": "\u5b58\u53d6\u6b0a\u6756", @@ -29,7 +50,11 @@ "access_token": "\u5b58\u53d6\u6b0a\u6756", "email": "\u96fb\u5b50\u90f5\u4ef6" }, - "description": "\u5fc5\u9808\u5148\u8a3b\u518a Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\uff1ahttps://developer.getawair.com/onboard/login" + "description": "\u9078\u64c7\u672c\u5730\u7aef\u4ee5\u7372\u5f97\u6700\u4f73\u4f7f\u7528\u9ad4\u9a57\u3002\u50c5\u65bc\u88dd\u7f6e\u8207 Home Assistant \u4e26\u975e\u8655\u65bc\u76f8\u540c\u7db2\u8def\u4e0b\u3001\u6216\u8005\u4f7f\u7528\u820a\u8a2d\u5099\u7684\u60c5\u6cc1\u4e0b\u4f7f\u7528\u96f2\u7aef\u3002", + "menu_options": { + "cloud": "\u900f\u904e\u96f2\u7aef\u9023\u7dda", + "local": "\u672c\u5730\u7aef\u9023\u7dda\uff08\u9996\u9078\uff09" + } } } } diff --git a/homeassistant/components/axis/translations/es.json b/homeassistant/components/axis/translations/es.json index 6a406614b9f..87497280775 100644 --- a/homeassistant/components/axis/translations/es.json +++ b/homeassistant/components/axis/translations/es.json @@ -2,16 +2,16 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "link_local_address": "Las direcciones de enlace locales no son compatibles", + "link_local_address": "Las direcciones de enlace local no son compatibles", "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en marcha.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, - "flow_title": "Dispositivo Axis: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/es.json b/homeassistant/components/azure_devops/translations/es.json index c71fc22b1bd..2795286e5c7 100644 --- a/homeassistant/components/azure_devops/translations/es.json +++ b/homeassistant/components/azure_devops/translations/es.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "project_error": "No se pudo obtener informaci\u00f3n del proyecto." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/blink/translations/es.json b/homeassistant/components/blink/translations/es.json index 7ff0b349555..72f68ab4fff 100644 --- a/homeassistant/components/blink/translations/es.json +++ b/homeassistant/components/blink/translations/es.json @@ -14,7 +14,7 @@ "data": { "2fa": "C\u00f3digo de dos factores" }, - "description": "Introduce el pin enviado a tu correo electr\u00f3nico. Si el correo electr\u00f3nico no contiene un pin, d\u00e9jalo en blanco", + "description": "Introduce el PIN enviado a su correo electr\u00f3nico", "title": "Autenticaci\u00f3n de dos factores" }, "user": { @@ -32,7 +32,7 @@ "data": { "scan_interval": "Intervalo de escaneo (segundos)" }, - "description": "Configurar la integraci\u00f3n de Blink", + "description": "Configurar la integraci\u00f3n Blink", "title": "Opciones de Blink" } } diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index 401a254eeaf..118125f63e0 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Configura la integraci\u00f3n del televisor Sony Bravia. Si tienes problemas con la configuraci\u00f3n, ve a: https://www.home-assistant.io/integrations/braviatv\n\nAseg\u00farate de que tu televisor est\u00e1 encendido." + "description": "Aseg\u00farate de que tu TV est\u00e9 encendida antes de intentar configurarla." } } }, diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json index 96f791979d4..97a71e2cf61 100644 --- a/homeassistant/components/broadlink/translations/es.json +++ b/homeassistant/components/broadlink/translations/es.json @@ -13,7 +13,7 @@ "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "unknown": "Error inesperado" }, - "flow_title": "{name} ( {model} en {host} )", + "flow_title": "{name} ({model} en {host})", "step": { "auth": { "title": "Autenticarse en el dispositivo" @@ -22,17 +22,17 @@ "data": { "name": "Nombre" }, - "title": "Elija un nombre para el dispositivo" + "title": "Elige un nombre para el dispositivo" }, "reset": { - "description": "Su dispositivo est\u00e1 bloqueado para la autenticaci\u00f3n. Siga las instrucciones para desbloquearlo:\n1. Reinicie el dispositivo de f\u00e1brica.\n2. Use la aplicaci\u00f3n oficial para agregar el dispositivo a su red local.\n3. Pare. No termine la configuraci\u00f3n. Cierre la aplicaci\u00f3n.\n4. Haga clic en Enviar.", + "description": "{name} ({model} en {host}) est\u00e1 bloqueado. Debes desbloquear el dispositivo para autenticarse y completar la configuraci\u00f3n. Instrucciones:\n1. Abre la aplicaci\u00f3n Broadlink.\n2. Haz clic en el dispositivo.\n3. Haz clic en `...` en la parte superior derecha.\n4. Despl\u00e1zate hasta la parte inferior de la p\u00e1gina.\n5. Desactiva el bloqueo.", "title": "Desbloquear el dispositivo" }, "unlock": { "data": { "unlock": "S\u00ed, hazlo." }, - "description": "Tu dispositivo est\u00e1 bloqueado. Esto puede provocar problemas de autenticaci\u00f3n en Home Assistant. \u00bfQuieres desbloquearlo?", + "description": "{name} ({model} en {host}) est\u00e1 bloqueado. Esto puede generar problemas de autenticaci\u00f3n en Home Assistant. \u00bfTe gustar\u00eda desbloquearlo?", "title": "Desbloquear el dispositivo (opcional)" }, "user": { diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 5f1e55c8d0d..1e1c29d24a4 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -17,7 +17,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Configura tu dispositivo BSB-Lan para integrarse con Home Assistant.", + "description": "Configura tu dispositivo BSB-Lan para que se integre con Home Assistant.", "title": "Conectar con el dispositivo BSB-Lan" } } diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index eff36eefcfa..d3ea7a842c5 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -10,7 +10,7 @@ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "invalid_zone": "Zona no v\u00e1lida" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/daikin/translations/es.json b/homeassistant/components/daikin/translations/es.json index ce72440bf7d..8ddfb8241e0 100644 --- a/homeassistant/components/daikin/translations/es.json +++ b/homeassistant/components/daikin/translations/es.json @@ -17,7 +17,7 @@ "host": "Host", "password": "Contrase\u00f1a" }, - "description": "Introduce la direcci\u00f3n IP de tu aire acondicionado Daikin.\n\nTen en cuenta que la Clave API y la Contrase\u00f1a son usadas por los dispositivos BRP072Cxx y SKYFi respectivamente.", + "description": "Introduce la Direcci\u00f3n IP de tu aire acondicionado Daikin. \n\nTen en cuenta que Clave API y Contrase\u00f1a solo los utilizan los dispositivos BRP072Cxx y SKYFi respectivamente.", "title": "Configurar aire acondicionado Daikin" } } diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 3c4d8a89134..6308bf7fed8 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -15,11 +15,11 @@ "step": { "hassio_confirm": { "description": "\u00bfQuieres configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento {addon} ?", - "title": "Pasarela de enlace de CONZ Zigbee v\u00eda complemento de Home Assistant" + "title": "Puerta de enlace Zigbee deCONZ a trav\u00e9s del complemento Home Assistant" }, "link": { - "description": "Desbloquea tu gateway de deCONZ para registrarte con Home Assistant.\n\n1. Dir\u00edgete a deCONZ Settings -> Gateway -> Advanced\n2. Pulsa el bot\u00f3n \"Authenticate app\"", - "title": "Enlazar con deCONZ" + "description": "Desbloquea tu puerta de enlace deCONZ para registrarlo con Home Assistant. \n\n 1. Ve a Configuraci\u00f3n deCONZ -> Puerta de enlace -> Avanzado\n 2. Presiona el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", + "title": "Vincular con deCONZ" }, "manual_input": { "data": { @@ -71,7 +71,7 @@ "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" qu\u00edntuple pulsaci\u00f3n", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", "remote_button_rotated_fast": "Bot\u00f3n \"{subtype}\" girado r\u00e1pido", - "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtype}\" detenido", + "remote_button_rotation_stopped": "Se detuvo la rotaci\u00f3n del bot\u00f3n \"{subtype}\"", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" triple pulsaci\u00f3n", diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index d601c6a9ff0..55936dee05d 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar, int\u00e9ntelo de nuevo, desconectar los cables de alimentaci\u00f3n y Ethernet y volver a conectarlos puede ayudar", - "not_denonavr_manufacturer": "No es un Receptor AVR Denon AVR en Red, el fabricante detectado no concuerda", + "not_denonavr_manufacturer": "No es un receptor de red Denon AVR, el fabricante descubierto no coincide", "not_denonavr_missing": "No es un Receptor AVR Denon AVR en Red, la informaci\u00f3n detectada no est\u00e1 completa" }, "error": { diff --git a/homeassistant/components/dialogflow/translations/es.json b/homeassistant/components/dialogflow/translations/es.json index 8c2c5b4993e..eb4c3b179a6 100644 --- a/homeassistant/components/dialogflow/translations/es.json +++ b/homeassistant/components/dialogflow/translations/es.json @@ -6,7 +6,7 @@ "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, necesitas configurar [Integraci\u00f3n de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, necesitas configurar la [Integraci\u00f3n webhook de Dialogflow]({dialogflow_url}).\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nConsulta [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/directv/translations/es.json b/homeassistant/components/directv/translations/es.json index 92cf160462c..2a268ac2148 100644 --- a/homeassistant/components/directv/translations/es.json +++ b/homeassistant/components/directv/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo ya configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 83ec7ed7a6a..5aefd6eb3af 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "link_local_address": "No se admiten direcciones locales", + "link_local_address": "Las direcciones de enlace local no son compatibles", "not_doorbird_device": "Este dispositivo no es un DoorBird" }, "error": { diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json index 91ee7c58952..a6363ebdea1 100644 --- a/homeassistant/components/dsmr/translations/es.json +++ b/homeassistant/components/dsmr/translations/es.json @@ -11,8 +11,6 @@ "cannot_connect": "No se pudo conectar" }, "step": { - "one": "Vac\u00edo", - "other": "Vac\u00edo", "setup_network": { "data": { "dsmr_version": "Seleccione la versi\u00f3n de DSMR", diff --git a/homeassistant/components/dunehd/translations/es.json b/homeassistant/components/dunehd/translations/es.json index 7e2c1282ca6..78a8fd1733d 100644 --- a/homeassistant/components/dunehd/translations/es.json +++ b/homeassistant/components/dunehd/translations/es.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "description": "Configura la integraci\u00f3n de Dune HD. Si tienes problemas con la configuraci\u00f3n, ve a: https://www.home-assistant.io/integrations/dunehd \n\n Aseg\u00farate de que tu reproductor est\u00e1 encendido." + "description": "Aseg\u00farate de que tu reproductor est\u00e9 encendido." } } } diff --git a/homeassistant/components/ecobee/translations/es.json b/homeassistant/components/ecobee/translations/es.json index f4980f46bbb..a7fdcfb2116 100644 --- a/homeassistant/components/ecobee/translations/es.json +++ b/homeassistant/components/ecobee/translations/es.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "Por favor, autorizar esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo pin:\n\n{pin}\n\nA continuaci\u00f3n, pulse Enviar.", + "description": "Por favor, autoriza esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con el c\u00f3digo PIN: \n\n{pin}\n\nLuego, presiona Enviar.", "title": "Autorizar aplicaci\u00f3n en ecobee.com" }, "user": { diff --git a/homeassistant/components/elgato/translations/es.json b/homeassistant/components/elgato/translations/es.json index a41872115fa..eb0d7fde9b4 100644 --- a/homeassistant/components/elgato/translations/es.json +++ b/homeassistant/components/elgato/translations/es.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u00bfQuieres a\u00f1adir el Key Light de Elgato con n\u00famero de serie `{serial_number}` a Home Assistant?", - "title": "Descubierto dispositivo Elgato Key Light" + "title": "Dispositivo Elgato Light descubierto" } } } diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index 25c676caafc..1cbae31550f 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -41,7 +41,7 @@ "data": { "device": "Dispositivo" }, - "description": "La cadena de direcci\u00f3n debe estar en el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no-seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no-seguro' y 2601 para 'seguro'. Para el protocolo serie, la direcci\u00f3n debe tener la forma 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. Los baudios son opcionales y el valor predeterminado es 115200.", + "description": "Elige un sistema descubierto o 'Entrada manual' si no se han descubierto dispositivos.", "title": "Conectar con Control Elk-M1" } } diff --git a/homeassistant/components/emulated_roku/translations/es.json b/homeassistant/components/emulated_roku/translations/es.json index abbcac75479..8904c03ffe2 100644 --- a/homeassistant/components/emulated_roku/translations/es.json +++ b/homeassistant/components/emulated_roku/translations/es.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "advertise_ip": "IP para anunciar", - "advertise_port": "Puerto de advertencias", + "advertise_ip": "Direcci\u00f3n IP anunciada", + "advertise_port": "Puerto anunciado", "host_ip": "Direcci\u00f3n IP del host", "listen_port": "Puerto de escucha", "name": "Nombre", diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index f3e73d97a52..f8e933f7711 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "ESP ya est\u00e1 configurado", - "already_in_progress": "La configuraci\u00f3n del ESP ya est\u00e1 en marcha", + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "connection_error": "No se puede conectar a ESP. Aseg\u00farate de que tu archivo YAML contenga una l\u00ednea 'api:'.", + "connection_error": "No se puede conectar a ESP. Por favor, aseg\u00farate de que tu archivo YAML contiene una l\u00ednea 'api:'.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_psk": "La clave de cifrado de transporte no es v\u00e1lida. Por favor, aseg\u00farate de que coincida con lo que tienes en tu configuraci\u00f3n", "resolve_error": "No se puede resolver la direcci\u00f3n del ESP. Si este error persiste, por favor, configura una direcci\u00f3n IP est\u00e1tica" diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json index b86ba3b2164..19ba8da21dd 100644 --- a/homeassistant/components/fireservicerota/translations/es.json +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -15,7 +15,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Los tokens de autenticaci\u00f3n ya no son v\u00e1lidos, inicia sesi\u00f3n para recrearlos." + "description": "Los tokens de autenticaci\u00f3n dejaron de ser v\u00e1lidos, inicia sesi\u00f3n para volver a crearlos." }, "user": { "data": { diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index b2ffbe162a8..12152dc0211 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Esta cuenta ya est\u00e1 configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json index b205be8270c..3a2c41cc735 100644 --- a/homeassistant/components/flunearyou/translations/es.json +++ b/homeassistant/components/flunearyou/translations/es.json @@ -12,7 +12,7 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "Monitorizar reportes de usuarios y del CDC para un par de coordenadas", + "description": "Supervisa los informes del CDC y los basados en los usuarios para un par de coordenadas.", "title": "Configurar Flu Near You" } } diff --git a/homeassistant/components/forked_daapd/translations/es.json b/homeassistant/components/forked_daapd/translations/es.json index 54b1adb479f..af47865dd66 100644 --- a/homeassistant/components/forked_daapd/translations/es.json +++ b/homeassistant/components/forked_daapd/translations/es.json @@ -6,7 +6,7 @@ }, "error": { "forbidden": "No se puede conectar. Compruebe los permisos de red de bifurcaci\u00f3n.", - "unknown_error": "Error desconocido.", + "unknown_error": "Error inesperado", "websocket_not_enabled": "Websocket no activado en servidor forked-daapd.", "wrong_host_or_port": "No se ha podido conectar. Por favor comprueba host y puerto.", "wrong_password": "Contrase\u00f1a incorrecta.", diff --git a/homeassistant/components/freebox/translations/es.json b/homeassistant/components/freebox/translations/es.json index de176304d1f..9cae8cc2089 100644 --- a/homeassistant/components/freebox/translations/es.json +++ b/homeassistant/components/freebox/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado." + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", @@ -11,7 +11,7 @@ "step": { "link": { "description": "Pulsa \"Enviar\", despu\u00e9s pulsa en la flecha derecha en el router para registrar Freebox con Home Assistant\n\n![Localizaci\u00f3n del bot\u00f3n en el router](/static/images/config_freebox.png)", - "title": "Enlazar router Freebox" + "title": "Vincular router Freebox" }, "user": { "data": { diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index a8f5168902f..a5263d4e056 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "ignore_ip6_link_local": "La direcci\u00f3n local del enlace IPv6 no es compatible.", + "ignore_ip6_link_local": "La direcci\u00f3n de enlace local IPv6 no es compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index 33dfc59d543..71e5a2f5cf1 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "Este AVM FRITZ!Box ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "ignore_ip6_link_local": "La direcci\u00f3n local del enlace IPv6 no es compatible.", + "ignore_ip6_link_local": "La direcci\u00f3n de enlace local IPv6 no es compatible.", "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.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" @@ -11,7 +11,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/geofency/translations/es.json b/homeassistant/components/geofency/translations/es.json index 8e3c806f708..2462ec5bcc7 100644 --- a/homeassistant/components/geofency/translations/es.json +++ b/homeassistant/components/geofency/translations/es.json @@ -6,7 +6,7 @@ "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 Geofency.\n\nRellene la siguiente informaci\u00f3n:\n\n- URL: ``{webhook_url}``\n- M\u00e9todo: POST\n\nVer[la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en Geofency.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nConsulta [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/glances/translations/en.json b/homeassistant/components/glances/translations/en.json index aa7005bddea..87c53c3cf48 100644 --- a/homeassistant/components/glances/translations/en.json +++ b/homeassistant/components/glances/translations/en.json @@ -11,13 +11,15 @@ "user": { "data": { "host": "Host", + "name": "Name", "password": "Password", "port": "Port", "ssl": "Uses an SSL certificate", "username": "Username", "verify_ssl": "Verify SSL certificate", "version": "Glances API Version (2 or 3)" - } + }, + "title": "Setup Glances" } } }, diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json index 9772f3f6d91..c75fc143141 100644 --- a/homeassistant/components/goalzero/translations/es.json +++ b/homeassistant/components/goalzero/translations/es.json @@ -19,7 +19,7 @@ "host": "Host", "name": "Nombre" }, - "description": "Primero, tienes que descargar la aplicaci\u00f3n Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSigue las instrucciones para conectar tu Yeti a tu red Wifi. Luego obt\u00e9n la IP de tu router. El DHCP debe estar configurado en los ajustes de tu router para asegurar que la IP de host del dispositivo no cambie. Consulta el manual de usuario de tu router." + "description": "Por favor, consulta la documentaci\u00f3n para asegurarte de que se cumplen todos los requisitos." } } } diff --git a/homeassistant/components/gogogate2/translations/es.json b/homeassistant/components/gogogate2/translations/es.json index 877147e46a0..c585deb9958 100644 --- a/homeassistant/components/gogogate2/translations/es.json +++ b/homeassistant/components/gogogate2/translations/es.json @@ -16,7 +16,7 @@ "username": "Nombre de usuario" }, "description": "Proporciona la informaci\u00f3n requerida a continuaci\u00f3n.", - "title": "Configurar GotoGate2" + "title": "Configurar Gogogate2 o ismartgate" } } } diff --git a/homeassistant/components/gpslogger/translations/es.json b/homeassistant/components/gpslogger/translations/es.json index ea3221ee2f5..bf6f3052e23 100644 --- a/homeassistant/components/gpslogger/translations/es.json +++ b/homeassistant/components/gpslogger/translations/es.json @@ -6,7 +6,7 @@ "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 GPSLogger.\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nEcha un vistazo a [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en GPSLogger.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nConsulta [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json index 8f6a77ac2ec..2a4963c8649 100644 --- a/homeassistant/components/guardian/translations/el.json +++ b/homeassistant/components/guardian/translations/el.json @@ -17,5 +17,18 @@ "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{alternate_service}` \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c5 `{alternate_target}`. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03a0\u039f\u0392\u039f\u039b\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c3\u03b7\u03bc\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1 \u03c9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03c5\u03bc\u03ad\u03bd\u03bf.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } + }, + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 42c08aba617..c918a3fe583 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "La configuraci\u00f3n del dispositivo Guardian ya est\u00e1 en proceso.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar" }, "step": { diff --git a/homeassistant/components/hangouts/translations/es.json b/homeassistant/components/hangouts/translations/es.json index a2aba99c24c..29fe36ea23c 100644 --- a/homeassistant/components/hangouts/translations/es.json +++ b/homeassistant/components/hangouts/translations/es.json @@ -2,19 +2,18 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "unknown": "Error desconocido" + "unknown": "Error inesperado" }, "error": { - "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, por favor, int\u00e9ntelo de nuevo.", - "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar en el tel\u00e9fono).", + "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, por favor, int\u00e9ntalo de nuevo.", + "invalid_2fa_method": "M\u00e9todo 2FA no v\u00e1lido (verificar en el tel\u00e9fono).", "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "step": { "2fa": { "data": { - "2fa": "Pin 2FA" + "2fa": "PIN 2FA" }, - "description": "Vac\u00edo", "title": "Autenticaci\u00f3n de 2 factores" }, "user": { @@ -23,7 +22,6 @@ "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" }, - "description": "Vac\u00edo", "title": "Inicio de sesi\u00f3n de Google Chat" } } diff --git a/homeassistant/components/heos/translations/es.json b/homeassistant/components/heos/translations/es.json index dde95e24384..436e2089993 100644 --- a/homeassistant/components/heos/translations/es.json +++ b/homeassistant/components/heos/translations/es.json @@ -11,7 +11,7 @@ "data": { "host": "Host" }, - "description": "Introduce el nombre de host o direcci\u00f3n IP de un dispositivo Heos (preferiblemente conectado por cable a la red).", + "description": "Por favor, introduce el nombre de host o la direcci\u00f3n IP de un dispositivo Heos (preferiblemente uno conectado por cable a la red).", "title": "Conectar a Heos" } } diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 170df5c4a13..1813b96dd7a 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -12,8 +12,8 @@ "data": { "include_domains": "Dominios para incluir" }, - "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" + "description": "Elige los dominios a incluir. Se incluir\u00e1n todas las entidades admitidas en el dominio, excepto las entidades categorizadas. Se crear\u00e1 una instancia separada de HomeKit en modo accesorio para cada reproductor multimedia de TV, control remoto basado en actividad, cerradura y c\u00e1mara.", + "title": "Selecciona los dominios que se incluir\u00e1n" } } }, @@ -29,7 +29,7 @@ "data": { "devices": "Dispositivos (Disparadores)" }, - "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", + "description": "Se crean interruptores programables para cada dispositivo seleccionado. Cuando se dispara un dispositivo, HomeKit se puede configurar para ejecutar una automatizaci\u00f3n o una escena.", "title": "Configuraci\u00f3n avanzada" }, "cameras": { @@ -38,7 +38,7 @@ "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" }, "description": "Verifica todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", - "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." + "title": "Configuraci\u00f3n de la c\u00e1mara" }, "exclude": { "data": { @@ -60,12 +60,12 @@ "include_exclude_mode": "Modo de inclusi\u00f3n", "mode": "Mode de HomeKit" }, - "description": "Las entidades de los \"Dominios que se van a incluir\" se establecer\u00e1n en HomeKit. Podr\u00e1 seleccionar qu\u00e9 entidades excluir de esta lista en la siguiente pantalla.", + "description": "HomeKit se puede configurar para exponer un puente o un solo accesorio. En el modo accesorio, solo se puede usar una sola entidad. El modo accesorio es necesario para que los reproductores multimedia con la clase de dispositivo TV funcionen correctamente. Las entidades en los \"Dominios para incluir\" se incluir\u00e1n en HomeKit. Podr\u00e1s seleccionar qu\u00e9 entidades incluir o excluir de esta lista en la siguiente pantalla.", "title": "Selecciona el modo y dominios." }, "yaml": { "description": "Esta entrada se controla a trav\u00e9s de YAML", - "title": "Ajustar las opciones del puente HomeKit" + "title": "Ajustar las opciones de HomeKit" } } } diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index 48015d0418d..b446c87e70f 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -3,20 +3,20 @@ "abort": { "accessory_not_found_error": "No se puede a\u00f1adir el emparejamiento porque ya no se puede encontrar el dispositivo.", "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", - "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en marcha.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.", "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", - "invalid_config_entry": "Este dispositivo se muestra como listo para vincular, pero ya existe una entrada que causa conflicto en Home Assistant y se debe eliminar primero.", + "invalid_config_entry": "Este dispositivo se muestra como listo para emparejarse, pero ya hay una entrada de configuraci\u00f3n conflictiva para \u00e9l en Home Assistant que primero debe eliminarse.", "invalid_properties": "Propiedades no v\u00e1lidas anunciadas por dispositivo.", "no_devices": "No se encontraron dispositivos no emparejados" }, "error": { - "authentication_error": "C\u00f3digo HomeKit incorrecto. Por favor, compru\u00e9belo e int\u00e9ntelo de nuevo.", + "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor, rev\u00edsalo e int\u00e9ntalo de nuevo.", "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado no es seguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", "max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.", - "pairing_failed": "Se ha producido un error no controlado al intentar emparejarse con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no est\u00e9 admitido en este momento.", - "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", - "unknown_error": "El dispositivo report\u00f3 un error desconocido. La vinculaci\u00f3n ha fallado." + "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no sea compatible actualmente.", + "unable_to_pair": "No se puede emparejar, int\u00e9ntalo de nuevo.", + "unknown_error": "El dispositivo report\u00f3 un error desconocido. El emparejamiento ha fallado." }, "flow_title": "{name} ({category})", "step": { @@ -31,10 +31,10 @@ "pair": { "data": { "allow_insecure_setup_codes": "Permitir el emparejamiento con c\u00f3digos de configuraci\u00f3n inseguros.", - "pairing_code": "C\u00f3digo de vinculaci\u00f3n" + "pairing_code": "C\u00f3digo de emparejamiento" }, "description": "El controlador HomeKit se comunica con {name} ({category}) a trav\u00e9s de la red de \u00e1rea local mediante una conexi\u00f3n cifrada segura sin un controlador HomeKit o iCloud por separado. Introduce tu c\u00f3digo de emparejamiento de HomeKit (en el formato XXX-XX-XXX) para usar este accesorio. Este c\u00f3digo suele encontrarse en el propio dispositivo o en el embalaje.", - "title": "Vincular un dispositivo a trav\u00e9s del protocolo de accesorios HomeKit" + "title": "Emparejar con un dispositivo a trav\u00e9s del protocolo de accesorios HomeKit" }, "protocol_error": { "description": "Es posible que el dispositivo no est\u00e9 en modo de emparejamiento y que requiera que se presione un bot\u00f3n f\u00edsico o virtual. Aseg\u00farate de que el dispositivo est\u00e1 en modo de emparejamiento o intenta reiniciar el dispositivo, luego contin\u00faa para reanudar el emparejamiento.", @@ -44,7 +44,7 @@ "data": { "device": "Dispositivo" }, - "description": "El controlador de HomeKit se comunica a trav\u00e9s de la red de \u00e1rea local usando una conexi\u00f3n encriptada segura sin un controlador HomeKit separado o iCloud. Selecciona el dispositivo que quieres vincular:", + "description": "El controlador HomeKit se comunica a trav\u00e9s de la red de \u00e1rea local mediante una conexi\u00f3n cifrada segura sin un controlador HomeKit o iCloud por separado. Selecciona el dispositivo que deseas emparejar:", "title": "Selecci\u00f3n del dispositivo" } } diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json index 454fa8f9f2a..afc637bbaa7 100644 --- a/homeassistant/components/homematicip_cloud/translations/es.json +++ b/homeassistant/components/homematicip_cloud/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "El punto de acceso ya est\u00e1 configurado", - "connection_aborted": "Fall\u00f3 la conexi\u00f3n", - "unknown": "Se ha producido un error desconocido." + "already_configured": "El dispositivo ya est\u00e1 configurado", + "connection_aborted": "No se pudo conectar", + "unknown": "Error inesperado" }, "error": { - "invalid_sgtin_or_pin": "PIN no v\u00e1lido, por favor int\u00e9ntalo de nuevo.", + "invalid_sgtin_or_pin": "SGTIN o C\u00f3digo PIN no v\u00e1lido, int\u00e9ntalo de nuevo.", "press_the_button": "Por favor, pulsa el bot\u00f3n azul", "register_failed": "No se pudo registrar, por favor int\u00e9ntalo de nuevo.", "timeout_button": "Se agot\u00f3 el tiempo de espera para presionar el bot\u00f3n azul. Vuelve a intentarlo." @@ -15,14 +15,14 @@ "init": { "data": { "hapid": "ID de punto de acceso (SGTIN)", - "name": "Nombre (opcional, utilizado como prefijo para todos los dispositivos)", - "pin": "C\u00f3digo PIN (opcional)" + "name": "Nombre (opcional, usado como prefijo de nombre para todos los dispositivos)", + "pin": "C\u00f3digo PIN" }, "title": "Elegir punto de acceso HomematicIP" }, "link": { "description": "Pulsa el bot\u00f3n azul en el punto de acceso y el bot\u00f3n de env\u00edo para registrar HomematicIP en Home Assistant.\n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "Enlazar punto de acceso" + "title": "Vincular punto de acceso" } } } diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 4d1ffba4608..9bef5d09eec 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -21,7 +21,7 @@ "url": "URL", "username": "Nombre de usuario" }, - "description": "Introduzca los detalles de acceso al dispositivo. La especificaci\u00f3n del nombre de usuario y la contrase\u00f1a es opcional, pero permite admitir m\u00e1s funciones de integraci\u00f3n. Por otro lado, el uso de una conexi\u00f3n autorizada puede causar problemas para acceder a la interfaz web del dispositivo desde fuera de Home Assistant mientras la integraci\u00f3n est\u00e1 activa, y viceversa.", + "description": "Introduce los detalles de acceso del dispositivo.", "title": "Configurar Huawei LTE" } } diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index db5a72c7fe2..456668fec88 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -13,7 +13,7 @@ }, "error": { "linking": "Error inesperado", - "register_failed": "No se pudo registrar, intente de nuevo" + "register_failed": "No se pudo registrar, int\u00e9ntalo de nuevo" }, "step": { "init": { @@ -23,8 +23,8 @@ "title": "Elige la pasarela Hue" }, "link": { - "description": "Presione el bot\u00f3n en la pasarela para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_philips_hue.jpg)", - "title": "Link Hub" + "description": "Presiona el bot\u00f3n en la pasarela para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_philips_hue.jpg)", + "title": "Vincular pasarela" }, "manual": { "data": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json index 924edd5394f..c0d2ad2e4e5 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/es.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/iaqualink/translations/es.json b/homeassistant/components/iaqualink/translations/es.json index 7587b393c3f..60f26b1c64d 100644 --- a/homeassistant/components/iaqualink/translations/es.json +++ b/homeassistant/components/iaqualink/translations/es.json @@ -13,7 +13,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Introduce el nombre de usuario y contrase\u00f1a de tu cuenta de iAqualink.", + "description": "Por favor, introduce el nombre de usuario y contrase\u00f1a de tu cuenta iAqualink.", "title": "Conexi\u00f3n con iAqualink" } } diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index 31db2283f66..9140c843483 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -16,7 +16,7 @@ "password": "Contrase\u00f1a" }, "description": "La contrase\u00f1a introducida anteriormente para {username} ya no funciona. Actualiza tu contrase\u00f1a para seguir usando esta integraci\u00f3n.", - "title": "Credenciales de iCloud" + "title": "Volver a autenticar la integraci\u00f3n" }, "trusted_device": { "data": { diff --git a/homeassistant/components/ifttt/translations/es.json b/homeassistant/components/ifttt/translations/es.json index a296af6a6d6..6ffb7b9e421 100644 --- a/homeassistant/components/ifttt/translations/es.json +++ b/homeassistant/components/ifttt/translations/es.json @@ -6,12 +6,12 @@ "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 debes usar la acci\u00f3n \"Make a web request\" del [applet IFTTT Webhook]({applet_url}).\n\nCompleta la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n- Tipo de contenido: application/json\n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant, deber\u00e1s usar la acci\u00f3n \"Hacer una solicitud web\" del [applet IFTTT Webhook]({applet_url}). \n\nCompleta la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n- Tipo de contenido: application/json \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar automatizaciones para manejar los datos entrantes." }, "step": { "user": { "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?", - "title": "Configurar el applet de webhook IFTTT" + "title": "Configurar el applet IFTTT Webhook" } } } diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 768c590151e..730277dd5c0 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -6,7 +6,7 @@ "single_instance_allowed": "Ya esta configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "No se conect\u00f3 al m\u00f3dem Insteon, por favor, int\u00e9ntelo de nuevo.", + "cannot_connect": "No se pudo conectar", "select_single": "Seleccione una opci\u00f3n." }, "flow_title": "{name}", @@ -36,7 +36,7 @@ "data": { "device": "Ruta del dispositivo USB" }, - "description": "Configure el M\u00f3dem Insteon PowerLink (PLM).", + "description": "Configura el M\u00f3dem Insteon PowerLink (PLM).", "title": "Insteon PLM" }, "user": { diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index f6948374561..a5a067b5ffd 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo ya configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "connection_upgrade": "No se pudo conectar con la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n.", "ipp_error": "Error IPP encontrado.", @@ -13,7 +13,7 @@ "cannot_connect": "No se pudo conectar", "connection_upgrade": "No se pudo conectar con la impresora. Int\u00e9ntalo de nuevo con la opci\u00f3n SSL/TLS marcada." }, - "flow_title": "Impresora: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/iqvia/translations/es.json b/homeassistant/components/iqvia/translations/es.json index cecbb7af591..dc26ca6c065 100644 --- a/homeassistant/components/iqvia/translations/es.json +++ b/homeassistant/components/iqvia/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Este c\u00f3digo postal ya ha sido configurado." + "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { "invalid_zip_code": "El c\u00f3digo postal no es v\u00e1lido" diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index dd5e97923d6..4c3c4475743 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -41,8 +41,8 @@ "sensor_string": "Cadena Nodo Sensor", "variable_sensor_string": "Cadena de Sensor Variable" }, - "description": "Configura las opciones para la integraci\u00f3n de ISY: \n \u2022 Cadena Nodo Sensor: Cualquier dispositivo o carpeta que contenga 'Cadena Nodo Sensor' en el nombre ser\u00e1 tratada como un sensor o un sensor binario. \n \u2022 Ignorar Cadena: Cualquier dispositivo con 'Ignorar Cadena' en el nombre ser\u00e1 ignorado. \n \u2022 Restaurar Intensidad de la Luz: Si se habilita, la intensidad anterior ser\u00e1 restaurada al encender una luz en lugar de usar el nivel predeterminado del dispositivo.", - "title": "Opciones ISY994" + "description": "Configura las opciones para la integraci\u00f3n ISY:\n\u2022 Cadena de sensor de nodo: Cualquier dispositivo o carpeta que contenga 'Cadena de sensor de nodo' en el nombre se tratar\u00e1 como un sensor o un sensor binario.\n\u2022 Ignorar Cadena: Cualquier dispositivo con 'Ignorar Cadena' en el nombre ser\u00e1 ignorado.\n\u2022 Cadena de sensor variable: cualquier variable que contenga 'Cadena de sensor variable' se a\u00f1adir\u00e1 como sensor.\n\u2022 Restaurar brillo de luz: si est\u00e1 habilitado, el brillo anterior se restaurar\u00e1 al encender una luz en lugar del nivel de encendido integrado del dispositivo.", + "title": "Opciones ISY" } } }, diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json index 290d7aa53ee..0b7b37a6e11 100644 --- a/homeassistant/components/kodi/translations/es.json +++ b/homeassistant/components/kodi/translations/es.json @@ -12,7 +12,7 @@ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { @@ -37,7 +37,7 @@ "data": { "ws_port": "Puerto" }, - "description": "El puerto WebSocket (a veces llamado puerto TCP en Kodi). Para conectarse a trav\u00e9s de WebSocket, necesitas habilitar \"Permitir a los programas... controlar Kodi\" en Sistema/Configuraci\u00f3n/Red/Servicios. Si el WebSocket no est\u00e1 habilitado, elimine el puerto y d\u00e9jelo vac\u00edo." + "description": "El puerto WebSocket (a veces llamado puerto TCP en Kodi). Para conectar a trav\u00e9s de WebSocket, debes habilitar \"Permitir que los programas... controlen Kodi\" en Sistema/Configuraci\u00f3n/Red/Servicios. Si WebSocket no est\u00e1 habilitado, elimina el puerto y d\u00e9jalo vac\u00edo." } } }, diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 901c858b33c..7ba726e8e6a 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "not_konn_panel": "No es un dispositivo Konnected.io reconocido", - "unknown": "Se produjo un error desconocido" + "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar" @@ -39,19 +39,19 @@ "options_binary": { "data": { "inverse": "Invertir el estado de apertura/cierre", - "name": "Nombre (opcional)", + "name": "Nombre", "type": "Tipo de sensor binario" }, - "description": "Seleccione las opciones para el sensor binario conectado a {zone}", + "description": "Opciones de {zone}", "title": "Configurar sensor binario" }, "options_digital": { "data": { - "name": "Nombre (opcional)", - "poll_interval": "Intervalo de sondeo (minutos) (opcional)", + "name": "Nombre", + "poll_interval": "Intervalo de sondeo (minutos)", "type": "Tipo de sensor" }, - "description": "Seleccione las opciones para el sensor digital conectado a {zone}", + "description": "Opciones de {zone}", "title": "Configurar el sensor digital" }, "options_io": { @@ -84,8 +84,8 @@ }, "options_misc": { "data": { - "api_host": "Sustituye la URL del host de la API (opcional)", - "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado", + "api_host": "Anular la URL de la API del host", + "blink": "Parpadear el LED del panel al enviar un cambio de estado", "discovery": "Responde a las solicitudes de descubrimiento en tu red", "override_api_host": "Reemplazar la URL predeterminada del panel host de la API de Home Assistant" }, @@ -95,13 +95,13 @@ "options_switch": { "data": { "activation": "Salida cuando est\u00e1 activada", - "momentary": "Duraci\u00f3n del pulso (ms) (opcional)", + "momentary": "Duraci\u00f3n del pulso (ms)", "more_states": "Configurar estados adicionales para esta zona", - "name": "Nombre (opcional)", - "pause": "Pausa entre pulsos (ms) (opcional)", - "repeat": "Tiempos de repetici\u00f3n (-1 = infinito) (opcional)" + "name": "Nombre", + "pause": "Pausa entre pulsos (ms)", + "repeat": "Veces que se repite (-1=infinito)" }, - "description": "Selecciona las opciones de salida para {zone}: state {state}", + "description": "Opciones de {zone}: estado {state}", "title": "Configurar la salida conmutable" } } diff --git a/homeassistant/components/locative/translations/es.json b/homeassistant/components/locative/translations/es.json index e3c63d7b35d..676c1863b2e 100644 --- a/homeassistant/components/locative/translations/es.json +++ b/homeassistant/components/locative/translations/es.json @@ -6,11 +6,11 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar ubicaciones a Home Assistant, es necesario configurar la caracter\u00edstica webhook en la app de Locative.\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nRevisa [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar ubicaciones a Home Assistant, es necesario configurar la caracter\u00edstica webhook en la app de Locative.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nConsulta [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar el webhook de Locative?", + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Configurar el webhook de Locative" } } diff --git a/homeassistant/components/logi_circle/translations/es.json b/homeassistant/components/logi_circle/translations/es.json index 7fe62a28d05..ac1fdc8dc5a 100644 --- a/homeassistant/components/logi_circle/translations/es.json +++ b/homeassistant/components/logi_circle/translations/es.json @@ -2,18 +2,18 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "external_error": "Se produjo una excepci\u00f3n de otro flujo.", - "external_setup": "Logi Circle se ha configurado correctamente a partir de otro flujo.", + "external_error": "Ocurri\u00f3 una excepci\u00f3n de otro flujo.", + "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." }, "error": { "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de presionar Enviar.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "auth": { - "description": "Accede al siguiente enlace y Acepta el acceso a tu cuenta Logi Circle, despu\u00e9s vuelve y pulsa en Enviar a continuaci\u00f3n.\n\n[Link]({authorization_url})", + "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Logi Circle, luego regresa y presiona **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", "title": "Autenticaci\u00f3n con Logi Circle" }, "user": { diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index d13fded562e..c6e90ec6364 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -15,7 +15,7 @@ "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.", + "description": "Para emparejar con {name} ({host}), despu\u00e9s de enviar este formulario, presiona el bot\u00f3n negro en la parte posterior del puente.", "title": "Emparejar con el puente" }, "user": { diff --git a/homeassistant/components/mailgun/translations/es.json b/homeassistant/components/mailgun/translations/es.json index 1fcd54a5266..85007655134 100644 --- a/homeassistant/components/mailgun/translations/es.json +++ b/homeassistant/components/mailgun/translations/es.json @@ -6,7 +6,7 @@ "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 debes configurar los [Webhooks en Mailgun]({mailgun_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n - Tipo de contenido: application/json \n\n Consulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Mailgun]({mailgun_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/json \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." }, "step": { "user": { diff --git a/homeassistant/components/melcloud/translations/es.json b/homeassistant/components/melcloud/translations/es.json index be4f6cabe6c..94583ed2589 100644 --- a/homeassistant/components/melcloud/translations/es.json +++ b/homeassistant/components/melcloud/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_auth": "Autentificaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/meteo_france/translations/es.json b/homeassistant/components/meteo_france/translations/es.json index cd6d2d80812..a16c47e4aa9 100644 --- a/homeassistant/components/meteo_france/translations/es.json +++ b/homeassistant/components/meteo_france/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", - "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde" + "unknown": "Error inesperado" }, "error": { "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, comprueba el campo de la ciudad" diff --git a/homeassistant/components/mikrotik/translations/es.json b/homeassistant/components/mikrotik/translations/es.json index d4751c19a9a..b1cd62a1763 100644 --- a/homeassistant/components/mikrotik/translations/es.json +++ b/homeassistant/components/mikrotik/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Conexi\u00f3n fallida", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "name_exists": "El nombre ya existe" }, diff --git a/homeassistant/components/minecraft_server/translations/es.json b/homeassistant/components/minecraft_server/translations/es.json index 1c3891d3fb5..a5c5ae531d9 100644 --- a/homeassistant/components/minecraft_server/translations/es.json +++ b/homeassistant/components/minecraft_server/translations/es.json @@ -15,7 +15,7 @@ "name": "Nombre" }, "description": "Configura tu instancia de Minecraft Server para permitir la supervisi\u00f3n.", - "title": "Enlace su servidor Minecraft" + "title": "Vincula tu servidor Minecraft" } } } diff --git a/homeassistant/components/mobile_app/translations/es.json b/homeassistant/components/mobile_app/translations/es.json index 8ac5c909e17..adb7b4212a9 100644 --- a/homeassistant/components/mobile_app/translations/es.json +++ b/homeassistant/components/mobile_app/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Abre la aplicaci\u00f3n en el m\u00f3vil para configurar la integraci\u00f3n con Home Assistant. Echa un vistazo a [la documentaci\u00f3n]({apps_url}) para ver una lista de apps compatibles." + "install_app": "Abre la aplicaci\u00f3n m\u00f3vil para configurar la integraci\u00f3n con Home Assistant. Consulta [la documentaci\u00f3n]({apps_url}) para obtener una lista de aplicaciones compatibles." }, "step": { "confirm": { diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index e316f882712..c7469a93820 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -8,7 +8,7 @@ "error": { "discovery_error": "No se pudo descubrir un detector de movimiento" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 915af215a26..4c88a5a66f7 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo se permite una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "No se puede conectar" + "cannot_connect": "No se pudo conectar" }, "step": { "broker": { @@ -16,13 +16,13 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT." + "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu br\u00f3ker MQTT." }, "hassio_confirm": { "data": { "discovery": "Habilitar descubrimiento" }, - "description": "\u00bfQuieres configurar Home Assistant para conectarse al agente MQTT proporcionado por el complemento {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse al agente MQTT proporcionado por el complemento {addon}?", "title": "Br\u00f3ker MQTT a trav\u00e9s de complemento de Home Assistant" } } diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index a6b81fbcbbc..9f20a31ff15 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "MyQ ya est\u00e1 configurado", + "already_configured": "El servicio ya est\u00e1 configurado", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 00ba28474ac..12ae1b235ba 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -16,7 +16,7 @@ "step": { "link": { "description": "Mant\u00e9n presionado el bot\u00f3n de encendido de tu Nanoleaf durante 5 segundos hasta que los LED del bot\u00f3n comiencen a parpadear, luego haz clic en **ENVIAR** en los siguientes 30 segundos.", - "title": "Enlazar Nanoleaf" + "title": "Vincular Nanoleaf" }, "user": { "data": { diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 36b5d64e253..93dd7eb12a2 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -5,7 +5,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", @@ -70,7 +70,7 @@ "data": { "code": "C\u00f3digo PIN" }, - "description": "Para vincular tu cuenta de Nest, [autoriza tu cuenta]({url}).\n\nDespu\u00e9s de la autorizaci\u00f3n, copia y pega el c\u00f3digo pin a continuaci\u00f3n.", + "description": "Para vincular tu cuenta Nest, [autoriza tu cuenta]({url}). \n\nDespu\u00e9s de la autorizaci\u00f3n, copia y pega el c\u00f3digo PIN proporcionado a continuaci\u00f3n.", "title": "Vincular cuenta de Nest" }, "pick_implementation": { diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index c658c0046a1..0d8e4167ab8 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -47,10 +47,10 @@ "public_weather": { "data": { "area_name": "Nombre del \u00e1rea", - "lat_ne": "Longitud esquina noreste", - "lat_sw": "Latitud esquina suroeste", - "lon_ne": "Longitud esquina noreste", - "lon_sw": "Longitud esquina Suroeste", + "lat_ne": "Latitud Esquina noreste", + "lat_sw": "Latitud Esquina suroeste", + "lon_ne": "Longitud Esquina noreste", + "lon_sw": "Longitud Esquina suroeste", "mode": "C\u00e1lculo", "show_on_map": "Mostrar en el mapa" }, diff --git a/homeassistant/components/nexia/translations/es.json b/homeassistant/components/nexia/translations/es.json index ee89d9db6f5..9e9e720dc15 100644 --- a/homeassistant/components/nexia/translations/es.json +++ b/homeassistant/components/nexia/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Este nexia home ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", @@ -13,7 +13,7 @@ "data": { "brand": "Marca", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/nightscout/translations/es.json b/homeassistant/components/nightscout/translations/es.json index d98b8e345b6..ab9ce8baa02 100644 --- a/homeassistant/components/nightscout/translations/es.json +++ b/homeassistant/components/nightscout/translations/es.json @@ -14,7 +14,7 @@ "api_key": "Clave API", "url": "URL" }, - "description": "- URL: la direcci\u00f3n de tu instancia de nightscout. Por ejemplo: https://myhomeassistant.duckdns.org:5423 \n- Clave API (opcional): util\u00edzala s\u00f3lo si tu instancia est\u00e1 protegida (auth_default_roles! = readable).", + "description": "- URL: la direcci\u00f3n de tu instancia nightscout. Por ejemplo: https://myhomeassistant.duckdns.org:5423\n- Clave de API (opcional): usar solo si tu instancia est\u00e1 protegida (auth_default_roles != legible).", "title": "Introduce la informaci\u00f3n del servidor de Nightscout." } } diff --git a/homeassistant/components/nws/translations/es.json b/homeassistant/components/nws/translations/es.json index 4e7732c062d..522e601d050 100644 --- a/homeassistant/components/nws/translations/es.json +++ b/homeassistant/components/nws/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "station": "C\u00f3digo de estaci\u00f3n METAR" }, - "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, se utilizar\u00e1n la latitud y la longitud para encontrar la estaci\u00f3n m\u00e1s cercana.", + "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, la latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n m\u00e1s cercana. Por ahora, una clave API puede ser cualquier cosa. Se recomienda utilizar una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida.", "title": "Conectar con el National Weather Service" } } diff --git a/homeassistant/components/onewire/translations/es.json b/homeassistant/components/onewire/translations/es.json index 986a36a30d4..81a4d564d9c 100644 --- a/homeassistant/components/onewire/translations/es.json +++ b/homeassistant/components/onewire/translations/es.json @@ -12,7 +12,7 @@ "host": "Host", "port": "Puerto" }, - "title": "Configurar 1 cable" + "title": "Establecer detalles del servidor" } } }, diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 7858a4561fe..aa1d40e3f14 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ONVIF ya est\u00e1 configurado.", - "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ONVIF ya est\u00e1 en marcha.", + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifique la configuraci\u00f3n del perfil en su dispositivo.", "no_mac": "No se pudo configurar una identificaci\u00f3n \u00fanica para el dispositivo ONVIF.", "onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Comprueba el registro para m\u00e1s informaci\u00f3n." diff --git a/homeassistant/components/owntracks/translations/es.json b/homeassistant/components/owntracks/translations/es.json index dc420a806f4..55f5a45dd54 100644 --- a/homeassistant/components/owntracks/translations/es.json +++ b/homeassistant/components/owntracks/translations/es.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { - "default": "\n\nEn Android, abre [la aplicaci\u00f3n OwnTracks]({android_url}), ve a preferencias -> conexi\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP privado\n - Host: {webhook_url}\n - Identificaci\u00f3n:\n - Nombre de usuario: \n - ID de dispositivo: \n\nEn iOS, abre [la aplicaci\u00f3n OwnTracks] ({ios_url}), pulsa el icono (i) en la parte superior izquierda -> configuraci\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP\n - URL: {webhook_url}\n - Activar la autenticaci\u00f3n\n - UserID: \n\n{secret}\n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s informaci\u00f3n." + "default": "\n\nEn Android, abre [la aplicaci\u00f3n OwnTracks]({android_url}), ve a preferencias -> conexi\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP privado\n - Host: {webhook_url}\n - Identificaci\u00f3n:\n - Nombre de usuario: \n - ID de dispositivo: \n\nEn iOS, abre [la aplicaci\u00f3n OwnTracks]({ios_url}), pulsa el icono (i) en la parte superior izquierda -> configuraci\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP\n - URL: {webhook_url}\n - Activar la autenticaci\u00f3n\n - UserID: \n\n{secret}\n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s informaci\u00f3n." }, "step": { "user": { diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index 35597af49f2..dca8e8c7f98 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -19,9 +19,9 @@ "location": "Ubicaci\u00f3n", "name": "Nombre", "port": "Puerto", - "ssl": "Usar SSL", + "ssl": "Utiliza un certificado SSL", "statistics_only": "S\u00f3lo las estad\u00edsticas", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index 693c6bf99c7..df1ffb99cb1 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/translations/es.json @@ -10,7 +10,7 @@ "default": "\u00a1El dispositivo Plaato {device_type} con nombre **{device_name}** se ha configurado correctamente!" }, "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", + "invalid_webhook_device": "Has seleccionado un dispositivo que no admite el env\u00edo de datos a un webhook. Solo est\u00e1 disponible para el 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" }, diff --git a/homeassistant/components/plant/translations/es.json b/homeassistant/components/plant/translations/es.json index 957a07a1d51..da9470e5ea0 100644 --- a/homeassistant/components/plant/translations/es.json +++ b/homeassistant/components/plant/translations/es.json @@ -5,5 +5,5 @@ "problem": "Problema" } }, - "title": "Planta" + "title": "Monitor de planta" } \ No newline at end of file diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 8071e9e11fe..0b030c2a958 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -22,7 +22,7 @@ "host": "Host", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "token": "Token (Opcional)", + "token": "Token (opcional)", "verify_ssl": "Verificar el certificado SSL" }, "title": "Configuraci\u00f3n Manual de Plex" @@ -35,7 +35,7 @@ "title": "Seleccione el servidor Plex" }, "user": { - "description": "Continuar hacia [plex.tv](https://plex.tv) para vincular un servidor Plex." + "description": "Contin\u00faa hacia [plex.tv](https://plex.tv) para vincular un servidor Plex." }, "user_advanced": { "data": { diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 1a0ae166aaa..570184aaabf 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -20,8 +20,8 @@ "port": "Puerto", "username": "Nombre de usuario de Smile" }, - "description": "Producto:", - "title": "Conectarse a Smile" + "description": "Por favor, introduce", + "title": "Conectar a Smile" }, "user_gateway": { "data": { @@ -30,7 +30,7 @@ "port": "Puerto", "username": "Nombre de usuario Smile" }, - "description": "Por favor introduce:", + "description": "Por favor, introduce:", "title": "Conectarse a Smile" } } diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index 3d1f47c2ceb..2e1de2f1f6f 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -11,12 +11,12 @@ "default": "Autenticado correctamente" }, "error": { - "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de presionar Enviar", "no_token": "Token de acceso no v\u00e1lido" }, "step": { "auth": { - "description": "Accede al siguiente enlace y Acepta el acceso a tu cuenta Minut, despu\u00e9s vuelve y pulsa en Enviar a continuaci\u00f3n.\n\n[Link]({authorization_url})", + "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Minut, luego regresa y presiona **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", "title": "Autenticaci\u00f3n con Point" }, "user": { diff --git a/homeassistant/components/profiler/translations/es.json b/homeassistant/components/profiler/translations/es.json index a39a0a24d2a..d5552505c73 100644 --- a/homeassistant/components/profiler/translations/es.json +++ b/homeassistant/components/profiler/translations/es.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar el Profiler?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } } diff --git a/homeassistant/components/ps4/translations/es.json b/homeassistant/components/ps4/translations/es.json index 3655b72110c..fd1283ccfd8 100644 --- a/homeassistant/components/ps4/translations/es.json +++ b/homeassistant/components/ps4/translations/es.json @@ -3,19 +3,19 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "credential_error": "Error al obtener las credenciales.", - "no_devices_found": "No se encuentran dispositivos PlayStation 4 en la red.", - "port_987_bind_error": "No se ha podido unir al puerto 987. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para m\u00e1s informaci\u00f3n.", - "port_997_bind_error": "No se ha podido unir al puerto 997. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para m\u00e1s informaci\u00f3n." + "no_devices_found": "No se encontraron dispositivos en la red", + "port_987_bind_error": "No se pudo vincular al puerto 987. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para obtener informaci\u00f3n adicional.", + "port_997_bind_error": "No se pudo vincular al puerto 997. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para obtener informaci\u00f3n adicional." }, "error": { "cannot_connect": "No se pudo conectar", - "credential_timeout": "Se agot\u00f3 el tiempo para el servicio de credenciales. Pulsa enviar para reiniciar.", - "login_failed": "No se ha podido vincular con PlayStation 4. Verifica que el PIN sea correcto.", - "no_ipaddress": "Introduce la direcci\u00f3n IP de la PlayStation 4 que quieres configurar." + "credential_timeout": "Se agot\u00f3 el tiempo de espera del servicio de credenciales. Presiona enviar para reiniciar.", + "login_failed": "No se pudo emparejar con PlayStation 4. Verifica que el C\u00f3digo PIN sea correcto.", + "no_ipaddress": "Introduce la Direcci\u00f3n IP de la PlayStation 4 que deseas configurar." }, "step": { "creds": { - "description": "Credenciales necesarias. Pulsa 'Enviar' y, a continuaci\u00f3n, en la app de segunda pantalla de PS4, actualiza la lista de dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar." + "description": "Credenciales necesarias. Presiona 'Enviar' y luego en la aplicaci\u00f3n PS4 Second Screen, actualiza los dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar." }, "link": { "data": { @@ -25,12 +25,12 @@ "region": "Regi\u00f3n" }, "data_description": { - "code": "Ve a 'Configuraci\u00f3n' en tu consola PlayStation 4. Luego navega hasta 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y selecciona 'Agregar dispositivo' para obtener el PIN." + "code": "Navega a 'Configuraci\u00f3n' en tu consola PlayStation 4. Luego navega hasta 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y selecciona 'Agregar dispositivo' para obtener el PIN." } }, "mode": { "data": { - "ip_address": "Direcci\u00f3n IP (d\u00e9jalo en blanco si usas la detecci\u00f3n autom\u00e1tica).", + "ip_address": "Direcci\u00f3n IP (D\u00e9jalo en blanco si usas la detecci\u00f3n autom\u00e1tica).", "mode": "Modo configuraci\u00f3n" }, "data_description": { diff --git a/homeassistant/components/rachio/translations/es.json b/homeassistant/components/rachio/translations/es.json index f3821b7aa5c..671a00a334c 100644 --- a/homeassistant/components/rachio/translations/es.json +++ b/homeassistant/components/rachio/translations/es.json @@ -13,7 +13,7 @@ "data": { "api_key": "Clave API" }, - "description": "Necesitar\u00e1s la clave API de https://app.rach.io/. Selecciona 'Account Settings' y luego haz clic en 'GET API KEY'.", + "description": "Necesitar\u00e1s la clave API de https://app.rach.io/. Ve a Configuraci\u00f3n, luego haz clic en 'GET API KEY'.", "title": "Conectar a tu dispositivo Rachio" } } diff --git a/homeassistant/components/renault/translations/es.json b/homeassistant/components/renault/translations/es.json index 1d8b5f447e3..75a0ca557c5 100644 --- a/homeassistant/components/renault/translations/es.json +++ b/homeassistant/components/renault/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "kamereon_no_account": "No se pudo encontrar la cuenta de Kamereon.", + "kamereon_no_account": "No se puede encontrar la cuenta de Kamereon", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json index 997a6b0a0bf..aa567d0093d 100644 --- a/homeassistant/components/rfxtrx/translations/es.json +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_configured": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "cannot_connect": "No se pudo conectar" }, "error": { diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json index 6f85fe0c4ab..8d0c0460a3c 100644 --- a/homeassistant/components/risco/translations/es.json +++ b/homeassistant/components/risco/translations/es.json @@ -32,7 +32,7 @@ }, "init": { "data": { - "code_arm_required": "Requiere un c\u00f3digo PIN para armar", + "code_arm_required": "Requiere un C\u00f3digo PIN para armar", "code_disarm_required": "Requiere un c\u00f3digo PIN para desactivar", "scan_interval": "Con qu\u00e9 frecuencia sondear Risco (en segundos)" }, diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index f2664baee31..cc2954335b6 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -9,17 +9,17 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "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" + "description": "Mant\u00e9n presionado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (alrededor de dos segundos), luego haz clic en enviar en los siguientes 30 segundos.", + "title": "Recuperar 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}", + "description": "La contrase\u00f1a no se pudo recuperar del dispositivo autom\u00e1ticamente. Sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", "title": "Escribe la contrase\u00f1a" }, "manual": { @@ -33,7 +33,7 @@ "data": { "host": "Host" }, - "description": "Actualmente recuperar el BLID y la contrase\u00f1a es un proceso manual. Sigue los pasos descritos en la documentaci\u00f3n en: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "Seleccione una Roomba o Braava.", "title": "Conexi\u00f3n autom\u00e1tica con el dispositivo" } } diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 70bd9f30b9b..9a5c8c5fa51 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -6,7 +6,7 @@ "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", - "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible.", + "not_supported": "Este dispositivo Samsung no es actualmente compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/schedule/translations/el.json b/homeassistant/components/schedule/translations/el.json new file mode 100644 index 00000000000..2c6a17ab298 --- /dev/null +++ b/homeassistant/components/schedule/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u03a0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/fr.json b/homeassistant/components/schedule/translations/fr.json new file mode 100644 index 00000000000..e7327a3fffa --- /dev/null +++ b/homeassistant/components/schedule/translations/fr.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" + } + }, + "title": "Planification" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/hu.json b/homeassistant/components/schedule/translations/hu.json new file mode 100644 index 00000000000..44b70c0497b --- /dev/null +++ b/homeassistant/components/schedule/translations/hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Ki", + "on": "Be" + } + }, + "title": "Id\u0151z\u00edt\u00e9s" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/pt-BR.json b/homeassistant/components/schedule/translations/pt-BR.json new file mode 100644 index 00000000000..f6aa495fc69 --- /dev/null +++ b/homeassistant/components/schedule/translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Desligado", + "on": "Ligado" + } + }, + "title": "Hor\u00e1rio" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/zh-Hant.json b/homeassistant/components/schedule/translations/zh-Hant.json new file mode 100644 index 00000000000..84612eacc84 --- /dev/null +++ b/homeassistant/components/schedule/translations/zh-Hant.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + } + }, + "title": "\u6392\u7a0b" +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/es.json b/homeassistant/components/sentry/translations/es.json index 0bb2f70720c..058cc27642f 100644 --- a/homeassistant/components/sentry/translations/es.json +++ b/homeassistant/components/sentry/translations/es.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" } } } diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 85394365c1a..9d344f83992 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -10,7 +10,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, - "flow_title": "Shelly: {name}", + "flow_title": "{name}", "step": { "confirm_discovery": { "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte, puedes activar manualmente el dispositivo ahora con un bot\u00f3n o esperar la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." @@ -25,7 +25,7 @@ "data": { "host": "Host" }, - "description": "Antes de configurarlo, el dispositivo que funciona con bater\u00eda debe despertarse presionando el bot\u00f3n del dispositivo." + "description": "Antes de configurar, los dispositivos alimentados por bater\u00eda deben despertarse, puede despertar el dispositivo ahora con un bot\u00f3n." } } }, @@ -42,7 +42,7 @@ "btn_up": "Bot\u00f3n {subtype} soltado", "double": "Pulsaci\u00f3n doble de {subtype}", "double_push": "Pulsaci\u00f3n doble de {subtype}", - "long": "Pulsaci\u00f3n larga de {subtype}", + "long": "{subtype} con pulsaci\u00f3n larga", "long_push": "Pulsaci\u00f3n larga de {subtype}", "long_single": "Pulsaci\u00f3n larga de {subtype} seguida de una pulsaci\u00f3n simple", "single": "Pulsaci\u00f3n simple de {subtype}", diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index b47b3870d46..1558f96fa4f 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -33,7 +33,7 @@ "data": { "auth_code": "C\u00f3digo de Autorizaci\u00f3n", "password": "Contrase\u00f1a", - "username": "Correo electr\u00f3nico" + "username": "Nombre de usuario" }, "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\nCuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." } diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index 808eeffbbcd..1a9e2ee28dd 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -9,7 +9,7 @@ "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smartthings/translations/es.json b/homeassistant/components/smartthings/translations/es.json index f354e3914e0..8aee6337cef 100644 --- a/homeassistant/components/smartthings/translations/es.json +++ b/homeassistant/components/smartthings/translations/es.json @@ -9,7 +9,7 @@ "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", - "webhook_error": "SmartThings no ha podido validar el endpoint configurado en 'base_url'. Por favor, revisa los requisitos del componente." + "webhook_error": "SmartThings no pudo validar la URL del webhook. Aseg\u00farate de que la URL del webhook sea accesible desde Internet y vuelve a intentarlo." }, "step": { "authorize": { @@ -30,7 +30,7 @@ "title": "Seleccionar Ubicaci\u00f3n" }, "user": { - "description": "Los SmartThings se configurar\u00e1n para enviar las actualizaciones a Home Assistant en:\n> {webhook_url}\n\nSi esto no es correcto, por favor, actualiza tu configuraci\u00f3n, reinicia Home Assistant e int\u00e9ntalo de nuevo.", + "description": "SmartThings se configurar\u00e1 para enviar actualizaciones a Home Assistant en:\n> {webhook_url}\n\nSi esto no es correcto, por favor, actualiza tu configuraci\u00f3n, reinicia Home Assistant e int\u00e9ntalo de nuevo.", "title": "Confirmar URL de devoluci\u00f3n de llamada" } } diff --git a/homeassistant/components/soma/translations/es.json b/homeassistant/components/soma/translations/es.json index 0ae2d26adec..0e2312bd335 100644 --- a/homeassistant/components/soma/translations/es.json +++ b/homeassistant/components/soma/translations/es.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", + "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "connection_error": "No se ha podido conectar a SOMA Connect.", + "connection_error": "No se pudo conectar", "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n.", "result_error": "SOMA Connect respondi\u00f3 con un error." }, diff --git a/homeassistant/components/somfy_mylink/translations/es.json b/homeassistant/components/somfy_mylink/translations/es.json index 8647f7bb8fd..4e59c634e67 100644 --- a/homeassistant/components/somfy_mylink/translations/es.json +++ b/homeassistant/components/somfy_mylink/translations/es.json @@ -8,7 +8,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { @@ -16,7 +16,7 @@ "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." + "description": "El ID del sistema se puede obtener en la aplicaci\u00f3n MyLink en Integraci\u00f3n seleccionando cualquier servicio que no sea en la nube." } } }, diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index 440ab7bf21f..3d8fc33de59 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -9,10 +9,10 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "La integraci\u00f3n de Sonarr necesita volver a autenticarse manualmente con la API de Sonarr alojada en: {host}", + "description": "La integraci\u00f3n de Sonarr debe volver a autenticarse manualmente con la API de Sonarr alojada en: {url}", "title": "Reautenticaci\u00f3n de la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/songpal/translations/es.json b/homeassistant/components/songpal/translations/es.json index fa62eb3ba1e..153d107eacd 100644 --- a/homeassistant/components/songpal/translations/es.json +++ b/homeassistant/components/songpal/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "Dispositivo ya configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "not_songpal_device": "No es un dispositivo Songpal" }, "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "\u00bfQuieres configurar {name} ({host})?" diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index 42f263971ba..90fe06ccf55 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar SpeedTest?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index 09de377d195..377c2a9df58 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_account_mismatch": "La cuenta de Spotify con la que est\u00e1s autenticado, no coincide con la cuenta necesaria para re-autenticaci\u00f3n." @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "La integraci\u00f3n de Spotify necesita volver a autenticarse con Spotify para la cuenta: {account}", - "title": "Volver a autenticar con Spotify" + "title": "Volver a autenticar la integraci\u00f3n" } } }, diff --git a/homeassistant/components/sql/translations/es.json b/homeassistant/components/sql/translations/es.json index 322485ae446..427510a3f1d 100644 --- a/homeassistant/components/sql/translations/es.json +++ b/homeassistant/components/sql/translations/es.json @@ -12,7 +12,7 @@ "data": { "column": "Columna", "db_url": "URL de la base de datos", - "name": "[%key:component::sql::config::step::user::data::name%]", + "name": "Nombre", "query": "Selecciona la consulta", "unit_of_measurement": "Unidad de medida", "value_template": "Plantilla de valor" @@ -38,7 +38,7 @@ "data": { "column": "Columna", "db_url": "URL de la base de datos", - "name": "[%key:component::sql::config::step::user::data::name%]", + "name": "Nombre", "query": "Selecciona la consulta", "unit_of_measurement": "Unidad de medida", "value_template": "Plantilla de valor" diff --git a/homeassistant/components/starline/translations/es.json b/homeassistant/components/starline/translations/es.json index eff1da9773a..75ded3aaef2 100644 --- a/homeassistant/components/starline/translations/es.json +++ b/homeassistant/components/starline/translations/es.json @@ -11,7 +11,7 @@ "app_id": "ID de la aplicaci\u00f3n", "app_secret": "Secreto" }, - "description": "ID de la aplicaci\u00f3n y c\u00f3digo secreto de la cuenta de desarrollador de StarLine", + "description": "ID de aplicaci\u00f3n y c\u00f3digo secreto de [cuenta de desarrollador StarLine](https://my.starline.ru/developer)", "title": "Credenciales de la aplicaci\u00f3n" }, "auth_captcha": { diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index 585dd3ccbd4..d38179c0702 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -9,6 +9,12 @@ }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "password": { + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae {name} \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, "user": { "data": { "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index 41d3d369115..af0d17d7d39 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -24,7 +24,7 @@ "data": { "password": "Contrase\u00f1a", "port": "Puerto", - "ssl": "Usar SSL/TLS para conectar con tu NAS", + "ssl": "Utiliza un certificado SSL", "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" }, @@ -42,8 +42,8 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "ssl": "Usar SSL/TLS para conectar con tu NAS", - "username": "Usuario", + "ssl": "Utiliza un certificado SSL", + "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" } } diff --git a/homeassistant/components/tado/translations/es.json b/homeassistant/components/tado/translations/es.json index 58745783774..42090cccb15 100644 --- a/homeassistant/components/tado/translations/es.json +++ b/homeassistant/components/tado/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "no_homes": "No hay casas asociadas a esta cuenta de Tado", + "no_homes": "No hay casas vinculadas a esta cuenta Tado", "unknown": "Error inesperado" }, "step": { @@ -25,7 +25,7 @@ "data": { "fallback": "Elige el modo alternativo." }, - "description": "El modo de salvaguarda volver\u00e1 a la Planificaci\u00f3n Inteligente en el siguiente cambio de programaci\u00f3n despu\u00e9s de ajustar manualmente una zona.", + "description": "El modo de respaldo te permite elegir cu\u00e1ndo retroceder a Smart Schedule desde tu superposici\u00f3n de zona manual. (NEXT_TIME_BLOCK:= Cambiar en el pr\u00f3ximo cambio de Smart Schedule; MANUAL:= No cambiar hasta que canceles; TADO_DEFAULT:= Cambiar seg\u00fan tu configuraci\u00f3n en la aplicaci\u00f3n Tado).", "title": "Ajustar las opciones de Tado" } } diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index 5971c89f43e..fbda5550ac3 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", - "unknown": "Se produjo un error desconocido", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "unknown": "Error inesperado", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "error": { @@ -11,14 +11,13 @@ }, "step": { "auth": { - "description": "Para vincular tu cuenta de Telldus Live:\n 1. Pulsa el siguiente enlace\n 2. Inicia sesi\u00f3n en Telldus Live\n 3. Autoriza **{app_name}** (pulsa en **Yes**).\n 4. Vuelve atr\u00e1s y pulsa **ENVIAR**.\n\n [Link TelldusLive account]({auth_url})", + "description": "Para vincular tu cuenta de Telldus Live:\n1. Pulsa el siguiente enlace\n2. Inicia sesi\u00f3n en Telldus Live\n3. Autoriza **{app_name}** (pulsa en **Yes**).\n4. Vuelve atr\u00e1s y pulsa **ENVIAR**.\n\n[Enlace a la cuenta TelldusLive]({auth_url})", "title": "Autenticaci\u00f3n contra TelldusLive" }, "user": { "data": { "host": "Host" }, - "description": "Vac\u00edo", "title": "Elige el punto final." } } diff --git a/homeassistant/components/tibber/translations/es.json b/homeassistant/components/tibber/translations/es.json index 0840339ac24..53c0c2aaab7 100644 --- a/homeassistant/components/tibber/translations/es.json +++ b/homeassistant/components/tibber/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Una cuenta de Tibber ya est\u00e1 configurada." + "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/tradfri/translations/es.json b/homeassistant/components/tradfri/translations/es.json index 500b357319c..6ed8b96a986 100644 --- a/homeassistant/components/tradfri/translations/es.json +++ b/homeassistant/components/tradfri/translations/es.json @@ -6,8 +6,8 @@ }, "error": { "cannot_authenticate": "No se puede autenticar, \u00bfest\u00e1 la puerta de enlace emparejada con otro servidor como, por ejemplo, Homekit?", - "cannot_connect": "No se puede conectar a la puerta de enlace.", - "invalid_key": "No se ha podido registrar con la clave proporcionada. Si esto sigue ocurriendo, intenta reiniciar el gateway.", + "cannot_connect": "No se pudo conectar", + "invalid_key": "No se pudo registrar con la clave proporcionada. Si esto sigue ocurriendo, intenta reiniciar la puerta de enlace.", "timeout": "Tiempo de espera agotado validando el c\u00f3digo." }, "step": { @@ -16,8 +16,8 @@ "host": "Host", "security_code": "C\u00f3digo de seguridad" }, - "description": "Puedes encontrar el c\u00f3digo de seguridad en la parte posterior de tu gateway.", - "title": "Introduzca el c\u00f3digo de seguridad" + "description": "Puedes encontrar el c\u00f3digo de seguridad en la parte posterior de tu puerta de enlace.", + "title": "Introduce el c\u00f3digo de seguridad" } } } diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 6c293a59d4c..7ae9bfa87fa 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "name_exists": "El nombre ya existe" + "name_exists": "Nombre ya existente" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index ca46860a208..55ab464c3c2 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -9,9 +9,9 @@ "data": { "access_id": "ID de acceso a Tuya IoT", "access_secret": "Secreto de acceso a Tuya IoT", - "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", + "country_code": "Pa\u00eds", "password": "Contrase\u00f1a", - "username": "Nombre de usuario" + "username": "Cuenta" }, "description": "Introduce tus credencial de Tuya" } diff --git a/homeassistant/components/twilio/translations/es.json b/homeassistant/components/twilio/translations/es.json index 41057c5f4a0..3743b0ee163 100644 --- a/homeassistant/components/twilio/translations/es.json +++ b/homeassistant/components/twilio/translations/es.json @@ -6,11 +6,11 @@ "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 debes configurar los [Webhooks en Twilio]({twilio_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n - Tipo de contenido: application/x-www-form-urlencoded \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Twilio]({twilio_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/x-www-form-urlencoded \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." }, "step": { "user": { - "description": "\u00bfQuieres empezar la configuraci\u00f3n?", + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Configurar el Webhook de Twilio" } } diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index c6d17b369b4..ff9b9575b78 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "El sitio del controlador ya est\u00e1 configurado", - "configuration_updated": "Configuraci\u00f3n actualizada.", + "already_configured": "El sitio UniFi Network ya est\u00e1 configurado", + "configuration_updated": "Configuraci\u00f3n actualizada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "faulty_credentials": "Autenticaci\u00f3n no v\u00e1lida", - "service_unavailable": "Error al conectar", + "service_unavailable": "No se pudo conectar", "unknown_client_mac": "Ning\u00fan cliente disponible en esa direcci\u00f3n MAC" }, - "flow_title": "Red UniFi {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { @@ -19,9 +19,9 @@ "port": "Puerto", "site": "ID del sitio", "username": "Nombre de usuario", - "verify_ssl": "Controlador usando el certificado adecuado" + "verify_ssl": "Verificar el certificado SSL" }, - "title": "Configuraci\u00f3n de UniFi Network" + "title": "Configurar UniFi Network" } } }, @@ -37,7 +37,7 @@ "poe_clients": "Permitir control PoE de clientes" }, "description": "Configurar controles de cliente\n\nCrea conmutadores para los n\u00fameros de serie para los que deseas controlar el acceso a la red.", - "title": "Opciones UniFi 2/3" + "title": "Opciones de UniFi Network 2/3" }, "device_tracker": { "data": { @@ -49,7 +49,7 @@ "track_wired_clients": "Incluir clientes de red cableada" }, "description": "Configurar dispositivo de seguimiento", - "title": "Opciones UniFi 1/3" + "title": "Opciones de UniFi Network 1/3" }, "simple_options": { "data": { diff --git a/homeassistant/components/upnp/translations/es.json b/homeassistant/components/upnp/translations/es.json index 9a8a9e0160b..60e9ebaadf9 100644 --- a/homeassistant/components/upnp/translations/es.json +++ b/homeassistant/components/upnp/translations/es.json @@ -1,14 +1,10 @@ { "config": { "abort": { - "already_configured": "UPnP / IGD ya est\u00e1 configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "incomplete_discovery": "Descubrimiento incompleto", "no_devices_found": "No se encontraron dispositivos en la red" }, - "error": { - "one": "UNO", - "other": "OTRO" - }, "flow_title": "{name}", "step": { "ssdp_confirm": { diff --git a/homeassistant/components/vera/translations/es.json b/homeassistant/components/vera/translations/es.json index 8299b2ea6c9..8d9686b0a13 100644 --- a/homeassistant/components/vera/translations/es.json +++ b/homeassistant/components/vera/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "No se pudo conectar con el controlador con url {base_url}" + "cannot_connect": "No se pudo conectar al controlador con URL {base_url}" }, "step": { "user": { diff --git a/homeassistant/components/vizio/translations/es.json b/homeassistant/components/vizio/translations/es.json index d3ba768e933..6ac88586cad 100644 --- a/homeassistant/components/vizio/translations/es.json +++ b/homeassistant/components/vizio/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "updated_entry": "Esta entrada ya ha sido configurada pero el nombre y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n previamente importada, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia." + "updated_entry": "Esta entrada ya se configur\u00f3, pero el nombre, las aplicaciones y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n importada anteriormente, por lo que la entrada de configuraci\u00f3n se actualiz\u00f3 en consecuencia." }, "error": { "cannot_connect": "No se pudo conectar", @@ -19,11 +19,11 @@ "title": "Completar Proceso de Emparejamiento" }, "pairing_complete": { - "description": "El dispositivo VIZIO SmartCast ahora est\u00e1 conectado a Home Assistant.", + "description": "Tu VIZIO SmartCast Device ahora est\u00e1 conectado a Home Assistant.", "title": "Emparejamiento Completado" }, "pairing_complete_import": { - "description": "El dispositivo VIZIO SmartCast TV ahora est\u00e1 conectado a Home Assistant.\n\nEl token de acceso es '**{access_token}**'.", + "description": "Tu VIZIO SmartCast Device ahora est\u00e1 conectado a Home Assistant. \n\nTu Token de acceso es '**{access_token}**'.", "title": "Emparejamiento Completado" }, "user": { @@ -33,7 +33,7 @@ "host": "Host", "name": "Nombre" }, - "description": "El token de acceso solo se necesita para las televisiones. Si est\u00e1s configurando una televisi\u00f3n y a\u00fan no tienes un token de acceso, d\u00e9jalo en blanco para iniciar el proceso de sincronizaci\u00f3n.", + "description": "Solo se necesita un Token de acceso para TVs. Si est\u00e1s configurando una TV y a\u00fan no tienes un Token de acceso, d\u00e9jalo en blanco para pasar por un proceso de emparejamiento.", "title": "VIZIO SmartCast Device" } } @@ -47,7 +47,7 @@ "volume_step": "Tama\u00f1o del paso de volumen" }, "description": "Si tienes un Smart TV, opcionalmente puedes filtrar su lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en su lista de fuentes.", - "title": "Actualizar las opciones de SmartCast de Vizo" + "title": "Actualizar las opciones de VIZIO SmartCast Device" } } } diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index 1303ca27690..d90f1ccacc3 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -21,12 +21,12 @@ "data": { "profile": "Nombre de perfil" }, - "description": "\u00bfQu\u00e9 perfil seleccion\u00f3 en el sitio web de Withings? Es importante que los perfiles coincidan, de lo contrario los datos se etiquetar\u00e1n incorrectamente.", + "description": "Proporciona un nombre de perfil \u00fanico para estos datos. Por lo general, este es el nombre del perfil que seleccionaste en el paso anterior.", "title": "Perfil de usuario." }, "reauth": { "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", - "title": "Re-autentificar el perfil" + "title": "Volver a autenticar la integraci\u00f3n" }, "reauth_confirm": { "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index 7a428960d62..0547b1598e3 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Este dispositivo WLED ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "cct_unsupported": "Este dispositivo WLED utiliza canales CCT, que no son compatibles con esta integraci\u00f3n" }, diff --git a/homeassistant/components/wolflink/translations/es.json b/homeassistant/components/wolflink/translations/es.json index 359a2d0b27e..601026f784a 100644 --- a/homeassistant/components/wolflink/translations/es.json +++ b/homeassistant/components/wolflink/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -18,7 +18,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conexi\u00f3n WOLF SmartSet" } diff --git a/homeassistant/components/wolflink/translations/sensor.es.json b/homeassistant/components/wolflink/translations/sensor.es.json index 98c6b5bd242..e321ecb72f4 100644 --- a/homeassistant/components/wolflink/translations/sensor.es.json +++ b/homeassistant/components/wolflink/translations/sensor.es.json @@ -28,9 +28,9 @@ "frost_heizkreis": "Escarcha del circuito de calefacci\u00f3n", "frost_warmwasser": "Heladas de DHW", "frostschutz": "Protecci\u00f3n contra las heladas", - "gasdruck": "Presion del gas", + "gasdruck": "Presi\u00f3n del gas", "glt_betrieb": "Modo BMS", - "gradienten_uberwachung": "Monitoreo de gradiente", + "gradienten_uberwachung": "Supervisi\u00f3n de gradiente", "heizbetrieb": "Modo de calefacci\u00f3n", "heizgerat_mit_speicher": "Caldera con cilindro", "heizung": "Calefacci\u00f3n", @@ -59,7 +59,7 @@ "schornsteinfeger": "Prueba de emisiones", "smart_grid": "SmartGrid", "smart_home": "SmartHome", - "softstart": "Arranque suave.", + "softstart": "Arranque suave", "solarbetrieb": "Modo solar", "sparbetrieb": "Modo econ\u00f3mico", "sparen": "Econom\u00eda", diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index d8a36306af7..41d3ffa8207 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -12,13 +12,13 @@ "invalid_key": "Clave del gateway inv\u00e1lida", "invalid_mac": "Direcci\u00f3n Mac no v\u00e1lida" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { "select_ip": "Direcci\u00f3n IP" }, - "description": "Ejecuta la configuraci\u00f3n de nuevo si deseas conectar gateways adicionales" + "description": "Selecciona la puerta de enlace Xiaomi Aqara que deseas conectar" }, "settings": { "data": { @@ -34,7 +34,7 @@ "interface": "La interfaz de la red a usar", "mac": "Direcci\u00f3n Mac (opcional)" }, - "description": "Con\u00e9ctate a tu Xiaomi Aqara Gateway, si las direcciones IP y mac se dejan vac\u00edas, se utiliza el auto-descubrimiento." + "description": "Si las direcciones IP y MAC se dejan vac\u00edas, se utiliza la detecci\u00f3n autom\u00e1tica" } } } diff --git a/homeassistant/components/yalexs_ble/translations/el.json b/homeassistant/components/yalexs_ble/translations/el.json new file mode 100644 index 00000000000..2a0243266be --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/el.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "no_unconfigured_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2." + }, + "error": { + "invalid_key_format": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte.", + "invalid_key_index": "\u0397 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0 \u03ba\u03b1\u03b9 255." + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} \u03bc\u03ad\u03c3\u03c9 Bluetooth \u03bc\u03b5 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {address};" + }, + "user": { + "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 Bluetooth", + "key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte)" + }, + "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {docs_url} \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json index ef93c17e72c..6ec5b64a3fd 100644 --- a/homeassistant/components/yeelight/translations/es.json +++ b/homeassistant/components/yeelight/translations/es.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Modelo (opcional)", + "model": "Modelo", "nightlight_switch": "Usar interruptor de luz nocturna", "save_on_change": "Guardar estado al cambiar", "transition": "Tiempo de transici\u00f3n (ms)", From 58883feaf69f5204c73812283fc7509ce5cb3c0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 21:55:48 -1000 Subject: [PATCH 328/903] Small cleanups to Yale Access Bluetooth (#76691) - Abort the discovery flow if the user has already started interacting with a user flow or bluetooth discovery - Remove docs_url from the flow - Fix useless return --- .../components/yalexs_ble/config_flow.py | 44 ++++++---- homeassistant/components/yalexs_ble/lock.py | 4 +- .../components/yalexs_ble/strings.json | 3 +- .../yalexs_ble/translations/en.json | 3 +- .../components/yalexs_ble/test_config_flow.py | 80 +++++++++++++++++++ 5 files changed, 113 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/yalexs_ble/config_flow.py b/homeassistant/components/yalexs_ble/config_flow.py index 7f632ebfab0..c7213eefbe9 100644 --- a/homeassistant/components/yalexs_ble/config_flow.py +++ b/homeassistant/components/yalexs_ble/config_flow.py @@ -16,7 +16,7 @@ from yalexs_ble import ( ) from yalexs_ble.const import YALE_MFR_ID -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, @@ -25,7 +25,6 @@ from homeassistant.const import CONF_ADDRESS from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import DiscoveryInfoType -from homeassistant.loader import async_get_integration from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN from .util import async_get_service_info, human_readable_name @@ -85,39 +84,52 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovery_info["key"], discovery_info["slot"], ) + + address = lock_cfg.address + local_name = lock_cfg.local_name + hass = self.hass + # We do not want to raise on progress as integration_discovery takes # precedence over other discovery flows since we already have the keys. - await self.async_set_unique_id(lock_cfg.address, raise_on_progress=False) + # + # After we do discovery we will abort the flows that do not have the keys + # below unless the user is already setting them up. + await self.async_set_unique_id(address, raise_on_progress=False) new_data = {CONF_KEY: lock_cfg.key, CONF_SLOT: lock_cfg.slot} self._abort_if_unique_id_configured(updates=new_data) for entry in self._async_current_entries(): if entry.data.get(CONF_LOCAL_NAME) == lock_cfg.local_name: - if self.hass.config_entries.async_update_entry( + if hass.config_entries.async_update_entry( entry, data={**entry.data, **new_data} ): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) + hass.async_create_task( + hass.config_entries.async_reload(entry.entry_id) ) raise AbortFlow(reason="already_configured") try: self._discovery_info = await async_get_service_info( - self.hass, lock_cfg.local_name, lock_cfg.address + hass, local_name, address ) except asyncio.TimeoutError: return self.async_abort(reason="no_devices_found") + # Integration discovery should abort other flows unless they + # are already in the process of being set up since this discovery + # will already have all the keys and the user can simply confirm. for progress in self._async_in_progress(include_uninitialized=True): - # Integration discovery should abort other discovery types - # since it already has the keys and slots, and the other - # discovery types do not. context = progress["context"] if ( - not context.get("active") - and context.get("local_name") == lock_cfg.local_name - or context.get("unique_id") == lock_cfg.address - ): - self.hass.config_entries.flow.async_abort(progress["flow_id"]) + local_name_is_unique(local_name) + and context.get("local_name") == local_name + ) or context.get("unique_id") == address: + if context.get("active"): + # The user has already started interacting with this flow + # and entered the keys. We abort the discovery flow since + # we assume they do not want to use the discovered keys for + # some reason. + raise data_entry_flow.AbortFlow("already_in_progress") + hass.config_entries.flow.async_abort(progress["flow_id"]) self._lock_cfg = lock_cfg self.context["title_placeholders"] = { @@ -228,12 +240,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_SLOT): int, } ) - integration = await async_get_integration(self.hass, DOMAIN) return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors, - description_placeholders={"docs_url": integration.documentation}, ) diff --git a/homeassistant/components/yalexs_ble/lock.py b/homeassistant/components/yalexs_ble/lock.py index 3f75a282f67..9e97c2f080f 100644 --- a/homeassistant/components/yalexs_ble/lock.py +++ b/homeassistant/components/yalexs_ble/lock.py @@ -55,8 +55,8 @@ class YaleXSBLELock(YALEXSBLEEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" - return await self._device.unlock() + await self._device.unlock() async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" - return await self._device.lock() + await self._device.lock() diff --git a/homeassistant/components/yalexs_ble/strings.json b/homeassistant/components/yalexs_ble/strings.json index 4d867474dbe..df5ac713b07 100644 --- a/homeassistant/components/yalexs_ble/strings.json +++ b/homeassistant/components/yalexs_ble/strings.json @@ -3,7 +3,7 @@ "flow_title": "{name}", "step": { "user": { - "description": "Check the documentation at {docs_url} for how to find the offline key.", + "description": "Check the documentation for how to find the offline key.", "data": { "address": "Bluetooth address", "key": "Offline Key (32-byte hex string)", @@ -22,6 +22,7 @@ "invalid_key_index": "The offline key slot must be an integer between 0 and 255." }, "abort": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_unconfigured_devices": "No unconfigured devices found.", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" diff --git a/homeassistant/components/yalexs_ble/translations/en.json b/homeassistant/components/yalexs_ble/translations/en.json index 6d817499270..f8ebc0737ac 100644 --- a/homeassistant/components/yalexs_ble/translations/en.json +++ b/homeassistant/components/yalexs_ble/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", "no_devices_found": "No devices found on the network", "no_unconfigured_devices": "No unconfigured devices found." }, @@ -23,7 +24,7 @@ "key": "Offline Key (32-byte hex string)", "slot": "Offline Key Slot (Integer between 0 and 255)" }, - "description": "Check the documentation at {docs_url} for how to find the offline key." + "description": "Check the documentation for how to find the offline key." } } } diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index 7607b710934..6ea1b4e8a63 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -752,3 +752,83 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_ if flow["handler"] == DOMAIN ] assert len(flows) == 1 + + +async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle( + hass: HomeAssistant, +) -> None: + """Test that the user is setting up the lock and waiting for validation and the keys get discovered. + + In this case the integration discovery should abort and let the user continue setting up the lock. + """ + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[NOT_YALE_DISCOVERY_INFO, YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + user_flow_event = asyncio.Event() + valdidate_started = asyncio.Event() + + async def _wait_for_user_flow(): + valdidate_started.set() + await user_flow_event.wait() + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + side_effect=_wait_for_user_flow, + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + user_flow_task = asyncio.create_task( + hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + ) + await valdidate_started.wait() + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ): + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert discovery_result["type"] == FlowResultType.ABORT + assert discovery_result["reason"] == "already_in_progress" + + user_flow_event.set() + user_flow_result = await user_flow_task + + assert user_flow_result["type"] == FlowResultType.CREATE_ENTRY + assert user_flow_result["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert user_flow_result["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert ( + user_flow_result["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + ) + assert len(mock_setup_entry.mock_calls) == 1 From c9c84639ade69a8648fdff97f24d0b3ec9d243c0 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 13 Aug 2022 10:30:39 +0200 Subject: [PATCH 329/903] Remove `charging_time_label` sensor in BMW Connected Drive (#76616) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/sensor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index f1046881ed3..26fbe19b5b1 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -72,11 +72,6 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, ), - "charging_time_label": BMWSensorEntityDescription( - key="charging_time_label", - key_class="fuel_and_battery", - entity_registry_enabled_default=False, - ), "charging_status": BMWSensorEntityDescription( key="charging_status", key_class="fuel_and_battery", From e44b1fa98c9cd1df051dd709ddbf88e0a126a473 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 13 Aug 2022 10:32:58 +0200 Subject: [PATCH 330/903] Bump nettigo-air-monitor to 1.4.2 (#76670) --- homeassistant/components/nam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nam/fixtures/diagnostics_data.json | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 88048b59162..b70c2054808 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -3,7 +3,7 @@ "name": "Nettigo Air Monitor", "documentation": "https://www.home-assistant.io/integrations/nam", "codeowners": ["@bieniu"], - "requirements": ["nettigo-air-monitor==1.3.0"], + "requirements": ["nettigo-air-monitor==1.4.2"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 9ec008d1a45..ce8a44fc23c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1091,7 +1091,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.3.0 +nettigo-air-monitor==1.4.2 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09d654157e5..02b8258cdca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -778,7 +778,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.3.0 +nettigo-air-monitor==1.4.2 # homeassistant.components.nexia nexia==2.0.2 diff --git a/tests/components/nam/fixtures/diagnostics_data.json b/tests/components/nam/fixtures/diagnostics_data.json index b90e51f34b8..1506adaa824 100644 --- a/tests/components/nam/fixtures/diagnostics_data.json +++ b/tests/components/nam/fixtures/diagnostics_data.json @@ -11,11 +11,15 @@ "heca_humidity": 50.0, "heca_temperature": 8.0, "mhz14a_carbon_dioxide": 865, + "sds011_caqi": 19, + "sds011_caqi_level": "very low", "sds011_p1": 19, "sds011_p2": 11, "sht3x_humidity": 34.7, "sht3x_temperature": 6.3, "signal": -72, + "sps30_caqi": 54, + "sps30_caqi_level": "medium", "sps30_p0": 31, "sps30_p1": 21, "sps30_p2": 34, From 9181b0171a82c31d179dcad7c6e9bd8e1714df97 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 13 Aug 2022 10:35:13 +0200 Subject: [PATCH 331/903] Bump pyoverkiz to 1.5.0 (#76682) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 6e6e57f12e5..f2d17bab3f9 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.4.2"], + "requirements": ["pyoverkiz==1.5.0"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index ce8a44fc23c..a76e6aa0cc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1742,7 +1742,7 @@ pyotgw==2.0.2 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.4.2 +pyoverkiz==1.5.0 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02b8258cdca..2b1b691b9bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1213,7 +1213,7 @@ pyotgw==2.0.2 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.4.2 +pyoverkiz==1.5.0 # homeassistant.components.openweathermap pyowm==3.2.0 From 93a80d8fc36e115fe33c1fa8aace43f3f56d08a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 22:36:30 -1000 Subject: [PATCH 332/903] Bump yalexs-ble to 1.4.0 (#76685) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index aa3cdcd592b..4bc21e17d3a 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.3.1"], + "requirements": ["yalexs-ble==1.4.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index a76e6aa0cc4..058218dcefb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.3.1 +yalexs-ble==1.4.0 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b1b691b9bc..875ad6e7faa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1695,7 +1695,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.3.1 +yalexs-ble==1.4.0 # homeassistant.components.august yalexs==1.2.1 From bcc0a92e2b447571f90b01b941427762b4827b17 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 13 Aug 2022 14:39:04 +0200 Subject: [PATCH 333/903] Motion Blinds fix OperationNotAllowed (#76712) fix OperationNotAllowed homeassistant.config_entries.OperationNotAllowed --- homeassistant/components/motion_blinds/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index dfbc6ab74a7..a023fc05d14 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -121,8 +121,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: multicast_interface = entry.data.get(CONF_INTERFACE, DEFAULT_INTERFACE) wait_for_push = entry.options.get(CONF_WAIT_FOR_PUSH, DEFAULT_WAIT_FOR_PUSH) - entry.async_on_unload(entry.add_update_listener(update_listener)) - # Create multicast Listener async with setup_lock: if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]: @@ -213,6 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True From b969ed00b97d9d752770cacccb0f9df12aedc45f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 13 Aug 2022 06:02:32 -0700 Subject: [PATCH 334/903] Fix google calendar disabled entity handling (#76699) Fix google calendar disable entity handling --- homeassistant/components/google/calendar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index ca98b3da087..77ed922e511 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -192,7 +192,6 @@ async def async_setup_entry( calendar_id, data.get(CONF_SEARCH), ) - await coordinator.async_config_entry_first_refresh() entities.append( GoogleCalendarEntity( coordinator, @@ -342,6 +341,9 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() + # We do not ask for an update with async_add_entities() + # because it will update disabled entities + await self.coordinator.async_request_refresh() self._apply_coordinator_update() async def async_get_events( From db03c273f1a84617f1cee62002561a329ff03ff6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 13 Aug 2022 15:17:49 +0200 Subject: [PATCH 335/903] Netgear skip devices withouth mac (#76626) skip devices withouth mac --- homeassistant/components/netgear/router.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index ae4186abbb5..8e370a6b5e0 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -198,6 +198,9 @@ class NetgearRouter: _LOGGER.debug("Netgear scan result: \n%s", ntg_devices) for ntg_device in ntg_devices: + if ntg_device.mac is None: + continue + device_mac = format_mac(ntg_device.mac) if not self.devices.get(device_mac): From a70252a24322129ccdb3595327c637b9df4efb42 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 13 Aug 2022 16:05:23 +0200 Subject: [PATCH 336/903] Log not allowed attributes only once in BMW binary sensors (#76708) * Log not allowed keys only once * Fix spelling Co-authored-by: Jan Bouwhuis Co-authored-by: rikroe Co-authored-by: Jan Bouwhuis --- .../bmw_connected_drive/binary_sensor.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 96e8a1fc0e4..d7e28eef41c 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -37,8 +37,10 @@ ALLOWED_CONDITION_BASED_SERVICE_KEYS = { "VEHICLE_CHECK", "VEHICLE_TUV", } +LOGGED_CONDITION_BASED_SERVICE_WARNINGS = set() ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {"ENGINE_OIL", "TIRE_PRESSURE"} +LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS = set() def _condition_based_services( @@ -46,12 +48,16 @@ def _condition_based_services( ) -> dict[str, Any]: extra_attributes = {} for report in vehicle.condition_based_services.messages: - if report.service_type not in ALLOWED_CONDITION_BASED_SERVICE_KEYS: + if ( + report.service_type not in ALLOWED_CONDITION_BASED_SERVICE_KEYS + and report.service_type not in LOGGED_CONDITION_BASED_SERVICE_WARNINGS + ): _LOGGER.warning( "'%s' not an allowed condition based service (%s)", report.service_type, report, ) + LOGGED_CONDITION_BASED_SERVICE_WARNINGS.add(report.service_type) continue extra_attributes.update(_format_cbs_report(report, unit_system)) @@ -61,17 +67,19 @@ def _condition_based_services( def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]: extra_attributes: dict[str, Any] = {} for message in vehicle.check_control_messages.messages: - if message.description_short not in ALLOWED_CHECK_CONTROL_MESSAGE_KEYS: + if ( + message.description_short not in ALLOWED_CHECK_CONTROL_MESSAGE_KEYS + and message.description_short not in LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS + ): _LOGGER.warning( "'%s' not an allowed check control message (%s)", message.description_short, message, ) + LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS.add(message.description_short) continue - extra_attributes.update( - {message.description_short.lower(): message.state.value} - ) + extra_attributes[message.description_short.lower()] = message.state.value return extra_attributes From cf7c716bda484a497573c65963f696be8c294f69 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:46:08 +0200 Subject: [PATCH 337/903] Fix implicit Optional [core] (#76719) --- .../components/application_credentials/__init__.py | 2 +- homeassistant/components/mobile_app/helpers.py | 6 +++--- homeassistant/components/mqtt/mixins.py | 2 +- homeassistant/components/template/template_entity.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index 14ae049cfca..6dd2d562307 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -168,7 +168,7 @@ async def async_import_client_credential( hass: HomeAssistant, domain: str, credential: ClientCredential, - auth_domain: str = None, + auth_domain: str | None = None, ) -> None: """Import an existing credential from configuration.yaml.""" if DOMAIN not in hass.data: diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index e1c0841984f..a116e6196f4 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -117,7 +117,7 @@ def registration_context(registration: dict) -> Context: def empty_okay_response( - headers: dict = None, status: HTTPStatus = HTTPStatus.OK + headers: dict | None = None, status: HTTPStatus = HTTPStatus.OK ) -> Response: """Return a Response with empty JSON object and a 200.""" return Response( @@ -129,7 +129,7 @@ def error_response( code: str, message: str, status: HTTPStatus = HTTPStatus.BAD_REQUEST, - headers: dict = None, + headers: dict | None = None, ) -> Response: """Return an error Response.""" return json_response( @@ -177,7 +177,7 @@ def webhook_response( *, registration: dict, status: HTTPStatus = HTTPStatus.OK, - headers: dict = None, + headers: dict | None = None, ) -> Response: """Return a encrypted response if registration supports it.""" data = json.dumps(data, cls=JSONEncoder) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index f0cc0c4ad44..4b7ae92c6f4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -287,7 +287,7 @@ async def async_discover_yaml_entities( async def async_get_platform_config_from_yaml( hass: HomeAssistant, platform_domain: str, - config_yaml: ConfigType = None, + config_yaml: ConfigType | None = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 901834237c6..87c8ec651d2 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -79,7 +79,7 @@ LEGACY_FIELDS = { def rewrite_common_legacy_to_modern_conf( - entity_cfg: dict[str, Any], extra_legacy_fields: dict[str, str] = None + entity_cfg: dict[str, Any], extra_legacy_fields: dict[str, str] | None = None ) -> dict[str, Any]: """Rewrite legacy config.""" entity_cfg = {**entity_cfg} From 5a046ae7be68f0e8c6e8e1aae8b82f030ba7959b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:46:34 +0200 Subject: [PATCH 338/903] Fix implicit Optional [a-n] (#76720) --- homeassistant/components/climacell/config_flow.py | 4 +++- homeassistant/components/directv/config_flow.py | 2 +- homeassistant/components/escea/climate.py | 2 +- homeassistant/components/harmony/subscriber.py | 5 ++++- homeassistant/components/huisbaasje/sensor.py | 2 +- homeassistant/components/ipp/config_flow.py | 2 +- homeassistant/components/izone/climate.py | 2 +- homeassistant/components/minecraft_server/__init__.py | 2 +- homeassistant/components/motioneye/__init__.py | 2 +- homeassistant/components/motioneye/camera.py | 8 ++++---- homeassistant/components/netgear/router.py | 6 +++--- 11 files changed, 21 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index ffc76479a4d..07b85e4a4ab 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -19,7 +19,9 @@ class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): """Initialize ClimaCell options flow.""" self._config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the ClimaCell options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 34a09a04811..b3209638012 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -100,7 +100,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_ssdp_confirm() async def async_step_ssdp_confirm( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a confirmation flow initiated by SSDP.""" if user_input is None: diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py index 7bd7d54353c..1ddea9cb026 100644 --- a/homeassistant/components/escea/climate.py +++ b/homeassistant/components/escea/climate.py @@ -152,7 +152,7 @@ class ControllerEntity(ClimateEntity): ) @callback - def set_available(self, available: bool, ex: Exception = None) -> None: + def set_available(self, available: bool, ex: Exception | None = None) -> None: """Set availability for the controller.""" if self._attr_available == available: return diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index efb46f5c6e6..092eb0d6859 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -1,4 +1,5 @@ """Mixin class for handling harmony callback subscriptions.""" +from __future__ import annotations import asyncio import logging @@ -85,7 +86,9 @@ class HarmonySubscriberMixin: self.async_unlock_start_activity() self._call_callbacks("activity_started", activity_info) - def _call_callbacks(self, callback_func_name: str, argument: tuple = None) -> None: + def _call_callbacks( + self, callback_func_name: str, argument: tuple | None = None + ) -> None: for subscription in self._subscriptions: current_callback = getattr(subscription, callback_func_name) if current_callback: diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 2f7f607d493..c963f366323 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -42,7 +42,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): user_id: str, name: str, source_type: str, - device_class: str = None, + device_class: str | None = None, sensor_type: str = SENSOR_TYPE_RATE, unit_of_measurement: str = POWER_WATT, icon: str = "mdi:lightning-bolt", diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 432094eee8e..7f953c4cd9a 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -174,7 +174,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_zeroconf_confirm() async def async_step_zeroconf_confirm( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index a26490e78c8..58834f995dd 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -216,7 +216,7 @@ class ControllerDevice(ClimateEntity): return self._available @callback - def set_available(self, available: bool, ex: Exception = None) -> None: + def set_available(self, available: bool, ex: Exception | None = None) -> None: """ Set availability for the controller. diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 4abfbca9a2f..b2f7698d969 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -150,7 +150,7 @@ class MinecraftServer: ) self.online = False - async def async_update(self, now: datetime = None) -> None: + async def async_update(self, now: datetime | None = None) -> None: """Get server data from 3rd party library and update properties.""" # Check connection status. server_online_old = self.online diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 7c87dda1bd2..6a650142995 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -544,7 +544,7 @@ class MotionEyeEntity(CoordinatorEntity): client: MotionEyeClient, coordinator: DataUpdateCoordinator, options: MappingProxyType[str, Any], - entity_description: EntityDescription = None, + entity_description: EntityDescription | None = None, ) -> None: """Initialize a motionEye entity.""" self._camera_id = camera[KEY_ID] diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index e5e4f224fe6..ff825b43bf7 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -266,10 +266,10 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): async def async_set_text_overlay( self, - left_text: str = None, - right_text: str = None, - custom_left_text: str = None, - custom_right_text: str = None, + left_text: str | None = None, + right_text: str | None = None, + custom_left_text: str | None = None, + custom_right_text: str | None = None, ) -> None: """Set text overlay for a camera.""" # Fetch the very latest camera config to reduce the risk of updating with a diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 8e370a6b5e0..f69e88e83e2 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -42,9 +42,9 @@ _LOGGER = logging.getLogger(__name__) def get_api( password: str, - host: str = None, - username: str = None, - port: int = None, + host: str | None = None, + username: str | None = None, + port: int | None = None, ssl: bool = False, ) -> Netgear: """Get the Netgear API and login to it.""" From 5db1fec99e7397ed8276a8f4b25c012638afc707 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:46:49 +0200 Subject: [PATCH 339/903] Fix implicit Optional [p-s] (#76721) --- homeassistant/components/plaato/config_flow.py | 2 +- homeassistant/components/sia/config_flow.py | 14 ++++++++++---- homeassistant/components/sia/hub.py | 2 +- homeassistant/components/solax/config_flow.py | 6 +++++- homeassistant/components/switchbot/config_flow.py | 4 +++- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 637122b1d9c..654150ffa48 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -127,7 +127,7 @@ class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def _show_api_method_form( - self, device_type: PlaatoDeviceType, errors: dict = None + self, device_type: PlaatoDeviceType, errors: dict | None = None ): data_schema = vol.Schema({vol.Optional(CONF_TOKEN, default=""): str}) diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index df03882e995..516018c43a7 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -105,7 +105,9 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data: dict[str, Any] = {} self._options: Mapping[str, Any] = {CONF_ACCOUNTS: {}} - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial user step.""" errors: dict[str, str] | None = None if user_input is not None: @@ -117,7 +119,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_handle_data_and_route(user_input) async def async_step_add_account( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the additional accounts steps.""" errors: dict[str, str] | None = None @@ -179,7 +181,9 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.hub: SIAHub | None = None self.accounts_todo: list = [] - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the SIA options.""" self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] assert self.hub is not None @@ -187,7 +191,9 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] return await self.async_step_options() - async def async_step_options(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_options( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Create the options step for a account.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index 399da14c2ad..2c2fb0d2be9 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -68,7 +68,7 @@ class SIAHub: self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.async_shutdown) ) - async def async_shutdown(self, _: Event = None) -> None: + async def async_shutdown(self, _: Event | None = None) -> None: """Shutdown the SIA server.""" await self.sia_client.stop() diff --git a/homeassistant/components/solax/config_flow.py b/homeassistant/components/solax/config_flow.py index 56c6989cc7f..e3255a8e377 100644 --- a/homeassistant/components/solax/config_flow.py +++ b/homeassistant/components/solax/config_flow.py @@ -1,4 +1,6 @@ """Config flow for solax integration.""" +from __future__ import annotations + import logging from typing import Any @@ -40,7 +42,9 @@ async def validate_api(data) -> str: class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Solax.""" - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors: dict[str, Any] = {} if user_input is None: diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 0d7e91648f2..af2f43bdaa0 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -94,7 +94,9 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_confirm(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm a single device.""" assert self._discovered_adv is not None if user_input is not None: From 67e339c67beb7e1d6e4613536edcfffd0281b23f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:47:17 +0200 Subject: [PATCH 340/903] Fix implicit Optional [t-z] (#76722) --- .../components/tomorrowio/config_flow.py | 12 ++++++++--- homeassistant/components/toon/config_flow.py | 2 +- homeassistant/components/tuya/base.py | 2 +- homeassistant/components/vizio/config_flow.py | 20 ++++++++++++------- .../components/yamaha_musiccast/__init__.py | 2 +- .../components/yamaha_musiccast/number.py | 2 +- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py index a0fa7b0b34c..71df06394ed 100644 --- a/homeassistant/components/tomorrowio/config_flow.py +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -45,7 +45,9 @@ _LOGGER = logging.getLogger(__name__) def _get_config_schema( - hass: core.HomeAssistant, source: str | None, input_dict: dict[str, Any] = None + hass: core.HomeAssistant, + source: str | None, + input_dict: dict[str, Any] | None = None, ) -> vol.Schema: """ Return schema defaults for init step based on user input/config dict. @@ -99,7 +101,9 @@ class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow): """Initialize Tomorrow.io options flow.""" self._config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Tomorrow.io options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -134,7 +138,9 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return TomorrowioOptionsConfigFlow(config_entry) - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index e86d951069c..5c98e35bead 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -64,7 +64,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self.async_step_user() async def async_step_agreement( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Select Toon agreement to add.""" if len(self.agreements) == 1: diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 624bfd80cd4..f27101e373d 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -189,7 +189,7 @@ class TuyaEntity(Entity): dpcodes: str | DPCode | tuple[DPCode, ...] | None, *, prefer_function: bool = False, - dptype: DPType = None, + dptype: DPType | None = None, ) -> DPCode | EnumTypeData | IntegerTypeData | None: """Find a matching DP code available on for this device.""" if dpcodes is None: diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index a80105579fe..54e4ef53fe1 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -49,7 +49,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _get_config_schema(input_dict: dict[str, Any] = None) -> vol.Schema: +def _get_config_schema(input_dict: dict[str, Any] | None = None) -> vol.Schema: """ Return schema defaults for init step based on user input/config dict. @@ -81,7 +81,7 @@ def _get_config_schema(input_dict: dict[str, Any] = None) -> vol.Schema: ) -def _get_pairing_schema(input_dict: dict[str, Any] = None) -> vol.Schema: +def _get_pairing_schema(input_dict: dict[str, Any] | None = None) -> vol.Schema: """ Return schema defaults for pairing data based on user input. @@ -112,7 +112,9 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): """Initialize vizio options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the vizio options.""" if user_input is not None: if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): @@ -204,7 +206,9 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=input_dict[CONF_NAME], data=input_dict) - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -381,7 +385,9 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def async_step_pair_tv(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_pair_tv( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """ Start pairing process for TV. @@ -460,7 +466,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_pairing_complete( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """ Complete non-import sourced config flow. @@ -470,7 +476,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._pairing_complete("pairing_complete") async def async_step_pairing_complete_import( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """ Complete import sourced config flow. diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index b8117df056a..639f0b69a41 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -216,7 +216,7 @@ class MusicCastCapabilityEntity(MusicCastDeviceEntity): self, coordinator: MusicCastDataUpdateCoordinator, capability: Capability, - zone_id: str = None, + zone_id: str | None = None, ) -> None: """Initialize a capability based entity.""" if zone_id is not None: diff --git a/homeassistant/components/yamaha_musiccast/number.py b/homeassistant/components/yamaha_musiccast/number.py index 98cda92ffea..105cb0edb3a 100644 --- a/homeassistant/components/yamaha_musiccast/number.py +++ b/homeassistant/components/yamaha_musiccast/number.py @@ -42,7 +42,7 @@ class NumberCapability(MusicCastCapabilityEntity, NumberEntity): self, coordinator: MusicCastDataUpdateCoordinator, capability: NumberSetter, - zone_id: str = None, + zone_id: str | None = None, ) -> None: """Initialize the number entity.""" super().__init__(coordinator, capability, zone_id) From 4fc1d59b74238c307f3f4059f795f11fd5f64274 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:55:09 +0200 Subject: [PATCH 341/903] Bump actions/cache from 3.0.6 to 3.0.7 (#76648) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ae7d8cfd48d..e4a47ca2b03 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: cache: "pip" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -185,7 +185,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -211,7 +211,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -222,7 +222,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -260,7 +260,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -271,7 +271,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -312,7 +312,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -323,7 +323,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -353,7 +353,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -364,7 +364,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -480,7 +480,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -488,7 +488,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PIP_CACHE }} key: >- @@ -538,7 +538,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -570,7 +570,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -603,7 +603,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -647,7 +647,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -695,7 +695,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -749,7 +749,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From bac44cf473f84c1923a1181e4c5851257e873b83 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 19:33:57 +0200 Subject: [PATCH 342/903] Enable no_implicit_optional globally [mypy] (#76723) --- mypy.ini | 241 +-------------------------------- script/hassfest/mypy_config.py | 2 +- 2 files changed, 2 insertions(+), 241 deletions(-) diff --git a/mypy.ini b/mypy.ini index 700d96a4982..7338d9a67f0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -8,6 +8,7 @@ show_error_codes = true follow_imports = silent ignore_missing_imports = true strict_equality = true +no_implicit_optional = true warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true @@ -20,7 +21,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -124,7 +124,6 @@ disallow_subclassing_any = false disallow_untyped_calls = false disallow_untyped_decorators = false disallow_untyped_defs = false -no_implicit_optional = false warn_return_any = false warn_unreachable = false no_implicit_reexport = false @@ -136,7 +135,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true no_implicit_reexport = true @@ -148,7 +146,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -159,7 +156,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -170,7 +166,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -181,7 +176,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -192,7 +186,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -203,7 +196,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -214,7 +206,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -225,7 +216,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -236,7 +226,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -247,7 +236,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -258,7 +246,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -269,7 +256,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -280,7 +266,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -291,7 +276,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -302,7 +286,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -313,7 +296,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -324,7 +306,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -335,7 +316,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -346,7 +326,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -357,7 +336,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -368,7 +346,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -379,7 +356,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -390,7 +366,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -401,7 +376,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -412,7 +386,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -423,7 +396,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -434,7 +406,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -445,7 +416,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -456,7 +426,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -467,7 +436,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -478,7 +446,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -489,7 +456,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -500,7 +466,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -511,7 +476,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -522,7 +486,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -533,7 +496,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -544,7 +506,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -555,7 +516,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -566,7 +526,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -577,7 +536,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -588,7 +546,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -599,7 +556,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -610,7 +566,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -621,7 +576,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -632,7 +586,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -643,7 +596,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -654,7 +606,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -665,7 +616,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -676,7 +626,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -687,7 +636,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -698,7 +646,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -709,7 +656,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -720,7 +666,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -731,7 +676,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -742,7 +686,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -753,7 +696,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -764,7 +706,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -775,7 +716,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -786,7 +726,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -797,7 +736,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -808,7 +746,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -819,7 +756,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -830,7 +766,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -841,7 +776,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -852,7 +786,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -863,7 +796,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -874,7 +806,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -885,7 +816,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -896,7 +826,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -907,7 +836,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -918,7 +846,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -929,7 +856,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -940,7 +866,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -951,7 +876,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -962,7 +886,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -973,7 +896,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -984,7 +906,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -995,7 +916,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1006,7 +926,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1017,7 +936,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1028,7 +946,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1039,7 +956,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1050,7 +966,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1061,7 +976,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1072,7 +986,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1083,7 +996,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1094,7 +1006,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1105,7 +1016,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1116,7 +1026,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1127,7 +1036,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1138,7 +1046,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1149,7 +1056,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1160,7 +1066,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1171,7 +1076,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1182,7 +1086,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1193,7 +1096,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1204,7 +1106,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1215,7 +1116,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1226,7 +1126,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1237,7 +1136,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1248,7 +1146,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1259,7 +1156,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1270,7 +1166,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1281,7 +1176,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1292,7 +1186,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1303,7 +1196,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1314,7 +1206,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1325,7 +1216,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1336,7 +1226,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1347,7 +1236,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1358,7 +1246,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1369,7 +1256,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1380,7 +1266,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1391,7 +1276,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1402,7 +1286,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1413,7 +1296,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1424,7 +1306,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1435,7 +1316,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1446,7 +1326,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1457,7 +1336,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1468,7 +1346,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1479,7 +1356,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1490,7 +1366,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1501,7 +1376,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1512,7 +1386,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1523,7 +1396,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1534,7 +1406,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1545,7 +1416,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1556,7 +1426,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1567,7 +1436,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1578,7 +1446,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1589,7 +1456,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1600,7 +1466,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1611,7 +1476,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1622,7 +1486,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1633,7 +1496,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1644,7 +1506,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1655,7 +1516,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1666,7 +1526,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1677,7 +1536,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1688,7 +1546,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1699,7 +1556,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1710,7 +1566,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1721,7 +1576,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1732,7 +1586,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1743,7 +1596,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1754,7 +1606,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1765,7 +1616,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1776,7 +1626,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1787,7 +1636,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1798,7 +1646,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1809,7 +1656,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1820,7 +1666,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1831,7 +1676,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1842,7 +1686,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1853,7 +1696,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1864,7 +1706,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1875,7 +1716,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1886,7 +1726,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1897,7 +1736,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1908,7 +1746,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1919,7 +1756,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1930,7 +1766,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1941,7 +1776,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1952,7 +1786,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1963,7 +1796,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1974,7 +1806,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1985,7 +1816,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1996,7 +1826,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2007,7 +1836,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2018,7 +1846,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2029,7 +1856,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2040,7 +1866,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2051,7 +1876,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2062,7 +1886,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2073,7 +1896,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2084,7 +1906,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2095,7 +1916,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2106,7 +1926,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2117,7 +1936,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2128,7 +1946,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2139,7 +1956,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2150,7 +1966,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2161,7 +1976,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2172,7 +1986,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2183,7 +1996,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2194,7 +2006,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2205,7 +2016,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2216,7 +2026,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2227,7 +2036,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2238,7 +2046,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true no_implicit_reexport = true @@ -2250,7 +2057,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2261,7 +2067,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2272,7 +2077,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2283,7 +2087,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2294,7 +2097,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2305,7 +2107,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2316,7 +2117,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2327,7 +2127,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2338,7 +2137,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2349,7 +2147,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2360,7 +2157,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2371,7 +2167,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2382,7 +2177,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2393,7 +2187,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2404,7 +2197,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2415,7 +2207,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2426,7 +2217,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2437,7 +2227,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2448,7 +2237,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2459,7 +2247,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2470,7 +2257,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2481,7 +2267,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2492,7 +2277,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true no_implicit_reexport = true @@ -2504,7 +2288,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2515,7 +2298,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2526,7 +2308,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2537,7 +2318,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2548,7 +2328,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2559,7 +2338,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2570,7 +2348,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2581,7 +2358,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2592,7 +2368,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2603,7 +2378,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2614,7 +2388,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2625,7 +2398,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2636,7 +2408,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2647,7 +2418,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2658,7 +2428,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2669,7 +2438,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2680,7 +2448,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2691,7 +2458,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2702,7 +2468,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2713,7 +2478,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2724,7 +2488,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2735,7 +2498,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2755,7 +2517,6 @@ disallow_subclassing_any = false disallow_untyped_calls = false disallow_untyped_decorators = false disallow_untyped_defs = false -no_implicit_optional = false warn_return_any = false warn_unreachable = false diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 4bf8cfd68cf..b6c31751e12 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -54,6 +54,7 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { # Enable some checks globally. "ignore_missing_imports": "true", "strict_equality": "true", + "no_implicit_optional": "true", "warn_incomplete_stub": "true", "warn_redundant_casts": "true", "warn_unused_configs": "true", @@ -74,7 +75,6 @@ STRICT_SETTINGS: Final[list[str]] = [ "disallow_untyped_calls", "disallow_untyped_decorators", "disallow_untyped_defs", - "no_implicit_optional", "warn_return_any", "warn_unreachable", # TODO: turn these on, address issues From b4a840c00dd680f8f674c660860ee0e159845a69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Aug 2022 12:47:35 -1000 Subject: [PATCH 343/903] Avoid creating door sensor when it does no exist on older yalexs_ble locks (#76710) --- homeassistant/components/yalexs_ble/binary_sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yalexs_ble/binary_sensor.py b/homeassistant/components/yalexs_ble/binary_sensor.py index 3ee88dbaa5e..32421f67fbb 100644 --- a/homeassistant/components/yalexs_ble/binary_sensor.py +++ b/homeassistant/components/yalexs_ble/binary_sensor.py @@ -23,7 +23,9 @@ async def async_setup_entry( ) -> None: """Set up YALE XS binary sensors.""" data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] - async_add_entities([YaleXSBLEDoorSensor(data)]) + lock = data.lock + if lock.lock_info and lock.lock_info.door_sense: + async_add_entities([YaleXSBLEDoorSensor(data)]) class YaleXSBLEDoorSensor(YALEXSBLEEntity, BinarySensorEntity): From bec8e544f42e87ec6a79ed546414de84fc0a256a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 14 Aug 2022 00:25:47 +0000 Subject: [PATCH 344/903] [ci skip] Translation update --- .../components/abode/translations/es.json | 6 +-- .../accuweather/translations/es.json | 2 +- .../components/adax/translations/es.json | 2 +- .../components/adguard/translations/es.json | 6 +-- .../components/agent_dvr/translations/es.json | 2 +- .../components/airly/translations/es.json | 4 +- .../components/airvisual/translations/es.json | 10 ++-- .../alarm_control_panel/translations/es.json | 10 ++-- .../alarmdecoder/translations/es.json | 22 ++++----- .../components/almond/translations/es.json | 4 +- .../ambiclimate/translations/es.json | 8 ++-- .../android_ip_webcam/translations/es.json | 2 +- .../android_ip_webcam/translations/it.json | 6 +++ .../components/apple_tv/translations/es.json | 6 +-- .../components/asuswrt/translations/es.json | 4 +- .../components/atag/translations/es.json | 4 +- .../components/aurora/translations/es.json | 2 +- .../components/awair/translations/hu.json | 2 +- .../components/awair/translations/id.json | 2 +- .../components/awair/translations/it.json | 31 ++++++++++-- .../components/axis/translations/es.json | 2 +- .../azure_devops/translations/es.json | 4 +- .../binary_sensor/translations/es.json | 48 +++++++++---------- .../components/blink/translations/es.json | 2 +- .../bmw_connected_drive/translations/es.json | 2 +- .../components/bond/translations/es.json | 2 +- .../components/braviatv/translations/es.json | 10 ++-- .../components/broadlink/translations/es.json | 2 +- .../components/brother/translations/es.json | 2 +- .../components/bsblan/translations/es.json | 2 +- .../components/button/translations/es.json | 2 +- .../cert_expiry/translations/es.json | 2 +- .../components/climate/translations/es.json | 12 ++--- .../components/cloud/translations/es.json | 8 ++-- .../cloudflare/translations/es.json | 12 ++--- .../components/control4/translations/es.json | 8 ++-- .../coolmaster/translations/es.json | 14 +++--- .../coronavirus/translations/es.json | 2 +- .../components/cover/translations/es.json | 10 ++-- .../components/deconz/translations/es.json | 30 ++++++------ .../components/demo/translations/es.json | 2 +- .../components/denonavr/translations/es.json | 10 ++-- .../deutsche_bahn/translations/it.json | 1 + .../components/dexcom/translations/es.json | 2 +- .../components/directv/translations/es.json | 2 +- .../components/doorbird/translations/es.json | 2 +- .../components/eafm/translations/es.json | 6 +-- .../components/ecobee/translations/es.json | 8 ++-- .../components/elgato/translations/es.json | 4 +- .../components/enocean/translations/es.json | 12 ++--- .../components/escea/translations/it.json | 5 ++ .../components/fan/translations/es.json | 12 ++--- .../flick_electric/translations/es.json | 4 +- .../components/flo/translations/es.json | 4 +- .../components/flume/translations/es.json | 2 +- .../forked_daapd/translations/es.json | 12 ++--- .../components/freebox/translations/es.json | 6 +-- .../components/fritzbox/translations/es.json | 2 +- .../fritzbox_callmonitor/translations/es.json | 6 +-- .../components/gdacs/translations/es.json | 2 +- .../geocaching/translations/es.json | 4 +- .../geonetnz_quakes/translations/es.json | 2 +- .../geonetnz_volcano/translations/es.json | 2 +- .../components/glances/translations/es.json | 2 +- .../components/google/translations/es.json | 2 +- .../components/guardian/translations/es.json | 2 +- .../components/guardian/translations/hu.json | 6 +-- .../components/guardian/translations/it.json | 13 +++++ .../components/harmony/translations/es.json | 2 +- .../components/hassio/translations/es.json | 2 +- .../home_connect/translations/es.json | 2 +- .../home_plus_control/translations/es.json | 4 +- .../homeassistant/translations/es.json | 2 +- .../components/homekit/translations/es.json | 12 ++--- .../homekit_controller/translations/es.json | 16 +++---- .../translations/sensor.it.json | 10 ++++ .../huawei_lte/translations/es.json | 2 +- .../components/hue/translations/es.json | 6 +-- .../humidifier/translations/es.json | 8 ++-- .../translations/es.json | 4 +- .../hvv_departures/translations/es.json | 10 ++-- .../components/hyperion/translations/es.json | 18 +++---- .../components/iaqualink/translations/es.json | 2 +- .../components/icloud/translations/es.json | 8 ++-- .../input_datetime/translations/es.json | 2 +- .../components/insteon/translations/es.json | 40 ++++++++-------- .../components/ipma/translations/es.json | 4 +- .../components/ipp/translations/es.json | 8 ++-- .../components/iqvia/translations/es.json | 2 +- .../components/isy994/translations/es.json | 6 +-- .../keenetic_ndms2/translations/es.json | 2 +- .../components/kodi/translations/es.json | 10 ++-- .../components/konnected/translations/es.json | 28 +++++------ .../components/life360/translations/es.json | 6 +-- .../components/light/translations/es.json | 4 +- .../components/lock/translations/es.json | 8 ++-- .../logi_circle/translations/es.json | 12 ++--- .../lutron_caseta/translations/es.json | 24 +++++----- .../components/lyric/translations/es.json | 4 +- .../components/mazda/translations/es.json | 4 +- .../media_player/translations/es.json | 8 ++-- .../components/melcloud/translations/es.json | 8 ++-- .../components/met/translations/es.json | 4 +- .../meteo_france/translations/es.json | 4 +- .../components/metoffice/translations/es.json | 2 +- .../components/mikrotik/translations/es.json | 8 ++-- .../minecraft_server/translations/es.json | 8 ++-- .../motion_blinds/translations/es.json | 10 ++-- .../components/mqtt/translations/es.json | 10 ++-- .../components/myq/translations/es.json | 4 +- .../components/mysensors/translations/es.json | 28 +++++------ .../components/nanoleaf/translations/es.json | 2 +- .../components/neato/translations/es.json | 4 +- .../components/nest/translations/es.json | 12 ++--- .../components/netatmo/translations/es.json | 20 ++++---- .../components/nexia/translations/es.json | 2 +- .../nightscout/translations/es.json | 2 +- .../components/nuheat/translations/es.json | 4 +- .../components/nut/translations/es.json | 2 +- .../components/nzbget/translations/es.json | 2 +- .../ondilo_ico/translations/es.json | 4 +- .../components/onvif/translations/es.json | 16 +++---- .../openexchangerates/translations/it.json | 6 ++- .../opentherm_gw/translations/es.json | 2 +- .../ovo_energy/translations/es.json | 4 +- .../panasonic_viera/translations/es.json | 8 ++-- .../philips_js/translations/es.json | 4 +- .../components/plaato/translations/es.json | 20 ++++---- .../components/plex/translations/es.json | 10 ++-- .../components/plugwise/translations/es.json | 6 +-- .../components/point/translations/es.json | 4 +- .../components/powerwall/translations/es.json | 2 +- .../progettihwsw/translations/es.json | 2 +- .../components/ps4/translations/es.json | 4 +- .../components/rachio/translations/es.json | 4 +- .../recollect_waste/translations/es.json | 2 +- .../components/remote/translations/es.json | 6 +-- .../components/rfxtrx/translations/es.json | 22 ++++----- .../components/ring/translations/es.json | 2 +- .../components/risco/translations/es.json | 10 ++-- .../components/roku/translations/es.json | 4 +- .../components/roomba/translations/es.json | 10 ++-- .../components/rpi_power/translations/es.json | 2 +- .../components/samsungtv/translations/es.json | 12 ++--- .../components/schedule/translations/hu.json | 2 +- .../components/schedule/translations/it.json | 9 ++++ .../components/sensor/translations/es.json | 16 +++---- .../components/sentry/translations/es.json | 16 +++---- .../components/senz/translations/es.json | 4 +- .../components/shelly/translations/es.json | 6 +-- .../simplisafe/translations/es.json | 8 ++-- .../components/smappee/translations/es.json | 10 ++-- .../smartthings/translations/es.json | 4 +- .../components/solaredge/translations/es.json | 2 +- .../components/solarlog/translations/es.json | 4 +- .../components/soma/translations/es.json | 10 ++-- .../components/sonarr/translations/es.json | 4 +- .../components/songpal/translations/es.json | 2 +- .../soundtouch/translations/es.json | 2 +- .../speedtestdotnet/translations/es.json | 2 +- .../components/spotify/translations/es.json | 8 ++-- .../squeezebox/translations/es.json | 2 +- .../srp_energy/translations/es.json | 2 +- .../components/switch/translations/es.json | 4 +- .../components/switchbot/translations/it.json | 9 ++++ .../components/syncthru/translations/es.json | 2 +- .../synology_dsm/translations/es.json | 6 +-- .../system_health/translations/es.json | 2 +- .../components/tado/translations/es.json | 4 +- .../tellduslive/translations/es.json | 2 +- .../components/tibber/translations/es.json | 2 +- .../components/toon/translations/es.json | 6 +-- .../components/traccar/translations/es.json | 4 +- .../transmission/translations/es.json | 6 +-- .../components/tuya/translations/es.json | 2 +- .../twentemilieu/translations/es.json | 4 +- .../components/unifi/translations/es.json | 18 +++---- .../components/upb/translations/es.json | 4 +- .../components/vacuum/translations/es.json | 4 +- .../components/velbus/translations/es.json | 4 +- .../components/vera/translations/es.json | 8 ++-- .../components/vesync/translations/es.json | 2 +- .../components/vizio/translations/es.json | 16 +++---- .../components/weather/translations/es.json | 2 +- .../components/wilight/translations/es.json | 4 +- .../components/withings/translations/es.json | 6 +-- .../components/wled/translations/es.json | 2 +- .../components/wolflink/translations/es.json | 2 +- .../wolflink/translations/sensor.es.json | 20 ++++---- .../components/xbox/translations/es.json | 4 +- .../xiaomi_aqara/translations/es.json | 16 +++---- .../yalexs_ble/translations/de.json | 3 +- .../yalexs_ble/translations/es.json | 3 +- .../yalexs_ble/translations/fr.json | 4 +- .../yalexs_ble/translations/hu.json | 1 + .../yalexs_ble/translations/id.json | 3 +- .../yalexs_ble/translations/it.json | 31 ++++++++++++ .../yalexs_ble/translations/pt-BR.json | 3 +- .../components/yolink/translations/es.json | 4 +- .../components/zha/translations/es.json | 18 +++---- .../components/zwave_js/translations/es.json | 14 +++--- 201 files changed, 756 insertions(+), 636 deletions(-) create mode 100644 homeassistant/components/homekit_controller/translations/sensor.it.json create mode 100644 homeassistant/components/schedule/translations/it.json create mode 100644 homeassistant/components/yalexs_ble/translations/it.json diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json index 66cb5d13f22..c7db5e8db6a 100644 --- a/homeassistant/components/abode/translations/es.json +++ b/homeassistant/components/abode/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_mfa_code": "C\u00f3digo MFA inv\u00e1lido" + "invalid_mfa_code": "C\u00f3digo MFA no v\u00e1lido" }, "step": { "mfa": { @@ -21,14 +21,14 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "title": "Rellene su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + "title": "Completa tu informaci\u00f3n de inicio de sesi\u00f3n de Abode" }, "user": { "data": { "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "title": "Rellene la informaci\u00f3n de acceso Abode" + "title": "Completa tu informaci\u00f3n de inicio de sesi\u00f3n de Abode" } } } diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index df5b9c21494..9ec67fd88b8 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -34,7 +34,7 @@ }, "system_health": { "info": { - "can_reach_server": "Alcanzar el servidor AccuWeather", + "can_reach_server": "Se puede llegar al servidor AccuWeather", "remaining_requests": "Solicitudes permitidas restantes" } } diff --git a/homeassistant/components/adax/translations/es.json b/homeassistant/components/adax/translations/es.json index c2d224aacb1..90dc1ad05bc 100644 --- a/homeassistant/components/adax/translations/es.json +++ b/homeassistant/components/adax/translations/es.json @@ -21,7 +21,7 @@ "wifi_pswd": "Contrase\u00f1a Wi-Fi", "wifi_ssid": "SSID Wi-Fi" }, - "description": "Reinicia el calentador presionando + y OK hasta que la pantalla muestre 'Restablecer'. Luego mant\u00e9n presionado el bot\u00f3n OK en el calentador hasta que el led azul comience a parpadear antes de presionar Enviar. La configuraci\u00f3n del calentador puede tardar algunos minutos." + "description": "Reinicia el calentador pulsando + y OK hasta que la pantalla muestre 'Restablecer'. Luego mant\u00e9n pulsado el bot\u00f3n OK en el calentador hasta que el led azul comience a parpadear antes de pulsar Enviar. La configuraci\u00f3n del calentador puede tardar algunos minutos." }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index 96a4546f735..6cc1022f7ae 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para conectarse a AdGuard Home proporcionado por el complemento: {addon} ?", - "title": "AdGuard Home v\u00eda complemento de Home Assistant" + "description": "\u00bfQuieres configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento: {addon}?", + "title": "AdGuard Home a trav\u00e9s del complemento Home Assistant" }, "user": { "data": { @@ -21,7 +21,7 @@ "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" }, - "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." + "description": "Configura tu instancia AdGuard Home para permitir la supervisi\u00f3n y el control." } } } diff --git a/homeassistant/components/agent_dvr/translations/es.json b/homeassistant/components/agent_dvr/translations/es.json index 996e006898d..685f0c38045 100644 --- a/homeassistant/components/agent_dvr/translations/es.json +++ b/homeassistant/components/agent_dvr/translations/es.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Puerto" }, - "title": "Configurar el Agente de DVR" + "title": "Configurar Agent DVR" } } } diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index 1a4197b758b..efe0228e715 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "invalid_api_key": "Clave API no v\u00e1lida", - "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea." }, "step": { "user": { @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Alcanzar el servidor Airly", + "can_reach_server": "Se puede llegar al servidor Airly", "requests_per_day": "Solicitudes permitidas por d\u00eda", "requests_remaining": "Solicitudes permitidas restantes" } diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 5c38176bf22..739aaa818ed 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -17,7 +17,7 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "Utilice la API de la nube de AirVisual para supervisar una latitud/longitud.", + "description": "Utiliza la API de nube de AirVisual para supervisar una latitud/longitud.", "title": "Configurar una geograf\u00eda" }, "geography_by_name": { @@ -27,7 +27,7 @@ "country": "Pa\u00eds", "state": "estado" }, - "description": "Utilice la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", + "description": "Utiliza la API en la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", "title": "Configurar una geograf\u00eda" }, "node_pro": { @@ -35,7 +35,7 @@ "ip_address": "Host", "password": "Contrase\u00f1a" }, - "description": "Monitorizar una unidad personal AirVisual. La contrase\u00f1a puede ser recuperada desde la interfaz de la unidad.", + "description": "Supervisar una unidad AirVisual personal. La contrase\u00f1a se puede recuperar desde la IU de la unidad.", "title": "Configurar un AirVisual Node/Pro" }, "reauth_confirm": { @@ -45,7 +45,7 @@ "title": "Volver a autenticar AirVisual" }, "user": { - "description": "Elige qu\u00e9 tipo de datos de AirVisual quieres monitorizar.", + "description": "Elige qu\u00e9 tipo de datos de AirVisual quieres supervisar.", "title": "Configurar AirVisual" } } @@ -54,7 +54,7 @@ "step": { "init": { "data": { - "show_on_map": "Mostrar geograf\u00eda monitorizada en el mapa" + "show_on_map": "Mostrar geograf\u00eda supervisada en el mapa" }, "title": "Configurar AirVisual" } diff --git a/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant/components/alarm_control_panel/translations/es.json index 38c4c059b48..212e31b9670 100644 --- a/homeassistant/components/alarm_control_panel/translations/es.json +++ b/homeassistant/components/alarm_control_panel/translations/es.json @@ -1,12 +1,12 @@ { "device_automation": { "action_type": { - "arm_away": "Armar {entity_name} exterior", - "arm_home": "Armar {entity_name} modo casa", - "arm_night": "Armar {entity_name} por la noche", + "arm_away": "Armar ausente en {entity_name}", + "arm_home": "Armar en casa en {entity_name}", + "arm_night": "Armar noche en {entity_name}", "arm_vacation": "Armar de vacaciones {entity_name}", "disarm": "Desarmar {entity_name}", - "trigger": "Lanzar {entity_name}" + "trigger": "Disparar {entity_name}" }, "condition_type": { "is_armed_away": "{entity_name} est\u00e1 armada ausente", @@ -22,7 +22,7 @@ "armed_night": "{entity_name} armada noche", "armed_vacation": "{entity_name} en armada de vacaciones", "disarmed": "{entity_name} desarmada", - "triggered": "{entity_name} activado" + "triggered": "{entity_name} disparada" } }, "state": { diff --git a/homeassistant/components/alarmdecoder/translations/es.json b/homeassistant/components/alarmdecoder/translations/es.json index 5dfd7ab5745..a568c853365 100644 --- a/homeassistant/components/alarmdecoder/translations/es.json +++ b/homeassistant/components/alarmdecoder/translations/es.json @@ -23,23 +23,23 @@ "data": { "protocol": "Protocolo" }, - "title": "Elige el protocolo del AlarmDecoder" + "title": "Elige el protocolo AlarmDecoder" } } }, "options": { "error": { - "int": "El campo siguiente debe ser un n\u00famero entero.", - "loop_range": "El bucle RF debe ser un n\u00famero entero entre 1 y 4.", - "loop_rfid": "El bucle de RF no puede utilizarse sin el serie RF.", - "relay_inclusive": "La direcci\u00f3n de retransmisi\u00f3n y el canal de retransmisi\u00f3n son codependientes y deben incluirse a la vez." + "int": "El siguiente campo debe ser un n\u00famero entero.", + "loop_range": "RF Loop debe ser un n\u00famero entero entre 1 y 4.", + "loop_rfid": "RF Loop no se puede utilizar sin RF Serial.", + "relay_inclusive": "La direcci\u00f3n de retransmisi\u00f3n y el canal de retransmisi\u00f3n son c\u00f3digopendientes y deben incluirse a la vez." }, "step": { "arm_settings": { "data": { - "alt_night_mode": "Modo noche alternativo", - "auto_bypass": "Desv\u00edo autom\u00e1tico al armar", - "code_arm_required": "C\u00f3digo requerido para el armado" + "alt_night_mode": "Modo nocturno alternativo", + "auto_bypass": "Anulaci\u00f3n autom\u00e1tica en armado", + "code_arm_required": "C\u00f3digo Requerido para Armar" }, "title": "Configurar AlarmDecoder" }, @@ -52,14 +52,14 @@ }, "zone_details": { "data": { - "zone_loop": "Bucle RF", + "zone_loop": "RF Loop", "zone_name": "Nombre de zona", "zone_relayaddr": "Direcci\u00f3n de retransmisi\u00f3n", "zone_relaychan": "Canal de retransmisi\u00f3n", - "zone_rfid": "Serie RF", + "zone_rfid": "RF Serial", "zone_type": "Tipo de zona" }, - "description": "Introduce los detalles para la zona {zona_number}. Para borrar la zona {zone_number}, deja el nombre de la zona en blanco.", + "description": "Introduce los detalles para la zona {zone_number}. Para eliminar la zona {zone_number}, deja el nombre de la zona en blanco.", "title": "Configurar AlarmDecoder" }, "zone_select": { diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index a3383cd4b5f..1a5b3ddf074 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "No se pudo conectar", - "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, @@ -12,7 +12,7 @@ "title": "Almond a trav\u00e9s del complemento Home Assistant" }, "pick_implementation": { - "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } } diff --git a/homeassistant/components/ambiclimate/translations/es.json b/homeassistant/components/ambiclimate/translations/es.json index 1ac7a371f82..d3c5433a289 100644 --- a/homeassistant/components/ambiclimate/translations/es.json +++ b/homeassistant/components/ambiclimate/translations/es.json @@ -3,19 +3,19 @@ "abort": { "access_token": "Error desconocido al generar un token de acceso.", "already_configured": "La cuenta ya est\u00e1 configurada", - "missing_configuration": "El componente no est\u00e1 configurado. Siga la documentaci\u00f3n." + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." }, "create_entry": { "default": "Autenticado correctamente" }, "error": { - "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de pulsar Enviar", "no_token": "No autenticado con Ambiclimate" }, "step": { "auth": { - "description": "Por favor, sigue este [enlace]({authorization_url}) y **Permite** el acceso a tu cuenta Ambiclimate, luego regresa y presiona **Enviar** a continuaci\u00f3n.\n(Aseg\u00farate de que la URL de devoluci\u00f3n de llamada especificada sea {cb_url})", - "title": "Autenticaci\u00f3n de Ambiclimate" + "description": "Por favor, sigue este [enlace]({authorization_url}) y **Permite** el acceso a tu cuenta Ambiclimate, luego regresa y pulsa **Enviar** a continuaci\u00f3n.\n(Aseg\u00farate de que la URL de devoluci\u00f3n de llamada especificada sea {cb_url})", + "title": "Autenticar Ambiclimate" } } } diff --git a/homeassistant/components/android_ip_webcam/translations/es.json b/homeassistant/components/android_ip_webcam/translations/es.json index 71c0d6cc5bc..d004be2aeeb 100644 --- a/homeassistant/components/android_ip_webcam/translations/es.json +++ b/homeassistant/components/android_ip_webcam/translations/es.json @@ -19,7 +19,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Se eliminar\u00e1 la configuraci\u00f3n de la c\u00e1mara web IP de Android mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se eliminar\u00e1 la configuraci\u00f3n de la Android IP Webcam mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Android IP Webcam de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android" } } diff --git a/homeassistant/components/android_ip_webcam/translations/it.json b/homeassistant/components/android_ip_webcam/translations/it.json index 7c04ebfdaef..db0cfc79d84 100644 --- a/homeassistant/components/android_ip_webcam/translations/it.json +++ b/homeassistant/components/android_ip_webcam/translations/it.json @@ -16,5 +16,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Android IP Webcam tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Android IP Webcam dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Android IP Webcam sar\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 918a95de976..d1654272818 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -22,8 +22,8 @@ "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios de Home Assistant!", - "title": "Confirma la adici\u00f3n del Apple TV" + "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios en Home Assistant!", + "title": "Confirma para a\u00f1adir Apple TV" }, "pair_no_pin": { "description": "Se requiere emparejamiento para el servicio `{protocol}`. Por favor, introduce el PIN {pin} en tu dispositivo para continuar.", @@ -56,7 +56,7 @@ "data": { "device_input": "Dispositivo" }, - "description": "Comienza introduciendo el nombre del dispositivo (por ejemplo, cocina o dormitorio) o la direcci\u00f3n IP del Apple TV que deseas a\u00f1adir. \n\n Si no puedes ver tu dispositivo o experimentas alg\u00fan problema, intenta especificar la direcci\u00f3n IP del dispositivo.", + "description": "Comienza introduciendo el nombre del dispositivo (por ejemplo, cocina o dormitorio) o la direcci\u00f3n IP del Apple TV que deseas a\u00f1adir. \n\nSi no puedes ver tu dispositivo o experimentas alg\u00fan problema, intenta especificar la direcci\u00f3n IP del dispositivo.", "title": "Configurar un nuevo Apple TV" } } diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index f20f0faa3eb..57e7bb4cde6 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -7,8 +7,8 @@ "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", + "pwd_and_ssh": "Proporciona solo la contrase\u00f1a o el archivo de clave SSH", + "pwd_or_ssh": "Por favor, proporciona la contrase\u00f1a o el archivo de clave SSH", "ssh_not_file": "Archivo de clave SSH no encontrado", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/atag/translations/es.json b/homeassistant/components/atag/translations/es.json index c2da8173e22..c1bc879f64d 100644 --- a/homeassistant/components/atag/translations/es.json +++ b/homeassistant/components/atag/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "unauthorized": "Emparejamiento denegado, comprobar el dispositivo para la solicitud de autorizaci\u00f3n" + "unauthorized": "Emparejamiento denegado, verifica el dispositivo para la solicitud de autenticaci\u00f3n" }, "step": { "user": { @@ -13,7 +13,7 @@ "host": "Host", "port": "Puerto" }, - "title": "Conectarse al dispositivo" + "title": "Conectar al dispositivo" } } } diff --git a/homeassistant/components/aurora/translations/es.json b/homeassistant/components/aurora/translations/es.json index c722c95ef6f..c17fadbc0f9 100644 --- a/homeassistant/components/aurora/translations/es.json +++ b/homeassistant/components/aurora/translations/es.json @@ -22,5 +22,5 @@ } } }, - "title": "Sensor Aurora NOAA" + "title": "Sensor NOAA Aurora" } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 7ce394ccb4b..eae3a375bdf 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -52,7 +52,7 @@ }, "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login", "menu_options": { - "cloud": "Csatlakoz\u00e1s a felh\u0151n kereszt\u00fcl", + "cloud": "Felh\u0151n kereszt\u00fcli csatlakoz\u00e1s", "local": "Lok\u00e1lis csatlakoz\u00e1s (aj\u00e1nlott)" } } diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 53c41584413..835f0d7716a 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -50,7 +50,7 @@ "access_token": "Token Akses", "email": "Email" }, - "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: https://developer.getawair.com/onboard/login", + "description": "Pilih lokal untuk pengalaman terbaik. Hanya gunakan opsi cloud jika perangkat tidak terhubung ke jaringan yang sama dengan Home Assistant, atau jika Anda memiliki perangkat versi lawas.", "menu_options": { "cloud": "Terhubung melalui cloud", "local": "Terhubung secara lokal (lebih disukai)" diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json index 27ec006fb06..d82934fceb9 100644 --- a/homeassistant/components/awair/translations/it.json +++ b/homeassistant/components/awair/translations/it.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "already_configured_account": "L'account \u00e8 gi\u00e0 configurato", + "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", "no_devices_found": "Nessun dispositivo trovato sulla rete", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unreachable": "Impossibile connettersi" }, "error": { "invalid_access_token": "Token di accesso non valido", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "unreachable": "Impossibile connettersi" }, + "flow_title": "{model} ( {device_id} )", "step": { + "cloud": { + "data": { + "access_token": "Token di accesso", + "email": "Email" + }, + "description": "Devi registrarti per un token di accesso per sviluppatori Awair su: {url}" + }, + "discovery_confirm": { + "description": "Vuoi configurare {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Indirizzo IP" + }, + "description": "Awair Local API deve essere abilitato seguendo questi passaggi: {url}" + }, "reauth": { "data": { "access_token": "Token di accesso", @@ -29,7 +50,11 @@ "access_token": "Token di accesso", "email": "Email" }, - "description": "\u00c8 necessario registrarsi per un token di accesso per sviluppatori Awair all'indirizzo: https://developer.getawair.com/onboard/login" + "description": "Scegli locale per la migliore esperienza. Utilizza il cloud solo se il dispositivo non \u00e8 connesso alla stessa rete di Home Assistant o se disponi di un dispositivo legacy.", + "menu_options": { + "cloud": "Connettiti tramite il cloud", + "local": "Connetti localmente (preferito)" + } } } } diff --git a/homeassistant/components/axis/translations/es.json b/homeassistant/components/axis/translations/es.json index 87497280775..262228bb2d4 100644 --- a/homeassistant/components/axis/translations/es.json +++ b/homeassistant/components/axis/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "link_local_address": "Las direcciones de enlace local no son compatibles", - "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" + "not_axis_device": "El dispositivo descubierto no es un dispositivo Axis" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", diff --git a/homeassistant/components/azure_devops/translations/es.json b/homeassistant/components/azure_devops/translations/es.json index 2795286e5c7..8414e03a727 100644 --- a/homeassistant/components/azure_devops/translations/es.json +++ b/homeassistant/components/azure_devops/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "project_error": "No se pudo obtener informaci\u00f3n del proyecto." }, "flow_title": "{project_url}", diff --git a/homeassistant/components/binary_sensor/translations/es.json b/homeassistant/components/binary_sensor/translations/es.json index 4a30bed923d..2d3f7c7d4f7 100644 --- a/homeassistant/components/binary_sensor/translations/es.json +++ b/homeassistant/components/binary_sensor/translations/es.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_bat_low": "La bater\u00eda de {entity_name} est\u00e1 baja", "is_co": "{entity_name} est\u00e1 detectando mon\u00f3xido de carbono", "is_cold": "{entity_name} est\u00e1 fr\u00edo", "is_connected": "{entity_name} est\u00e1 conectado", @@ -14,9 +14,9 @@ "is_moving": "{entity_name} se est\u00e1 moviendo", "is_no_co": "{entity_name} no detecta mon\u00f3xido de carbono", "is_no_gas": "{entity_name} no detecta gas", - "is_no_light": "{entity_name} no detecta la luz", + "is_no_light": "{entity_name} no detecta luz", "is_no_motion": "{entity_name} no detecta movimiento", - "is_no_problem": "{entity_name} no detecta el problema", + "is_no_problem": "{entity_name} no detecta problema alguno", "is_no_smoke": "{entity_name} no detecta humo", "is_no_sound": "{entity_name} no detecta sonido", "is_no_update": "{entity_name} est\u00e1 actualizado", @@ -30,7 +30,7 @@ "is_not_moving": "{entity_name} no se mueve", "is_not_occupied": "{entity_name} no est\u00e1 ocupado", "is_not_open": "{entity_name} est\u00e1 cerrado", - "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_plugged_in": "{entity_name} est\u00e1 desenchufado", "is_not_powered": "{entity_name} no tiene alimentaci\u00f3n", "is_not_present": "{entity_name} no est\u00e1 presente", "is_not_running": "{entity_name} no se est\u00e1 ejecutando", @@ -40,8 +40,8 @@ "is_off": "{entity_name} est\u00e1 apagado", "is_on": "{entity_name} est\u00e1 activado", "is_open": "{entity_name} est\u00e1 abierto", - "is_plugged_in": "{entity_name} est\u00e1 conectado", - "is_powered": "{entity_name} est\u00e1 activado", + "is_plugged_in": "{entity_name} est\u00e1 enchufado", + "is_powered": "{entity_name} est\u00e1 alimentado", "is_present": "{entity_name} est\u00e1 presente", "is_problem": "{entity_name} est\u00e1 detectando un problema", "is_running": "{entity_name} se est\u00e1 ejecutando", @@ -50,56 +50,56 @@ "is_tampered": "{entity_name} est\u00e1 detectando manipulaci\u00f3n", "is_unsafe": "{entity_name} no es seguro", "is_update": "{entity_name} tiene una actualizaci\u00f3n disponible", - "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + "is_vibration": "{entity_name} est\u00e1 detectando vibraci\u00f3n" }, "trigger_type": { - "bat_low": "{entity_name} bater\u00eda baja", + "bat_low": "La bater\u00eda de {entity_name} es baja", "co": "{entity_name} comenz\u00f3 a detectar mon\u00f3xido de carbono", "cold": "{entity_name} se enfri\u00f3", "connected": "{entity_name} conectado", "gas": "{entity_name} empez\u00f3 a detectar gas", - "hot": "{entity_name} se est\u00e1 calentando", - "light": "{entity_name} empez\u00f3 a detectar la luz", + "hot": "{entity_name} se calent\u00f3", + "light": "{entity_name} empez\u00f3 a detectar luz", "locked": "{entity_name} bloqueado", - "moist": "{entity_name} se humedece", - "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moist": "{entity_name} se humedeci\u00f3", + "motion": "{entity_name} empez\u00f3 a detectar movimiento", "moving": "{entity_name} empez\u00f3 a moverse", "no_co": "{entity_name} dej\u00f3 de detectar mon\u00f3xido de carbono", "no_gas": "{entity_name} dej\u00f3 de detectar gas", - "no_light": "{entity_name} dej\u00f3 de detectar la luz", + "no_light": "{entity_name} dej\u00f3 de detectar luz", "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", - "no_problem": "{entity_name} dej\u00f3 de detectar el problema", + "no_problem": "{entity_name} dej\u00f3 de detectar alg\u00fan problema", "no_smoke": "{entity_name} dej\u00f3 de detectar humo", "no_sound": "{entity_name} dej\u00f3 de detectar sonido", "no_update": "{entity_name} se actualiz\u00f3", "no_vibration": "{entity_name} dej\u00f3 de detectar vibraci\u00f3n", "not_bat_low": "{entity_name} bater\u00eda normal", - "not_cold": "{entity_name} no se enfri\u00f3", + "not_cold": "{entity_name} dej\u00f3 de estar fr\u00edo", "not_connected": "{entity_name} desconectado", - "not_hot": "{entity_name} no se calent\u00f3", + "not_hot": "{entity_name} dej\u00f3 de estar caliente", "not_locked": "{entity_name} desbloqueado", "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", "not_occupied": "{entity_name} no est\u00e1 ocupado", - "not_opened": "{entity_name} se cierra", - "not_plugged_in": "{entity_name} desconectado", - "not_powered": "{entity_name} no est\u00e1 activado", + "not_opened": "{entity_name} cerrado", + "not_plugged_in": "{entity_name} desenchufado", + "not_powered": "{entity_name} no alimentado", "not_present": "{entity_name} no est\u00e1 presente", "not_running": "{entity_name} ya no se est\u00e1 ejecutando", "not_tampered": "{entity_name} dej\u00f3 de detectar manipulaci\u00f3n", "not_unsafe": "{entity_name} se volvi\u00f3 seguro", - "occupied": "{entity_name} se convirti\u00f3 en ocupado", + "occupied": "{entity_name} se volvi\u00f3 ocupado", "opened": "{entity_name} abierto", - "plugged_in": "{entity_name} se ha enchufado", + "plugged_in": "{entity_name} enchufado", "powered": "{entity_name} alimentado", "present": "{entity_name} presente", - "problem": "{entity_name} empez\u00f3 a detectar problemas", + "problem": "{entity_name} empez\u00f3 a detectar un problema", "running": "{entity_name} comenz\u00f3 a ejecutarse", "smoke": "{entity_name} empez\u00f3 a detectar humo", "sound": "{entity_name} empez\u00f3 a detectar sonido", "tampered": "{entity_name} comenz\u00f3 a detectar manipulaci\u00f3n", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido", "unsafe": "{entity_name} se volvi\u00f3 inseguro", "update": "{entity_name} tiene una actualizaci\u00f3n disponible", "vibration": "{entity_name} empez\u00f3 a detectar vibraciones" diff --git a/homeassistant/components/blink/translations/es.json b/homeassistant/components/blink/translations/es.json index 72f68ab4fff..17c724102eb 100644 --- a/homeassistant/components/blink/translations/es.json +++ b/homeassistant/components/blink/translations/es.json @@ -14,7 +14,7 @@ "data": { "2fa": "C\u00f3digo de dos factores" }, - "description": "Introduce el PIN enviado a su correo electr\u00f3nico", + "description": "Introduce el PIN enviado a tu correo electr\u00f3nico", "title": "Autenticaci\u00f3n de dos factores" }, "user": { diff --git a/homeassistant/components/bmw_connected_drive/translations/es.json b/homeassistant/components/bmw_connected_drive/translations/es.json index 2a4fd84f708..7d5c21534cb 100644 --- a/homeassistant/components/bmw_connected_drive/translations/es.json +++ b/homeassistant/components/bmw_connected_drive/translations/es.json @@ -21,7 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "S\u00f3lo lectura (s\u00f3lo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)" + "read_only": "S\u00f3lo lectura (s\u00f3lo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, ni cerraduras)" } } } diff --git a/homeassistant/components/bond/translations/es.json b/homeassistant/components/bond/translations/es.json index 33d3dbb4408..d426fca5862 100644 --- a/homeassistant/components/bond/translations/es.json +++ b/homeassistant/components/bond/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "old_firmware": "Firmware antiguo no compatible en el dispositivo Bond - actual\u00edzalo antes de continuar", + "old_firmware": "Firmware antiguo no compatible en el dispositivo Bond - por favor, actualiza antes de continuar", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index 118125f63e0..86436249717 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -2,20 +2,20 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "no_ip_control": "El Control de IP est\u00e1 desactivado en tu televisor o el televisor no es compatible." + "no_ip_control": "El Control IP est\u00e1 desactivado en tu TV o la TV no es compatible." }, "error": { "cannot_connect": "No se pudo conectar", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", - "unsupported_model": "Tu modelo de televisor no es compatible." + "unsupported_model": "Tu modelo de TV no es compatible." }, "step": { "authorize": { "data": { "pin": "C\u00f3digo PIN" }, - "description": "Introduce el c\u00f3digo PIN que se muestra en el televisor Sony Bravia.\n\nSi no se muestra ning\u00fan c\u00f3digo PIN, necesitas eliminar el registro de Home Assistant de tu televisor, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n del dispositivo remoto -> Eliminar el dispositivo remoto.", - "title": "Autorizaci\u00f3n del televisor Sony Bravia" + "description": "Introduce el c\u00f3digo PIN que se muestra en Sony Bravia TV. \n\nSi no se muestra el c\u00f3digo PIN, debes cancelar el registro de Home Assistant en tu TV, ve a: Configuraci\u00f3n - > Red - > Configuraci\u00f3n del dispositivo remoto - > Cancelar el registro del dispositivo remoto.", + "title": "Autorizar Sony Bravia TV" }, "user": { "data": { @@ -31,7 +31,7 @@ "data": { "ignored_sources": "Lista de fuentes ignoradas" }, - "title": "Opciones para el televisor Sony Bravia" + "title": "Opciones para Sony Bravia TV" } } } diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json index 97a71e2cf61..540675cd646 100644 --- a/homeassistant/components/broadlink/translations/es.json +++ b/homeassistant/components/broadlink/translations/es.json @@ -40,7 +40,7 @@ "host": "Host", "timeout": "L\u00edmite de tiempo" }, - "title": "Conectarse al dispositivo" + "title": "Conectar al dispositivo" } } } diff --git a/homeassistant/components/brother/translations/es.json b/homeassistant/components/brother/translations/es.json index 921f2806f0e..e56d922425e 100644 --- a/homeassistant/components/brother/translations/es.json +++ b/homeassistant/components/brother/translations/es.json @@ -22,7 +22,7 @@ "type": "Tipo de impresora" }, "description": "\u00bfQuieres a\u00f1adir la impresora {model} con n\u00famero de serie `{serial_number}` a Home Assistant?", - "title": "Impresora Brother encontrada" + "title": "Impresora Brother descubierta" } } } diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 1e1c29d24a4..0bf6d3bad9d 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "host": "Host", - "passkey": "Clave de acceso", + "passkey": "Cadena de clave de acceso", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario" diff --git a/homeassistant/components/button/translations/es.json b/homeassistant/components/button/translations/es.json index 69430e83f1b..7fd58a87159 100644 --- a/homeassistant/components/button/translations/es.json +++ b/homeassistant/components/button/translations/es.json @@ -4,7 +4,7 @@ "press": "Presiona el bot\u00f3n {entity_name}" }, "trigger_type": { - "pressed": "{entity_name} se ha presionado" + "pressed": "{entity_name} se ha pulsado" } }, "title": "Bot\u00f3n" diff --git a/homeassistant/components/cert_expiry/translations/es.json b/homeassistant/components/cert_expiry/translations/es.json index 1eaee01060f..9cad0193bb7 100644 --- a/homeassistant/components/cert_expiry/translations/es.json +++ b/homeassistant/components/cert_expiry/translations/es.json @@ -16,7 +16,7 @@ "name": "El nombre del certificado", "port": "Puerto" }, - "title": "Defina el certificado para probar" + "title": "Definir el certificado a probar" } } }, diff --git a/homeassistant/components/climate/translations/es.json b/homeassistant/components/climate/translations/es.json index 09806ddf5ef..bf7e37c71ff 100644 --- a/homeassistant/components/climate/translations/es.json +++ b/homeassistant/components/climate/translations/es.json @@ -2,22 +2,22 @@ "device_automation": { "action_type": { "set_hvac_mode": "Cambiar el modo HVAC de {entity_name}.", - "set_preset_mode": "Cambiar la configuraci\u00f3n prefijada de {entity_name}" + "set_preset_mode": "Cambiar la configuraci\u00f3n preestablecida de {entity_name}" }, "condition_type": { "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico", - "is_preset_mode": "{entity_name} se establece en un modo predeterminado espec\u00edfico" + "is_preset_mode": "{entity_name} se establece en un modo preestablecido espec\u00edfico" }, "trigger_type": { - "current_humidity_changed": "{entity_name} humedad medida cambi\u00f3", - "current_temperature_changed": "{entity_name} temperatura medida cambi\u00f3", - "hvac_mode_changed": "{entity_name} Modo HVAC cambiado" + "current_humidity_changed": "{entity_name} cambi\u00f3 la humedad medida", + "current_temperature_changed": "{entity_name} cambi\u00f3 la temperatura medida", + "hvac_mode_changed": "{entity_name} cambi\u00f3 el modo HVAC" } }, "state": { "_": { "auto": "Autom\u00e1tico", - "cool": "Enfr\u00eda", + "cool": "Fr\u00edo", "dry": "Seco", "fan_only": "Solo ventilador", "heat": "Calor", diff --git a/homeassistant/components/cloud/translations/es.json b/homeassistant/components/cloud/translations/es.json index f81c71e8292..7eddc5c4109 100644 --- a/homeassistant/components/cloud/translations/es.json +++ b/homeassistant/components/cloud/translations/es.json @@ -2,11 +2,11 @@ "system_health": { "info": { "alexa_enabled": "Alexa habilitada", - "can_reach_cert_server": "Servidor de Certificados accesible", - "can_reach_cloud": "Home Assistant Cloud accesible", - "can_reach_cloud_auth": "Servidor de Autenticaci\u00f3n accesible", + "can_reach_cert_server": "Se llega al Servidor de Certificados", + "can_reach_cloud": "Se llega a Home Assistant Cloud", + "can_reach_cloud_auth": "Se llega al Servidor de Autenticaci\u00f3n", "google_enabled": "Google habilitado", - "logged_in": "Iniciada sesi\u00f3n", + "logged_in": "Sesi\u00f3n iniciada", "relayer_connected": "Relayer conectado", "remote_connected": "Remoto conectado", "remote_enabled": "Remoto habilitado", diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index d3ea7a842c5..d47711bf0a5 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", - "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.", "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_zone": "Zona no v\u00e1lida" }, "flow_title": "{name}", @@ -22,20 +22,20 @@ "data": { "records": "Registros" }, - "title": "Elija los registros que desea actualizar" + "title": "Elige los registros para actualizar" }, "user": { "data": { - "api_token": "Token de la API" + "api_token": "Token API" }, - "description": "Esta integraci\u00f3n requiere un token de API creado con los permisos Zone:Zone:Read y Zone:DNS:Edit para todas las zonas de su cuenta.", + "description": "Esta integraci\u00f3n requiere un token de API creado con los permisos Zone:Zone:Read y Zone:DNS:Edit para todas las zonas de tu cuenta.", "title": "Conectar con Cloudflare" }, "zone": { "data": { "zone": "Zona" }, - "title": "Elija la zona para actualizar" + "title": "Elige la Zona a Actualizar" } } } diff --git a/homeassistant/components/control4/translations/es.json b/homeassistant/components/control4/translations/es.json index c5d49e30680..900f87206e5 100644 --- a/homeassistant/components/control4/translations/es.json +++ b/homeassistant/components/control4/translations/es.json @@ -4,8 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fallo al conectar", - "invalid_auth": "Autentificaci\u00f3n invalida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -13,9 +13,9 @@ "data": { "host": "Direcci\u00f3n IP", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Volver a autenticar la integraci\u00f3n" }, - "description": "Por favor, introduzca su cuenta de Control4 y la direcci\u00f3n IP de su controlador local." + "description": "Por favor, introduce los detalles de tu cuenta Control4 y la direcci\u00f3n IP de tu controlador local." } } }, diff --git a/homeassistant/components/coolmaster/translations/es.json b/homeassistant/components/coolmaster/translations/es.json index a2c830756bd..14e9179f122 100644 --- a/homeassistant/components/coolmaster/translations/es.json +++ b/homeassistant/components/coolmaster/translations/es.json @@ -2,20 +2,20 @@ "config": { "error": { "cannot_connect": "No se pudo conectar", - "no_units": "No se ha encontrado ninguna unidad HVAC en el host CoolMasterNet." + "no_units": "No se pudo encontrar ninguna unidad HVAC en el host CoolMasterNet." }, "step": { "user": { "data": { - "cool": "Soporta el modo de enfriamiento", - "dry": "Soporta el modo seco", - "fan_only": "Soporta modo solo ventilador", - "heat": "Soporta modo calor", - "heat_cool": "Soporta el modo autom\u00e1tico de calor/fr\u00edo", + "cool": "Admite modo fr\u00edo", + "dry": "Admite modo seco", + "fan_only": "Admite modo solo ventilador", + "heat": "Admite modo de calor", + "heat_cool": "Admite modo autom\u00e1tico de calor/fr\u00edo", "host": "Host", "off": "Se puede apagar" }, - "title": "Configure los detalles de su conexi\u00f3n a CoolMasterNet." + "title": "Configura los detalles de tu conexi\u00f3n CoolMasterNet." } } } diff --git a/homeassistant/components/coronavirus/translations/es.json b/homeassistant/components/coronavirus/translations/es.json index 160bdc219a6..d6e05a3a560 100644 --- a/homeassistant/components/coronavirus/translations/es.json +++ b/homeassistant/components/coronavirus/translations/es.json @@ -9,7 +9,7 @@ "data": { "country": "Pa\u00eds" }, - "title": "Elige un pa\u00eds para monitorizar" + "title": "Elige un pa\u00eds para supervisar" } } } diff --git a/homeassistant/components/cover/translations/es.json b/homeassistant/components/cover/translations/es.json index 550c9d368e9..708c4a2dc7d 100644 --- a/homeassistant/components/cover/translations/es.json +++ b/homeassistant/components/cover/translations/es.json @@ -19,11 +19,11 @@ }, "trigger_type": { "closed": "{entity_name} cerrado", - "closing": "{entity_name} cerrando", - "opened": "abierto {entity_name}", - "opening": "abriendo {entity_name}", - "position": "Posici\u00f3n cambiada de {entity_name}", - "tilt_position": "Cambia la posici\u00f3n de inclinaci\u00f3n de {entity_name}" + "closing": "{entity_name} cerr\u00e1ndose", + "opened": "{entity_name} abierto", + "opening": "{entity_name} abri\u00e9ndose", + "position": "{entity_name} cambi\u00f3 de posici\u00f3n", + "tilt_position": "{entity_name} cambi\u00f3 de posici\u00f3n de inclinaci\u00f3n" } }, "state": { diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 6308bf7fed8..4e0d9ee96fc 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -14,11 +14,11 @@ "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento {addon}?", "title": "Puerta de enlace Zigbee deCONZ a trav\u00e9s del complemento Home Assistant" }, "link": { - "description": "Desbloquea tu puerta de enlace deCONZ para registrarlo con Home Assistant. \n\n 1. Ve a Configuraci\u00f3n deCONZ -> Puerta de enlace -> Avanzado\n 2. Presiona el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", + "description": "Desbloquea tu puerta de enlace deCONZ para registrarlo con Home Assistant. \n\n 1. Ve a Configuraci\u00f3n deCONZ -> Puerta de enlace -> Avanzado\n 2. Pulsa el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", "title": "Vincular con deCONZ" }, "manual_input": { @@ -29,7 +29,7 @@ }, "user": { "data": { - "host": "Seleccione la puerta de enlace descubierta deCONZ" + "host": "Selecciona la puerta de enlace deCONZ descubierta" } } } @@ -64,18 +64,18 @@ }, "trigger_type": { "remote_awakened": "Dispositivo despertado", - "remote_button_double_press": "Bot\u00f3n \"{subtype}\" doble pulsaci\u00f3n", + "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces", "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", - "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" cu\u00e1druple pulsaci\u00f3n", - "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" qu\u00edntuple pulsaci\u00f3n", + "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces", + "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", "remote_button_rotated_fast": "Bot\u00f3n \"{subtype}\" girado r\u00e1pido", "remote_button_rotation_stopped": "Se detuvo la rotaci\u00f3n del bot\u00f3n \"{subtype}\"", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", - "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" triple pulsaci\u00f3n", - "remote_double_tap": "Dispositivo \" {subtype} \" doble pulsaci\u00f3n", + "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado tres veces", + "remote_double_tap": "Doble toque en dispositivo \"{subtype}\"", "remote_double_tap_any_side": "Dispositivo con doble toque en cualquier lado", "remote_falling": "Dispositivo en ca\u00edda libre", "remote_flip_180_degrees": "Dispositivo volteado 180 grados", @@ -83,12 +83,12 @@ "remote_gyro_activated": "Dispositivo sacudido", "remote_moved": "Dispositivo movido con \"{subtype}\" hacia arriba", "remote_moved_any_side": "Dispositivo movido con cualquier lado hacia arriba", - "remote_rotate_from_side_1": "Dispositivo girado del \"lado 1\" al \" {subtype} \"", - "remote_rotate_from_side_2": "Dispositivo girado del \"lado 2\" al \" {subtype} \"", - "remote_rotate_from_side_3": "Dispositivo girado del \"lado 3\" al \" {subtype} \"", - "remote_rotate_from_side_4": "Dispositivo girado del \"lado 4\" al \" {subtype} \"", - "remote_rotate_from_side_5": "Dispositivo girado del \"lado 5\" al \" {subtype} \"", - "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"", + "remote_rotate_from_side_1": "Dispositivo girado desde \"lado 1\" a \"{subtype}\"", + "remote_rotate_from_side_2": "Dispositivo girado desde \"lado 2\" a \"{subtype}\"", + "remote_rotate_from_side_3": "Dispositivo girado desde \"lado 3\" a \"{subtype}\"", + "remote_rotate_from_side_4": "Dispositivo girado desde \"lado 4\" a \"{subtype}\"", + "remote_rotate_from_side_5": "Dispositivo girado desde \"lado 5\" a \"{subtype}\"", + "remote_rotate_from_side_6": "Dispositivo girado desde \"lado 6\" a \"{subtype}\"", "remote_turned_clockwise": "Dispositivo girado en el sentido de las agujas del reloj", "remote_turned_counter_clockwise": "Dispositivo girado en sentido contrario a las agujas del reloj" } @@ -101,7 +101,7 @@ "allow_deconz_groups": "Permitir grupos de luz deCONZ", "allow_new_devices": "Permitir a\u00f1adir autom\u00e1ticamente nuevos dispositivos" }, - "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ", + "description": "Configura la visibilidad de los tipos de dispositivos deCONZ", "title": "Opciones deCONZ" } } diff --git a/homeassistant/components/demo/translations/es.json b/homeassistant/components/demo/translations/es.json index ba1e31265b9..3fd439a1b05 100644 --- a/homeassistant/components/demo/translations/es.json +++ b/homeassistant/components/demo/translations/es.json @@ -42,7 +42,7 @@ }, "options_2": { "data": { - "multi": "Multiselecci\u00f3n", + "multi": "Selecci\u00f3n m\u00faltiple", "select": "Selecciona una opci\u00f3n", "string": "Valor de cadena" } diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index 55936dee05d..cf645e78ce4 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -3,17 +3,17 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "cannot_connect": "No se pudo conectar, int\u00e9ntelo de nuevo, desconectar los cables de alimentaci\u00f3n y Ethernet y volver a conectarlos puede ayudar", - "not_denonavr_manufacturer": "No es un receptor de red Denon AVR, el fabricante descubierto no coincide", - "not_denonavr_missing": "No es un Receptor AVR Denon AVR en Red, la informaci\u00f3n detectada no est\u00e1 completa" + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo. Desconectar los cables de la alimentaci\u00f3n el\u00e9ctrica y ethernet y volvi\u00e9ndolos a conectar puede ayudar", + "not_denonavr_manufacturer": "No es un Denon AVR Network Receiver, el fabricante descubierto no coincide", + "not_denonavr_missing": "No es un Denon AVR Network Receiver, la informaci\u00f3n de descubrimiento no est\u00e1 completa" }, "error": { - "discovery_error": "Error detectando un Receptor AVR Denon en Red" + "discovery_error": "No se pudo detectar ning\u00fan Denon AVR Network Receiver" }, "flow_title": "{name}", "step": { "confirm": { - "description": "Por favor confirma la adici\u00f3n del receptor" + "description": "Por favor, confirma la adici\u00f3n del receptor" }, "select": { "data": { diff --git a/homeassistant/components/deutsche_bahn/translations/it.json b/homeassistant/components/deutsche_bahn/translations/it.json index d390f84ff0a..22449ef7a17 100644 --- a/homeassistant/components/deutsche_bahn/translations/it.json +++ b/homeassistant/components/deutsche_bahn/translations/it.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "L'integrazione Deutsce Bahn \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.11. \n\nL'integrazione sar\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovi la configurazione YAML di Deutsce Bahn dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione Deutsche Bahn sar\u00e0 rimossa" } } diff --git a/homeassistant/components/dexcom/translations/es.json b/homeassistant/components/dexcom/translations/es.json index 58b4a014f50..0aefcd1c7f9 100644 --- a/homeassistant/components/dexcom/translations/es.json +++ b/homeassistant/components/dexcom/translations/es.json @@ -16,7 +16,7 @@ "username": "Nombre de usuario" }, "description": "Introducir las credenciales de Dexcom Share", - "title": "Configurar integraci\u00f3n de Dexcom" + "title": "Configurar la integraci\u00f3n Dexcom" } } }, diff --git a/homeassistant/components/directv/translations/es.json b/homeassistant/components/directv/translations/es.json index 2a268ac2148..81206472829 100644 --- a/homeassistant/components/directv/translations/es.json +++ b/homeassistant/components/directv/translations/es.json @@ -5,7 +5,7 @@ "unknown": "Error inesperado" }, "error": { - "cannot_connect": "Error al conectar" + "cannot_connect": "No se pudo conectar" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 5aefd6eb3af..509b6e7d8c7 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -6,7 +6,7 @@ "not_doorbird_device": "Este dispositivo no es un DoorBird" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/eafm/translations/es.json b/homeassistant/components/eafm/translations/es.json index 01dca9b3af3..8e38ac23e0a 100644 --- a/homeassistant/components/eafm/translations/es.json +++ b/homeassistant/components/eafm/translations/es.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "no_stations": "No se encontraron estaciones de monitoreo de inundaciones." + "no_stations": "No se encontraron estaciones de supervisi\u00f3n de inundaciones." }, "step": { "user": { "data": { "station": "Estaci\u00f3n" }, - "description": "Seleccione la estaci\u00f3n que desea monitorear", - "title": "Rastrear una estaci\u00f3n de monitoreo de inundaciones" + "description": "Selecciona la estaci\u00f3n que deseas supervisar", + "title": "Seguimiento de una estaci\u00f3n de supervisi\u00f3n de inundaciones" } } } diff --git a/homeassistant/components/ecobee/translations/es.json b/homeassistant/components/ecobee/translations/es.json index a7fdcfb2116..8b003c12e5f 100644 --- a/homeassistant/components/ecobee/translations/es.json +++ b/homeassistant/components/ecobee/translations/es.json @@ -4,19 +4,19 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "pin_request_failed": "Error al solicitar el PIN de ecobee; verifique que la clave API sea correcta.", - "token_request_failed": "Error al solicitar tokens de ecobee; Int\u00e9ntalo de nuevo." + "pin_request_failed": "Error al solicitar PIN de ecobee; por favor, verifica que la clave API sea correcta.", + "token_request_failed": "Error al solicitar tokens de ecobee; por favor, int\u00e9ntalo de nuevo." }, "step": { "authorize": { - "description": "Por favor, autoriza esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con el c\u00f3digo PIN: \n\n{pin}\n\nLuego, presiona Enviar.", + "description": "Por favor, autoriza esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con el c\u00f3digo PIN: \n\n{pin}\n\nLuego, pulsa Enviar.", "title": "Autorizar aplicaci\u00f3n en ecobee.com" }, "user": { "data": { "api_key": "Clave API" }, - "description": "Introduzca la clave de API obtenida de ecobee.com.", + "description": "Por favor, introduce la clave API obtenida de ecobee.com.", "title": "Clave API de ecobee" } } diff --git a/homeassistant/components/elgato/translations/es.json b/homeassistant/components/elgato/translations/es.json index eb0d7fde9b4..adef400e613 100644 --- a/homeassistant/components/elgato/translations/es.json +++ b/homeassistant/components/elgato/translations/es.json @@ -14,10 +14,10 @@ "host": "Host", "port": "Puerto" }, - "description": "Configura la integraci\u00f3n de Elgato Light con Home Assistant." + "description": "Configura tu Elgato Light para que se integre con Home Assistant." }, "zeroconf_confirm": { - "description": "\u00bfQuieres a\u00f1adir el Key Light de Elgato con n\u00famero de serie `{serial_number}` a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el Light de Elgato con n\u00famero de serie `{serial_number}` a Home Assistant?", "title": "Dispositivo Elgato Light descubierto" } } diff --git a/homeassistant/components/enocean/translations/es.json b/homeassistant/components/enocean/translations/es.json index 2bcc1074a23..4a141857cc9 100644 --- a/homeassistant/components/enocean/translations/es.json +++ b/homeassistant/components/enocean/translations/es.json @@ -1,24 +1,24 @@ { "config": { "abort": { - "invalid_dongle_path": "Ruta a mochila no v\u00e1lida", + "invalid_dongle_path": "Ruta al dongle no v\u00e1lida", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "invalid_dongle_path": "No se ha encontrado ninguna mochila v\u00e1lida en esta ruta" + "invalid_dongle_path": "No se ha encontrado ning\u00fan dongle en esta ruta" }, "step": { "detect": { "data": { - "path": "Ruta a mochila USB" + "path": "Ruta al dongle USB" }, - "title": "Selecciona la ruta a su mochila ENOcean" + "title": "Selecciona la ruta a tu dongle ENOcean" }, "manual": { "data": { - "path": "Ruta a mochila USB" + "path": "Ruta al dongle USB" }, - "title": "Introduce la ruta a tu mochila ENOcean" + "title": "Introduce la ruta a tu dongle ENOcean" } } } diff --git a/homeassistant/components/escea/translations/it.json b/homeassistant/components/escea/translations/it.json index 047fc1d0ff1..9f21d9b917c 100644 --- a/homeassistant/components/escea/translations/it.json +++ b/homeassistant/components/escea/translations/it.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "confirm": { + "description": "Vuoi configurare un caminetto Escea?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/fan/translations/es.json b/homeassistant/components/fan/translations/es.json index 15486b23a48..45efee6d4ce 100644 --- a/homeassistant/components/fan/translations/es.json +++ b/homeassistant/components/fan/translations/es.json @@ -2,17 +2,17 @@ "device_automation": { "action_type": { "toggle": "Alternar {entity_name}", - "turn_off": "Desactivar {entity_name}", - "turn_on": "Activar {entity_name}" + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est\u00e1 desactivado", - "is_on": "{entity_name} est\u00e1 activado" + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido" }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado" + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } }, "state": { diff --git a/homeassistant/components/flick_electric/translations/es.json b/homeassistant/components/flick_electric/translations/es.json index 10b77911016..01415c08678 100644 --- a/homeassistant/components/flick_electric/translations/es.json +++ b/homeassistant/components/flick_electric/translations/es.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "client_id": "ID de cliente (Opcional)", - "client_secret": "Secreto de Cliente (Opcional)", + "client_id": "ID de cliente (opcional)", + "client_secret": "Secreto de Cliente (opcional)", "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, diff --git a/homeassistant/components/flo/translations/es.json b/homeassistant/components/flo/translations/es.json index 9267e363693..c1d3e57b02f 100644 --- a/homeassistant/components/flo/translations/es.json +++ b/homeassistant/components/flo/translations/es.json @@ -4,8 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fallo al conectar", - "invalid_auth": "Autentificaci\u00f3n no valida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index 12152dc0211..5de43c8dcd1 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -25,7 +25,7 @@ "username": "Nombre de usuario" }, "description": "Para acceder a la API Personal de Flume, tendr\u00e1s que solicitar un 'Client ID' y un 'Client Secret' en https://portal.flumetech.com/settings#token", - "title": "Conectar con tu cuenta de Flume" + "title": "Conectar con tu cuenta Flume" } } } diff --git a/homeassistant/components/forked_daapd/translations/es.json b/homeassistant/components/forked_daapd/translations/es.json index af47865dd66..2e6c986a61e 100644 --- a/homeassistant/components/forked_daapd/translations/es.json +++ b/homeassistant/components/forked_daapd/translations/es.json @@ -5,10 +5,10 @@ "not_forked_daapd": "El dispositivo no es un servidor forked-daapd." }, "error": { - "forbidden": "No se puede conectar. Compruebe los permisos de red de bifurcaci\u00f3n.", + "forbidden": "No se puede conectar. Comprueba los permisos de tu forked-daapd.", "unknown_error": "Error inesperado", - "websocket_not_enabled": "Websocket no activado en servidor forked-daapd.", - "wrong_host_or_port": "No se ha podido conectar. Por favor comprueba host y puerto.", + "websocket_not_enabled": "Websocket del servidor forked-daapd no habilitado.", + "wrong_host_or_port": "No se ha podido conectar. Por favor, comprueba host y puerto.", "wrong_password": "Contrase\u00f1a incorrecta.", "wrong_server_type": "La integraci\u00f3n forked-daapd requiere un servidor forked-daapd con versi\u00f3n >= 27.0." }, @@ -18,7 +18,7 @@ "data": { "host": "Host", "name": "Nombre amigable", - "password": "Contrase\u00f1a API (dejar en blanco si no hay contrase\u00f1a)", + "password": "Contrase\u00f1a API (d\u00e9jala en blanco si no hay contrase\u00f1a)", "port": "Puerto API" }, "title": "Configurar dispositivo forked-daapd" @@ -34,8 +34,8 @@ "tts_pause_time": "Segundos para pausar antes y despu\u00e9s del TTS", "tts_volume": "Volumen TTS (decimal en el rango [0,1])" }, - "description": "Ajustar varias opciones para la integraci\u00f3n de forked-daapd", - "title": "Configurar opciones para forked-daapd" + "description": "Establece varias opciones para la integraci\u00f3n de forked-daapd.", + "title": "Configurar opciones de forked-daapd" } } } diff --git a/homeassistant/components/freebox/translations/es.json b/homeassistant/components/freebox/translations/es.json index 9cae8cc2089..d7b0a6b492d 100644 --- a/homeassistant/components/freebox/translations/es.json +++ b/homeassistant/components/freebox/translations/es.json @@ -4,13 +4,13 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "register_failed": "No se pudo registrar, int\u00e9ntalo de nuevo", + "cannot_connect": "No se pudo conectar", + "register_failed": "No se pudo registrar, por favor, int\u00e9ntalo de nuevo", "unknown": "Error inesperado" }, "step": { "link": { - "description": "Pulsa \"Enviar\", despu\u00e9s pulsa en la flecha derecha en el router para registrar Freebox con Home Assistant\n\n![Localizaci\u00f3n del bot\u00f3n en el router](/static/images/config_freebox.png)", + "description": "Haz clic en \"Enviar\", luego toca la flecha derecha en el router para registrar Freebox con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en el enrutador](/static/images/config_freebox.png)", "title": "Vincular router Freebox" }, "user": { diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index 71e5a2f5cf1..ed8184d958a 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -25,7 +25,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Actualice la informaci\u00f3n de inicio de sesi\u00f3n para {name}." + "description": "Actualiza tu 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 3be51f3bb6d..8e7dbbc1b8e 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "insufficient_permissions": "El usuario no tiene permisos suficientes para acceder a la configuraci\u00f3n de AVM FRITZ! Box y sus agendas telef\u00f3nicas.", + "insufficient_permissions": "El usuario no tiene suficientes permisos para acceder a la configuraci\u00f3n de AVM FRITZ!Box y sus agendas telef\u00f3nicas.", "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { @@ -12,7 +12,7 @@ "step": { "phonebook": { "data": { - "phonebook": "Directorio telef\u00f3nico" + "phonebook": "Agenda telef\u00f3nica" } }, "user": { @@ -27,7 +27,7 @@ }, "options": { "error": { - "malformed_prefixes": "Los prefijos tienen un formato incorrecto, comprueba el formato." + "malformed_prefixes": "Los prefijos tienen un formato incorrecto, por favor, verifica el formato." }, "step": { "init": { diff --git a/homeassistant/components/gdacs/translations/es.json b/homeassistant/components/gdacs/translations/es.json index 9b7c2686d6f..f0c1ec6a2d1 100644 --- a/homeassistant/components/gdacs/translations/es.json +++ b/homeassistant/components/gdacs/translations/es.json @@ -8,7 +8,7 @@ "data": { "radius": "Radio" }, - "title": "Rellena los datos de tu filtro." + "title": "Completa los detalles de tu filtro." } } } diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json index 712f554ac19..14b534271f9 100644 --- a/homeassistant/components/geocaching/translations/es.json +++ b/homeassistant/components/geocaching/translations/es.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" diff --git a/homeassistant/components/geonetnz_quakes/translations/es.json b/homeassistant/components/geonetnz_quakes/translations/es.json index 371dc967a82..90d2486b270 100644 --- a/homeassistant/components/geonetnz_quakes/translations/es.json +++ b/homeassistant/components/geonetnz_quakes/translations/es.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "Radio" }, - "title": "Complete todos los campos requeridos" + "title": "Completa los detalles de tu filtro." } } } diff --git a/homeassistant/components/geonetnz_volcano/translations/es.json b/homeassistant/components/geonetnz_volcano/translations/es.json index 1621a42eb4a..109eaf73729 100644 --- a/homeassistant/components/geonetnz_volcano/translations/es.json +++ b/homeassistant/components/geonetnz_volcano/translations/es.json @@ -8,7 +8,7 @@ "data": { "radius": "Radio" }, - "title": "Complete los detalles de su filtro." + "title": "Completa los detalles de tu filtro." } } } diff --git a/homeassistant/components/glances/translations/es.json b/homeassistant/components/glances/translations/es.json index 01f2a132136..22187e65793 100644 --- a/homeassistant/components/glances/translations/es.json +++ b/homeassistant/components/glances/translations/es.json @@ -17,7 +17,7 @@ "ssl": "Utiliza un certificado SSL", "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL", - "version": "Versi\u00f3n API Glances (2 o 3)" + "version": "Versi\u00f3n API de Glances (2 o 3)" }, "title": "Configurar Glances" } diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index eaf84a11931..be94117d1bd 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -9,7 +9,7 @@ "cannot_connect": "No se pudo conectar", "code_expired": "El c\u00f3digo de autenticaci\u00f3n caduc\u00f3 o la configuraci\u00f3n de la credencial no es v\u00e1lida, por favor, int\u00e9ntalo de nuevo.", "invalid_access_token": "Token de acceso no v\u00e1lido", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index c918a3fe583..a7f54c0a726 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -23,7 +23,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con una ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`. A continuaci\u00f3n, haz clic en ENVIAR m\u00e1s abajo para marcar este problema como resuelto.", "title": "El servicio {deprecated_service} ser\u00e1 eliminado" } } diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 25a0abb5526..35787e95524 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -23,12 +23,12 @@ "fix_flow": { "step": { "confirm": { - "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", - "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette a(z) `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a(z) `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", + "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } }, - "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json index e6450dae21d..4f43ace1b71 100644 --- a/homeassistant/components/guardian/translations/it.json +++ b/homeassistant/components/guardian/translations/it.json @@ -17,5 +17,18 @@ "description": "Configura un dispositivo Elexa Guardian locale." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio e utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione di `{alternate_target}`. Quindi, fai clic su INVIA di seguito per contrassegnare questo problema come risolto.", + "title": "Il servizio {deprecated_service} verr\u00e0 rimosso" + } + } + }, + "title": "Il servizio {deprecated_service} verr\u00e0 rimosso" + } } } \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/es.json b/homeassistant/components/harmony/translations/es.json index 527cd8433e8..3acc9abb07e 100644 --- a/homeassistant/components/harmony/translations/es.json +++ b/homeassistant/components/harmony/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{name}", diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 4a6fda89d84..2d88b0d2252 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -7,7 +7,7 @@ "disk_used": "Disco usado", "docker_version": "Versi\u00f3n de Docker", "healthy": "Saludable", - "host_os": "Sistema operativo del Host", + "host_os": "Sistema Operativo del Host", "installed_addons": "Complementos instalados", "supervisor_api": "API del Supervisor", "supervisor_version": "Versi\u00f3n del Supervisor", diff --git a/homeassistant/components/home_connect/translations/es.json b/homeassistant/components/home_connect/translations/es.json index 9ee5769583b..a476741a8b1 100644 --- a/homeassistant/components/home_connect/translations/es.json +++ b/homeassistant/components/home_connect/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json index ff4e671fb65..4796a9e1236 100644 --- a/homeassistant/components/home_plus_control/translations/es.json +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index db63c818f61..5b447f7177f 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -7,7 +7,7 @@ "docker": "Docker", "hassio": "Supervisor", "installation_type": "Tipo de instalaci\u00f3n", - "os_name": "Familia del sistema operativo", + "os_name": "Familia de Sistema Operativo", "os_version": "Versi\u00f3n del Sistema Operativo", "python_version": "Versi\u00f3n de Python", "timezone": "Zona horaria", diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 1813b96dd7a..3bca7c0f253 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "port_name_in_use": "Ya existe un enlace o accesorio configurado con ese nombre o puerto." + "port_name_in_use": "Ya est\u00e1 configurado un accesorio o puente con el mismo nombre o puerto." }, "step": { "pairing": { - "description": "Para completar el emparejamiento, sigue las instrucciones en \"Notificaciones\" en \"Emparejamiento HomeKit\".", - "title": "Vinculaci\u00f3n HomeKit" + "description": "Para completar el emparejamiento, sigue las instrucciones en \"Notificaciones\" bajo \"Emparejamiento HomeKit\".", + "title": "Emparejar HomeKit" }, "user": { "data": { @@ -38,7 +38,7 @@ "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" }, "description": "Verifica todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", - "title": "Configuraci\u00f3n de la c\u00e1mara" + "title": "Configuraci\u00f3n de C\u00e1mara" }, "exclude": { "data": { @@ -58,10 +58,10 @@ "data": { "domains": "Dominios para incluir", "include_exclude_mode": "Modo de inclusi\u00f3n", - "mode": "Mode de HomeKit" + "mode": "Modo de HomeKit" }, "description": "HomeKit se puede configurar para exponer un puente o un solo accesorio. En el modo accesorio, solo se puede usar una sola entidad. El modo accesorio es necesario para que los reproductores multimedia con la clase de dispositivo TV funcionen correctamente. Las entidades en los \"Dominios para incluir\" se incluir\u00e1n en HomeKit. Podr\u00e1s seleccionar qu\u00e9 entidades incluir o excluir de esta lista en la siguiente pantalla.", - "title": "Selecciona el modo y dominios." + "title": "Selecciona el modo y los dominios." }, "yaml": { "description": "Esta entrada se controla a trav\u00e9s de YAML", diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index b446c87e70f..6185c432007 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -5,7 +5,7 @@ "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.", - "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", + "ignored_model": "El soporte HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", "invalid_config_entry": "Este dispositivo se muestra como listo para emparejarse, pero ya hay una entrada de configuraci\u00f3n conflictiva para \u00e9l en Home Assistant que primero debe eliminarse.", "invalid_properties": "Propiedades no v\u00e1lidas anunciadas por dispositivo.", "no_devices": "No se encontraron dispositivos no emparejados" @@ -13,20 +13,20 @@ "error": { "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor, rev\u00edsalo e int\u00e9ntalo de nuevo.", "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado no es seguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", - "max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.", + "max_peers_error": "El dispositivo se neg\u00f3 a a\u00f1adir el emparejamiento porque no tiene almacenamiento disponible para emparejamientos.", "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no sea compatible actualmente.", - "unable_to_pair": "No se puede emparejar, int\u00e9ntalo de nuevo.", + "unable_to_pair": "No se puede emparejar, por favor, int\u00e9ntalo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. El emparejamiento ha fallado." }, "flow_title": "{name} ({category})", "step": { "busy_error": { - "description": "Interrumpe el emparejamiento en todos los controladores o intenta reiniciar el dispositivo y luego contin\u00faa con el emparejamiento.", + "description": "Cancela el emparejamiento en todos los controladores, o intenta reiniciar el dispositivo, luego contin\u00faa con el emparejamiento.", "title": "El dispositivo ya est\u00e1 emparejando con otro controlador" }, "max_tries_error": { - "description": "El dispositivo ha recibido m\u00e1s de 100 intentos de autenticaci\u00f3n fallidos. Intenta reiniciar el dispositivo, luego contin\u00faa para reanudar el emparejamiento.", - "title": "M\u00e1ximo n\u00famero de intentos de autenticaci\u00f3n superados" + "description": "El dispositivo ha recibido m\u00e1s de 100 intentos de autenticaci\u00f3n fallidos. Intenta reiniciar el dispositivo, luego contin\u00faa con el emparejamiento.", + "title": "Se excedieron los intentos m\u00e1ximos de autenticaci\u00f3n" }, "pair": { "data": { @@ -37,8 +37,8 @@ "title": "Emparejar con un dispositivo a trav\u00e9s del protocolo de accesorios HomeKit" }, "protocol_error": { - "description": "Es posible que el dispositivo no est\u00e9 en modo de emparejamiento y que requiera que se presione un bot\u00f3n f\u00edsico o virtual. Aseg\u00farate de que el dispositivo est\u00e1 en modo de emparejamiento o intenta reiniciar el dispositivo, luego contin\u00faa para reanudar el emparejamiento.", - "title": "Error al comunicarse con el accesorio" + "description": "Es posible que el dispositivo no est\u00e9 en modo de emparejamiento y que se requiera pulsar un bot\u00f3n f\u00edsico o virtual. Aseg\u00farate de que el dispositivo est\u00e9 en modo de emparejamiento o intenta reiniciar el dispositivo, luego contin\u00faa con el emparejamiento.", + "title": "Error de comunicaci\u00f3n con el accesorio" }, "user": { "data": { diff --git a/homeassistant/components/homekit_controller/translations/sensor.it.json b/homeassistant/components/homekit_controller/translations/sensor.it.json new file mode 100644 index 00000000000..acfff28f0b2 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.it.json @@ -0,0 +1,10 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "none": "Nessuna" + }, + "homekit_controller__thread_status": { + "disabled": "Disabilitato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 9bef5d09eec..03b159989f2 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -9,7 +9,7 @@ "incorrect_username": "Nombre de usuario incorrecto", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_url": "URL no v\u00e1lida", - "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, int\u00e9ntelo de nuevo m\u00e1s tarde.", + "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, por favor, int\u00e9ntalo de nuevo m\u00e1s tarde.", "response_error": "Error desconocido del dispositivo", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index 456668fec88..e49438144b7 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -8,7 +8,7 @@ "discover_timeout": "Imposible encontrar pasarelas Philips Hue", "invalid_host": "Host inv\u00e1lido", "no_bridges": "No se han encontrado pasarelas Philips Hue.", - "not_hue_bridge": "No es un enlace Hue", + "not_hue_bridge": "No es un puente Hue", "unknown": "Error inesperado" }, "error": { @@ -53,7 +53,7 @@ }, "trigger_type": { "double_short_release": "Ambos \"{subtype}\" soltados", - "initial_press": "Bot\u00f3n \"{subtype}\" presionado inicialmente", + "initial_press": "Bot\u00f3n \"{subtype}\" pulsado inicialmente", "long_release": "Bot\u00f3n \"{subtype}\" soltado tras una pulsaci\u00f3n larga", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", @@ -68,7 +68,7 @@ "step": { "init": { "data": { - "allow_hue_groups": "Permitir grupos de Hue", + "allow_hue_groups": "Permitir grupos Hue", "allow_hue_scenes": "Permitir escenas Hue", "allow_unreachable": "Permitir que las bombillas inalcanzables informen su estado correctamente", "ignore_availability": "Ignorar el estado de conectividad de los siguientes dispositivos" diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json index 944361ef35d..7a03cf901fc 100644 --- a/homeassistant/components/humidifier/translations/es.json +++ b/homeassistant/components/humidifier/translations/es.json @@ -10,13 +10,13 @@ "condition_type": { "is_mode": "{entity_name} est\u00e1 configurado en un modo espec\u00edfico", "is_off": "{entity_name} est\u00e1 apagado", - "is_on": "{entity_name} est\u00e1 activado" + "is_on": "{entity_name} est\u00e1 encendido" }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", - "target_humidity_changed": "La humedad objetivo ha cambiado en {entity_name}", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado" + "target_humidity_changed": "{entity_name} cambi\u00f3 su humedad objetivo", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } }, "state": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json index c0d2ad2e4e5..7e880c09441 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/es.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json @@ -11,13 +11,13 @@ "step": { "link": { "description": "\u00bfQuieres configurar {name} ({host})?", - "title": "Conectar con el PowerView Hub" + "title": "Conectar al PowerView Hub" }, "user": { "data": { "host": "Direcci\u00f3n IP" }, - "title": "Conectar con el PowerView Hub" + "title": "Conectar al PowerView Hub" } } } diff --git a/homeassistant/components/hvv_departures/translations/es.json b/homeassistant/components/hvv_departures/translations/es.json index 8cfa90d6367..38a929c3575 100644 --- a/homeassistant/components/hvv_departures/translations/es.json +++ b/homeassistant/components/hvv_departures/translations/es.json @@ -11,13 +11,13 @@ "step": { "station": { "data": { - "station": "Estacion/Direccion" + "station": "Estaci\u00f3n/Direcci\u00f3n" }, - "title": "Introducir Estaci\u00f3n/Direcci\u00f3n" + "title": "Introduce la Estaci\u00f3n/Direcci\u00f3n" }, "station_select": { "data": { - "station": "Estacion/Direccion" + "station": "Estaci\u00f3n/Direcci\u00f3n" }, "title": "Seleccionar Estaci\u00f3n/Direcci\u00f3n" }, @@ -27,7 +27,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Conectar con el API de HVV" + "title": "Conectar con la API de HVV" } } }, @@ -36,7 +36,7 @@ "init": { "data": { "filter": "Seleccionar l\u00edneas", - "offset": "Desfase (minutos)", + "offset": "Compensaci\u00f3n (minutos)", "real_time": "Usar datos en tiempo real" }, "description": "Cambiar opciones para este sensor de salidas", diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index 45e41859c81..91143ec96a8 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -3,11 +3,11 @@ "abort": { "already_configured": "El servicio ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "auth_new_token_not_granted_error": "El token reci\u00e9n creado no se aprob\u00f3 en la interfaz de usuario de Hyperion", - "auth_new_token_not_work_error": "Error al autenticarse con el token reci\u00e9n creado", + "auth_new_token_not_granted_error": "El token reci\u00e9n creado no se aprob\u00f3 en la IU de Hyperion", + "auth_new_token_not_work_error": "No se pudo autenticar usando un token reci\u00e9n creado", "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n", "cannot_connect": "No se pudo conectar", - "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su identificaci\u00f3n", + "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su id", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { @@ -18,20 +18,20 @@ "auth": { "data": { "create_token": "Crea un nuevo token autom\u00e1ticamente", - "token": "O proporcionar un token preexistente" + "token": "O proporciona un token preexistente" }, - "description": "Configurar autorizaci\u00f3n a tu servidor Hyperion Ambilight" + "description": "Configura la autorizaci\u00f3n a tu servidor Hyperion Ambilight" }, "confirm": { - "description": "\u00bfQuieres a\u00f1adir el siguiente Ambilight de Hyperion a Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**ID**: {id}", - "title": "Confirmar la adici\u00f3n del servicio Hyperion Ambilight" + "description": "\u00bfQuieres a\u00f1adir el siguiente Hyperion Ambilight a Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**ID**: {id}", + "title": "Confirmar para a\u00f1adir el servicio Hyperion Ambilight" }, "create_token": { - "description": "Elige ** Enviar ** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Se te redirigir\u00e1 a la interfaz de usuario de Hyperion para aprobar la solicitud. Verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"", + "description": "Elige **Enviar** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Ser\u00e1s redirigido a la IU de Hyperion para aprobar la solicitud. Verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"", "title": "Crear autom\u00e1ticamente un nuevo token de autenticaci\u00f3n" }, "create_token_external": { - "title": "Aceptar nuevo token en la interfaz de usuario de Hyperion" + "title": "Aceptar nuevo token en la IU de Hyperion" }, "user": { "data": { diff --git a/homeassistant/components/iaqualink/translations/es.json b/homeassistant/components/iaqualink/translations/es.json index 60f26b1c64d..967d52b7319 100644 --- a/homeassistant/components/iaqualink/translations/es.json +++ b/homeassistant/components/iaqualink/translations/es.json @@ -14,7 +14,7 @@ "username": "Nombre de usuario" }, "description": "Por favor, introduce el nombre de usuario y contrase\u00f1a de tu cuenta iAqualink.", - "title": "Conexi\u00f3n con iAqualink" + "title": "Conectar a iAqualink" } } } diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index 9140c843483..e5e6a8927a9 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -7,7 +7,7 @@ }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n", + "send_verification_code": "No se pudo enviar el c\u00f3digo de verificaci\u00f3n", "validate_verification_code": "No se ha podido verificar el c\u00f3digo de verificaci\u00f3n, vuelve a intentarlo" }, "step": { @@ -22,7 +22,7 @@ "data": { "trusted_device": "Dispositivo de confianza" }, - "description": "Seleccione su dispositivo de confianza", + "description": "Selecciona tu dispositivo de confianza", "title": "Dispositivo de confianza iCloud" }, "user": { @@ -31,14 +31,14 @@ "username": "Correo electr\u00f3nico", "with_family": "Con la familia" }, - "description": "Ingrese sus credenciales", + "description": "Introduce tus credenciales", "title": "Credenciales iCloud" }, "verification_code": { "data": { "verification_code": "C\u00f3digo de verificaci\u00f3n" }, - "description": "Por favor, introduzca el c\u00f3digo de verificaci\u00f3n que acaba de recibir de iCloud", + "description": "Por favor, introduce el c\u00f3digo de verificaci\u00f3n que acabas de recibir de iCloud", "title": "C\u00f3digo de verificaci\u00f3n de iCloud" } } diff --git a/homeassistant/components/input_datetime/translations/es.json b/homeassistant/components/input_datetime/translations/es.json index 570c7b17592..e7824db9ac1 100644 --- a/homeassistant/components/input_datetime/translations/es.json +++ b/homeassistant/components/input_datetime/translations/es.json @@ -1,3 +1,3 @@ { - "title": "Entrada de data i hora" + "title": "Entrada de fecha y hora" } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 730277dd5c0..8dbe09d635c 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "not_insteon_device": "El dispositivo descubierto no es un dispositivo Insteon", - "single_instance_allowed": "Ya esta 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", - "select_single": "Seleccione una opci\u00f3n." + "select_single": "Selecciona una opci\u00f3n." }, "flow_title": "{name}", "step": { @@ -19,7 +19,7 @@ "host": "Direcci\u00f3n IP", "port": "Puerto" }, - "description": "Configure el Insteon Hub Versi\u00f3n 1 (anterior a 2014).", + "description": "Configura el Insteon Hub Versi\u00f3n 1 (anterior a 2014).", "title": "Insteon Hub Versi\u00f3n 1" }, "hubv2": { @@ -29,29 +29,29 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Configure el Insteon Hub versi\u00f3n 2.", + "description": "Configura el Insteon Hub Versi\u00f3n 2.", "title": "Insteon Hub Versi\u00f3n 2" }, "plm": { "data": { "device": "Ruta del dispositivo USB" }, - "description": "Configura el M\u00f3dem Insteon PowerLink (PLM).", + "description": "Configura el Insteon PowerLink Modem (PLM).", "title": "Insteon PLM" }, "user": { "data": { "modem_type": "Tipo de m\u00f3dem." }, - "description": "Seleccione el tipo de m\u00f3dem Insteon." + "description": "Selecciona el tipo de m\u00f3dem Insteon." } } }, "options": { "error": { "cannot_connect": "No se pudo conectar", - "input_error": "Entradas no v\u00e1lidas, compruebe sus valores.", - "select_single": "Selecciona una opci\u00f3n" + "input_error": "Entradas no v\u00e1lidas, por favor, verifica tus valores.", + "select_single": "Selecciona una opci\u00f3n." }, "step": { "add_override": { @@ -60,16 +60,16 @@ "cat": "Categor\u00eda del dispositivo (es decir, 0x10)", "subcat": "Subcategor\u00eda del dispositivo (es decir, 0x0a)" }, - "description": "Agregue una anulaci\u00f3n del dispositivo." + "description": "A\u00f1ade una anulaci\u00f3n de dispositivo." }, "add_x10": { "data": { - "housecode": "C\u00f3digo de casa (a - p)", + "housecode": "Housecode (a - p)", "platform": "Plataforma", "steps": "Pasos de atenuaci\u00f3n (s\u00f3lo para dispositivos de luz, por defecto 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "Cambie la contrase\u00f1a del Hub Insteon." + "description": "Cambia la contrase\u00f1a del Hub Insteon." }, "change_hub_config": { "data": { @@ -78,26 +78,26 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Cambiar la informaci\u00f3n de la conexi\u00f3n del Hub Insteon. Debes reiniciar el Home Assistant despu\u00e9s de hacer este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n del Hub usa la aplicaci\u00f3n Hub." + "description": "Cambia la informaci\u00f3n de conexi\u00f3n del Hub Insteon. Debes reiniciar Home Assistant despu\u00e9s de realizar este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n en el Hub, usa la aplicaci\u00f3n Hub." }, "init": { "data": { - "add_override": "Agregue una anulaci\u00f3n del dispositivo.", + "add_override": "A\u00f1ade una anulaci\u00f3n de dispositivo.", "add_x10": "A\u00f1ade un dispositivo X10.", - "change_hub_config": "Cambie la configuraci\u00f3n del Hub.", - "remove_override": "Eliminar una anulaci\u00f3n del dispositivo.", - "remove_x10": "Eliminar un dispositivo X10" + "change_hub_config": "Cambia la configuraci\u00f3n del Hub.", + "remove_override": "Eliminar una anulaci\u00f3n de dispositivo.", + "remove_x10": "Eliminar un dispositivo X10." } }, "remove_override": { "data": { - "address": "Seleccione una direcci\u00f3n del dispositivo para eliminar" + "address": "Selecciona una direcci\u00f3n de dispositivo para eliminar" }, - "description": "Eliminar una anulaci\u00f3n del dispositivo" + "description": "Elimina una anulaci\u00f3n de dispositivo" }, "remove_x10": { "data": { - "address": "Seleccione la direcci\u00f3n del dispositivo para eliminar" + "address": "Selecciona una direcci\u00f3n de dispositivo para eliminar" }, "description": "Eliminar un dispositivo X10" } diff --git a/homeassistant/components/ipma/translations/es.json b/homeassistant/components/ipma/translations/es.json index d942608ad87..089a58903b4 100644 --- a/homeassistant/components/ipma/translations/es.json +++ b/homeassistant/components/ipma/translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "El nombre ya existe" + "name_exists": "Nombre ya existe" }, "step": { "user": { @@ -18,7 +18,7 @@ }, "system_health": { "info": { - "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API IPMA" + "api_endpoint_reachable": "Se puede llegar al punto de conexi\u00f3n de la API IPMA" } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index a5a067b5ffd..845ba0b2b7b 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -4,9 +4,9 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "connection_upgrade": "No se pudo conectar con la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n.", - "ipp_error": "Error IPP encontrado.", - "ipp_version_error": "Versi\u00f3n de IPP no compatible con la impresora.", - "parse_error": "Error al analizar la respuesta de la impresora.", + "ipp_error": "Se encontr\u00f3 un error de IPP.", + "ipp_version_error": "La versi\u00f3n de IPP no es compatible con la impresora.", + "parse_error": "No se pudo analizar la respuesta de la impresora.", "unique_id_required": "El dispositivo no tiene identificaci\u00f3n \u00fanica necesaria para el descubrimiento." }, "error": { @@ -28,7 +28,7 @@ }, "zeroconf_confirm": { "description": "\u00bfQuieres configurar {name}?", - "title": "Impresora encontrada" + "title": "Impresora descubierta" } } } diff --git a/homeassistant/components/iqvia/translations/es.json b/homeassistant/components/iqvia/translations/es.json index dc26ca6c065..b864075d56f 100644 --- a/homeassistant/components/iqvia/translations/es.json +++ b/homeassistant/components/iqvia/translations/es.json @@ -11,7 +11,7 @@ "data": { "zip_code": "C\u00f3digo postal" }, - "description": "Indica tu c\u00f3digo postal de Estados Unidos o Canad\u00e1." + "description": "Completa tu c\u00f3digo postal de EE UU. o Canad\u00e1." } } } diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index 4c3c4475743..0890cf8e251 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", @@ -28,7 +28,7 @@ "username": "Nombre de usuario" }, "description": "La entrada del host debe estar en formato URL completo, por ejemplo, http://192.168.10.100:80", - "title": "Conexi\u00f3n con ISY" + "title": "Conectar con tu ISY" } } }, @@ -37,7 +37,7 @@ "init": { "data": { "ignore_string": "Ignorar Cadena", - "restore_light_state": "Restaurar Intensidad de la Luz", + "restore_light_state": "Restaurar el brillo de la luz", "sensor_string": "Cadena Nodo Sensor", "variable_sensor_string": "Cadena de Sensor Variable" }, diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json index 84e39aed3c5..15b266d4f39 100644 --- a/homeassistant/components/keenetic_ndms2/translations/es.json +++ b/homeassistant/components/keenetic_ndms2/translations/es.json @@ -30,7 +30,7 @@ "include_associated": "Usar datos de asociaciones de puntos de acceso WiFi (se ignoran si se usan datos de puntos de acceso)", "interfaces": "Elige las interfaces para escanear", "scan_interval": "Intervalo de escaneo", - "try_hotspot": "Usar los datos de 'ip hotspot' (m\u00e1s precisos)" + "try_hotspot": "Usar los datos de 'ip hotspot' (m\u00e1s preciso)" } } } diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json index 0b7b37a6e11..5ea61e0e0c9 100644 --- a/homeassistant/components/kodi/translations/es.json +++ b/homeassistant/components/kodi/translations/es.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autentificacion invalida", - "no_uuid": "La instancia de Kodi no tiene un identificador \u00fanico. Esto probablemente es debido a una versi\u00f3n antigua Kodi (17.x o inferior). Puedes configurar la integraci\u00f3n manualmente o actualizar a una versi\u00f3n m\u00e1s reciente de Kodi.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_uuid": "La instancia de Kodi no tiene un identificador \u00fanico. Lo m\u00e1s probable es que se deba a una versi\u00f3n antigua de Kodi (17.x o inferior). Puedes configurar la integraci\u00f3n manualmente o actualizar a una versi\u00f3n m\u00e1s reciente de Kodi.", "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "flow_title": "{name}", @@ -19,7 +19,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca su nombre de usuario y contrase\u00f1a de Kodi. Estos se pueden encontrar en Sistema/Configuraci\u00f3n/Red/Servicios." + "description": "Por favor, introduce tu nombre de usuario y contrase\u00f1a de Kodi. Estos se pueden encontrar en Sistema/Configuraci\u00f3n/Red/Servicios." }, "discovery_confirm": { "description": "\u00bfQuieres a\u00f1adir Kodi (`{name}`) a Home Assistant?", @@ -31,7 +31,7 @@ "port": "Puerto", "ssl": "Utiliza un certificado SSL" }, - "description": "Informaci\u00f3n de la conexi\u00f3n de Kodi. Por favor, aseg\u00farese de habilitar \"Permitir el control de Kodi v\u00eda HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." + "description": "Informaci\u00f3n de conexi\u00f3n de Kodi. Aseg\u00farate de habilitar \"Permitir el control de Kodi a trav\u00e9s de HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." }, "ws_port": { "data": { diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 7ba726e8e6a..47a4d1dc5b4 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -12,7 +12,7 @@ }, "step": { "confirm": { - "description": "Modelo: {model}\nID: {id}\nHost: {host}\nPuerto: {port}\n\nPuede configurar las E/S y el comportamiento del panel en los ajustes del Panel de Alarmas Konnected.", + "description": "Modelo: {model}\nID: {id}\nHost: {host}\nPuerto: {port}\n\nPuedes configurar la E/S y el comportamiento del panel en los ajustes del Panel de Alarmas Konnected.", "title": "Dispositivo Konnected Listo" }, "import_confirm": { @@ -24,7 +24,7 @@ "host": "Direcci\u00f3n IP", "port": "Puerto" }, - "description": "Introduzca la informaci\u00f3n del host de su panel Konnected." + "description": "Por favor, introduce la informaci\u00f3n del host para tu Konnected Panel." } } }, @@ -33,17 +33,17 @@ "not_konn_panel": "No es un dispositivo Konnected.io reconocido" }, "error": { - "bad_host": "La URL de sustituci\u00f3n del host de la API es inv\u00e1lida" + "bad_host": "Anulaci\u00f3n de la URL de la API del host no v\u00e1lida" }, "step": { "options_binary": { "data": { "inverse": "Invertir el estado de apertura/cierre", "name": "Nombre", - "type": "Tipo de sensor binario" + "type": "Tipo de Sensor Binario" }, "description": "Opciones de {zone}", - "title": "Configurar sensor binario" + "title": "Configurar Sensor Binario" }, "options_digital": { "data": { @@ -52,7 +52,7 @@ "type": "Tipo de sensor" }, "description": "Opciones de {zone}", - "title": "Configurar el sensor digital" + "title": "Configurar Sensor Digital" }, "options_io": { "data": { @@ -65,7 +65,7 @@ "7": "Zona 7", "out": "OUT" }, - "description": "Descubierto un {model} en {host} . Seleccione la configuraci\u00f3n base de cada I/O a continuaci\u00f3n: seg\u00fan la I/O, puede permitir sensores binarios (contactos de apertura / cierre), sensores digitales (dht y ds18b20) o salidas conmutables. Podr\u00e1 configurar opciones detalladas en los pr\u00f3ximos pasos.", + "description": "Descubierto un {model} en {host}. Selecciona la configuraci\u00f3n base de cada E/S a continuaci\u00f3n; seg\u00fan la E/S, puedes permitir sensores binarios (contactos abiertos/cerrados), sensores digitales (dht y ds18b20) o salidas conmutables. Podr\u00e1s configurar opciones detalladas en los siguientes pasos.", "title": "Configurar E/S" }, "options_io_ext": { @@ -75,34 +75,34 @@ "12": "Zona 12", "8": "Zona 8", "9": "Zona 9", - "alarm1": "ALARMA1", + "alarm1": "ALARM1", "alarm2_out2": "OUT2/ALARM2", "out1": "OUT1" }, - "description": "Seleccione la configuraci\u00f3n de las E/S restantes a continuaci\u00f3n. Podr\u00e1s configurar opciones detalladas en los pr\u00f3ximos pasos.", + "description": "Selecciona la configuraci\u00f3n de las E/S restantes a continuaci\u00f3n. Podr\u00e1s configurar opciones detalladas en los siguientes pasos.", "title": "Configurar E/S extendidas" }, "options_misc": { "data": { "api_host": "Anular la URL de la API del host", "blink": "Parpadear el LED del panel al enviar un cambio de estado", - "discovery": "Responde a las solicitudes de descubrimiento en tu red", - "override_api_host": "Reemplazar la URL predeterminada del panel host de la API de Home Assistant" + "discovery": "Responder a las solicitudes de descubrimiento en tu red", + "override_api_host": "Anular la URL predeterminada del panel de host de la API de Home Assistant" }, - "description": "Seleccione el comportamiento deseado para su panel", + "description": "Selecciona el comportamiento deseado para tu panel", "title": "Configurar miscel\u00e1neos" }, "options_switch": { "data": { "activation": "Salida cuando est\u00e1 activada", "momentary": "Duraci\u00f3n del pulso (ms)", - "more_states": "Configurar estados adicionales para esta zona", + "more_states": "Configura estados adicionales para esta zona", "name": "Nombre", "pause": "Pausa entre pulsos (ms)", "repeat": "Veces que se repite (-1=infinito)" }, "description": "Opciones de {zone}: estado {state}", - "title": "Configurar la salida conmutable" + "title": "Configurar salida conmutable" } } } diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index 4645b9c88b9..72e880463fe 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, @@ -12,7 +12,7 @@ "error": { "already_configured": "La cuenta ya est\u00e1 configurada", "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_username": "Nombre de usuario no v\u00e1lido", "unknown": "Error inesperado" }, @@ -29,7 +29,7 @@ "username": "Nombre de usuario" }, "description": "Para configurar las opciones avanzadas, consulta la [documentaci\u00f3n de Life360]({docs_url}).\nEs posible que quieras hacerlo antes de a\u00f1adir cuentas.", - "title": "Configurar la cuenta de Life360" + "title": "Configurar cuenta de Life360" } } }, diff --git a/homeassistant/components/light/translations/es.json b/homeassistant/components/light/translations/es.json index 94e28719702..7270ce6e990 100644 --- a/homeassistant/components/light/translations/es.json +++ b/homeassistant/components/light/translations/es.json @@ -3,7 +3,7 @@ "action_type": { "brightness_decrease": "Disminuir brillo de {entity_name}", "brightness_increase": "Aumentar brillo de {entity_name}", - "flash": "Destellos {entity_name}", + "flash": "Destellear {entity_name}", "toggle": "Alternar {entity_name}", "turn_off": "Apagar {entity_name}", "turn_on": "Encender {entity_name}" @@ -20,7 +20,7 @@ }, "state": { "_": { - "off": "Apagado", + "off": "Apagada", "on": "Encendida" } }, diff --git a/homeassistant/components/lock/translations/es.json b/homeassistant/components/lock/translations/es.json index 5cc0e80f97a..90347ba017d 100644 --- a/homeassistant/components/lock/translations/es.json +++ b/homeassistant/components/lock/translations/es.json @@ -6,12 +6,12 @@ "unlock": "Desbloquear {entity_name}" }, "condition_type": { - "is_locked": "{entity_name} est\u00e1 bloqueado", - "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + "is_locked": "{entity_name} est\u00e1 bloqueada", + "is_unlocked": "{entity_name} est\u00e1 desbloqueada" }, "trigger_type": { - "locked": "{entity_name} bloqueado", - "unlocked": "{entity_name} desbloqueado" + "locked": "{entity_name} bloqueada", + "unlocked": "{entity_name} desbloqueada" } }, "state": { diff --git a/homeassistant/components/logi_circle/translations/es.json b/homeassistant/components/logi_circle/translations/es.json index ac1fdc8dc5a..0bc9f8f6521 100644 --- a/homeassistant/components/logi_circle/translations/es.json +++ b/homeassistant/components/logi_circle/translations/es.json @@ -2,19 +2,19 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "external_error": "Ocurri\u00f3 una excepci\u00f3n de otro flujo.", + "external_error": "Ocurri\u00f3 una excepci\u00f3n desde otro flujo.", "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." }, "error": { - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de presionar Enviar.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de pulsar Enviar.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "auth": { - "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Logi Circle, luego regresa y presiona **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", - "title": "Autenticaci\u00f3n con Logi Circle" + "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Logi Circle, luego regresa y pulsa **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", + "title": "Autenticar con Logi Circle" }, "user": { "data": { diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index c6e90ec6364..aa33259c5a3 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "not_lutron_device": "El dispositivo descubierto no es un dispositivo de Lutron" + "not_lutron_device": "El dispositivo descubierto no es un dispositivo Lutron" }, "error": { "cannot_connect": "No se pudo conectar" @@ -11,8 +11,8 @@ "flow_title": "{name} ({host})", "step": { "import_failed": { - "description": "No se ha podido configurar el enlace (anfitri\u00f3n: {host}) importado de configuration.yaml.", - "title": "Error al importar la configuraci\u00f3n del bridge Cas\u00e9ta." + "description": "No se pudo configurar el puente (host: {host}) importado desde configuration.yaml.", + "title": "No se pudo importar la configuraci\u00f3n del puente Cas\u00e9ta." }, "link": { "description": "Para emparejar con {name} ({host}), despu\u00e9s de enviar este formulario, presiona el bot\u00f3n negro en la parte posterior del puente.", @@ -22,8 +22,8 @@ "data": { "host": "Host" }, - "description": "Introduzca la direcci\u00f3n ip del dispositivo.", - "title": "Conectar autom\u00e1ticamente con el dispositivo" + "description": "Introduce la direcci\u00f3n IP del dispositivo.", + "title": "Conectar autom\u00e1ticamente al puente" } } }, @@ -42,11 +42,11 @@ "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": "Bajar", + "lower_1": "Bajar 1", + "lower_2": "Bajar 2", + "lower_3": "Bajar 3", + "lower_4": "Bajar 4", "lower_all": "Bajar todo", "off": "Apagado", "on": "Encendido", @@ -69,8 +69,8 @@ "stop_all": "Detener todo" }, "trigger_type": { - "press": "\"{subtype}\" presionado", - "release": "\"{subtype}\" liberado" + "press": "\"{subtype}\" pulsado", + "release": "\"{subtype}\" soltado" } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index 5405ca19ffa..aaddd58b433 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index 01140eb3aad..009385a5bc1 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -5,7 +5,7 @@ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntelo de nuevo m\u00e1s tarde.", + "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntalo de nuevo m\u00e1s tarde.", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "email": "Correo electronico", + "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a", "region": "Regi\u00f3n" }, diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index 0924f212179..946062acdc7 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -4,7 +4,7 @@ "is_buffering": "{entity_name} est\u00e1 almacenando en b\u00fafer", "is_idle": "{entity_name} est\u00e1 inactivo", "is_off": "{entity_name} est\u00e1 apagado", - "is_on": "{entity_name} est\u00e1 activado", + "is_on": "{entity_name} est\u00e1 encendido", "is_paused": "{entity_name} est\u00e1 en pausa", "is_playing": "{entity_name} est\u00e1 reproduciendo" }, @@ -13,9 +13,9 @@ "changed_states": "{entity_name} cambi\u00f3 de estado", "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" + "playing": "{entity_name} comienza a reproducir", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } }, "state": { diff --git a/homeassistant/components/melcloud/translations/es.json b/homeassistant/components/melcloud/translations/es.json index 94583ed2589..904fe4af2bc 100644 --- a/homeassistant/components/melcloud/translations/es.json +++ b/homeassistant/components/melcloud/translations/es.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Integraci\u00f3n mELCloud ya configurada para este correo electr\u00f3nico. Se ha actualizado el token de acceso." + "already_configured": "Integraci\u00f3n MELCloud ya configurada para este correo electr\u00f3nico. El token de acceso se ha actualizado." }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -14,8 +14,8 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "description": "Con\u00e9ctate usando tu cuenta de MELCloud.", - "title": "Con\u00e9ctese a MELCloud" + "description": "Con\u00e9ctate usando tu cuenta MELCloud.", + "title": "Conectar a MELCloud" } } } diff --git a/homeassistant/components/met/translations/es.json b/homeassistant/components/met/translations/es.json index b03e6636cf8..645f37aa9a5 100644 --- a/homeassistant/components/met/translations/es.json +++ b/homeassistant/components/met/translations/es.json @@ -9,12 +9,12 @@ "step": { "user": { "data": { - "elevation": "Altitud", + "elevation": "Elevaci\u00f3n", "latitude": "Latitud", "longitude": "Longitud", "name": "Nombre" }, - "description": "Instituto de meteorolog\u00eda", + "description": "Meteorologisk institutt", "title": "Ubicaci\u00f3n" } } diff --git a/homeassistant/components/meteo_france/translations/es.json b/homeassistant/components/meteo_france/translations/es.json index a16c47e4aa9..b8363d01868 100644 --- a/homeassistant/components/meteo_france/translations/es.json +++ b/homeassistant/components/meteo_france/translations/es.json @@ -5,7 +5,7 @@ "unknown": "Error inesperado" }, "error": { - "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, comprueba el campo de la ciudad" + "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, verifica el campo de la ciudad" }, "step": { "cities": { @@ -18,7 +18,7 @@ "data": { "city": "Ciudad" }, - "description": "Introduzca el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad" + "description": "Introduce el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad" } } }, diff --git a/homeassistant/components/metoffice/translations/es.json b/homeassistant/components/metoffice/translations/es.json index 5751db1f760..0a533ecd130 100644 --- a/homeassistant/components/metoffice/translations/es.json +++ b/homeassistant/components/metoffice/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud" }, "description": "La latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana.", - "title": "Con\u00e9ctar con la Oficina Meteorol\u00f3gica del Reino Unido" + "title": "Conectar con la UK Met Office" } } } diff --git a/homeassistant/components/mikrotik/translations/es.json b/homeassistant/components/mikrotik/translations/es.json index b1cd62a1763..13bea3f0be0 100644 --- a/homeassistant/components/mikrotik/translations/es.json +++ b/homeassistant/components/mikrotik/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "name_exists": "El nombre ya existe" + "name_exists": "El nombre existe" }, "step": { "user": { @@ -18,7 +18,7 @@ "username": "Nombre de usuario", "verify_ssl": "Usar ssl" }, - "title": "Configurar el router Mikrotik" + "title": "Configurar router Mikrotik" } } }, @@ -27,8 +27,8 @@ "device_tracker": { "data": { "arp_ping": "Habilitar ping ARP", - "detection_time": "Considere el intervalo de inicio", - "force_dhcp": "Forzar el escaneo usando DHCP" + "detection_time": "Considerar el intervalo de inicio", + "force_dhcp": "Forzar escaneo usando DHCP" } } } diff --git a/homeassistant/components/minecraft_server/translations/es.json b/homeassistant/components/minecraft_server/translations/es.json index a5c5ae531d9..8fa95f0dd10 100644 --- a/homeassistant/components/minecraft_server/translations/es.json +++ b/homeassistant/components/minecraft_server/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "El servicio ya est\u00e1 configurado." + "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar al servidor. Comprueba el host y el puerto e int\u00e9ntalo de nuevo. Tambi\u00e9n aseg\u00farate de que est\u00e1s ejecutando al menos Minecraft versi\u00f3n 1.7 en tu servidor.", - "invalid_ip": "La direcci\u00f3n IP no es valida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo.", - "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo." + "cannot_connect": "Error al conectar con el servidor. Verifica el host y el puerto y vuelve a intentarlo. Tambi\u00e9n aseg\u00farate de estar ejecutando al menos la versi\u00f3n 1.7 de Minecraft en tu servidor.", + "invalid_ip": "La direcci\u00f3n IP no es v\u00e1lida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo y vuelve a intentarlo.", + "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo y vuelve a intentarlo." }, "step": { "user": { diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index c7469a93820..585a7c0a8ed 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -6,7 +6,7 @@ "connection_error": "No se pudo conectar" }, "error": { - "discovery_error": "No se pudo descubrir un detector de movimiento" + "discovery_error": "No se pudo descubrir un Motion Gateway" }, "flow_title": "{short_mac} ({ip_address})", "step": { @@ -14,20 +14,20 @@ "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" + "description": "Necesitar\u00e1s la clave API de 16 caracteres, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obtener instrucciones" }, "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" + "description": "Vuelve a ejecutar la configuraci\u00f3n si deseas conectar Motion Gateways adicionales", + "title": "Selecciona el Motion Gateway que deseas conectar" }, "user": { "data": { "host": "Direcci\u00f3n IP" }, - "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 establecida, se utilitzar\u00e1 la detecci\u00f3n autom\u00e1tica" + "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 configurada, se utiliza la detecci\u00f3n autom\u00e1tica" } } }, diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 4c88a5a66f7..016cb320cfd 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -43,7 +43,7 @@ "button_long_press": "\"{subtype}\" pulsado continuamente", "button_long_release": "\"{subtype}\" soltado despu\u00e9s de pulsaci\u00f3n larga", "button_quadruple_press": "\"{subtype}\" cu\u00e1druple pulsaci\u00f3n", - "button_quintuple_press": "\"{subtype}\" quintuple pulsaci\u00f3n", + "button_quintuple_press": "\"{subtype}\" qu\u00edntuple pulsaci\u00f3n", "button_short_press": "\"{subtype}\" pulsado", "button_short_release": "\"{subtype}\" soltado", "button_triple_press": "\"{subtype}\" triple pulsaci\u00f3n" @@ -51,8 +51,8 @@ }, "options": { "error": { - "bad_birth": "Tema de nacimiento inv\u00e1lido.", - "bad_will": "Tema de voluntad inv\u00e1lido.", + "bad_birth": "Tema de nacimiento no v\u00e1lido.", + "bad_will": "Tema de voluntad no v\u00e1lido.", "cannot_connect": "No se pudo conectar" }, "step": { @@ -63,7 +63,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT.", + "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu br\u00f3ker MQTT.", "title": "Opciones del br\u00f3ker" }, "options": { @@ -80,7 +80,7 @@ "will_retain": "Retenci\u00f3n del mensaje de voluntad", "will_topic": "Tema del mensaje de voluntad" }, - "description": "Descubrimiento - Si el descubrimiento est\u00e1 habilitado (recomendado), Home Assistant descubrir\u00e1 autom\u00e1ticamente los dispositivos y entidades que publiquen su configuraci\u00f3n en el br\u00f3ker MQTT. Si el descubrimiento est\u00e1 deshabilitado, toda la configuraci\u00f3n debe hacerse manualmente.\nMensaje de nacimiento - El mensaje de nacimiento se enviar\u00e1 cada vez que Home Assistant se (re)conecte al br\u00f3ker MQTT.\nMensaje de voluntad - El mensaje de voluntad se enviar\u00e1 cada vez que Home Assistant pierda su conexi\u00f3n con el br\u00f3ker, tanto en el caso de una desconexi\u00f3n limpia (por ejemplo, el cierre de Home Assistant) como en el caso de una desconexi\u00f3n no limpia (por ejemplo, el cierre de Home Assistant o la p\u00e9rdida de su conexi\u00f3n de red).", + "description": "Descubrimiento: si el descubrimiento est\u00e1 habilitado (recomendado), Home Assistant descubrir\u00e1 autom\u00e1ticamente los dispositivos y entidades que publican su configuraci\u00f3n en el br\u00f3ker MQTT. Si el descubrimiento est\u00e1 deshabilitado, toda la configuraci\u00f3n debe realizarse manualmente.\nMensaje de nacimiento: el mensaje de nacimiento se enviar\u00e1 cada vez que Home Assistant se (re)conecte con el br\u00f3ker MQTT.\nMensaje de voluntad: el mensaje de voluntad se enviar\u00e1 cada vez que Home Assistant pierda su conexi\u00f3n con el br\u00f3ker, tanto en caso de desconexi\u00f3n limpia (por ejemplo, que Home Assistant se apague) como en caso de desconexi\u00f3n no limpia (por ejemplo, Home Assistant se cuelgue o pierda su conexi\u00f3n de red).", "title": "Opciones de MQTT" } } diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index 9f20a31ff15..7cb7dcb9354 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -22,7 +22,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Conectar con el Gateway " + "title": "Conectar a la puerta de enlace MyQ" } } } diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 15234537136..62010958d67 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -13,10 +13,10 @@ "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", + "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", - "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", + "not_a_number": "Por favor, introduce un n\u00famero", + "port_out_of_range": "El n\u00famero de puerto debe ser al menos 1 y como m\u00e1ximo 65535", "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", "unknown": "Error inesperado" }, @@ -36,14 +36,14 @@ "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", "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", + "port_out_of_range": "El n\u00famero de puerto debe ser al menos 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)", + "persistence_file": "archivo de persistencia (d\u00e9jalo vac\u00edo para que se genere autom\u00e1ticamente)", "retain": "retenci\u00f3n 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)", @@ -55,19 +55,19 @@ "data": { "baud_rate": "tasa de baudios", "device": "Puerto serie", - "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", + "persistence_file": "archivo de persistencia (d\u00e9jalo vac\u00edo para que se genere autom\u00e1ticamente)", "version": "Versi\u00f3n de MySensors" }, - "description": "Configuraci\u00f3n de la pasarela en serie" + "description": "Configuraci\u00f3n de la puerta de enlace 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" + "device": "Direcci\u00f3n IP de la puerta de enlace", + "persistence_file": "archivo de persistencia (d\u00e9jalo vac\u00edo para que se genere autom\u00e1ticamente)", + "tcp_port": "puerto", + "version": "Versi\u00f3n de MySensors" }, - "description": "Configuraci\u00f3n de la pasarela Ethernet" + "description": "Configuraci\u00f3n de la puerta de enlace Ethernet" }, "select_gateway_type": { "description": "Selecciona qu\u00e9 puerta de enlace configurar.", @@ -79,9 +79,9 @@ }, "user": { "data": { - "gateway_type": "Tipo de pasarela" + "gateway_type": "Tipo de puerta de enlace" }, - "description": "Elija el m\u00e9todo de conexi\u00f3n con la pasarela" + "description": "Elige el m\u00e9todo de conexi\u00f3n a la puerta de enlace" } } } diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 12ae1b235ba..9de6598da5c 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -15,7 +15,7 @@ "flow_title": "{name}", "step": { "link": { - "description": "Mant\u00e9n presionado el bot\u00f3n de encendido de tu Nanoleaf durante 5 segundos hasta que los LED del bot\u00f3n comiencen a parpadear, luego haz clic en **ENVIAR** en los siguientes 30 segundos.", + "description": "Mant\u00e9n pulsado el bot\u00f3n de encendido de tu Nanoleaf durante 5 segundos hasta que los LED del bot\u00f3n comiencen a parpadear, luego haz clic en **ENVIAR** en los siguientes 30 segundos.", "title": "Vincular Nanoleaf" }, "user": { diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index 7eeecb4dde0..81f11ec51ff 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 93dd7eb12a2..7715f1f99bd 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -5,16 +5,16 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "invalid_access_token": "Token de acceso no v\u00e1lido", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "create_entry": { - "default": "Autenticado con \u00e9xito" + "default": "Autenticado correctamente" }, "error": { "bad_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (verifica la Cloud Console)", @@ -74,7 +74,7 @@ "title": "Vincular cuenta de Nest" }, "pick_implementation": { - "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "pubsub": { "data": { @@ -84,7 +84,7 @@ "title": "Configurar Google Cloud" }, "reauth_confirm": { - "description": "La integraci\u00f3n de Nest necesita volver a autenticar tu cuenta", + "description": "La integraci\u00f3n Nest necesita volver a autenticar tu cuenta", "title": "Volver a autenticar la integraci\u00f3n" } } @@ -103,7 +103,7 @@ "title": "Se va a eliminar la configuraci\u00f3n YAML de Nest" }, "removed_app_auth": { - "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de utilizar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n de su parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de integraci\u00f3n]({documentation_url}) de Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", + "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de utilizar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n por tu parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de integraci\u00f3n]({documentation_url}) de Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", "title": "Las credenciales de autenticaci\u00f3n de Nest deben actualizarse" } } diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index 0d8e4167ab8..3a8350932be 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo excedido generando la url de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Por favor, consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." @@ -12,7 +12,7 @@ }, "step": { "pick_implementation": { - "title": "Selecciona un m\u00e9todo de autenticaci\u00f3n" + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { "description": "La integraci\u00f3n Netatmo necesita volver a autenticar tu cuenta", @@ -47,23 +47,23 @@ "public_weather": { "data": { "area_name": "Nombre del \u00e1rea", - "lat_ne": "Latitud Esquina noreste", - "lat_sw": "Latitud Esquina suroeste", - "lon_ne": "Longitud Esquina noreste", - "lon_sw": "Longitud Esquina suroeste", + "lat_ne": "Latitud de la esquina noreste", + "lat_sw": "Latitud de la esquina suroeste", + "lon_ne": "Longitud de la esquina noreste", + "lon_sw": "Longitud de la esquina suroeste", "mode": "C\u00e1lculo", "show_on_map": "Mostrar en el mapa" }, "description": "Configura un sensor de clima p\u00fablico para un \u00e1rea.", - "title": "Sensor de clima p\u00fablico Netatmo" + "title": "Sensor meteorol\u00f3gico p\u00fablico Netatmo" }, "public_weather_areas": { "data": { "new_area": "Nombre del \u00e1rea", "weather_areas": "Zonas meteorol\u00f3gicas" }, - "description": "Configurar sensores de clima p\u00fablicos.", - "title": "Sensor de clima p\u00fablico Netatmo" + "description": "Configura sensores meteorol\u00f3gicos p\u00fablicos.", + "title": "Sensor meteorol\u00f3gico p\u00fablico Netatmo" } } } diff --git a/homeassistant/components/nexia/translations/es.json b/homeassistant/components/nexia/translations/es.json index 9e9e720dc15..206e27d1fd2 100644 --- a/homeassistant/components/nexia/translations/es.json +++ b/homeassistant/components/nexia/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/nightscout/translations/es.json b/homeassistant/components/nightscout/translations/es.json index ab9ce8baa02..2d41c4714ce 100644 --- a/homeassistant/components/nightscout/translations/es.json +++ b/homeassistant/components/nightscout/translations/es.json @@ -15,7 +15,7 @@ "url": "URL" }, "description": "- URL: la direcci\u00f3n de tu instancia nightscout. Por ejemplo: https://myhomeassistant.duckdns.org:5423\n- Clave de API (opcional): usar solo si tu instancia est\u00e1 protegida (auth_default_roles != legible).", - "title": "Introduce la informaci\u00f3n del servidor de Nightscout." + "title": "Introduce tu informaci\u00f3n del servidor Nightscout." } } } diff --git a/homeassistant/components/nuheat/translations/es.json b/homeassistant/components/nuheat/translations/es.json index a64a68e2e70..cca9cb83f05 100644 --- a/homeassistant/components/nuheat/translations/es.json +++ b/homeassistant/components/nuheat/translations/es.json @@ -16,8 +16,8 @@ "serial_number": "N\u00famero de serie del termostato.", "username": "Nombre de usuario" }, - "description": "Necesitas obtener el n\u00famero de serie o el ID de tu termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando tu(s) termostato(s).", - "title": "ConectarNuHeat" + "description": "Deber\u00e1s obtener el n\u00famero de serie num\u00e9rico o ID de tu termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando tu(s) termostato(s).", + "title": "Conectar al NuHeat" } } } diff --git a/homeassistant/components/nut/translations/es.json b/homeassistant/components/nut/translations/es.json index edb49ebcbcd..f02fa8017bc 100644 --- a/homeassistant/components/nut/translations/es.json +++ b/homeassistant/components/nut/translations/es.json @@ -12,7 +12,7 @@ "data": { "alias": "Alias" }, - "title": "Selecciona el UPS a monitorizar" + "title": "Elige el SAI a supervisar" }, "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/es.json b/homeassistant/components/nzbget/translations/es.json index eeb2b46ba8b..4785e8437cd 100644 --- a/homeassistant/components/nzbget/translations/es.json +++ b/homeassistant/components/nzbget/translations/es.json @@ -19,7 +19,7 @@ "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" }, - "title": "Conectarse a NZBGet" + "title": "Conectar a NZBGet" } } }, diff --git a/homeassistant/components/ondilo_ico/translations/es.json b/homeassistant/components/ondilo_ico/translations/es.json index 1de8399aced..3d55a3d6f31 100644 --- a/homeassistant/components/ondilo_ico/translations/es.json +++ b/homeassistant/components/ondilo_ico/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index aa1d40e3f14..0b450da3d46 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -3,9 +3,9 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifique la configuraci\u00f3n del perfil en su dispositivo.", - "no_mac": "No se pudo configurar una identificaci\u00f3n \u00fanica para el dispositivo ONVIF.", - "onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Comprueba el registro para m\u00e1s informaci\u00f3n." + "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifica la configuraci\u00f3n del perfil en tu dispositivo.", + "no_mac": "No se pudo configurar un ID \u00fanico para el dispositivo ONVIF.", + "onvif_error": "Error al configurar el dispositivo ONVIF. Consulta los registros para obtener m\u00e1s informaci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar" @@ -23,22 +23,22 @@ }, "configure_profile": { "data": { - "include": "Crear la entidad de la c\u00e1mara" + "include": "Crear la entidad de c\u00e1mara" }, "description": "\u00bfCrear la entidad de c\u00e1mara para {profile} a {resolution} de resoluci\u00f3n?", - "title": "Configurar los perfiles" + "title": "Configurar perfiles" }, "device": { "data": { - "host": "Seleccione el dispositivo ONVIF descubierto" + "host": "Selecciona el dispositivo ONVIF descubierto" }, - "title": "Seleccione el dispositivo ONVIF" + "title": "Selecciona el dispositivo ONVIF" }, "user": { "data": { "auto": "Buscar autom\u00e1ticamente" }, - "description": "Al hacer clic en Enviar, buscaremos en su red dispositivos ONVIF compatibles con el perfil S.\n\nAlgunos fabricantes han comenzado a desactivar ONVIF de forma predeterminada. Aseg\u00farese de que ONVIF est\u00e9 activado en la configuraci\u00f3n de la c\u00e1mara.", + "description": "Al hacer clic en enviar, buscaremos en tu red dispositivos ONVIF compatibles con el perfil S. \n\nAlgunos fabricantes han comenzado a deshabilitar ONVIF por defecto. Aseg\u00farate de que ONVIF est\u00e9 habilitado en la configuraci\u00f3n de tu c\u00e1mara.", "title": "Configuraci\u00f3n del dispositivo ONVIF" } } diff --git a/homeassistant/components/openexchangerates/translations/it.json b/homeassistant/components/openexchangerates/translations/it.json index d42f7122593..38fca960f50 100644 --- a/homeassistant/components/openexchangerates/translations/it.json +++ b/homeassistant/components/openexchangerates/translations/it.json @@ -17,13 +17,17 @@ "data": { "api_key": "Chiave API", "base": "Valuta di base" + }, + "data_description": { + "base": "L'utilizzo di una valuta di base diversa da USD richiede un [piano a pagamento]({signup})." } } } }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Open Exchange Rates tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Open Exchange Rates dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema." + "description": "La configurazione di Open Exchange Rates tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Open Exchange Rates dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Open Exchange Rates sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index 0310cdd8236..1088ce834cf 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -3,7 +3,7 @@ "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "id_exists": "El ID del Gateway ya existe", + "id_exists": "El ID de la puerta de enlace ya existe", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "step": { diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index a060f0f9552..0af57427980 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -11,7 +11,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Error de autenticaci\u00f3n para OVO Energy. Ingrese sus credenciales actuales.", + "description": "La autenticaci\u00f3n fall\u00f3 para OVO Energy. Introduce tus credenciales actuales.", "title": "Reautenticaci\u00f3n" }, "user": { @@ -19,7 +19,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Configurar una instancia de OVO Energy para acceder a su consumo de energ\u00eda.", + "description": "Configura una instancia de OVO Energy para acceder a tu consumo de energ\u00eda.", "title": "A\u00f1adir cuenta de OVO Energy" } } diff --git a/homeassistant/components/panasonic_viera/translations/es.json b/homeassistant/components/panasonic_viera/translations/es.json index d14cc6b878b..76037a96339 100644 --- a/homeassistant/components/panasonic_viera/translations/es.json +++ b/homeassistant/components/panasonic_viera/translations/es.json @@ -7,14 +7,14 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_pin_code": "El c\u00f3digo PIN que has introducido no es v\u00e1lido" + "invalid_pin_code": "El C\u00f3digo PIN que has introducido no es v\u00e1lido" }, "step": { "pairing": { "data": { "pin": "C\u00f3digo PIN" }, - "description": "Introduce el PIN que aparece en tu Televisor", + "description": "Introduce el C\u00f3digo PIN que se muestra en tu TV", "title": "Emparejamiento" }, "user": { @@ -22,8 +22,8 @@ "host": "Direcci\u00f3n IP", "name": "Nombre" }, - "description": "Introduce la direcci\u00f3n IP de tu Panasonic Viera TV", - "title": "Configura tu televisi\u00f3n" + "description": "Introduce la Direcci\u00f3n IP de tu Panasonic Viera TV", + "title": "Configura tu TV" } } } diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index a2842917b87..5814e5e4a30 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "api_version": "Versi\u00f3n del API", + "api_version": "Versi\u00f3n de la API", "host": "Host" } } @@ -27,7 +27,7 @@ }, "device_automation": { "trigger_type": { - "turn_on": "Se solicita al dispositivo que se encienda" + "turn_on": "Se solicita que el dispositivo se encienda" } }, "options": { diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index df1ffb99cb1..23acbb80c6c 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/translations/es.json @@ -7,12 +7,12 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "\u00a1El dispositivo Plaato {device_type} con nombre **{device_name}** se ha configurado correctamente!" + "default": "\u00a1Tu {device_type} Plaato con nombre **{device_name}** se configur\u00f3 correctamente!" }, "error": { "invalid_webhook_device": "Has seleccionado un dispositivo que no admite el env\u00edo de datos a un webhook. Solo est\u00e1 disponible para el 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" + "no_auth_token": "Necesitas a\u00f1adir un token de autenticaci\u00f3n" }, "step": { "api_method": { @@ -20,19 +20,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" + "description": "Para poder consultar la API, se requiere un `auth_token` que se puede obtener siguiendo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrucciones \n\nDispositivo seleccionado: ** {device_type} ** \n\nSi prefieres utilizar el m\u00e9todo de webhook incorporado (solo Airlock), marca la casilla a continuaci\u00f3n y deja el token de autenticaci\u00f3n en blanco", + "title": "Seleccionar el m\u00e9todo API" }, "user": { "data": { - "device_name": "Nombre de su dispositivo", + "device_name": "Nombra tu dispositivo", "device_type": "Tipo de dispositivo Plaato" }, - "description": "\u00bfQuieres empezar la configuraci\u00f3n?", - "title": "Configura dispositivos Plaato" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", + "title": "Configurar los dispositivos 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.", + "description": "Para enviar eventos a Home Assistant, deber\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\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles.", "title": "Webhook a utilizar" } } @@ -43,11 +43,11 @@ "data": { "update_interval": "Intervalo de actualizaci\u00f3n (minutos)" }, - "description": "Intervalo de actualizaci\u00f3n (minutos)", + "description": "Establecer el 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", + "description": "Informaci\u00f3n del webhook: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST\n\n", "title": "Opciones para Plaato Airlock" } } diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 0b030c2a958..14fe1b840a5 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -10,9 +10,9 @@ }, "error": { "faulty_credentials": "La autorizaci\u00f3n ha fallado, verifica el token", - "host_or_token": "Debes proporcionar al menos uno de Host o Token", + "host_or_token": "Debes proporcionar al menos uno entre Host y Token", "no_servers": "No hay servidores vinculados a la cuenta Plex", - "not_found": "No se ha encontrado el servidor Plex", + "not_found": "Servidor Plex no encontrado", "ssl_error": "Problema con el certificado SSL" }, "flow_title": "{name} ({host})", @@ -31,8 +31,8 @@ "data": { "server": "Servidor" }, - "description": "Varios servidores disponibles, seleccione uno:", - "title": "Seleccione el servidor Plex" + "description": "M\u00faltiples servidores disponibles, selecciona uno:", + "title": "Selecciona el servidor Plex" }, "user": { "description": "Contin\u00faa hacia [plex.tv](https://plex.tv) para vincular un servidor Plex." @@ -50,7 +50,7 @@ "data": { "ignore_new_shared_users": "Ignorar nuevos usuarios administrados/compartidos", "ignore_plex_web_clients": "Ignorar clientes web de Plex", - "monitored_users": "Usuarios monitorizados", + "monitored_users": "Usuarios supervisados", "use_episode_art": "Usar el arte de episodios" }, "description": "Opciones para reproductores multimedia Plex" diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 570184aaabf..50e440f6d6a 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -5,7 +5,7 @@ "anna_with_adam": "Tanto Anna como Adam han sido detectados. A\u00f1ade tu Adam en lugar de tu Anna" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_setup": "A\u00f1ade tu Adam en lugar de tu Anna, consulta la documentaci\u00f3n de la integraci\u00f3n Home Assistant Plugwise para m\u00e1s informaci\u00f3n", "unknown": "Error inesperado" @@ -26,12 +26,12 @@ "user_gateway": { "data": { "host": "Direcci\u00f3n IP", - "password": "ID Smile", + "password": "ID de Smile", "port": "Puerto", "username": "Nombre de usuario Smile" }, "description": "Por favor, introduce:", - "title": "Conectarse a Smile" + "title": "Conectar a Smile" } } }, diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index 2e1de2f1f6f..c0a86f70d55 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "external_setup": "Point se ha configurado correctamente a partir de otro flujo.", - "no_flows": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_flows": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index baf6fbda838..0e34589a260 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -9,7 +9,7 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado", - "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Por favor, considera actualizar o informar este problema para que pueda resolverse." + "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Por favor, considera actualizar o informar de este problema para que pueda resolverse." }, "flow_title": "{name} ({ip_address})", "step": { diff --git a/homeassistant/components/progettihwsw/translations/es.json b/homeassistant/components/progettihwsw/translations/es.json index d4915dcb4cd..16a77ce813a 100644 --- a/homeassistant/components/progettihwsw/translations/es.json +++ b/homeassistant/components/progettihwsw/translations/es.json @@ -34,7 +34,7 @@ "host": "Host", "port": "Puerto" }, - "title": "Configurar tablero" + "title": "Configurar placa" } } } diff --git a/homeassistant/components/ps4/translations/es.json b/homeassistant/components/ps4/translations/es.json index fd1283ccfd8..01df37c8dfb 100644 --- a/homeassistant/components/ps4/translations/es.json +++ b/homeassistant/components/ps4/translations/es.json @@ -9,13 +9,13 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "credential_timeout": "Se agot\u00f3 el tiempo de espera del servicio de credenciales. Presiona enviar para reiniciar.", + "credential_timeout": "Se agot\u00f3 el tiempo de espera del servicio de credenciales. Pulsa enviar para reiniciar.", "login_failed": "No se pudo emparejar con PlayStation 4. Verifica que el C\u00f3digo PIN sea correcto.", "no_ipaddress": "Introduce la Direcci\u00f3n IP de la PlayStation 4 que deseas configurar." }, "step": { "creds": { - "description": "Credenciales necesarias. Presiona 'Enviar' y luego en la aplicaci\u00f3n PS4 Second Screen, actualiza los dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar." + "description": "Credenciales necesarias. Pulsa 'Enviar' y luego en la aplicaci\u00f3n PS4 Second Screen, actualiza los dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar." }, "link": { "data": { diff --git a/homeassistant/components/rachio/translations/es.json b/homeassistant/components/rachio/translations/es.json index 671a00a334c..d78f48c9333 100644 --- a/homeassistant/components/rachio/translations/es.json +++ b/homeassistant/components/rachio/translations/es.json @@ -4,8 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/recollect_waste/translations/es.json b/homeassistant/components/recollect_waste/translations/es.json index 69a39d435eb..924c162f7bf 100644 --- a/homeassistant/components/recollect_waste/translations/es.json +++ b/homeassistant/components/recollect_waste/translations/es.json @@ -21,7 +21,7 @@ "data": { "friendly_name": "Utilizar nombres descriptivos para los tipos de recogida (cuando sea posible)" }, - "title": "Configurar la recogida de residuos" + "title": "Configurar ReCollect Waste" } } } diff --git a/homeassistant/components/remote/translations/es.json b/homeassistant/components/remote/translations/es.json index b2c8aea25cc..26770bb1233 100644 --- a/homeassistant/components/remote/translations/es.json +++ b/homeassistant/components/remote/translations/es.json @@ -7,12 +7,12 @@ }, "condition_type": { "is_off": "{entity_name} est\u00e1 apagado", - "is_on": "{entity_name} est\u00e1 activado" + "is_on": "{entity_name} est\u00e1 encendido" }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado" + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } }, "state": { diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json index aa567d0093d..a3536c867d4 100644 --- a/homeassistant/components/rfxtrx/translations/es.json +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -13,11 +13,11 @@ "host": "Host", "port": "Puerto" }, - "title": "Seleccionar la direcci\u00f3n de conexi\u00f3n" + "title": "Selecciona la direcci\u00f3n de conexi\u00f3n" }, "setup_serial": { "data": { - "device": "Seleccionar dispositivo" + "device": "Selecciona el dispositivo" }, "title": "Dispositivo" }, @@ -31,7 +31,7 @@ "data": { "type": "Tipo de conexi\u00f3n" }, - "title": "Seleccionar tipo de conexi\u00f3n" + "title": "Selecciona el tipo de conexi\u00f3n" } } }, @@ -49,18 +49,18 @@ "error": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", "invalid_event_code": "C\u00f3digo de evento no v\u00e1lido", - "invalid_input_2262_off": "Entrada inv\u00e1lida para el comando de apagado", - "invalid_input_2262_on": "Entrada inv\u00e1lida para el comando de encendido", - "invalid_input_off_delay": "Entrada inv\u00e1lida para el retardo de apagado", + "invalid_input_2262_off": "Entrada no v\u00e1lida para el comando de apagado", + "invalid_input_2262_on": "Entrada no v\u00e1lida para el comando de encendido", + "invalid_input_off_delay": "Entrada no v\u00e1lida para retardo de desconexi\u00f3n", "unknown": "Error inesperado" }, "step": { "prompt_options": { "data": { - "automatic_add": "Activar la adici\u00f3n autom\u00e1tica", - "debug": "Activar la depuraci\u00f3n", - "device": "Seleccionar dispositivo para configurar", - "event_code": "Introducir el c\u00f3digo de evento para a\u00f1adir", + "automatic_add": "Habilitar a\u00f1adir autom\u00e1ticamente", + "debug": "Habilitar depuraci\u00f3n", + "device": "Selecciona el dispositivo a configurar", + "event_code": "Introduce el c\u00f3digo de evento para a\u00f1adir", "protocols": "Protocolos" }, "title": "Opciones de Rfxtrx" @@ -72,7 +72,7 @@ "data_bit": "N\u00famero de bits de datos", "off_delay": "Retraso de apagado", "off_delay_enabled": "Activar retardo de apagado", - "replace_device": "Seleccione el dispositivo que desea reemplazar", + "replace_device": "Selecciona el dispositivo para reemplazar", "venetian_blind_mode": "Modo de persiana veneciana" }, "title": "Configurar las opciones del dispositivo" diff --git a/homeassistant/components/ring/translations/es.json b/homeassistant/components/ring/translations/es.json index 4a3947c5efc..07d09b7b08d 100644 --- a/homeassistant/components/ring/translations/es.json +++ b/homeassistant/components/ring/translations/es.json @@ -19,7 +19,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Iniciar sesi\u00f3n con cuenta de Ring" + "title": "Iniciar sesi\u00f3n con cuenta Ring" } } } diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json index 8d0c0460a3c..ff6176afc5a 100644 --- a/homeassistant/components/risco/translations/es.json +++ b/homeassistant/components/risco/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -28,12 +28,12 @@ "armed_night": "Armada Noche" }, "description": "Selecciona en qu\u00e9 estado quieres configurar la alarma Risco cuando armes la alarma de Home Assistant", - "title": "Mapear estados de Home Assistant a estados Risco" + "title": "Mapear estados de Home Assistant a estados de Risco" }, "init": { "data": { - "code_arm_required": "Requiere un C\u00f3digo PIN para armar", - "code_disarm_required": "Requiere un c\u00f3digo PIN para desactivar", + "code_arm_required": "Requerir C\u00f3digo PIN para armar", + "code_disarm_required": "Requerir C\u00f3digo PIN para desarmar", "scan_interval": "Con qu\u00e9 frecuencia sondear Risco (en segundos)" }, "title": "Configurar opciones" @@ -48,7 +48,7 @@ "partial_arm": "Parcialmente Armada (EN CASA)" }, "description": "Selecciona qu\u00e9 estado reportar\u00e1 la alarma de tu Home Assistant para cada estado reportado por Risco", - "title": "Asignar estados de Risco a estados de Home Assistant" + "title": "Mapear estados de Risco a estados de Home Assistant" } } } diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 015360d76c9..cedfbc6d4b1 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -6,12 +6,12 @@ "unknown": "Error inesperado" }, "error": { - "cannot_connect": "Fallo al conectar" + "cannot_connect": "No se pudo conectar" }, "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "\u00bfQuieres configurar {name} ?" + "description": "\u00bfQuieres configurar {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index cc2954335b6..0744bf05620 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -12,15 +12,15 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Mant\u00e9n presionado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (alrededor de dos segundos), luego haz clic en enviar en los siguientes 30 segundos.", + "description": "Mant\u00e9n pulsado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (alrededor de dos segundos), luego haz clic en enviar en los siguientes 30 segundos.", "title": "Recuperar Contrase\u00f1a" }, "link_manual": { "data": { "password": "Contrase\u00f1a" }, - "description": "La contrase\u00f1a no se pudo recuperar del dispositivo autom\u00e1ticamente. Sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", - "title": "Escribe la contrase\u00f1a" + "description": "La contrase\u00f1a no se pudo recuperar del dispositivo autom\u00e1ticamente. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", + "title": "Introduce la contrase\u00f1a" }, "manual": { "data": { @@ -33,8 +33,8 @@ "data": { "host": "Host" }, - "description": "Seleccione una Roomba o Braava.", - "title": "Conexi\u00f3n autom\u00e1tica con el dispositivo" + "description": "Selecciona una Roomba o Braava.", + "title": "Conectar autom\u00e1ticamente al dispositivo" } } }, diff --git a/homeassistant/components/rpi_power/translations/es.json b/homeassistant/components/rpi_power/translations/es.json index 209cb867d23..806a049bb1b 100644 --- a/homeassistant/components/rpi_power/translations/es.json +++ b/homeassistant/components/rpi_power/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "No se puede encontrar la clase de sistema necesaria para este componente, aseg\u00farate de que tu kernel es reciente y el hardware es compatible", + "no_devices_found": "No se puede encontrar la clase de sistema necesaria para este componente, aseg\u00farate de que tu kernel es reciente y que el hardware es compatible", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 9a5c8c5fa51..6a5487175e7 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -3,27 +3,27 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a esta TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", - "not_supported": "Este dispositivo Samsung no es actualmente compatible.", + "not_supported": "Este dispositivo Samsung no es compatible actualmente.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, "error": { - "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a esta TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "invalid_pin": "El PIN no es v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "flow_title": "{device}", "step": { "confirm": { - "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." + "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes, deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." }, "encrypted_pairing": { "description": "Por favor, introduce el PIN que aparece en {device}." }, "pairing": { - "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." + "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes, deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." }, "reauth_confirm": { "description": "Despu\u00e9s de enviar, acepta la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos o introduce el PIN." @@ -36,7 +36,7 @@ "host": "Host", "name": "Nombre" }, - "description": "Introduce la informaci\u00f3n de tu televisi\u00f3n Samsung. Si nunca antes te conectaste con Home Assistant, deber\u00edas ver un mensaje en tu televisi\u00f3n pidiendo autorizaci\u00f3n." + "description": "Introduce la informaci\u00f3n de tu TV Samsung. Si nunca la has conectado a Home Assistant antes, deber\u00edas ver una ventana emergente en tu TV solicitando autorizaci\u00f3n." } } } diff --git a/homeassistant/components/schedule/translations/hu.json b/homeassistant/components/schedule/translations/hu.json index 44b70c0497b..c543b7b2e07 100644 --- a/homeassistant/components/schedule/translations/hu.json +++ b/homeassistant/components/schedule/translations/hu.json @@ -5,5 +5,5 @@ "on": "Be" } }, - "title": "Id\u0151z\u00edt\u00e9s" + "title": "Id\u0151z\u00edt\u0151" } \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/it.json b/homeassistant/components/schedule/translations/it.json new file mode 100644 index 00000000000..c50d8a66d1c --- /dev/null +++ b/homeassistant/components/schedule/translations/it.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Spento", + "on": "Acceso" + } + }, + "title": "Programma" +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index a99b59dceae..fd8a1581530 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -31,15 +31,15 @@ }, "trigger_type": { "apparent_power": "{entity_name} ha cambiado de potencia aparente", - "battery_level": "Cambios de nivel de bater\u00eda de {entity_name}", + "battery_level": "El nivel de bater\u00eda de {entity_name} cambia", "carbon_dioxide": "{entity_name} cambios en la concentraci\u00f3n de di\u00f3xido de carbono", "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono", "current": "Cambio de corriente en {entity_name}", "energy": "Cambio de energ\u00eda en {entity_name}", "frequency": "{entity_name} ha cambiado de frecuencia", "gas": "{entity_name} ha hambiado gas", - "humidity": "Cambios de humedad de {entity_name}", - "illuminance": "Cambios de luminosidad de {entity_name}", + "humidity": "La humedad de {entity_name} cambia", + "illuminance": "La luminosidad de {entity_name} cambia", "nitrogen_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno", "nitrogen_monoxide": "{entity_name} ha cambiado en la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno", "nitrous_oxide": "{entity_name} ha cambiado en la concentraci\u00f3n de \u00f3xido nitroso", @@ -47,14 +47,14 @@ "pm1": "{entity_name} ha cambiado en la concentraci\u00f3n de PM1", "pm10": "{entity_name} ha cambiado en la concentraci\u00f3n de PM10", "pm25": "{entity_name} ha cambiado en la concentraci\u00f3n de PM2.5", - "power": "Cambios de potencia de {entity_name}", + "power": "La potencia de {entity_name} cambia", "power_factor": "Cambio de factor de potencia en {entity_name}", - "pressure": "Cambios de presi\u00f3n de {entity_name}", + "pressure": "La presi\u00f3n de {entity_name} cambia", "reactive_power": "{entity_name} ha cambiado de potencia reactiva", - "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name}", + "signal_strength": "La intensidad de se\u00f1al de {entity_name} cambia", "sulphur_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de azufre", - "temperature": "{entity_name} cambios de temperatura", - "value": "Cambios de valor de la {entity_name}", + "temperature": "La temperatura de {entity_name} cambia", + "value": "El valor de {entity_name} cambia", "volatile_organic_compounds": "{entity_name} ha cambiado la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles", "voltage": "Cambio de voltaje en {entity_name}" } diff --git a/homeassistant/components/sentry/translations/es.json b/homeassistant/components/sentry/translations/es.json index 058cc27642f..b27f797e8b4 100644 --- a/homeassistant/components/sentry/translations/es.json +++ b/homeassistant/components/sentry/translations/es.json @@ -1,7 +1,7 @@ { "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": { "bad_dsn": "DSN no v\u00e1lido", @@ -20,13 +20,13 @@ "init": { "data": { "environment": "Nombre opcional del entorno.", - "event_custom_components": "Env\u00eda eventos desde componentes personalizados", - "event_handled": "Enviar eventos controlados", - "event_third_party_packages": "Env\u00eda eventos desde paquetes de terceros", - "logging_event_level": "El nivel de registro Sentry registrar\u00e1 un evento para", - "logging_level": "El nivel de registro Sentry registrar\u00e1 registros como migas de pan para", - "tracing": "Habilitar el seguimiento del rendimiento", - "tracing_sample_rate": "Seguimiento de la frecuencia de muestreo; entre 0.0 y 1.0 (1.0 = 100%)" + "event_custom_components": "Enviar eventos desde componentes personalizados", + "event_handled": "Enviar eventos gestionados", + "event_third_party_packages": "Enviar eventos desde paquetes de terceros", + "logging_event_level": "El nivel de registro para el que Sentry registrar\u00e1 un evento", + "logging_level": "El nivel de registro para el que Sentry guardar\u00e1 registros como migas de pan", + "tracing": "Habilitar traza del rendimiento", + "tracing_sample_rate": "Frecuencia de muestreo de la traza; entre 0,0 y 1,0 (1,0 = 100%)" } } } diff --git a/homeassistant/components/senz/translations/es.json b/homeassistant/components/senz/translations/es.json index fc1c1ae2d13..e08ed208ac6 100644 --- a/homeassistant/components/senz/translations/es.json +++ b/homeassistant/components/senz/translations/es.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos." }, diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 9d344f83992..585ec778f7c 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -13,7 +13,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte, puedes activar manualmente el dispositivo ahora con un bot\u00f3n o esperar la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." + "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte. Puedes activar manualmente el dispositivo ahora con un bot\u00f3n del mismo o esperar a la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." }, "credentials": { "data": { @@ -25,7 +25,7 @@ "data": { "host": "Host" }, - "description": "Antes de configurar, los dispositivos alimentados por bater\u00eda deben despertarse, puede despertar el dispositivo ahora con un bot\u00f3n." + "description": "Antes de configurarlos, los dispositivos alimentados por bater\u00eda deben despertarse. Puedes despertar el dispositivo ahora usando uno de sus botones." } } }, @@ -42,7 +42,7 @@ "btn_up": "Bot\u00f3n {subtype} soltado", "double": "Pulsaci\u00f3n doble de {subtype}", "double_push": "Pulsaci\u00f3n doble de {subtype}", - "long": "{subtype} con pulsaci\u00f3n larga", + "long": "Pulsaci\u00f3n larga de {subtype}", "long_push": "Pulsaci\u00f3n larga de {subtype}", "long_single": "Pulsaci\u00f3n larga de {subtype} seguida de una pulsaci\u00f3n simple", "single": "Pulsaci\u00f3n simple de {subtype}", diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 1558f96fa4f..842a2245fe3 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -8,7 +8,7 @@ }, "error": { "identifier_exists": "Cuenta ya registrada", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_auth_code_length": "Los c\u00f3digos de autorizaci\u00f3n de SimpliSafe tienen 45 caracteres de longitud", "unknown": "Error inesperado" }, @@ -20,8 +20,8 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Vuelve a introducir la contrase\u00f1a de {username}", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "description": "Por favor, vuelve a introducir la contrase\u00f1a de {username}", + "title": "Volver a autenticar la integraci\u00f3n" }, "sms_2fa": { "data": { @@ -43,7 +43,7 @@ "step": { "init": { "data": { - "code": "C\u00f3digo (utilizado en el interfaz de usuario de Home Assistant)" + "code": "C\u00f3digo (utilizado en la IU de Home Assistant)" }, "title": "Configurar SimpliSafe" } diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index 1a9e2ee28dd..ebffb459169 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -3,17 +3,17 @@ "abort": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", "already_configured_local_device": "Los dispositivos locales ya est\u00e1n configurados. Elim\u00ednelos primero antes de configurar un dispositivo en la nube.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "cannot_connect": "No se pudo conectar", - "invalid_mdns": "Dispositivo no compatible para la integraci\u00f3n de Smappee.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "invalid_mdns": "Dispositivo no compatible con la integraci\u00f3n de Smappee.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "flow_title": "{name}", "step": { "environment": { "data": { - "environment": "Ambiente" + "environment": "Entorno" }, "description": "Configura tu Smappee para que se integre con Home Assistant." }, @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Ingrese el host para iniciar la integraci\u00f3n local de Smappee" + "description": "Introduce el host para iniciar la integraci\u00f3n local de Smappee" }, "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" diff --git a/homeassistant/components/smartthings/translations/es.json b/homeassistant/components/smartthings/translations/es.json index 8aee6337cef..b77cb803205 100644 --- a/homeassistant/components/smartthings/translations/es.json +++ b/homeassistant/components/smartthings/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida: \n > {webhook_url} \n\n Actualiza tu configuraci\u00f3n seg\u00fan las [instrucciones]({component_url}), reinicia Home Assistant e int\u00e9ntalo de nuevo.", + "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida:\n> {webhook_url} \n\nActualiza tu configuraci\u00f3n seg\u00fan las [instrucciones]({component_url}), reinicia Home Assistant y vuelve a intentarlo.", "no_available_locations": "No hay Ubicaciones SmartThings disponibles para configurar en Home Assistant." }, "error": { - "app_setup_error": "No se pudo configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", + "app_setup_error": "No se puede configurar la SmartApp. Por favor, int\u00e9ntalo de nuevo.", "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", diff --git a/homeassistant/components/solaredge/translations/es.json b/homeassistant/components/solaredge/translations/es.json index d152481ae0d..71151cfbe1f 100644 --- a/homeassistant/components/solaredge/translations/es.json +++ b/homeassistant/components/solaredge/translations/es.json @@ -14,7 +14,7 @@ "data": { "api_key": "Clave API", "name": "El nombre de esta instalaci\u00f3n", - "site_id": "La identificaci\u00f3n del sitio de SolarEdge" + "site_id": "El ID del sitio de SolarEdge" }, "title": "Definir los par\u00e1metros de la API para esta instalaci\u00f3n" } diff --git a/homeassistant/components/solarlog/translations/es.json b/homeassistant/components/solarlog/translations/es.json index a5d8b41ba49..0324b8ee5e6 100644 --- a/homeassistant/components/solarlog/translations/es.json +++ b/homeassistant/components/solarlog/translations/es.json @@ -11,9 +11,9 @@ "user": { "data": { "host": "Host", - "name": "El prefijo que se utilizar\u00e1 para los sensores Solar-Log" + "name": "El prefijo que se utilizar\u00e1 para tus sensores Solar-Log" }, - "title": "Defina su conexi\u00f3n Solar-Log" + "title": "Define tu conexi\u00f3n Solar-Log" } } } diff --git a/homeassistant/components/soma/translations/es.json b/homeassistant/components/soma/translations/es.json index 0e2312bd335..f57970c4f77 100644 --- a/homeassistant/components/soma/translations/es.json +++ b/homeassistant/components/soma/translations/es.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "connection_error": "No se pudo conectar", - "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n.", - "result_error": "SOMA Connect respondi\u00f3 con un error." + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", + "result_error": "SOMA Connect respondi\u00f3 con un estado de error." }, "create_entry": { - "default": "Autenticaci\u00f3n exitosa" + "default": "Autenticado correctamente" }, "step": { "user": { @@ -16,7 +16,7 @@ "host": "Host", "port": "Puerto" }, - "description": "Por favor, introduzca los ajustes de conexi\u00f3n de SOMA Connect.", + "description": "Por favor, introduce la configuraci\u00f3n de conexi\u00f3n de tu SOMA Connect.", "title": "SOMA Connect" } } diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index 3d8fc33de59..eee1e025e36 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -12,8 +12,8 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "La integraci\u00f3n de Sonarr debe volver a autenticarse manualmente con la API de Sonarr alojada en: {url}", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "description": "La integraci\u00f3n Sonarr debe volver a autenticarse manualmente con la API de Sonarr alojada en: {url}", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/songpal/translations/es.json b/homeassistant/components/songpal/translations/es.json index 153d107eacd..eb3abdea6a0 100644 --- a/homeassistant/components/songpal/translations/es.json +++ b/homeassistant/components/songpal/translations/es.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "endpoint": "Endpoint" + "endpoint": "Extremo" } } } diff --git a/homeassistant/components/soundtouch/translations/es.json b/homeassistant/components/soundtouch/translations/es.json index 0ee2968bf09..c8c13367ca4 100644 --- a/homeassistant/components/soundtouch/translations/es.json +++ b/homeassistant/components/soundtouch/translations/es.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Se va a eliminar la configuraci\u00f3n de Bose SoundTouch mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimine la configuraci\u00f3n YAML de Bose SoundTouch de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Bose SoundTouch mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Bose SoundTouch de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Bose SoundTouch" } } diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index 90fe06ccf55..9ba5fcbd4bb 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -15,7 +15,7 @@ "data": { "manual": "Desactivar actualizaci\u00f3n autom\u00e1tica", "scan_interval": "Frecuencia de actualizaci\u00f3n (minutos)", - "server_name": "Seleccione el servidor de prueba" + "server_name": "Selecciona el servidor de prueba" } } } diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index 377c2a9df58..edb2ff4bf82 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", - "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_account_mismatch": "La cuenta de Spotify con la que est\u00e1s autenticado, no coincide con la cuenta necesaria para re-autenticaci\u00f3n." }, "create_entry": { - "default": "Autentificado con \u00e9xito con Spotify." + "default": "Autenticado con \u00e9xito con Spotify." }, "step": { "pick_implementation": { @@ -27,7 +27,7 @@ }, "system_health": { "info": { - "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API de Spotify" + "api_endpoint_reachable": "Se puede llegar al punto de conexi\u00f3n de la API de Spotify" } } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/es.json b/homeassistant/components/squeezebox/translations/es.json index b6745920acd..b8e8965b621 100644 --- a/homeassistant/components/squeezebox/translations/es.json +++ b/homeassistant/components/squeezebox/translations/es.json @@ -19,7 +19,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "title": "Editar la informaci\u00f3n de conexi\u00f3n" + "title": "Edita la informaci\u00f3n de conexi\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json index b82a4e6f6f6..eed1c040338 100644 --- a/homeassistant/components/srp_energy/translations/es.json +++ b/homeassistant/components/srp_energy/translations/es.json @@ -13,7 +13,7 @@ "user": { "data": { "id": "ID de la cuenta", - "is_tou": "Es el plan de tiempo de uso", + "is_tou": "Es un plan por Tiempo de uso", "password": "Contrase\u00f1a", "username": "Nombre de usuario" } diff --git a/homeassistant/components/switch/translations/es.json b/homeassistant/components/switch/translations/es.json index a605ceb238b..5c72f292911 100644 --- a/homeassistant/components/switch/translations/es.json +++ b/homeassistant/components/switch/translations/es.json @@ -6,8 +6,8 @@ "turn_on": "Encender {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est\u00e1 apagada", - "is_on": "{entity_name} est\u00e1 encendida" + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido" }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index bcd6465acae..3592ce065e3 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -13,6 +13,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Vuoi configurare {name}?" + }, + "password": { + "data": { + "password": "Password" + }, + "description": "Il dispositivo {name} richiede una password" + }, "user": { "data": { "address": "Indirizzo del dispositivo", diff --git a/homeassistant/components/syncthru/translations/es.json b/homeassistant/components/syncthru/translations/es.json index da3002496db..2aa03a63d20 100644 --- a/homeassistant/components/syncthru/translations/es.json +++ b/homeassistant/components/syncthru/translations/es.json @@ -6,7 +6,7 @@ "error": { "invalid_url": "URL no v\u00e1lida", "syncthru_not_supported": "El dispositivo no es compatible con SyncThru", - "unknown_state": "Estado de la impresora desconocido, verifica la URL y la conectividad de la red" + "unknown_state": "Estado de la impresora desconocido, verifica la URL y la conectividad de red" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index af0d17d7d39..817001cdc20 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "reconfigure_successful": "La reconfiguraci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "missing_data": "Faltan datos: por favor, vuelva a intentarlo m\u00e1s tarde o pruebe con otra configuraci\u00f3n", - "otp_failed": "La autenticaci\u00f3n de dos pasos fall\u00f3, vuelva a intentar con un nuevo c\u00f3digo de acceso", + "missing_data": "Faltan datos: por favor, vuelve a intentarlo m\u00e1s tarde o intenta otra configuraci\u00f3n", + "otp_failed": "La autenticaci\u00f3n en dos pasos fall\u00f3, vuelve a intentarlo con un nuevo c\u00f3digo de acceso", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/system_health/translations/es.json b/homeassistant/components/system_health/translations/es.json index ada0964a358..9015f0899ac 100644 --- a/homeassistant/components/system_health/translations/es.json +++ b/homeassistant/components/system_health/translations/es.json @@ -1,3 +1,3 @@ { - "title": "Estado del sistema" + "title": "Salud del Sistema" } \ No newline at end of file diff --git a/homeassistant/components/tado/translations/es.json b/homeassistant/components/tado/translations/es.json index 42090cccb15..e7acf6d3d95 100644 --- a/homeassistant/components/tado/translations/es.json +++ b/homeassistant/components/tado/translations/es.json @@ -15,7 +15,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Conectar con tu cuenta de Tado" + "title": "Conectar con tu cuenta Tado" } } }, @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "fallback": "Elige el modo alternativo." + "fallback": "Elige el modo de respaldo." }, "description": "El modo de respaldo te permite elegir cu\u00e1ndo retroceder a Smart Schedule desde tu superposici\u00f3n de zona manual. (NEXT_TIME_BLOCK:= Cambiar en el pr\u00f3ximo cambio de Smart Schedule; MANUAL:= No cambiar hasta que canceles; TADO_DEFAULT:= Cambiar seg\u00fan tu configuraci\u00f3n en la aplicaci\u00f3n Tado).", "title": "Ajustar las opciones de Tado" diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index fbda5550ac3..2fd67d46d01 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "unknown": "Error inesperado", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, diff --git a/homeassistant/components/tibber/translations/es.json b/homeassistant/components/tibber/translations/es.json index 53c0c2aaab7..349f35a05f8 100644 --- a/homeassistant/components/tibber/translations/es.json +++ b/homeassistant/components/tibber/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_access_token": "Token de acceso inv\u00e1lido", + "invalid_access_token": "Token de acceso no v\u00e1lido", "timeout": "Tiempo de espera para conectarse a Tibber" }, "step": { diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index 423d81dde0c..89f85a222a1 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "El acuerdo seleccionado ya est\u00e1 configurado.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." @@ -17,7 +17,7 @@ "title": "Selecciona tu acuerdo" }, "pick_implementation": { - "title": "Elige el arrendatario con el cual deseas autenticarte" + "title": "Elige tu inquilino con el que autenticarse" } } } diff --git a/homeassistant/components/traccar/translations/es.json b/homeassistant/components/traccar/translations/es.json index 21e90275d17..1177250f930 100644 --- a/homeassistant/components/traccar/translations/es.json +++ b/homeassistant/components/traccar/translations/es.json @@ -6,12 +6,12 @@ "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, tendr\u00e1s que configurar la opci\u00f3n webhook de Traccar.\n\nUtilice la siguiente url: `{webhook_url}`\n\nConsulte la [documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, deber\u00e1s configurar la funci\u00f3n de webhook en Traccar. \n\nUtiliza la siguiente URL: `{webhook_url}` \n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles." }, "step": { "user": { "description": "\u00bfEst\u00e1s seguro de que quieres configurar Traccar?", - "title": "Configura Traccar" + "title": "Configurar Traccar" } } } diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 7ae9bfa87fa..d722d0a528a 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -25,7 +25,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "title": "Configuraci\u00f3n del cliente de transmisi\u00f3n" + "title": "Configuraci\u00f3n del cliente Transmission" } } }, @@ -34,10 +34,10 @@ "init": { "data": { "limit": "L\u00edmite", - "order": "Pedido", + "order": "Ordenar", "scan_interval": "Frecuencia de actualizaci\u00f3n" }, - "title": "Configurar opciones para la transmisi\u00f3n" + "title": "Configurar opciones para Transmission" } } } diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index 55ab464c3c2..6191d5acf12 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -13,7 +13,7 @@ "password": "Contrase\u00f1a", "username": "Cuenta" }, - "description": "Introduce tus credencial de Tuya" + "description": "Introduce tus credenciales Tuya" } } } diff --git a/homeassistant/components/twentemilieu/translations/es.json b/homeassistant/components/twentemilieu/translations/es.json index 93effca8aad..6bd230b377b 100644 --- a/homeassistant/components/twentemilieu/translations/es.json +++ b/homeassistant/components/twentemilieu/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_address": "No se ha encontrado la direcci\u00f3n en el \u00e1rea de servicio de Twente Milieu." + "invalid_address": "Direcci\u00f3n no encontrada en el \u00e1rea de servicio de Twente Milieu." }, "step": { "user": { @@ -14,7 +14,7 @@ "house_number": "N\u00famero de casa", "post_code": "C\u00f3digo postal" }, - "description": "Configura Twente Milieu con informaci\u00f3n de la recogida de residuos en tu direcci\u00f3n." + "description": "Configura Twente Milieu proporcionando informaci\u00f3n de recogida de residuos en tu direcci\u00f3n." } } } diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index ff9b9575b78..1bc3e90ac91 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -36,16 +36,16 @@ "dpi_restrictions": "Permitir el control de los grupos de restricci\u00f3n de DPI", "poe_clients": "Permitir control PoE de clientes" }, - "description": "Configurar controles de cliente\n\nCrea conmutadores para los n\u00fameros de serie para los que deseas controlar el acceso a la red.", + "description": "Configurar controles de cliente \n\nCrea interruptores para los n\u00fameros de serie para los que quieras controlar el acceso a la red.", "title": "Opciones de UniFi Network 2/3" }, "device_tracker": { "data": { - "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", - "ignore_wired_bug": "Desactiva la l\u00f3gica de errores de UniFi Network", - "ssid_filter": "Seleccione los SSIDs para realizar seguimiento de clientes inal\u00e1mbricos", + "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta que se considera ausente", + "ignore_wired_bug": "Deshabilitar la l\u00f3gica de errores cableada de UniFi Network", + "ssid_filter": "Selecciona los SSIDs para realizar el seguimiento de clientes inal\u00e1mbricos", "track_clients": "Seguimiento de los clientes de red", - "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)", + "track_devices": "Seguimiento de dispositivos de red (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de red cableada" }, "description": "Configurar dispositivo de seguimiento", @@ -53,15 +53,15 @@ }, "simple_options": { "data": { - "block_client": "Acceso controlado a la red de los clientes", - "track_clients": "Rastree clientes de red", - "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)" + "block_client": "Clientes con acceso controlado a la red", + "track_clients": "Seguimiento de los clientes de red", + "track_devices": "Seguimiento de dispositivos de red (dispositivos Ubiquiti)" }, "description": "Configura la integraci\u00f3n UniFi Network" }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para los clientes de la red", + "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para clientes de red", "allow_uptime_sensors": "Sensores de tiempo de actividad para clientes de la red" }, "description": "Configurar estad\u00edsticas de los sensores", diff --git a/homeassistant/components/upb/translations/es.json b/homeassistant/components/upb/translations/es.json index 7cbf3446c6a..4b4a44ce0d3 100644 --- a/homeassistant/components/upb/translations/es.json +++ b/homeassistant/components/upb/translations/es.json @@ -11,11 +11,11 @@ "step": { "user": { "data": { - "address": "Direcci\u00f3n (v\u00e9ase la descripci\u00f3n anterior)", + "address": "Direcci\u00f3n (consulta la descripci\u00f3n anterior)", "file_path": "Ruta y nombre del archivo de exportaci\u00f3n UPStart UPB.", "protocol": "Protocolo" }, - "description": "Conecte un M\u00f3dulo de Interfaz Universal Powerline Bus Powerline (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n [: puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serie, la direcci\u00f3n debe estar en la forma 'tty [: baudios]'. El baud es opcional y el valor predeterminado es 4800. Ejemplo: '/ dev / ttyS1'.", + "description": "Conecta un Universal Powerline Bus Powerline Interface Module (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baudios]'. El par\u00e1metro baudios es opcional y el valor predeterminado es 4800. Ejemplo: '/dev/ttyS1'.", "title": "Conectar con UPB PIM" } } diff --git a/homeassistant/components/vacuum/translations/es.json b/homeassistant/components/vacuum/translations/es.json index 87a79a4e5da..c5aa4366e5e 100644 --- a/homeassistant/components/vacuum/translations/es.json +++ b/homeassistant/components/vacuum/translations/es.json @@ -1,8 +1,8 @@ { "device_automation": { "action_type": { - "clean": "Deje que {entity_name} limpie", - "dock": "Deje que {entity_name} regrese a la base" + "clean": "Dejar que {entity_name} limpie", + "dock": "Dejar que {entity_name} regrese a la base" }, "condition_type": { "is_cleaning": "{entity_name} est\u00e1 limpiando", diff --git a/homeassistant/components/velbus/translations/es.json b/homeassistant/components/velbus/translations/es.json index ed2585a03e9..cb600d577e7 100644 --- a/homeassistant/components/velbus/translations/es.json +++ b/homeassistant/components/velbus/translations/es.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "name": "Nombre de la conexi\u00f3n Velbus", + "name": "El nombre de esta conexi\u00f3n velbus", "port": "Cadena de conexi\u00f3n" }, - "title": "Tipo de conexi\u00f3n Velbus" + "title": "Definir el tipo de conexi\u00f3n velbus" } } } diff --git a/homeassistant/components/vera/translations/es.json b/homeassistant/components/vera/translations/es.json index 8d9686b0a13..8aacb4f6a96 100644 --- a/homeassistant/components/vera/translations/es.json +++ b/homeassistant/components/vera/translations/es.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "exclude": "Identificadores de dispositivos Vera a excluir de Home Assistant", - "lights": "Identificadores de interruptores Vera que deben ser tratados como luces en Home Assistant", + "exclude": "IDs de dispositivos Vera para excluir de Home Assistant.", + "lights": "IDs de dispositivos interruptores Vera para tratarlos como luces en Home Assistant.", "vera_controller_url": "URL del controlador" }, "data_description": { @@ -20,10 +20,10 @@ "step": { "init": { "data": { - "exclude": "Identificadores de dispositivos Vera a excluir de Home Assistant", + "exclude": "IDs de dispositivos Vera para excluir de Home Assistant.", "lights": "Identificadores de interruptores Vera que deben ser tratados como luces en Home Assistant" }, - "description": "Consulte la documentaci\u00f3n de Vera para obtener detalles sobre los par\u00e1metros opcionales: https://www.home-assistant.io/integrations/vera/. Nota: Cualquier cambio aqu\u00ed necesitar\u00e1 un reinicio del servidor de Home Assistant. Para borrar valores, introduce un espacio.", + "description": "Consulta la documentaci\u00f3n de vera para obtener detalles sobre los par\u00e1metros opcionales: https://www.home-assistant.io/integrations/vera/. Nota: Cualquier cambio aqu\u00ed necesitar\u00e1 un reinicio en el servidor Home Assistant. Para borrar valores, proporciona un espacio.", "title": "Opciones del controlador Vera" } } diff --git a/homeassistant/components/vesync/translations/es.json b/homeassistant/components/vesync/translations/es.json index dd5f95a2e13..4f03e42dec0 100644 --- a/homeassistant/components/vesync/translations/es.json +++ b/homeassistant/components/vesync/translations/es.json @@ -12,7 +12,7 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "title": "Introduzca el nombre de usuario y la contrase\u00f1a" + "title": "Introduce el nombre de usuario y la contrase\u00f1a" } } } diff --git a/homeassistant/components/vizio/translations/es.json b/homeassistant/components/vizio/translations/es.json index 6ac88586cad..874f19ab119 100644 --- a/homeassistant/components/vizio/translations/es.json +++ b/homeassistant/components/vizio/translations/es.json @@ -7,23 +7,23 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "complete_pairing_failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que el televisor sigue encendido y conectado a la red antes de volver a enviarlo.", - "existing_config_entry_found": "Ya se ha configurado una entrada VIZIO SmartCast Device con el mismo n\u00famero de serie. Debes borrar la entrada existente para configurar \u00e9sta." + "complete_pairing_failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que la TV sigue encendida y conectada a la red antes de volver a enviarlo.", + "existing_config_entry_found": "Ya se ha configurado una entrada Dispositivo VIZIO SmartCast con el mismo n\u00famero de serie. Debes borrar la entrada existente para configurar \u00e9sta." }, "step": { "pair_tv": { "data": { "pin": "C\u00f3digo PIN" }, - "description": "Tu TV debe estar mostrando un c\u00f3digo. Escribe ese c\u00f3digo en el formulario y contin\u00faa con el paso siguiente para completar el emparejamiento.", + "description": "Tu TV deber\u00eda mostrar un c\u00f3digo. Introduce ese c\u00f3digo en el formulario y luego contin\u00faa con el siguiente paso para completar el emparejamiento.", "title": "Completar Proceso de Emparejamiento" }, "pairing_complete": { - "description": "Tu VIZIO SmartCast Device ahora est\u00e1 conectado a Home Assistant.", + "description": "Tu Dispositivo VIZIO SmartCast ahora est\u00e1 conectado a Home Assistant.", "title": "Emparejamiento Completado" }, "pairing_complete_import": { - "description": "Tu VIZIO SmartCast Device ahora est\u00e1 conectado a Home Assistant. \n\nTu Token de acceso es '**{access_token}**'.", + "description": "Tu Dispositivo VIZIO SmartCast ahora est\u00e1 conectado a Home Assistant. \n\nTu Token de acceso es '**{access_token}**'.", "title": "Emparejamiento Completado" }, "user": { @@ -34,7 +34,7 @@ "name": "Nombre" }, "description": "Solo se necesita un Token de acceso para TVs. Si est\u00e1s configurando una TV y a\u00fan no tienes un Token de acceso, d\u00e9jalo en blanco para pasar por un proceso de emparejamiento.", - "title": "VIZIO SmartCast Device" + "title": "Dispositivo VIZIO SmartCast" } } }, @@ -46,8 +46,8 @@ "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?", "volume_step": "Tama\u00f1o del paso de volumen" }, - "description": "Si tienes un Smart TV, opcionalmente puedes filtrar su lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en su lista de fuentes.", - "title": "Actualizar las opciones de VIZIO SmartCast Device" + "description": "Si tiene un Smart TV, puede filtrar opcionalmente tu lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en tu lista de fuentes.", + "title": "Actualizar las opciones de Dispositivo VIZIO SmartCast" } } } diff --git a/homeassistant/components/weather/translations/es.json b/homeassistant/components/weather/translations/es.json index a6a7336e612..a616bc9ef01 100644 --- a/homeassistant/components/weather/translations/es.json +++ b/homeassistant/components/weather/translations/es.json @@ -9,7 +9,7 @@ "lightning": "Rel\u00e1mpagos", "lightning-rainy": "Rel\u00e1mpagos, lluvioso", "partlycloudy": "Parcialmente nublado", - "pouring": "Lluvia", + "pouring": "Lluvia torrencial", "rainy": "Lluvioso", "snowy": "Nevado", "snowy-rainy": "Nevado, lluvioso", diff --git a/homeassistant/components/wilight/translations/es.json b/homeassistant/components/wilight/translations/es.json index 5836867c0e7..150cb5c91b2 100644 --- a/homeassistant/components/wilight/translations/es.json +++ b/homeassistant/components/wilight/translations/es.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "not_supported_device": "Este WiLight no es compatible actualmente", - "not_wilight_device": "Este dispositivo no es un Wilight" + "not_wilight_device": "Este dispositivo no es un WiLight" }, "flow_title": "{name}", "step": { "confirm": { - "description": "Se admiten los siguientes componentes: {componentes}" + "description": "Se admiten los siguientes componentes: {components}" } } } diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index d90f1ccacc3..07734a7263f 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Configuraci\u00f3n actualizada para el perfil.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { - "default": "Autenticado correctamente con Withings." + "default": "Autenticado con \u00e9xito con Withings." }, "error": { "already_configured": "La cuenta ya est\u00e1 configurada" diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index 0547b1598e3..1b8d0eeeb00 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u00bfQuieres a\u00f1adir el WLED `{name}` a Home Assistant?", - "title": "Dispositivo WLED detectado" + "title": "Dispositivo WLED descubierto" } } }, diff --git a/homeassistant/components/wolflink/translations/es.json b/homeassistant/components/wolflink/translations/es.json index 601026f784a..a98fe86a983 100644 --- a/homeassistant/components/wolflink/translations/es.json +++ b/homeassistant/components/wolflink/translations/es.json @@ -13,7 +13,7 @@ "data": { "device_name": "Dispositivo" }, - "title": "Seleccionar dispositivo WOLF" + "title": "Selecciona dispositivo WOLF" }, "user": { "data": { diff --git a/homeassistant/components/wolflink/translations/sensor.es.json b/homeassistant/components/wolflink/translations/sensor.es.json index e321ecb72f4..43f83e8aed6 100644 --- a/homeassistant/components/wolflink/translations/sensor.es.json +++ b/homeassistant/components/wolflink/translations/sensor.es.json @@ -7,8 +7,8 @@ "absenkstop": "Parada de retroceso", "aktiviert": "Activado", "antilegionellenfunktion": "Funci\u00f3n anti legionela", - "at_abschaltung": "Apagado de OT", - "at_frostschutz": "OT protecci\u00f3n contra heladas", + "at_abschaltung": "Apagado OT", + "at_frostschutz": "Protecci\u00f3n contra heladas OT", "aus": "Deshabilitado", "auto": "Autom\u00e1tico", "auto_off_cool": "AutoOffCool", @@ -16,7 +16,7 @@ "automatik_aus": "Apagado autom\u00e1tico", "automatik_ein": "Encendido autom\u00e1tico", "bereit_keine_ladung": "Listo, no est\u00e1 cargando", - "betrieb_ohne_brenner": "Trabajando sin quemador", + "betrieb_ohne_brenner": "Trabajar sin quemador", "cooling": "Enfriamiento", "deaktiviert": "Inactivo", "dhw_prior": "DHWPrior", @@ -24,7 +24,7 @@ "ein": "Habilitado", "estrichtrocknung": "Secado en regla", "externe_deaktivierung": "Desactivaci\u00f3n externa", - "fernschalter_ein": "Mando a distancia activado", + "fernschalter_ein": "Control remoto habilitado", "frost_heizkreis": "Escarcha del circuito de calefacci\u00f3n", "frost_warmwasser": "Heladas de DHW", "frostschutz": "Protecci\u00f3n contra las heladas", @@ -37,15 +37,15 @@ "initialisierung": "Inicializaci\u00f3n", "kalibration": "Calibraci\u00f3n", "kalibration_heizbetrieb": "Calibraci\u00f3n del modo de calefacci\u00f3n", - "kalibration_kombibetrieb": "Calibraci\u00f3n en modo combinado", + "kalibration_kombibetrieb": "Calibraci\u00f3n en modo mixto", "kalibration_warmwasserbetrieb": "Calibraci\u00f3n DHW", "kaskadenbetrieb": "Operaci\u00f3n en cascada", - "kombibetrieb": "Modo combinado", - "kombigerat": "Caldera combinada", - "kombigerat_mit_solareinbindung": "Caldera Combi con integraci\u00f3n solar", - "mindest_kombizeit": "Tiempo m\u00ednimo de combinaci\u00f3n", + "kombibetrieb": "Modo mixto", + "kombigerat": "Caldera mixta", + "kombigerat_mit_solareinbindung": "Caldera mixta con integraci\u00f3n solar", + "mindest_kombizeit": "Tiempo combinado m\u00ednimo", "nachlauf_heizkreispumpe": "Bomba de circuito de calefacci\u00f3n en ejecuci\u00f3n", - "nachspulen": "Enviar enjuague", + "nachspulen": "Post-lavado", "nur_heizgerat": "S\u00f3lo la caldera", "parallelbetrieb": "Modo paralelo", "partymodus": "Modo fiesta", diff --git a/homeassistant/components/xbox/translations/es.json b/homeassistant/components/xbox/translations/es.json index 7b7fe2d32fe..f1b1945c832 100644 --- a/homeassistant/components/xbox/translations/es.json +++ b/homeassistant/components/xbox/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index 41d3ffa8207..6c0667af5d5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "not_xiaomi_aqara": "No es un Xiaomi Aqara Gateway, el dispositivo descubierto no coincide con los gateways conocidos" + "not_xiaomi_aqara": "No es un Xiaomi Aqara Gateway, el dispositivo descubierto no coincide con las puertas de enlace conocidas" }, "error": { "discovery_error": "No se pudo descubrir un Xiaomi Aqara Gateway, intenta utilizar la IP del dispositivo que ejecuta HomeAssistant como interfaz", - "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos, consulte https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", - "invalid_interface": "Interfaz de red inv\u00e1lida", - "invalid_key": "Clave del gateway inv\u00e1lida", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos , consulta https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "Interfaz de red no v\u00e1lida", + "invalid_key": "Clave de puerta de enlace no v\u00e1lida", "invalid_mac": "Direcci\u00f3n Mac no v\u00e1lida" }, "flow_title": "{name}", @@ -18,14 +18,14 @@ "data": { "select_ip": "Direcci\u00f3n IP" }, - "description": "Selecciona la puerta de enlace Xiaomi Aqara que deseas conectar" + "description": "Selecciona el Xiaomi Aqara Gateway que deseas conectar" }, "settings": { "data": { - "key": "La clave de tu gateway", - "name": "Nombre del Gateway" + "key": "La clave de tu puerta de enlace", + "name": "Nombre de la puerta de enlace" }, - "description": "La clave (contrase\u00f1a) se puede obtener con este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Si no se proporciona la clave solo se podr\u00e1 acceder a los sensores", + "description": "La clave (contrase\u00f1a) se puede recuperar con este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Si no se proporciona la llave, solo se podr\u00e1 acceder a los sensores", "title": "Configuraciones opcionales" }, "user": { diff --git a/homeassistant/components/yalexs_ble/translations/de.json b/homeassistant/components/yalexs_ble/translations/de.json index cfd368aadef..416761bd91a 100644 --- a/homeassistant/components/yalexs_ble/translations/de.json +++ b/homeassistant/components/yalexs_ble/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "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", "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden." }, @@ -23,7 +24,7 @@ "key": "Offline-Schl\u00fcssel (32-Byte-Hex-String)", "slot": "Offline-Schl\u00fcssel-Slot (Ganzzahl zwischen 0 und 255)" }, - "description": "Lies in der Dokumentation unter {docs_url} nach, wie du den Offline-Schl\u00fcssel finden kannst." + "description": "Lies in der Dokumentation nach, wie du den Offline-Schl\u00fcssel finden kannst." } } } diff --git a/homeassistant/components/yalexs_ble/translations/es.json b/homeassistant/components/yalexs_ble/translations/es.json index 32863d750d2..adad8a18c40 100644 --- a/homeassistant/components/yalexs_ble/translations/es.json +++ b/homeassistant/components/yalexs_ble/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_devices_found": "No se encontraron dispositivos en la red", "no_unconfigured_devices": "No se encontraron dispositivos no configurados." }, @@ -23,7 +24,7 @@ "key": "Clave sin conexi\u00f3n (cadena hexadecimal de 32 bytes)", "slot": "Ranura de clave sin conexi\u00f3n (entero entre 0 y 255)" }, - "description": "Consulta la documentaci\u00f3n en {docs_url} para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." + "description": "Consulta la documentaci\u00f3n para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." } } } diff --git a/homeassistant/components/yalexs_ble/translations/fr.json b/homeassistant/components/yalexs_ble/translations/fr.json index 7ec2edfc86e..e073f5afee3 100644 --- a/homeassistant/components/yalexs_ble/translations/fr.json +++ b/homeassistant/components/yalexs_ble/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", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9." }, @@ -15,7 +16,8 @@ "user": { "data": { "address": "Adresse Bluetooth" - } + }, + "description": "Consultez la documentation pour d\u00e9couvrir comment trouver la cl\u00e9 hors ligne." } } } diff --git a/homeassistant/components/yalexs_ble/translations/hu.json b/homeassistant/components/yalexs_ble/translations/hu.json index 44e14971cc7..4208d98a1e6 100644 --- a/homeassistant/components/yalexs_ble/translations/hu.json +++ b/homeassistant/components/yalexs_ble/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "no_unconfigured_devices": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan eszk\u00f6z." }, diff --git a/homeassistant/components/yalexs_ble/translations/id.json b/homeassistant/components/yalexs_ble/translations/id.json index fda1e243e0c..3f2e275e458 100644 --- a/homeassistant/components/yalexs_ble/translations/id.json +++ b/homeassistant/components/yalexs_ble/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "no_unconfigured_devices": "Tidak ditemukan perangkat yang tidak dikonfigurasi." }, @@ -23,7 +24,7 @@ "key": "Kunci Offline (string heksadesimal 32 byte)", "slot": "Slot Kunci Offline (Bilangan Bulat antara 0 dan 255)" }, - "description": "Lihat dokumentasi di {docs_url} untuk mengetahui cara menemukan kunci offline." + "description": "Lihat dokumentasi untuk mengetahui cara menemukan kunci offline." } } } diff --git a/homeassistant/components/yalexs_ble/translations/it.json b/homeassistant/components/yalexs_ble/translations/it.json new file mode 100644 index 00000000000..0ca0e6c96ee --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "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", + "no_unconfigured_devices": "Nessun dispositivo non configurato trovato." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_key_format": "La chiave offline deve essere una stringa esadecimale di 32 byte.", + "invalid_key_index": "Lo slot della chiave offline deve essere un numero intero compreso tra 0 e 255.", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Vuoi configurare {name} tramite Bluetooth con l'indirizzo {address}?" + }, + "user": { + "data": { + "address": "Indirizzo Bluetooth", + "key": "Chiave offline (stringa esadecimale a 32 byte)", + "slot": "Slot chiave offline (numero intero compreso tra 0 e 255)" + }, + "description": "Controlla la documentazione su come trovare la chiave offline" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/pt-BR.json b/homeassistant/components/yalexs_ble/translations/pt-BR.json index 68a52803cbe..19467e26e36 100644 --- a/homeassistant/components/yalexs_ble/translations/pt-BR.json +++ b/homeassistant/components/yalexs_ble/translations/pt-BR.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado." }, @@ -23,7 +24,7 @@ "key": "Chave offline (sequ\u00eancia hexadecimal de 32 bytes)", "slot": "Slot de chave offline (inteiro entre 0 e 255)" }, - "description": "Verifique a documenta\u00e7\u00e3o em {docs_url} para saber como encontrar a chave off-line." + "description": "Verifique a documenta\u00e7\u00e3o para saber como encontrar a chave offline." } } } diff --git a/homeassistant/components/yolink/translations/es.json b/homeassistant/components/yolink/translations/es.json index c4bcfa6e75b..82f45972147 100644 --- a/homeassistant/components/yolink/translations/es.json +++ b/homeassistant/components/yolink/translations/es.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 0378ee6cdd0..6dcc3c84e42 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -87,12 +87,12 @@ }, "trigger_type": { "device_dropped": "Dispositivo ca\u00eddo", - "device_flipped": "Dispositivo volteado \" {subtype} \"", - "device_knocked": "Dispositivo eliminado \" {subtype} \"", - "device_offline": "Dispositivo desconectado", - "device_rotated": "Dispositivo girado \" {subtype} \"", + "device_flipped": "Dispositivo volteado \"{subtype}\"", + "device_knocked": "Dispositivo golpeado \"{subtype}\"", + "device_offline": "Dispositivo sin conexi\u00f3n", + "device_rotated": "Dispositivo girado \"{subtype}\"", "device_shaken": "Dispositivo agitado", - "device_slid": "Dispositivo deslizado \" {subtype} \"", + "device_slid": "Dispositivo deslizado \"{subtype}\"", "device_tilted": "Dispositivo inclinado", "remote_button_alt_double_press": "Bot\u00f3n \"{subtype}\" doble pulsaci\u00f3n (modo Alternativo)", "remote_button_alt_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente (modo Alternativo)", @@ -102,14 +102,14 @@ "remote_button_alt_short_press": "Bot\u00f3n \"{subtype}\" pulsado (modo Alternativo)", "remote_button_alt_short_release": "Bot\u00f3n \"{subtype}\" soltado (modo Alternativo)", "remote_button_alt_triple_press": "Bot\u00f3n \"{subtype}\" triple pulsaci\u00f3n (modo Alternativo)", - "remote_button_double_press": "Bot\u00f3n \"{subtype}\" doble pulsaci\u00f3n", + "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces", "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", - "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" cu\u00e1druple pulsaci\u00f3n", - "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" qu\u00edntuple pulsaci\u00f3n", + "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces", + "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", - "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" triple pulsaci\u00f3n" + "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado tres veces" } } } \ 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 9e3a2f154bc..834c8a1116b 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -1,10 +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_get_discovery_info_failed": "No se pudo obtener 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_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", + "addon_install_failed": "No se pudo instalar el complemento Z-Wave JS.", + "addon_set_config_failed": "No se pudo establecer la configuraci\u00f3n de Z-Wave JS.", "addon_start_failed": "No se pudo 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 curso", @@ -13,14 +13,14 @@ "not_zwave_device": "El dispositivo descubierto no es un dispositivo Z-Wave." }, "error": { - "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Comprueba la configuraci\u00f3n.", + "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Verifica la configuraci\u00f3n.", "cannot_connect": "No se pudo conectar", "invalid_ws_url": "URL de websocket no v\u00e1lida", "unknown": "Error inesperado" }, "flow_title": "{name}", "progress": { - "install_addon": "Espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Puede tardar varios minutos.", + "install_addon": "Por favor, espera mientras finaliza la instalaci\u00f3n del complemento Z-Wave JS. Esto puede tardar varios minutos.", "start_addon": "Por favor, espera mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos." }, "step": { @@ -33,7 +33,7 @@ "usb_path": "Ruta del dispositivo USB" }, "description": "El complemento generar\u00e1 claves de seguridad si esos campos se dejan vac\u00edos.", - "title": "Introduzca la configuraci\u00f3n del complemento Z-Wave JS" + "title": "Introduce 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" @@ -50,7 +50,7 @@ "data": { "use_addon": "Usar el complemento Z-Wave JS Supervisor" }, - "description": "\u00bfQuieres utilizar el complemento Z-Wave JS Supervisor?", + "description": "\u00bfQuieres usar el complemento Z-Wave JS Supervisor?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { From 0d45cef6f6adce870d0967232fde5a244af958be Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 14 Aug 2022 06:31:39 +0200 Subject: [PATCH 345/903] =?UTF-8?q?Update=20xknx=20to=201.0.0=20?= =?UTF-8?q?=F0=9F=8E=89=20(#76734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 266eceaacee..0f9b6b4b95a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.22.1"], + "requirements": ["xknx==1.0.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 058218dcefb..85915ca6b9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2483,7 +2483,7 @@ xboxapi==2.0.1 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==0.22.1 +xknx==1.0.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 875ad6e7faa..17dfb974aaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1681,7 +1681,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==0.22.1 +xknx==1.0.0 # homeassistant.components.bluesound # homeassistant.components.fritz From f55c274d8390a2dedce1b16d6883dcb4c799faa7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Aug 2022 21:00:27 -1000 Subject: [PATCH 346/903] Add Qingping integration (BLE) (#76598) * Add Qingping integration (BLE) * commit the binary sensor * add binary_sensor file * Update homeassistant/components/qingping/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/qingping/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/qingping/sensor.py Co-authored-by: Martin Hjelmare * fix const CONCENTRATION_MICROGRAMS_PER_CUBIC_METER * cover case where config flow is started, another path adds it, and then they resume * fix missed values Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 + homeassistant/components/qingping/__init__.py | 49 ++++ .../components/qingping/binary_sensor.py | 95 +++++++ .../components/qingping/config_flow.py | 121 +++++++++ homeassistant/components/qingping/const.py | 3 + homeassistant/components/qingping/device.py | 31 +++ .../components/qingping/manifest.json | 11 + homeassistant/components/qingping/sensor.py | 173 +++++++++++++ .../components/qingping/strings.json | 22 ++ .../components/qingping/translations/en.json | 22 ++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/qingping/__init__.py | 40 +++ tests/components/qingping/conftest.py | 8 + .../components/qingping/test_binary_sensor.py | 46 ++++ tests/components/qingping/test_config_flow.py | 234 ++++++++++++++++++ tests/components/qingping/test_sensor.py | 50 ++++ 19 files changed, 918 insertions(+) create mode 100644 homeassistant/components/qingping/__init__.py create mode 100644 homeassistant/components/qingping/binary_sensor.py create mode 100644 homeassistant/components/qingping/config_flow.py create mode 100644 homeassistant/components/qingping/const.py create mode 100644 homeassistant/components/qingping/device.py create mode 100644 homeassistant/components/qingping/manifest.json create mode 100644 homeassistant/components/qingping/sensor.py create mode 100644 homeassistant/components/qingping/strings.json create mode 100644 homeassistant/components/qingping/translations/en.json create mode 100644 tests/components/qingping/__init__.py create mode 100644 tests/components/qingping/conftest.py create mode 100644 tests/components/qingping/test_binary_sensor.py create mode 100644 tests/components/qingping/test_config_flow.py create mode 100644 tests/components/qingping/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 5de44d30fb3..3ac50eeb1db 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -844,6 +844,8 @@ build.json @home-assistant/supervisor /homeassistant/components/pvpc_hourly_pricing/ @azogue /tests/components/pvpc_hourly_pricing/ @azogue /homeassistant/components/qbittorrent/ @geoffreylagaisse +/homeassistant/components/qingping/ @bdraco +/tests/components/qingping/ @bdraco /homeassistant/components/qld_bushfire/ @exxamalte /tests/components/qld_bushfire/ @exxamalte /homeassistant/components/qnap_qsw/ @Noltari diff --git a/homeassistant/components/qingping/__init__.py b/homeassistant/components/qingping/__init__.py new file mode 100644 index 00000000000..9155c02c07c --- /dev/null +++ b/homeassistant/components/qingping/__init__.py @@ -0,0 +1,49 @@ +"""The Qingping integration.""" +from __future__ import annotations + +import logging + +from qingping_ble import QingpingBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Qingping BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = QingpingBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py new file mode 100644 index 00000000000..273a9a93351 --- /dev/null +++ b/homeassistant/components/qingping/binary_sensor.py @@ -0,0 +1,95 @@ +"""Support for Qingping binary sensors.""" +from __future__ import annotations + +from typing import Optional + +from qingping_ble import ( + BinarySensorDeviceClass as QingpingBinarySensorDeviceClass, + SensorUpdate, +) + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +BINARY_SENSOR_DESCRIPTIONS = { + QingpingBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=QingpingBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + QingpingBinarySensorDeviceClass.LIGHT: BinarySensorEntityDescription( + key=QingpingBinarySensorDeviceClass.LIGHT, + device_class=BinarySensorDeviceClass.LIGHT, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[ + description.device_class + ] + for device_key, description in sensor_update.binary_entity_descriptions.items() + if description.device_class + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Qingping BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + QingpingBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class QingpingBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + BinarySensorEntity, +): + """Representation of a Qingping binary sensor.""" + + @property + def is_on(self) -> bool | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/qingping/config_flow.py b/homeassistant/components/qingping/config_flow.py new file mode 100644 index 00000000000..c4ebc4c4273 --- /dev/null +++ b/homeassistant/components/qingping/config_flow.py @@ -0,0 +1,121 @@ +"""Config flow for Qingping integration.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from qingping_ble import QingpingBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_discovered_service_info, + async_process_advertisements, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +# How long to wait for additional advertisement packets if we don't have the right ones +ADDITIONAL_DISCOVERY_TIMEOUT = 60 + + +class QingpingConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for qingping.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def _async_wait_for_full_advertisement( + self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData + ) -> BluetoothServiceInfoBleak: + """Wait for the full advertisement. + + Sometimes the first advertisement we receive is blank or incomplete. + """ + if device.supported(discovery_info): + return discovery_info + return await async_process_advertisements( + self.hass, + device.supported, + {"address": discovery_info.address}, + BluetoothScanningMode.ACTIVE, + ADDITIONAL_DISCOVERY_TIMEOUT, + ) + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + try: + self._discovery_info = await self._async_wait_for_full_advertisement( + discovery_info, device + ) + except asyncio.TimeoutError: + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/qingping/const.py b/homeassistant/components/qingping/const.py new file mode 100644 index 00000000000..ccc87ac28f4 --- /dev/null +++ b/homeassistant/components/qingping/const.py @@ -0,0 +1,3 @@ +"""Constants for the Qingping integration.""" + +DOMAIN = "qingping" diff --git a/homeassistant/components/qingping/device.py b/homeassistant/components/qingping/device.py new file mode 100644 index 00000000000..4e4f29b8db8 --- /dev/null +++ b/homeassistant/components/qingping/device.py @@ -0,0 +1,31 @@ +"""Support for Qingping devices.""" +from __future__ import annotations + +from qingping_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a qingping device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json new file mode 100644 index 00000000000..221087de8c4 --- /dev/null +++ b/homeassistant/components/qingping/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "qingping", + "name": "Qingping", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/qingping", + "bluetooth": [{ "local_name": "Qingping*" }], + "requirements": ["qingping-ble==0.2.3"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/qingping/sensor.py b/homeassistant/components/qingping/sensor.py new file mode 100644 index 00000000000..1affd320af2 --- /dev/null +++ b/homeassistant/components/qingping/sensor.py @@ -0,0 +1,173 @@ +"""Support for Qingping sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from qingping_ble import ( + SensorDeviceClass as QingpingSensorDeviceClass, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +SENSOR_DESCRIPTIONS = { + (QingpingSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + ( + QingpingSensorDeviceClass.CO2, + Units.CONCENTRATION_PARTS_PER_MILLION, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + (QingpingSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (QingpingSensorDeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + QingpingSensorDeviceClass.PM10, + Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.PM10}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + QingpingSensorDeviceClass.PM25, + Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.PM25}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + (QingpingSensorDeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + QingpingSensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + ( + QingpingSensorDeviceClass.TEMPERATURE, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Qingping BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + QingpingBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class QingpingBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a Qingping sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/qingping/strings.json b/homeassistant/components/qingping/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/qingping/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/qingping/translations/en.json b/homeassistant/components/qingping/translations/en.json new file mode 100644 index 00000000000..ebd9760c161 --- /dev/null +++ b/homeassistant/components/qingping/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Device not supported" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 2575be9aa69..39eab0fb973 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -97,6 +97,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "moat", "local_name": "Moat_S*" }, + { + "domain": "qingping", + "local_name": "Qingping*" + }, { "domain": "sensorpush", "local_name": "SensorPush*" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a20f1229a39..ce95cb66cc5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -289,6 +289,7 @@ FLOWS = { "pure_energie", "pvoutput", "pvpc_hourly_pricing", + "qingping", "qnap_qsw", "rachio", "radio_browser", diff --git a/requirements_all.txt b/requirements_all.txt index 85915ca6b9c..089903ea467 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2063,6 +2063,9 @@ pyzbar==0.1.7 # homeassistant.components.zerproc pyzerproc==0.4.8 +# homeassistant.components.qingping +qingping-ble==0.2.3 + # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17dfb974aaf..c69121c1a8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1405,6 +1405,9 @@ pyws66i==1.1 # homeassistant.components.zerproc pyzerproc==0.4.8 +# homeassistant.components.qingping +qingping-ble==0.2.3 + # homeassistant.components.rachio rachiopy==1.0.3 diff --git a/tests/components/qingping/__init__.py b/tests/components/qingping/__init__.py new file mode 100644 index 00000000000..66d706463be --- /dev/null +++ b/tests/components/qingping/__init__.py @@ -0,0 +1,40 @@ +"""Tests for the Qingping integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_QINGPING_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +LIGHT_AND_SIGNAL_SERVICE_INFO = BluetoothServiceInfo( + name="Qingping Motion & Light", + manufacturer_data={}, + service_uuids=[], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={ + "0000fdcd-0000-1000-8000-00805f9b34fb": b"H\x12" + b"\xcd\xd5`4-X\x08\x04\x00\r\x00\x00\x0f\x01\xee" + }, + source="local", +) + + +NO_DATA_SERVICE_INFO = BluetoothServiceInfo( + name="Qingping Motion & Light", + manufacturer_data={}, + service_uuids=[], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={ + "0000fdcd-0000-1000-8000-00805f9b34fb": b"0X\x83\n\x02\xcd\xd5`4-X\x08" + }, + source="local", +) diff --git a/tests/components/qingping/conftest.py b/tests/components/qingping/conftest.py new file mode 100644 index 00000000000..e74bf38b26d --- /dev/null +++ b/tests/components/qingping/conftest.py @@ -0,0 +1,8 @@ +"""Qingping session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/qingping/test_binary_sensor.py b/tests/components/qingping/test_binary_sensor.py new file mode 100644 index 00000000000..54e863cd158 --- /dev/null +++ b/tests/components/qingping/test_binary_sensor.py @@ -0,0 +1,46 @@ +"""Test the Qingping binary sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.qingping.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME + +from . import LIGHT_AND_SIGNAL_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_binary_sensors(hass): + """Test setting up creates the binary sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("binary_sensor")) == 0 + saved_callback(LIGHT_AND_SIGNAL_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all("binary_sensor")) == 1 + + motion_sensor = hass.states.get("binary_sensor.motion_light_eeff_motion") + assert motion_sensor.state == "off" + assert motion_sensor.attributes[ATTR_FRIENDLY_NAME] == "Motion & Light EEFF Motion" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/qingping/test_config_flow.py b/tests/components/qingping/test_config_flow.py new file mode 100644 index 00000000000..a68b7c050fc --- /dev/null +++ b/tests/components/qingping/test_config_flow.py @@ -0,0 +1,234 @@ +"""Test the Qingping config flow.""" + +import asyncio +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.qingping.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + LIGHT_AND_SIGNAL_SERVICE_INFO, + NO_DATA_SERVICE_INFO, + NOT_QINGPING_SERVICE_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Motion & Light EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_enough_info_at_start(hass): + """Test discovery via bluetooth with only a partial adv at the start.""" + with patch( + "homeassistant.components.qingping.config_flow.async_process_advertisements", + return_value=LIGHT_AND_SIGNAL_SERVICE_INFO, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NO_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Qingping Motion & Light" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_qingping(hass): + """Test discovery via bluetooth not qingping.""" + with patch( + "homeassistant.components.qingping.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_QINGPING_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.qingping.config_flow.async_discovered_service_info", + return_value=[LIGHT_AND_SIGNAL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Motion & Light EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.qingping.config_flow.async_discovered_service_info", + return_value=[LIGHT_AND_SIGNAL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.qingping.config_flow.async_discovered_service_info", + return_value=[LIGHT_AND_SIGNAL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.qingping.config_flow.async_discovered_service_info", + return_value=[LIGHT_AND_SIGNAL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Motion & Light EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/qingping/test_sensor.py b/tests/components/qingping/test_sensor.py new file mode 100644 index 00000000000..0f7e7c6a58e --- /dev/null +++ b/tests/components/qingping/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Qingping sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.qingping.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import LIGHT_AND_SIGNAL_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + saved_callback(LIGHT_AND_SIGNAL_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 1 + + lux_sensor = hass.states.get("sensor.motion_light_eeff_illuminance") + lux_sensor_attrs = lux_sensor.attributes + assert lux_sensor.state == "13" + assert lux_sensor_attrs[ATTR_FRIENDLY_NAME] == "Motion & Light EEFF Illuminance" + assert lux_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert lux_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 5f827f4ca64067ca886e0e4dcb0d1498689e1ded Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Aug 2022 21:56:57 -1000 Subject: [PATCH 347/903] Bump aiohomekit to 1.2.10 (#76738) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 4bd9a0b70f9..49635dd797c 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.9"], + "requirements": ["aiohomekit==1.2.10"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 089903ea467..f11c78be101 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.9 +aiohomekit==1.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c69121c1a8a..fd28a98e9ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.9 +aiohomekit==1.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http From 7fc2d9e087a906ef1121c121ffc2d69f86ddec72 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 14 Aug 2022 16:57:25 -0400 Subject: [PATCH 348/903] Persist previous mic/record values for UniFi Protect privacy mode (#76472) --- .../components/unifiprotect/switch.py | 140 +++++++++++++----- tests/components/unifiprotect/test_switch.py | 41 ++++- 2 files changed, 133 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 5bc4e1f17eb..e0ddddd4c53 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -2,22 +2,23 @@ from __future__ import annotations from dataclasses import dataclass -import logging from typing import Any from pyunifiprotect.data import ( Camera, ProtectAdoptableDeviceModel, + ProtectModelWithId, RecordingMode, VideoMode, ) from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData @@ -25,7 +26,8 @@ from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd -_LOGGER = logging.getLogger(__name__) +ATTR_PREV_MIC = "prev_mic_level" +ATTR_PREV_RECORD = "prev_record_mode" @dataclass @@ -35,9 +37,6 @@ class ProtectSwitchEntityDescription( """Describes UniFi Protect Switch entity.""" -_KEY_PRIVACY_MODE = "privacy_mode" - - async def _set_highfps(obj: Camera, value: bool) -> None: if value: await obj.set_video_mode(VideoMode.HIGH_FPS) @@ -86,15 +85,6 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_set_method_fn=_set_highfps, ufp_perm=PermRequired.WRITE, ), - ProtectSwitchEntityDescription( - key=_KEY_PRIVACY_MODE, - name="Privacy Mode", - icon="mdi:eye-settings", - entity_category=EntityCategory.CONFIG, - ufp_required_field="feature_flags.has_privacy_mask", - ufp_value="is_privacy_on", - ufp_perm=PermRequired.WRITE, - ), ProtectSwitchEntityDescription( key="system_sounds", name="System Sounds", @@ -192,6 +182,16 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ) +PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera]( + key="privacy_mode", + name="Privacy Mode", + icon="mdi:eye-settings", + entity_category=EntityCategory.CONFIG, + ufp_required_field="feature_flags.has_privacy_mask", + ufp_value="is_privacy_on", + ufp_perm=PermRequired.WRITE, +) + SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="status_light", @@ -316,6 +316,11 @@ async def async_setup_entry( viewer_descs=VIEWER_SWITCHES, ufp_device=device, ) + entities += async_all_device_entities( + data, + ProtectPrivacyModeSwitch, + camera_descs=[PRIVACY_MODE_SWITCH], + ) async_add_entities(entities) entry.async_on_unload( @@ -331,6 +336,11 @@ async def async_setup_entry( lock_descs=DOORLOCK_SWITCHES, viewer_descs=VIEWER_SWITCHES, ) + entities += async_all_device_entities( + data, + ProtectPrivacyModeSwitch, + camera_descs=[PRIVACY_MODE_SWITCH], + ) async_add_entities(entities) @@ -350,17 +360,6 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): self._attr_name = f"{self.device.display_name} {self.entity_description.name}" self._switch_type = self.entity_description.key - if not isinstance(self.device, Camera): - return - - if self.entity_description.key == _KEY_PRIVACY_MODE: - if self.device.is_privacy_on: - self._previous_mic_level = 100 - self._previous_record_mode = RecordingMode.ALWAYS - else: - self._previous_mic_level = self.device.mic_volume - self._previous_record_mode = self.device.recording_settings.mode - @property def is_on(self) -> bool: """Return true if device is on.""" @@ -368,24 +367,83 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - if self._switch_type == _KEY_PRIVACY_MODE: - assert isinstance(self.device, Camera) - self._previous_mic_level = self.device.mic_volume - self._previous_record_mode = self.device.recording_settings.mode - await self.device.set_privacy(True, 0, RecordingMode.NEVER) - else: - await self.entity_description.ufp_set(self.device, True) + + await self.entity_description.ufp_set(self.device, True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - if self._switch_type == _KEY_PRIVACY_MODE: - assert isinstance(self.device, Camera) - _LOGGER.debug( - "Setting Privacy Mode to false for %s", self.device.display_name - ) - await self.device.set_privacy( - False, self._previous_mic_level, self._previous_record_mode + await self.entity_description.ufp_set(self.device, False) + + +class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): + """A UniFi Protect Switch.""" + + device: Camera + + def __init__( + self, + data: ProtectData, + device: ProtectAdoptableDeviceModel, + description: ProtectSwitchEntityDescription, + ) -> None: + """Initialize an UniFi Protect Switch.""" + super().__init__(data, device, description) + + if self.device.is_privacy_on: + extra_state = self.extra_state_attributes or {} + self._previous_mic_level = extra_state.get(ATTR_PREV_MIC, 100) + self._previous_record_mode = extra_state.get( + ATTR_PREV_RECORD, RecordingMode.ALWAYS ) else: - await self.entity_description.ufp_set(self.device, False) + self._previous_mic_level = self.device.mic_volume + self._previous_record_mode = self.device.recording_settings.mode + + @callback + def _update_previous_attr(self) -> None: + if self.is_on: + self._attr_extra_state_attributes = { + ATTR_PREV_MIC: self._previous_mic_level, + ATTR_PREV_RECORD: self._previous_record_mode, + } + else: + self._attr_extra_state_attributes = {} + + @callback + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) + + # do not add extra state attribute on initialize + if self.entity_id: + self._update_previous_attr() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + + self._previous_mic_level = self.device.mic_volume + self._previous_record_mode = self.device.recording_settings.mode + await self.device.set_privacy(True, 0, RecordingMode.NEVER) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + + extra_state = self.extra_state_attributes or {} + prev_mic = extra_state.get(ATTR_PREV_MIC, self._previous_mic_level) + prev_record = extra_state.get(ATTR_PREV_RECORD, self._previous_record_mode) + await self.device.set_privacy(False, prev_mic, prev_record) + + async def async_added_to_hass(self) -> None: + """Restore extra state attributes on startp up.""" + await super().async_added_to_hass() + + if not (last_state := await self.async_get_last_state()): + return + + self._previous_mic_level = last_state.attributes.get( + ATTR_PREV_MIC, self._previous_mic_level + ) + self._previous_record_mode = last_state.attributes.get( + ATTR_PREV_RECORD, self._previous_record_mode + ) + self._update_previous_attr() diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 684e3b8e441..6fa718c4952 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -9,8 +9,11 @@ from pyunifiprotect.data import Camera, Light, Permission, RecordingMode, VideoM from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.switch import ( + ATTR_PREV_MIC, + ATTR_PREV_RECORD, CAMERA_SWITCHES, LIGHT_SWITCHES, + PRIVACY_MODE_SWITCH, ProtectSwitchEntityDescription, ) from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_OFF, Platform @@ -347,31 +350,55 @@ async def test_switch_camera_highfps( async def test_switch_camera_privacy( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): - """Tests Privacy Mode switch for cameras.""" + """Tests Privacy Mode switch for cameras with privacy mode defaulted on.""" + + previous_mic = doorbell.mic_volume = 53 + previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 13, 12) - description = CAMERA_SWITCHES[4] + description = PRIVACY_MODE_SWITCH doorbell.__fields__["set_privacy"] = Mock() doorbell.set_privacy = AsyncMock() _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) + state = hass.states.get(entity_id) + assert state and state.state == "off" + assert ATTR_PREV_MIC not in state.attributes + assert ATTR_PREV_RECORD not in state.attributes + await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - doorbell.set_privacy.assert_called_once_with(True, 0, RecordingMode.NEVER) + doorbell.set_privacy.assert_called_with(True, 0, RecordingMode.NEVER) + + new_doorbell = doorbell.copy() + new_doorbell.add_privacy_zone() + new_doorbell.mic_volume = 0 + new_doorbell.recording_settings.mode = RecordingMode.NEVER + ufp.api.bootstrap.cameras = {new_doorbell.id: new_doorbell} + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = new_doorbell + ufp.ws_msg(mock_msg) + + state = hass.states.get(entity_id) + assert state and state.state == "on" + assert state.attributes[ATTR_PREV_MIC] == previous_mic + assert state.attributes[ATTR_PREV_RECORD] == previous_record.value + + doorbell.set_privacy.reset_mock() await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - doorbell.set_privacy.assert_called_with( - False, doorbell.mic_volume, doorbell.recording_settings.mode - ) + doorbell.set_privacy.assert_called_with(False, previous_mic, previous_record) async def test_switch_camera_privacy_already_on( @@ -383,7 +410,7 @@ async def test_switch_camera_privacy_already_on( await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 13, 12) - description = CAMERA_SWITCHES[4] + description = PRIVACY_MODE_SWITCH doorbell.__fields__["set_privacy"] = Mock() doorbell.set_privacy = AsyncMock() From aadecdf6cb60642fe11e7b9d1c0d5c07e9aba01a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 14 Aug 2022 23:54:25 +0200 Subject: [PATCH 349/903] Add type hints to MediaPlayerEntity (#76743) * Add media-player checks to pylint plugin * Fix invalid hints * Add tests * Adjust tests * Add extra test * Adjust regex * Cleanup comment * Revert * Revert * Update homeassistant/components/media_player/__init__.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Update homeassistant/components/denonavr/media_player.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/denonavr/media_player.py | 2 +- .../components/group/media_player.py | 2 +- .../components/media_player/__init__.py | 158 ++++++++++-------- .../components/webostv/media_player.py | 2 +- 4 files changed, 95 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 16814b72bc7..7c5d98ca1b3 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -448,7 +448,7 @@ class DenonDevice(MediaPlayerEntity): await self._receiver.async_volume_down() @async_log_errors - async def async_set_volume_level(self, volume: int): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" # Volume has to be sent in a format like -50.0. Minimum is -80.0, # maximum is 18.0 diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index e0cbf84a693..60cb37f46ba 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -277,7 +277,7 @@ class MediaPlayerGroup(MediaPlayerEntity): context=self._context, ) - async def async_media_seek(self, position: int) -> None: + async def async_media_seek(self, position: float) -> None: """Send seek command.""" data = { ATTR_ENTITY_ID: self._features[KEY_SEEK], diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 9ca9613278d..29c75a4fc22 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -677,213 +677,231 @@ class MediaPlayerEntity(Entity): """Flag media player features that are supported.""" return self._attr_supported_features - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" raise NotImplementedError() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" await self.hass.async_add_executor_job(self.turn_on) - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" raise NotImplementedError() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self.hass.async_add_executor_job(self.turn_off) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute the volume.""" raise NotImplementedError() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" await self.hass.async_add_executor_job(self.mute_volume, mute) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" raise NotImplementedError() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self.hass.async_add_executor_job(self.set_volume_level, volume) - def media_play(self): + def media_play(self) -> None: """Send play command.""" raise NotImplementedError() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self.hass.async_add_executor_job(self.media_play) - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" raise NotImplementedError() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self.hass.async_add_executor_job(self.media_pause) - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" raise NotImplementedError() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" await self.hass.async_add_executor_job(self.media_stop) - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" raise NotImplementedError() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self.hass.async_add_executor_job(self.media_previous_track) - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" raise NotImplementedError() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self.hass.async_add_executor_job(self.media_next_track) - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Send seek command.""" raise NotImplementedError() - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" await self.hass.async_add_executor_job(self.media_seek, position) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play a piece of media.""" raise NotImplementedError() - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" await self.hass.async_add_executor_job( ft.partial(self.play_media, media_type, media_id, **kwargs) ) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" raise NotImplementedError() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" await self.hass.async_add_executor_job(self.select_source, source) - def select_sound_mode(self, sound_mode): + def select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" raise NotImplementedError() - async def async_select_sound_mode(self, sound_mode): + async def async_select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" await self.hass.async_add_executor_job(self.select_sound_mode, sound_mode) - def clear_playlist(self): + def clear_playlist(self) -> None: """Clear players playlist.""" raise NotImplementedError() - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" await self.hass.async_add_executor_job(self.clear_playlist) - def set_shuffle(self, shuffle): + def set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" raise NotImplementedError() - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" await self.hass.async_add_executor_job(self.set_shuffle, shuffle) - def set_repeat(self, repeat): + def set_repeat(self, repeat: str) -> None: """Set repeat mode.""" raise NotImplementedError() - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: str) -> None: """Set repeat mode.""" await self.hass.async_add_executor_job(self.set_repeat, repeat) # No need to overwrite these. + @final @property - def support_play(self): + def support_play(self) -> bool: """Boolean if play is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.PLAY) + @final @property - def support_pause(self): + def support_pause(self) -> bool: """Boolean if pause is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.PAUSE) + @final @property - def support_stop(self): + def support_stop(self) -> bool: """Boolean if stop is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.STOP) + @final @property - def support_seek(self): + def support_seek(self) -> bool: """Boolean if seek is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.SEEK) + @final @property - def support_volume_set(self): + def support_volume_set(self) -> bool: """Boolean if setting volume is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.VOLUME_SET) + @final @property - def support_volume_mute(self): + def support_volume_mute(self) -> bool: """Boolean if muting volume is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.VOLUME_MUTE) + @final @property - def support_previous_track(self): + def support_previous_track(self) -> bool: """Boolean if previous track command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.PREVIOUS_TRACK) + @final @property - def support_next_track(self): + def support_next_track(self) -> bool: """Boolean if next track command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.NEXT_TRACK) + @final @property - def support_play_media(self): + def support_play_media(self) -> bool: """Boolean if play media command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.PLAY_MEDIA) + @final @property - def support_select_source(self): + def support_select_source(self) -> bool: """Boolean if select source command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.SELECT_SOURCE) + @final @property - def support_select_sound_mode(self): + def support_select_sound_mode(self) -> bool: """Boolean if select sound mode command supported.""" return bool( self.supported_features & MediaPlayerEntityFeature.SELECT_SOUND_MODE ) + @final @property - def support_clear_playlist(self): + def support_clear_playlist(self) -> bool: """Boolean if clear playlist command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.CLEAR_PLAYLIST) + @final @property - def support_shuffle_set(self): + def support_shuffle_set(self) -> bool: """Boolean if shuffle is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.SHUFFLE_SET) + @final @property - def support_grouping(self): + def support_grouping(self) -> bool: """Boolean if player grouping is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.GROUPING) - async def async_toggle(self): + async def async_toggle(self) -> None: """Toggle the power on the media player.""" if hasattr(self, "toggle"): - await self.hass.async_add_executor_job(self.toggle) + await self.hass.async_add_executor_job( + self.toggle # type: ignore[attr-defined] + ) return if self.state in (STATE_OFF, STATE_IDLE, STATE_STANDBY): @@ -891,40 +909,48 @@ class MediaPlayerEntity(Entity): else: await self.async_turn_off() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Turn volume up for media player. This method is a coroutine. """ if hasattr(self, "volume_up"): - await self.hass.async_add_executor_job(self.volume_up) + await self.hass.async_add_executor_job( + self.volume_up # type: ignore[attr-defined] + ) return if ( - self.volume_level < 1 + self.volume_level is not None + and self.volume_level < 1 and self.supported_features & MediaPlayerEntityFeature.VOLUME_SET ): await self.async_set_volume_level(min(1, self.volume_level + 0.1)) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Turn volume down for media player. This method is a coroutine. """ if hasattr(self, "volume_down"): - await self.hass.async_add_executor_job(self.volume_down) + await self.hass.async_add_executor_job( + self.volume_down # type: ignore[attr-defined] + ) return if ( - self.volume_level > 0 + self.volume_level is not None + and self.volume_level > 0 and self.supported_features & MediaPlayerEntityFeature.VOLUME_SET ): await self.async_set_volume_level(max(0, self.volume_level - 0.1)) - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Play or pause the media player.""" if hasattr(self, "media_play_pause"): - await self.hass.async_add_executor_job(self.media_play_pause) + await self.hass.async_add_executor_job( + self.media_play_pause # type: ignore[attr-defined] + ) return if self.state == STATE_PLAYING: @@ -933,7 +959,7 @@ class MediaPlayerEntity(Entity): await self.async_media_play() @property - def entity_picture(self): + def entity_picture(self) -> str | None: """Return image of the media playing.""" if self.state == STATE_OFF: return None @@ -944,7 +970,7 @@ class MediaPlayerEntity(Entity): return self.media_image_local @property - def media_image_local(self): + def media_image_local(self) -> str | None: """Return local url to media image.""" if (image_hash := self.media_image_hash) is None: return None @@ -955,10 +981,10 @@ class MediaPlayerEntity(Entity): ) @property - def capability_attributes(self): + def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" supported_features = self.supported_features or 0 - data = {} + data: dict[str, Any] = {} if supported_features & MediaPlayerEntityFeature.SELECT_SOURCE and ( source_list := self.source_list @@ -974,9 +1000,9 @@ class MediaPlayerEntity(Entity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - state_attr = {} + state_attr: dict[str, Any] = {} if self.support_grouping: state_attr[ATTR_GROUP_MEMBERS] = self.group_members @@ -1005,19 +1031,19 @@ class MediaPlayerEntity(Entity): """ raise NotImplementedError() - def join_players(self, group_members): + def join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" raise NotImplementedError() - async def async_join_players(self, group_members): + async def async_join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" await self.hass.async_add_executor_job(self.join_players, group_members) - def unjoin_player(self): + def unjoin_player(self) -> None: """Remove this player from any group.""" raise NotImplementedError() - async def async_unjoin_player(self): + async def async_unjoin_player(self) -> None: """Remove this player from any group.""" await self.hass.async_add_executor_job(self.unjoin_player) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 3806ee6c2bb..36941b15240 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -340,7 +340,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): await self._client.volume_down() @cmd - async def async_set_volume_level(self, volume: int) -> None: + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" tv_volume = int(round(volume * 100)) await self._client.set_volume(tv_volume) From 8a85881ca076082e9df0777a0aabe219638866d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 19:53:27 -1000 Subject: [PATCH 350/903] Bump pySwitchbot to 0.18.10 to handle empty data and disconnects (#76684) * Bump pySwitchbot to 0.18.7 to handle empty data Fixes #76621 * bump again * bump * bump for rssi on disconnect logging --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index e26100108c9..e70f467ae74 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.6"], + "requirements": ["PySwitchbot==0.18.10"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f11c78be101..1e7db64277c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.6 +PySwitchbot==0.18.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd28a98e9ea..10e88fcc1ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.6 +PySwitchbot==0.18.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 4f9e6d240771680ff7ab7248163cb6fbf5d7dba7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Aug 2022 08:23:05 +0200 Subject: [PATCH 351/903] Improve vacuum type hints (#76747) * Improve vacuum type hints * Black * Black * Adjust * One more --- homeassistant/components/vacuum/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 74636e82e69..24d4718540e 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -114,7 +114,7 @@ SUPPORT_START = 8192 @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the vacuum is on based on the statemachine.""" return hass.states.is_state(entity_id, STATE_ON) @@ -286,13 +286,19 @@ class _BaseVacuum(Entity): ) def send_command( - self, command: str, params: dict | list | None = None, **kwargs: Any + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, ) -> None: """Send a command to a vacuum cleaner.""" raise NotImplementedError() async def async_send_command( - self, command: str, params: dict | list | None = None, **kwargs: Any + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, ) -> None: """Send a command to a vacuum cleaner. From f72cfef7be56d2e5561fa711273a3837f1ea5646 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 15 Aug 2022 08:26:17 +0200 Subject: [PATCH 352/903] Fix MQTT camera encoding (#76124) * Fix MQTT camera encoding * Reduce code * Add test for using image_encoding parameter * Move deprecation check to validation * Dependency * Set correct strings and log warning * Rename constant * Use better issue string identifier * Revert unwanted change to hassio test * Avoid term `deprecated` in issue description * Revert changes using the repairs API * Add a notice when work-a-round will be removed * Update homeassistant/components/mqtt/camera.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/camera.py | 42 +++++++++-- tests/components/mqtt/test_camera.py | 74 +++++++++++++++++++ 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index ddbced5286d..32b67874a45 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -80,6 +80,7 @@ ABBREVIATIONS = { "hs_stat_t": "hs_state_topic", "hs_val_tpl": "hs_value_template", "ic": "icon", + "img_e": "image_encoding", "init": "initial", "hum_cmd_t": "target_humidity_command_topic", "hum_cmd_tpl": "target_humidity_command_template", diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index f213bec9bb6..61c87e86888 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -3,6 +3,7 @@ from __future__ import annotations from base64 import b64decode import functools +import logging import voluptuous as vol @@ -17,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC +from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_ENCODING from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, @@ -29,6 +30,10 @@ from .mixins import ( ) from .util import valid_subscribe_topic +_LOGGER = logging.getLogger(__name__) + +CONF_IMAGE_ENCODING = "image_encoding" + DEFAULT_NAME = "MQTT Camera" MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( @@ -40,20 +45,41 @@ MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( + +# Using CONF_ENCODING to set b64 encoding for images is deprecated as of Home Assistant 2022.9 +# use CONF_IMAGE_ENCODING instead, support for the work-a-round will be removed with Home Assistant 2022.11 +def repair_legacy_encoding(config: ConfigType) -> ConfigType: + """Check incorrect deprecated config of image encoding.""" + if config[CONF_ENCODING] == "b64": + config[CONF_IMAGE_ENCODING] = "b64" + config[CONF_ENCODING] = DEFAULT_ENCODING + _LOGGER.warning( + "Using the `encoding` parameter to set image encoding has been deprecated, use `image_encoding` instead" + ) + return config + + +PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_IMAGE_ENCODING): "b64", } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Camera under the camera platform key is deprecated in HA Core 2022.6 -PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), - warn_for_legacy_schema(camera.DOMAIN), +PLATFORM_SCHEMA_MODERN = vol.All( + PLATFORM_SCHEMA_BASE.schema, + repair_legacy_encoding, ) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Camera under the camera platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_BASE.schema), + warn_for_legacy_schema(camera.DOMAIN), + repair_legacy_encoding, +) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_platform( @@ -124,7 +150,7 @@ class MqttCamera(MqttEntity, Camera): @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" - if self._config[CONF_ENCODING] == "b64": + if CONF_IMAGE_ENCODING in self._config: self._last_image = b64decode(msg.payload) else: self._last_image = msg.payload diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index c6d116b6a74..a76025a608a 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -110,6 +110,80 @@ async def test_run_camera_b64_encoded( assert body == "grass" +# Using CONF_ENCODING to set b64 encoding for images is deprecated Home Assistant 2022.9, use CONF_IMAGE_ENCODING instead +async def test_legacy_camera_b64_encoded_with_availability( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): + """Test availability works if b64 encoding (legacy mode) is turned on.""" + topic = "test/camera" + topic_availability = "test/camera_availability" + await async_setup_component( + hass, + "camera", + { + "camera": { + "platform": "mqtt", + "topic": topic, + "name": "Test Camera", + "encoding": "b64", + "availability": {"topic": topic_availability}, + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + # Make sure we are available + async_fire_mqtt_message(hass, topic_availability, "online") + + url = hass.states.get("camera.test_camera").attributes["entity_picture"] + + async_fire_mqtt_message(hass, topic, b64encode(b"grass")) + + client = await hass_client_no_auth() + resp = await client.get(url) + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == "grass" + + +async def test_camera_b64_encoded_with_availability( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): + """Test availability works if b64 encoding is turned on.""" + topic = "test/camera" + topic_availability = "test/camera_availability" + await async_setup_component( + hass, + "camera", + { + "camera": { + "platform": "mqtt", + "topic": topic, + "name": "Test Camera", + "encoding": "utf-8", + "image_encoding": "b64", + "availability": {"topic": topic_availability}, + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + # Make sure we are available + async_fire_mqtt_message(hass, topic_availability, "online") + + url = hass.states.get("camera.test_camera").attributes["entity_picture"] + + async_fire_mqtt_message(hass, topic, b64encode(b"grass")) + + client = await hass_client_no_auth() + resp = await client.get(url) + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == "grass" + + async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config ): From 161e533c5f0a5225327ff0710fa7eb2ac2a704ec Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 15 Aug 2022 08:27:37 +0200 Subject: [PATCH 353/903] Remove MQTT climate support for hold and away modes (#76299) Remove support for hold and away modes --- homeassistant/components/mqtt/climate.py | 191 ++---------- tests/components/mqtt/test_climate.py | 359 ----------------------- 2 files changed, 23 insertions(+), 527 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index bf53544b491..f44cf6fe8fc 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -19,7 +19,6 @@ from homeassistant.components.climate.const import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, - PRESET_AWAY, PRESET_NONE, SWING_OFF, SWING_ON, @@ -68,10 +67,11 @@ CONF_ACTION_TOPIC = "action_topic" CONF_AUX_COMMAND_TOPIC = "aux_command_topic" CONF_AUX_STATE_TEMPLATE = "aux_state_template" CONF_AUX_STATE_TOPIC = "aux_state_topic" -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 +# AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 CONF_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic" CONF_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template" CONF_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic" + CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template" CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic" CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" @@ -79,12 +79,13 @@ CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" CONF_FAN_MODE_LIST = "fan_modes" CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 +# AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 CONF_HOLD_COMMAND_TEMPLATE = "hold_command_template" CONF_HOLD_COMMAND_TOPIC = "hold_command_topic" CONF_HOLD_STATE_TEMPLATE = "hold_state_template" CONF_HOLD_STATE_TOPIC = "hold_state_topic" CONF_HOLD_LIST = "hold_modes" + CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_LIST = "modes" @@ -150,12 +151,8 @@ MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( VALUE_TEMPLATE_KEYS = ( CONF_AUX_STATE_TEMPLATE, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_AWAY_MODE_STATE_TEMPLATE, CONF_CURRENT_TEMP_TEMPLATE, CONF_FAN_MODE_STATE_TEMPLATE, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_HOLD_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, CONF_POWER_STATE_TEMPLATE, CONF_ACTION_TEMPLATE, @@ -168,8 +165,6 @@ VALUE_TEMPLATE_KEYS = ( COMMAND_TEMPLATE_KEYS = { CONF_FAN_MODE_COMMAND_TEMPLATE, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_HOLD_COMMAND_TEMPLATE, CONF_MODE_COMMAND_TEMPLATE, CONF_PRESET_MODE_COMMAND_TEMPLATE, CONF_SWING_MODE_COMMAND_TEMPLATE, @@ -178,32 +173,14 @@ COMMAND_TEMPLATE_KEYS = { CONF_TEMP_LOW_COMMAND_TEMPLATE, } -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -DEPRECATED_INVALID = [ - CONF_AWAY_MODE_COMMAND_TOPIC, - CONF_AWAY_MODE_STATE_TEMPLATE, - CONF_AWAY_MODE_STATE_TOPIC, - CONF_HOLD_COMMAND_TEMPLATE, - CONF_HOLD_COMMAND_TOPIC, - CONF_HOLD_STATE_TEMPLATE, - CONF_HOLD_STATE_TOPIC, - CONF_HOLD_LIST, -] - TOPIC_KEYS = ( CONF_ACTION_TOPIC, CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_AWAY_MODE_COMMAND_TOPIC, - CONF_AWAY_MODE_STATE_TOPIC, CONF_CURRENT_TEMP_TOPIC, CONF_FAN_MODE_COMMAND_TOPIC, CONF_FAN_MODE_STATE_TOPIC, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_HOLD_COMMAND_TOPIC, - CONF_HOLD_STATE_TOPIC, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, CONF_POWER_COMMAND_TOPIC, @@ -225,12 +202,6 @@ def valid_preset_mode_configuration(config): """Validate that the preset mode reset payload is not one of the preset modes.""" if PRESET_NONE in config.get(CONF_PRESET_MODES_LIST): raise ValueError("preset_modes must not include preset mode 'none'") - if config.get(CONF_PRESET_MODE_COMMAND_TOPIC): - for config_parameter in DEPRECATED_INVALID: - if config.get(config_parameter): - raise vol.MultipleInvalid( - "preset_modes cannot be used with deprecated away or hold mode config options" - ) return config @@ -239,10 +210,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic, vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template, @@ -253,12 +220,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_HOLD_LIST): cv.ensure_list, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( @@ -334,15 +295,15 @@ PLATFORM_SCHEMA = vol.All( cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), - cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), - cv.deprecated(CONF_AWAY_MODE_STATE_TOPIC), - cv.deprecated(CONF_HOLD_COMMAND_TEMPLATE), - cv.deprecated(CONF_HOLD_COMMAND_TOPIC), - cv.deprecated(CONF_HOLD_STATE_TEMPLATE), - cv.deprecated(CONF_HOLD_STATE_TOPIC), - cv.deprecated(CONF_HOLD_LIST), + # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 + cv.removed(CONF_AWAY_MODE_COMMAND_TOPIC), + cv.removed(CONF_AWAY_MODE_STATE_TEMPLATE), + cv.removed(CONF_AWAY_MODE_STATE_TOPIC), + cv.removed(CONF_HOLD_COMMAND_TEMPLATE), + cv.removed(CONF_HOLD_COMMAND_TOPIC), + cv.removed(CONF_HOLD_STATE_TEMPLATE), + cv.removed(CONF_HOLD_STATE_TOPIC), + cv.removed(CONF_HOLD_LIST), valid_preset_mode_configuration, warn_for_legacy_schema(climate.DOMAIN), ) @@ -353,15 +314,15 @@ DISCOVERY_SCHEMA = vol.All( _DISCOVERY_SCHEMA_BASE, # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), - cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), - cv.deprecated(CONF_AWAY_MODE_STATE_TOPIC), - cv.deprecated(CONF_HOLD_COMMAND_TEMPLATE), - cv.deprecated(CONF_HOLD_COMMAND_TOPIC), - cv.deprecated(CONF_HOLD_STATE_TEMPLATE), - cv.deprecated(CONF_HOLD_STATE_TOPIC), - cv.deprecated(CONF_HOLD_LIST), + # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 + cv.removed(CONF_AWAY_MODE_COMMAND_TOPIC), + cv.removed(CONF_AWAY_MODE_STATE_TEMPLATE), + cv.removed(CONF_AWAY_MODE_STATE_TOPIC), + cv.removed(CONF_HOLD_COMMAND_TEMPLATE), + cv.removed(CONF_HOLD_COMMAND_TOPIC), + cv.removed(CONF_HOLD_STATE_TEMPLATE), + cv.removed(CONF_HOLD_STATE_TOPIC), + cv.removed(CONF_HOLD_LIST), valid_preset_mode_configuration, ) @@ -419,12 +380,10 @@ class MqttClimate(MqttEntity, ClimateEntity): """Initialize the climate device.""" self._action = None self._aux = False - self._away = False self._current_fan_mode = None self._current_operation = None self._current_swing_mode = None self._current_temp = None - self._hold = None self._preset_mode = None self._target_temp = None self._target_temp_high = None @@ -435,10 +394,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._feature_preset_mode = False self._optimistic_preset_mode = None - # AWAY and HOLD mode topics and templates are deprecated, - # support will be removed with release 2022.9 - self._hold_list = [] - MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -477,9 +432,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._preset_modes = [] self._optimistic_preset_mode = CONF_PRESET_MODE_STATE_TOPIC not in config self._action = None - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - self._away = False - self._hold = None self._aux = False value_templates = {} @@ -507,11 +459,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._command_templates = command_templates - # AWAY and HOLD mode topics and templates are deprecated, - # support will be removed with release 2022.9 - if CONF_HOLD_LIST in config: - self._hold_list = config[CONF_HOLD_LIST] - def _prepare_subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -682,15 +629,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self.async_write_ha_state() - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - @callback - @log_messages(self.hass, self.entity_id) - def handle_away_mode_received(msg): - """Handle receiving away mode via MQTT.""" - handle_onoff_mode_received(msg, CONF_AWAY_MODE_STATE_TEMPLATE, "_away") - - add_subscription(topics, CONF_AWAY_MODE_STATE_TOPIC, handle_away_mode_received) - @callback @log_messages(self.hass, self.entity_id) def handle_aux_mode_received(msg): @@ -699,22 +637,6 @@ class MqttClimate(MqttEntity, ClimateEntity): add_subscription(topics, CONF_AUX_STATE_TOPIC, handle_aux_mode_received) - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - @callback - @log_messages(self.hass, self.entity_id) - def handle_hold_mode_received(msg): - """Handle receiving hold mode via MQTT.""" - payload = render_template(msg, CONF_HOLD_STATE_TEMPLATE) - - if payload == "off": - payload = None - - self._hold = payload - self._preset_mode = None - self.async_write_ha_state() - - add_subscription(topics, CONF_HOLD_STATE_TOPIC, handle_hold_mode_received) - @callback @log_messages(self.hass, self.entity_id) def handle_preset_mode_received(msg): @@ -802,11 +724,6 @@ class MqttClimate(MqttEntity, ClimateEntity): """Return preset mode.""" if self._feature_preset_mode and self._preset_mode is not None: return self._preset_mode - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - if self._hold: - return self._hold - if self._away: - return PRESET_AWAY return PRESET_NONE @property @@ -814,17 +731,6 @@ class MqttClimate(MqttEntity, ClimateEntity): """Return preset modes.""" presets = [] presets.extend(self._preset_modes) - - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - if (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) or ( - self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None - ): - presets.append(PRESET_AWAY) - - # AWAY and HOLD mode topics and templates are deprecated, - # support will be removed with release 2022.9 - presets.extend(self._hold_list) - if presets: presets.insert(0, PRESET_NONE) @@ -895,7 +801,6 @@ class MqttClimate(MqttEntity, ClimateEntity): "_target_temp_high", ) - # Always optimistic? self.async_write_ha_state() async def async_set_swing_mode(self, swing_mode: str) -> None: @@ -962,49 +867,6 @@ class MqttClimate(MqttEntity, ClimateEntity): return - # Update hold or away mode: Track if we should optimistic update the state - optimistic_update = await self._set_away_mode(preset_mode == PRESET_AWAY) - hold_mode: str | None = preset_mode - if preset_mode in [PRESET_NONE, PRESET_AWAY]: - hold_mode = None - optimistic_update = await self._set_hold_mode(hold_mode) or optimistic_update - - if optimistic_update: - self.async_write_ha_state() - - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - async def _set_away_mode(self, state): - """Set away mode. - - Returns if we should optimistically write the state. - """ - await self._publish( - CONF_AWAY_MODE_COMMAND_TOPIC, - self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], - ) - - if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None: - return False - - self._away = state - return True - - async def _set_hold_mode(self, hold_mode): - """Set hold mode. - - Returns if we should optimistically write the state. - """ - payload = self._command_templates[CONF_HOLD_COMMAND_TEMPLATE]( - hold_mode or "off" - ) - await self._publish(CONF_HOLD_COMMAND_TOPIC, payload) - - if self._topic[CONF_HOLD_STATE_TOPIC] is not None: - return False - - self._hold = hold_mode - return True - async def _set_aux_heat(self, state): await self._publish( CONF_AUX_COMMAND_TOPIC, @@ -1053,14 +915,7 @@ class MqttClimate(MqttEntity, ClimateEntity): ): support |= ClimateEntityFeature.SWING_MODE - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - if ( - self._feature_preset_mode - or (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) - or (self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None) - or (self._topic[CONF_HOLD_STATE_TOPIC] is not None) - or (self._topic[CONF_HOLD_COMMAND_TOPIC] is not None) - ): + if self._feature_preset_mode: support |= ClimateEntityFeature.PRESET_MODE if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or ( diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 679f853a3a8..d5164e85718 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -13,15 +13,12 @@ from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HVAC_ACTION, - ATTR_PRESET_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_ACTIONS, DOMAIN as CLIMATE_DOMAIN, - PRESET_AWAY, PRESET_ECO, - PRESET_NONE, ClimateEntityFeature, HVACMode, ) @@ -89,23 +86,6 @@ DEFAULT_CONFIG = { } } -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -DEFAULT_LEGACY_CONFIG = { - CLIMATE_DOMAIN: { - "platform": "mqtt", - "name": "test", - "mode_command_topic": "mode-topic", - "temperature_command_topic": "temperature-topic", - "temperature_low_command_topic": "temperature-low-topic", - "temperature_high_command_topic": "temperature-high-topic", - "fan_mode_command_topic": "fan-mode-topic", - "swing_mode_command_topic": "swing-mode-topic", - "aux_command_topic": "aux-topic", - "away_mode_command_topic": "away-mode-topic", - "hold_command_topic": "hold-topic", - } -} - @pytest.fixture(autouse=True) def climate_platform_only(): @@ -654,241 +634,6 @@ async def test_set_preset_mode_pessimistic( assert state.attributes.get("preset_mode") == "home" -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): - """Test setting of the away mode.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["away_mode_state_topic"] = "away-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - async_fire_mqtt_message(hass, "away-state", "ON") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - async_fire_mqtt_message(hass, "away-state", "OFF") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - async_fire_mqtt_message(hass, "away-state", "nonsense") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode(hass, mqtt_mock_entry_with_yaml_config): - """Test setting of the away mode.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["payload_on"] = "AN" - config["climate"]["payload_off"] = "AUS" - - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - mqtt_mock.async_publish.reset_mock() - await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "AN", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - await common.async_set_preset_mode(hass, PRESET_NONE, ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "AUS", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.reset_mock() - - await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "AN", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold_pessimistic(hass, mqtt_mock_entry_with_yaml_config): - """Test setting the hold mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["hold_state_topic"] = "hold-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("hold_mode") is None - - await common.async_set_preset_mode(hass, "hold", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("hold_mode") is None - - async_fire_mqtt_message(hass, "hold-state", "on") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "on" - - async_fire_mqtt_message(hass, "hold-state", "off") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold(hass, mqtt_mock_entry_with_yaml_config): - """Test setting the hold mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "eco", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_ECO - - await common.async_set_preset_mode(hass, PRESET_NONE, ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away(hass, mqtt_mock_entry_with_yaml_config): - """Test setting the hold mode and away mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_NONE - - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - await common.async_set_preset_mode(hass, PRESET_AWAY, ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "ON", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_AWAY - - await common.async_set_preset_mode(hass, "hold-on-again", ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on-again", 0, False) - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on-again" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away_pessimistic(hass, mqtt_mock_entry_with_yaml_config): - """Test setting the hold mode and away mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["hold_state_topic"] = "hold-state" - config["climate"]["away_mode_state_topic"] = "away-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_NONE - - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_NONE - - async_fire_mqtt_message(hass, "hold-state", "hold-on") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - await common.async_set_preset_mode(hass, PRESET_AWAY, ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "ON", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - async_fire_mqtt_message(hass, "away-state", "ON") - async_fire_mqtt_message(hass, "hold-state", "off") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_AWAY - - await common.async_set_preset_mode(hass, "hold-on-again", ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on-again", 0, False) - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_AWAY - - async_fire_mqtt_message(hass, "hold-state", "hold-on-again") - async_fire_mqtt_message(hass, "away-state", "OFF") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on-again" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_mode_twice(hass, mqtt_mock_entry_with_yaml_config): - """Test setting of the same mode twice only publishes once.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - async def test_set_aux_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -1103,57 +848,6 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog ) -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_get_with_hold_and_away_mode_and_templates( - hass, mqtt_mock_entry_with_yaml_config, caplog -): - """Test getting various for hold and away mode attributes with templates.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["mode_state_topic"] = "mode-state" - # By default, just unquote the JSON-strings - config["climate"]["value_template"] = "{{ value_json }}" - # Something more complicated for hold mode - config["climate"]["hold_state_template"] = "{{ value_json.attribute }}" - config["climate"]["away_mode_state_topic"] = "away-state" - config["climate"]["hold_state_topic"] = "hold-state" - - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - # Operation Mode - state = hass.states.get(ENTITY_CLIMATE) - async_fire_mqtt_message(hass, "mode-state", '"cool"') - state = hass.states.get(ENTITY_CLIMATE) - assert state.state == "cool" - - # Away Mode - assert state.attributes.get("preset_mode") == "none" - async_fire_mqtt_message(hass, "away-state", '"ON"') - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - # Away Mode with JSON values - async_fire_mqtt_message(hass, "away-state", "false") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - async_fire_mqtt_message(hass, "away-state", "true") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - # Hold Mode - async_fire_mqtt_message( - hass, - "hold-state", - """ - { "attribute": "somemode" } - """, - ) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "somemode" - - async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -1232,29 +926,6 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) assert state.attributes.get("target_temp_high") == 23 -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_with_away_and_hold_modes_and_templates( - hass, mqtt_mock_entry_with_yaml_config, caplog -): - """Test setting various attributes on hold and away mode with templates.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - # Create simple templates - config["climate"]["hold_command_template"] = "hold: {{ value }}" - - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - # Hold Mode - await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold: eco", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_ECO - - async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom min temp.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -1404,12 +1075,8 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ("action_topic", "heating", ATTR_HVAC_ACTION, "heating"), ("action_topic", "cooling", ATTR_HVAC_ACTION, "cooling"), ("aux_state_topic", "ON", ATTR_AUX_HEAT, "on"), - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - ("away_mode_state_topic", "ON", ATTR_PRESET_MODE, "away"), ("current_temperature_topic", "22.1", ATTR_CURRENT_TEMPERATURE, 22.1), ("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"), - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - ("hold_state_topic", "mode1", ATTR_PRESET_MODE, "mode1"), ("mode_state_topic", "cool", None, None), ("mode_state_topic", "fan_only", None, None), ("swing_mode_state_topic", "on", ATTR_SWING_MODE, "on"), @@ -1429,11 +1096,6 @@ async def test_encoding_subscribable_topics( ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - if topic in ["hold_state_topic", "away_mode_state_topic"]: - config["hold_modes"] = ["mode1", "mode2"] - del config["preset_modes"] - del config["preset_mode_command_topic"] await help_test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, @@ -1638,27 +1300,6 @@ async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config): "sleep", "preset_mode_command_template", ), - ( - climate.SERVICE_SET_PRESET_MODE, - "away_mode_command_topic", - {"preset_mode": "away"}, - "ON", - None, - ), - ( - climate.SERVICE_SET_PRESET_MODE, - "hold_command_topic", - {"preset_mode": "eco"}, - "eco", - "hold_command_template", - ), - ( - climate.SERVICE_SET_PRESET_MODE, - "hold_command_topic", - {"preset_mode": "comfort"}, - "comfort", - "hold_command_template", - ), ( climate.SERVICE_SET_FAN_MODE, "fan_mode_command_topic", From 9dedba4843e1348edfaa9800b6894373065b6741 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 20:48:06 -1000 Subject: [PATCH 354/903] Fix bad data with inkbird bbq sensors (#76739) --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/inkbird/test_config_flow.py | 6 +++--- tests/components/inkbird/test_sensor.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index b0ef08143c2..f65177ab6e2 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -10,7 +10,7 @@ { "local_name": "xBBQ*" }, { "local_name": "tps" } ], - "requirements": ["inkbird-ble==0.5.2"], + "requirements": ["inkbird-ble==0.5.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1e7db64277c..1ab91b4cd05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -899,7 +899,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.2 +inkbird-ble==0.5.5 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10e88fcc1ab..a360de10112 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -652,7 +652,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.2 +inkbird-ble==0.5.5 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index fe210f75f4b..4d9fbc65df7 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -25,7 +25,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "iBBQ 6AADDD4CAC3D" + assert result2["title"] == "iBBQ AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -69,7 +69,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -184,7 +184,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index cafc22911c3..c54c6e3c242 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -39,10 +39,10 @@ async def test_sensors(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 3 - temp_sensor = hass.states.get("sensor.ibs_th_75bbe1738105_battery") + temp_sensor = hass.states.get("sensor.ibs_th_8105_battery") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "87" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH 75BBE1738105 Battery" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH 8105 Battery" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From cf19536c43490d5c00ce5ae778669f74a67179d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 20:48:28 -1000 Subject: [PATCH 355/903] Fix stale data with SensorPush sensors (#76771) --- homeassistant/components/sensorpush/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index a5d900aaf3b..906b5c22f6b 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -8,7 +8,7 @@ "local_name": "SensorPush*" } ], - "requirements": ["sensorpush-ble==1.5.1"], + "requirements": ["sensorpush-ble==1.5.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1ab91b4cd05..1d5151cdf08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2173,7 +2173,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.5.1 +sensorpush-ble==1.5.2 # homeassistant.components.sentry sentry-sdk==1.9.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a360de10112..13d91ebe5fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1470,7 +1470,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.5.1 +sensorpush-ble==1.5.2 # homeassistant.components.sentry sentry-sdk==1.9.3 From 68979009e349cf165f4f0deb4dfeb499f2d57ea2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 20:54:49 -1000 Subject: [PATCH 356/903] Bump aiohomekit to 1.2.11 (#76784) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 49635dd797c..ece53d29406 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.10"], + "requirements": ["aiohomekit==1.2.11"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 1d5151cdf08..057a979f156 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.10 +aiohomekit==1.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13d91ebe5fc..ec83da82d2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.10 +aiohomekit==1.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http From c9feda1562d7c96146d7cb216126445a1ec57cfa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 15 Aug 2022 01:13:11 -0600 Subject: [PATCH 357/903] Fix missing state classes on various Ambient PWS entities (#76683) --- .../components/ambient_station/sensor.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 1a51e57bfa3..a04c279915f 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -203,78 +203,91 @@ SENSOR_DESCRIPTIONS = ( name="Humidity 10", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY1, name="Humidity 1", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY2, name="Humidity 2", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY3, name="Humidity 3", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY4, name="Humidity 4", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY5, name="Humidity 5", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY6, name="Humidity 6", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY7, name="Humidity 7", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY8, name="Humidity 8", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY9, name="Humidity 9", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITYIN, name="Humidity in", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_LASTRAIN, name="Last rain", icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_DAY, @@ -335,60 +348,70 @@ SENSOR_DESCRIPTIONS = ( name="Soil humidity 10", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM1, name="Soil humidity 1", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM2, name="Soil humidity 2", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM3, name="Soil humidity 3", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM4, name="Soil humidity 4", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM5, name="Soil humidity 5", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM6, name="Soil humidity 6", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM7, name="Soil humidity 7", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM8, name="Soil humidity 8", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM9, name="Soil humidity 9", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP10F, From 6243f24b05e2c766a107ffaa44072b6cef036255 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Aug 2022 09:48:03 +0200 Subject: [PATCH 358/903] Add media-player checks to pylint plugin (#76675) * Add media-player checks to pylint plugin * Fix invalid hints * Add tests * Adjust tests * Add extra test * Adjust regex * Cleanup comment * Move media player tests up --- pylint/plugins/hass_enforce_type_hints.py | 318 +++++++++++++++++++++- tests/pylint/test_enforce_type_hints.py | 30 ++ 2 files changed, 347 insertions(+), 1 deletion(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 852c5b544c4..b6eecdadfee 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -59,7 +59,7 @@ _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), } -_INNER_MATCH = r"((?:\w+)|(?:\.{3})|(?:\w+\[.+\]))" +_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\]))" _INNER_MATCH_POSSIBILITIES = [i + 1 for i in range(5)] _TYPE_HINT_MATCHERS.update( { @@ -1465,6 +1465,322 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "media_player": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="MediaPlayerEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["MediaPlayerDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="state", + return_type=["str", None], + ), + TypeHintMatch( + function_name="access_token", + return_type="str", + ), + TypeHintMatch( + function_name="volume_level", + return_type=["float", None], + ), + TypeHintMatch( + function_name="is_volume_muted", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="media_content_id", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_content_type", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_duration", + return_type=["int", None], + ), + TypeHintMatch( + function_name="media_position", + return_type=["int", None], + ), + TypeHintMatch( + function_name="media_position_updated_at", + return_type=["datetime", None], + ), + TypeHintMatch( + function_name="media_image_url", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_image_remotely_accessible", + return_type="bool", + ), + TypeHintMatch( + function_name="media_image_hash", + return_type=["str", None], + ), + TypeHintMatch( + function_name="async_get_media_image", + return_type="tuple[bytes | None, str | None]", + ), + TypeHintMatch( + function_name="async_get_browse_image", + arg_types={ + 1: "str", + 2: "str", + 3: "str | None", + }, + return_type="tuple[bytes | None, str | None]", + ), + TypeHintMatch( + function_name="media_title", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_artist", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_album_name", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_album_artist", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_track", + return_type=["int", None], + ), + TypeHintMatch( + function_name="media_series_title", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_season", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_episode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_channel", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_playlist", + return_type=["str", None], + ), + TypeHintMatch( + function_name="app_id", + return_type=["str", None], + ), + TypeHintMatch( + function_name="app_name", + return_type=["str", None], + ), + TypeHintMatch( + function_name="source", + return_type=["str", None], + ), + TypeHintMatch( + function_name="source_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="sound_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="sound_mode_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="shuffle", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="repeat", + return_type=["str", None], + ), + TypeHintMatch( + function_name="group_members", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="turn_on", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_off", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="mute_volume", + arg_types={ + 1: "bool", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_volume_level", + arg_types={ + 1: "float", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_play", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_pause", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_stop", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_previous_track", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_next_track", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_seek", + arg_types={ + 1: "float", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="play_media", + arg_types={ + 1: "str", + 2: "str", + }, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="select_source", + arg_types={ + 1: "str", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="select_sound_mode", + arg_types={ + 1: "str", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="clear_playlist", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_shuffle", + arg_types={ + 1: "bool", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_repeat", + arg_types={ + 1: "str", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="toggle", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="volume_up", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="volume_down", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_play_pause", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_image_local", + return_type=["str", None], + ), + TypeHintMatch( + function_name="capability_attributes", + return_type="dict[str, Any]", + ), + TypeHintMatch( + function_name="async_browse_media", + arg_types={ + 1: "str | None", + 2: "str | None", + }, + return_type="BrowseMedia", + ), + TypeHintMatch( + function_name="join_players", + arg_types={ + 1: "list[str]", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="unjoin_player", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="get_browse_image_url", + arg_types={ + 1: "str", + 2: "str", + 3: "str | None", + }, + return_type="str", + ), + ], + ), + ], "number": [ ClassTypeHintMatch( base_class="Entity", diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index b3c233d1c3b..1381ed34a7b 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -53,6 +53,7 @@ def test_regex_get_module_platform( ("Awaitable[None]", 1, ("Awaitable", "None")), ("list[dict[str, str]]", 1, ("list", "dict[str, str]")), ("list[dict[str, Any]]", 1, ("list", "dict[str, Any]")), + ("tuple[bytes | None, str | None]", 2, ("tuple", "bytes | None", "str | None")), ], ) def test_regex_x_of_y_i( @@ -902,6 +903,35 @@ def test_invalid_device_class( type_hint_checker.visit_classdef(class_node) +def test_media_player_entity( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure valid hints are accepted for media_player entity.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + class_node = astroid.extract_node( + """ + class Entity(): + pass + + class MediaPlayerEntity(Entity): + pass + + class MyMediaPlayer( #@ + MediaPlayerEntity + ): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: + pass + """, + "homeassistant.components.pylint_test.media_player", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) + + def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: """Ensure valid hints are accepted for number entity.""" # Set bypass option From f4c23ad68dd70bda8cc5e0eeee3a71f736f01694 Mon Sep 17 00:00:00 2001 From: Frank <46161394+BraveChicken1@users.noreply.github.com> Date: Mon, 15 Aug 2022 10:44:14 +0200 Subject: [PATCH 359/903] Bump homeconnect to 0.7.2 (#76773) --- homeassistant/components/home_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/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index ca6e0f012ac..c9aa5d229b8 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "dependencies": ["application_credentials"], "codeowners": ["@DavidMStraub"], - "requirements": ["homeconnect==0.7.1"], + "requirements": ["homeconnect==0.7.2"], "config_flow": true, "iot_class": "cloud_push", "loggers": ["homeconnect"] diff --git a/requirements_all.txt b/requirements_all.txt index 057a979f156..130c40541d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ holidays==0.14.2 home-assistant-frontend==20220802.0 # homeassistant.components.home_connect -homeconnect==0.7.1 +homeconnect==0.7.2 # homeassistant.components.homematicip_cloud homematicip==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec83da82d2d..0977ec957ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ holidays==0.14.2 home-assistant-frontend==20220802.0 # homeassistant.components.home_connect -homeconnect==0.7.1 +homeconnect==0.7.2 # homeassistant.components.homematicip_cloud homematicip==1.0.7 From 669e6bec39f1507320aefa2bd7c5a28e93338360 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 15 Aug 2022 11:23:38 +0200 Subject: [PATCH 360/903] Bump aiohue to 4.5.0 (#76757) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index b3dbe4df50a..3854b861c98 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.4.2"], + "requirements": ["aiohue==4.5.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 130c40541d8..8533e2276e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aiohomekit==1.2.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.2 +aiohue==4.5.0 # homeassistant.components.imap aioimaplib==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0977ec957ec..1a4f9c64229 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ aiohomekit==1.2.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.2 +aiohue==4.5.0 # homeassistant.components.apache_kafka aiokafka==0.7.2 From 8f0f734c288e92b4f2f74aee45465214d278a346 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 15 Aug 2022 11:35:53 +0200 Subject: [PATCH 361/903] Fix entity category for LIFX buttons (#76788) --- homeassistant/components/lifx/button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/button.py b/homeassistant/components/lifx/button.py index 3a4c73d2889..76afdc785e9 100644 --- a/homeassistant/components/lifx/button.py +++ b/homeassistant/components/lifx/button.py @@ -19,13 +19,13 @@ RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription( key=RESTART, name="Restart", device_class=ButtonDeviceClass.RESTART, - entity_category=EntityCategory.DIAGNOSTIC, + entity_category=EntityCategory.CONFIG, ) IDENTIFY_BUTTON_DESCRIPTION = ButtonEntityDescription( key=IDENTIFY, name="Identify", - entity_category=EntityCategory.DIAGNOSTIC, + entity_category=EntityCategory.CONFIG, ) From f443edfef4808cf1df26d8ffde586e1d92592f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 15 Aug 2022 11:49:22 +0200 Subject: [PATCH 362/903] Enable statistics for WLED WiFi RSSI/Signal sensors (#76789) --- homeassistant/components/wled/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 4a677910273..1b5e81ec469 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -96,6 +96,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( name="Wi-Fi signal", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.wifi.signal if device.info.wifi else None, @@ -105,6 +106,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( name="Wi-Fi RSSI", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.wifi.rssi if device.info.wifi else None, From cf867730cdad643bf9875cfe1e5bf855d143a3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 15 Aug 2022 12:12:31 +0200 Subject: [PATCH 363/903] Update aioqsw to v0.2.2 (#76760) --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/qnap_qsw/test_coordinator.py | 6 ++++++ tests/components/qnap_qsw/util.py | 16 ++++++++++++++++ 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 690297d69bc..91d77a7362f 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -3,7 +3,7 @@ "name": "QNAP QSW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", - "requirements": ["aioqsw==0.2.0"], + "requirements": ["aioqsw==0.2.2"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioqsw"], diff --git a/requirements_all.txt b/requirements_all.txt index 8533e2276e0..a500a38fa1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -232,7 +232,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.2.0 +aioqsw==0.2.2 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a4f9c64229..a3289b4b028 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -207,7 +207,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.2.0 +aioqsw==0.2.2 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index 125b333c8d6..61d1fa04200 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -18,6 +18,7 @@ from .util import ( FIRMWARE_CONDITION_MOCK, FIRMWARE_INFO_MOCK, FIRMWARE_UPDATE_CHECK_MOCK, + LACP_INFO_MOCK, PORTS_STATISTICS_MOCK, PORTS_STATUS_MOCK, SYSTEM_BOARD_MOCK, @@ -46,6 +47,9 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", return_value=FIRMWARE_UPDATE_CHECK_MOCK, ) as mock_firmware_update_check, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_lacp_info", + return_value=LACP_INFO_MOCK, + ) as mock_lacp_info, patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", return_value=PORTS_STATISTICS_MOCK, ) as mock_ports_statistics, patch( @@ -73,6 +77,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.assert_called_once() mock_firmware_info.assert_called_once() mock_firmware_update_check.assert_called_once() + mock_lacp_info.assert_called_once() mock_ports_statistics.assert_called_once() mock_ports_status.assert_called_once() mock_system_board.assert_called_once() @@ -84,6 +89,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.reset_mock() mock_firmware_info.reset_mock() mock_firmware_update_check.reset_mock() + mock_lacp_info.reset_mock() mock_ports_statistics.reset_mock() mock_ports_status.reset_mock() mock_system_board.reset_mock() diff --git a/tests/components/qnap_qsw/util.py b/tests/components/qnap_qsw/util.py index d3a62d413fa..5ae801283bc 100644 --- a/tests/components/qnap_qsw/util.py +++ b/tests/components/qnap_qsw/util.py @@ -23,6 +23,8 @@ from aioqsw.const import ( API_KEY, API_LINK, API_MAC_ADDR, + API_MAX_PORT_CHANNELS, + API_MAX_PORTS_PER_PORT_CHANNEL, API_MAX_SWITCH_TEMP, API_MESSAGE, API_MODEL, @@ -36,6 +38,7 @@ from aioqsw.const import ( API_RX_OCTETS, API_SERIAL, API_SPEED, + API_START_INDEX, API_SWITCH_TEMP, API_TRUNK_NUM, API_TX_OCTETS, @@ -120,6 +123,16 @@ FIRMWARE_UPDATE_CHECK_MOCK = { }, } +LACP_INFO_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: { + API_START_INDEX: 28, + API_MAX_PORT_CHANNELS: 8, + API_MAX_PORTS_PER_PORT_CHANNEL: 8, + }, +} + PORTS_STATISTICS_MOCK = { API_ERROR_CODE: 200, API_ERROR_MESSAGE: "OK", @@ -499,6 +512,9 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", return_value=FIRMWARE_UPDATE_CHECK_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_lacp_info", + return_value=LACP_INFO_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", return_value=PORTS_STATISTICS_MOCK, From 4239757c2c3a22bc2518211c69e58a8117a3e022 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 15 Aug 2022 13:55:23 +0200 Subject: [PATCH 364/903] Bump bimmer_connected to 0.10.2 (#76751) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 0381035a63e..f540176a837 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.10.1"], + "requirements": ["bimmer_connected==0.10.2"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index a500a38fa1b..10e48ccf2ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ beautifulsoup4==4.11.1 bellows==0.32.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.1 +bimmer_connected==0.10.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3289b4b028..796138e09ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -323,7 +323,7 @@ beautifulsoup4==4.11.1 bellows==0.32.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.1 +bimmer_connected==0.10.2 # homeassistant.components.bluetooth bleak==0.15.1 From c93d9d9a90227fbffd2e36c04710183c8cca999d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:37:11 +0200 Subject: [PATCH 365/903] Move `AutomationActionType` to helpers.trigger (#76790) --- .../components/automation/__init__.py | 42 ++++++------------- .../binary_sensor/device_trigger.py | 11 ++--- homeassistant/helpers/trigger.py | 39 +++++++++++++---- pylint/plugins/hass_enforce_type_hints.py | 4 +- 4 files changed, 50 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c3a669511bc..43ff31dfab8 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import Any, Protocol, TypedDict, cast +from typing import Any, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -72,8 +72,13 @@ from homeassistant.helpers.trace import ( trace_get, trace_path, ) -from homeassistant.helpers.trigger import async_initialize_triggers -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.trigger import ( + TriggerActionType, + TriggerData, + TriggerInfo, + async_initialize_triggers, +) +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime @@ -112,32 +117,11 @@ SERVICE_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) -class AutomationActionType(Protocol): - """Protocol type for automation action callback.""" - - async def __call__( - self, - run_variables: dict[str, Any], - context: Context | None = None, - ) -> None: - """Define action callback type.""" - - -class AutomationTriggerData(TypedDict): - """Automation trigger data.""" - - id: str - idx: str - - -class AutomationTriggerInfo(TypedDict): - """Information about automation trigger.""" - - domain: str - name: str - home_assistant_start: bool - variables: TemplateVarsType - trigger_data: AutomationTriggerData +# AutomationActionType, AutomationTriggerData, +# and AutomationTriggerInfo are deprecated as of 2022.9. +AutomationActionType = TriggerActionType +AutomationTriggerData = TriggerData +AutomationTriggerInfo = TriggerInfo @bind_hass diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 12b620b8c4f..a6dfc762d45 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -1,10 +1,6 @@ """Provides device triggers for binary sensors.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.const import ( CONF_TURNED_OFF, @@ -15,6 +11,7 @@ from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import get_device_class +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN, BinarySensorDeviceClass @@ -263,8 +260,8 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] @@ -283,7 +280,7 @@ async def async_attach_trigger( state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index e90c684365d..a48ccbbb7b9 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Callable import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Protocol, TypedDict import voluptuous as vol @@ -27,6 +27,34 @@ _PLATFORM_ALIASES = { } +class TriggerActionType(Protocol): + """Protocol type for trigger action callback.""" + + async def __call__( + self, + run_variables: dict[str, Any], + context: Context | None = None, + ) -> None: + """Define action callback type.""" + + +class TriggerData(TypedDict): + """Trigger data.""" + + id: str + idx: str + + +class TriggerInfo(TypedDict): + """Information about trigger.""" + + domain: str + name: str + home_assistant_start: bool + variables: TemplateVarsType + trigger_data: TriggerData + + async def _async_get_trigger_platform( hass: HomeAssistant, config: ConfigType ) -> DeviceAutomationTriggerProtocol: @@ -93,11 +121,6 @@ async def async_initialize_triggers( variables: TemplateVarsType = None, ) -> CALLBACK_TYPE | None: """Initialize triggers.""" - from homeassistant.components.automation import ( # pylint:disable=[import-outside-toplevel] - AutomationTriggerData, - AutomationTriggerInfo, - ) - triggers = [] for idx, conf in enumerate(trigger_config): # Skip triggers that are not enabled @@ -107,8 +130,8 @@ async def async_initialize_triggers( platform = await _async_get_trigger_platform(hass, conf) trigger_id = conf.get(CONF_ID, f"{idx}") trigger_idx = f"{idx}" - trigger_data = AutomationTriggerData(id=trigger_id, idx=trigger_idx) - info = AutomationTriggerInfo( + trigger_data = TriggerData(id=trigger_id, idx=trigger_idx) + info = TriggerInfo( domain=domain, name=name, home_assistant_start=home_assistant_start, diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index b6eecdadfee..158848ba4d4 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -337,8 +337,8 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { arg_types={ 0: "HomeAssistant", 1: "ConfigType", - 2: "AutomationActionType", - 3: "AutomationTriggerInfo", + # 2: "AutomationActionType", # AutomationActionType is deprecated -> TriggerActionType + # 3: "AutomationTriggerInfo", # AutomationTriggerInfo is deprecated -> TriggerInfo }, return_type="CALLBACK_TYPE", ), From 64898af58ee4dc265d71720c5242659a8f8d880a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 15 Aug 2022 14:00:29 +0100 Subject: [PATCH 366/903] Update systembridgeconnector to 3.4.4 (#75362) Co-authored-by: Martin Hjelmare --- .../components/system_bridge/__init__.py | 18 +- .../components/system_bridge/config_flow.py | 32 ++- .../components/system_bridge/coordinator.py | 13 +- .../components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../system_bridge/test_config_flow.py | 208 ++++++++++++------ 7 files changed, 183 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 19bcc224a66..74be1faed40 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -10,6 +10,10 @@ from systembridgeconnector.exceptions import ( ConnectionClosedException, ConnectionErrorException, ) +from systembridgeconnector.models.keyboard_key import KeyboardKey +from systembridgeconnector.models.keyboard_text import KeyboardText +from systembridgeconnector.models.open_path import OpenPath +from systembridgeconnector.models.open_url import OpenUrl from systembridgeconnector.version import SUPPORTED_VERSION, Version import voluptuous as vol @@ -149,7 +153,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.open_path(call.data[CONF_PATH]) + await coordinator.websocket_client.open_path( + OpenPath(path=call.data[CONF_PATH]) + ) async def handle_open_url(call: ServiceCall) -> None: """Handle the open url service call.""" @@ -157,21 +163,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.open_url(call.data[CONF_URL]) + await coordinator.websocket_client.open_url(OpenUrl(url=call.data[CONF_URL])) async def handle_send_keypress(call: ServiceCall) -> None: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.keyboard_keypress(call.data[CONF_KEY]) + await coordinator.websocket_client.keyboard_keypress( + KeyboardKey(key=call.data[CONF_KEY]) + ) async def handle_send_text(call: ServiceCall) -> None: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.keyboard_text(call.data[CONF_TEXT]) + await coordinator.websocket_client.keyboard_text( + KeyboardText(text=call.data[CONF_TEXT]) + ) hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 9d89cf83288..995df6391cc 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -7,12 +7,13 @@ import logging from typing import Any import async_timeout -from systembridgeconnector.const import EVENT_MODULE, EVENT_TYPE, TYPE_DATA_UPDATE from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) +from systembridgeconnector.models.get_data import GetData +from systembridgeconnector.models.system import System from systembridgeconnector.websocket_client import WebSocketClient import voluptuous as vol @@ -38,7 +39,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input( +async def _validate_input( hass: HomeAssistant, data: dict[str, Any], ) -> dict[str, str]: @@ -56,15 +57,12 @@ async def validate_input( try: async with async_timeout.timeout(30): await websocket_client.connect(session=async_get_clientsession(hass)) - await websocket_client.get_data(["system"]) - while True: - message = await websocket_client.receive_message() - _LOGGER.debug("Message: %s", message) - if ( - message[EVENT_TYPE] == TYPE_DATA_UPDATE - and message[EVENT_MODULE] == "system" - ): - break + hass.async_create_task(websocket_client.listen()) + response = await websocket_client.get_data(GetData(modules=["system"])) + _LOGGER.debug("Got response: %s", response.json()) + if response.data is None or not isinstance(response.data, System): + raise CannotConnect("No data received") + system: System = response.data except AuthenticationException as exception: _LOGGER.warning( "Authentication error when connecting to %s: %s", data[CONF_HOST], exception @@ -81,14 +79,12 @@ async def validate_input( except asyncio.TimeoutError as exception: _LOGGER.warning("Timed out connecting to %s: %s", data[CONF_HOST], exception) raise CannotConnect from exception + except ValueError as exception: + raise CannotConnect from exception - _LOGGER.debug("%s Message: %s", TYPE_DATA_UPDATE, message) + _LOGGER.debug("Got System data: %s", system.json()) - if "uuid" not in message["data"]: - error = "No UUID in result!" - raise CannotConnect(error) - - return {"hostname": host, "uuid": message["data"]["uuid"]} + return {"hostname": host, "uuid": system.uuid} async def _async_get_info( @@ -98,7 +94,7 @@ async def _async_get_info( errors = {} try: - info = await validate_input(hass, user_input) + info = await _validate_input(hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 1719d951cf0..695dca44342 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Callable from datetime import timedelta import logging +from typing import Any import async_timeout from pydantic import BaseModel # pylint: disable=no-name-in-module @@ -17,8 +18,10 @@ from systembridgeconnector.models.battery import Battery from systembridgeconnector.models.cpu import Cpu from systembridgeconnector.models.disk import Disk from systembridgeconnector.models.display import Display +from systembridgeconnector.models.get_data import GetData from systembridgeconnector.models.gpu import Gpu from systembridgeconnector.models.memory import Memory +from systembridgeconnector.models.register_data_listener import RegisterDataListener from systembridgeconnector.models.system import System from systembridgeconnector.websocket_client import WebSocketClient @@ -93,12 +96,14 @@ class SystemBridgeDataUpdateCoordinator( if not self.websocket_client.connected: await self._setup_websocket() - self.hass.async_create_task(self.websocket_client.get_data(modules)) + self.hass.async_create_task( + self.websocket_client.get_data(GetData(modules=modules)) + ) async def async_handle_module( self, module_name: str, - module, + module: Any, ) -> None: """Handle data from the WebSocket client.""" self.logger.debug("Set new data for: %s", module_name) @@ -174,7 +179,9 @@ class SystemBridgeDataUpdateCoordinator( self.hass.async_create_task(self._listen_for_data()) - await self.websocket_client.register_data_listener(MODULES) + await self.websocket_client.register_data_listener( + RegisterDataListener(modules=MODULES) + ) self.last_update_success = True self.async_update_listeners() diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 4fb2201e2c7..7968b588814 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridgeconnector==3.3.2"], + "requirements": ["systembridgeconnector==3.4.4"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._tcp.local."], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 10e48ccf2ef..4bb2e474f03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2294,7 +2294,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.3.2 +systembridgeconnector==3.4.4 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 796138e09ee..9909033875d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1558,7 +1558,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridgeconnector==3.3.2 +systembridgeconnector==3.4.4 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 45131353550..d01ed9a3ff8 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -2,21 +2,14 @@ import asyncio from unittest.mock import patch -from systembridgeconnector.const import ( - EVENT_DATA, - EVENT_MESSAGE, - EVENT_MODULE, - EVENT_SUBTYPE, - EVENT_TYPE, - SUBTYPE_BAD_API_KEY, - TYPE_DATA_UPDATE, - TYPE_ERROR, -) +from systembridgeconnector.const import MODEL_SYSTEM, TYPE_DATA_UPDATE from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) +from systembridgeconnector.models.response import Response +from systembridgeconnector.models.system import LastUpdated, System from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf @@ -48,8 +41,8 @@ FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( addresses=["1.1.1.1"], port=9170, hostname="test-bridge.local.", - type="_system-bridge._udp.local.", - name="System Bridge - test-bridge._system-bridge._udp.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", properties={ "address": "http://test-bridge:9170", "fqdn": "test-bridge", @@ -66,34 +59,70 @@ FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( addresses=["1.1.1.1"], port=9170, hostname="test-bridge.local.", - type="_system-bridge._udp.local.", - name="System Bridge - test-bridge._system-bridge._udp.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", properties={ "something": "bad", }, ) -FIXTURE_DATA_SYSTEM = { - EVENT_TYPE: TYPE_DATA_UPDATE, - EVENT_MESSAGE: "Data changed", - EVENT_MODULE: "system", - EVENT_DATA: { - "uuid": FIXTURE_UUID, - }, -} -FIXTURE_DATA_SYSTEM_BAD = { - EVENT_TYPE: TYPE_DATA_UPDATE, - EVENT_MESSAGE: "Data changed", - EVENT_MODULE: "system", - EVENT_DATA: {}, -} +FIXTURE_SYSTEM = System( + id=FIXTURE_UUID, + boot_time=1, + fqdn="", + hostname="1.1.1.1", + ip_address_4="1.1.1.1", + mac_address=FIXTURE_MAC_ADDRESS, + platform="", + platform_version="", + uptime=1, + uuid=FIXTURE_UUID, + version="", + version_latest="", + version_newer_available=False, + last_updated=LastUpdated( + boot_time=1, + fqdn=1, + hostname=1, + ip_address_4=1, + mac_address=1, + platform=1, + platform_version=1, + uptime=1, + uuid=1, + version=1, + version_latest=1, + version_newer_available=1, + ), +) -FIXTURE_DATA_AUTH_ERROR = { - EVENT_TYPE: TYPE_ERROR, - EVENT_SUBTYPE: SUBTYPE_BAD_API_KEY, - EVENT_MESSAGE: "Invalid api-key", -} +FIXTURE_DATA_RESPONSE = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data=FIXTURE_SYSTEM, +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) async def test_show_user_form(hass: HomeAssistant) -> None: @@ -117,9 +146,11 @@ async def test_user_flow(hass: HomeAssistant) -> None: with patch( "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch("systembridgeconnector.websocket_client.WebSocketClient.get_data"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen" ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, @@ -167,11 +198,13 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=ConnectionClosedException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -192,11 +225,13 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=asyncio.TimeoutError, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -217,11 +252,13 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=AuthenticationException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -242,11 +279,40 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM_BAD, + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=ValueError, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + ): + 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.FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_value_error(hass: HomeAssistant) -> None: + """Test we handle error from bad value.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE_BAD, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -267,11 +333,13 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=Exception, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -292,11 +360,13 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=AuthenticationException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -340,11 +410,13 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=ConnectionClosedException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -370,11 +442,13 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM, + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen" ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, @@ -402,11 +476,13 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM, + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen" ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, From 1557a7c36d6b0f2e46599f3d6d18550eb5b31703 Mon Sep 17 00:00:00 2001 From: hansgoed Date: Mon, 15 Aug 2022 15:07:39 +0200 Subject: [PATCH 367/903] =?UTF-8?q?=F0=9F=90=9B=20Fix=20"The=20request=20c?= =?UTF-8?q?ontent=20was=20malformed"=20error=20in=20home=5Fconnect=20(#764?= =?UTF-8?q?11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/home_connect/__init__.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 6e664ad07e4..0fa14682f44 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -138,11 +138,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Execute calls to services taking a program.""" program = call.data[ATTR_PROGRAM] device_id = call.data[ATTR_DEVICE_ID] - options = { - ATTR_KEY: call.data.get(ATTR_KEY), - ATTR_VALUE: call.data.get(ATTR_VALUE), - ATTR_UNIT: call.data.get(ATTR_UNIT), - } + + options = [] + + option_key = call.data.get(ATTR_KEY) + if option_key is not None: + option = {ATTR_KEY: option_key, ATTR_VALUE: call.data[ATTR_VALUE]} + + option_unit = call.data.get(ATTR_UNIT) + if option_unit is not None: + option[ATTR_UNIT] = option_unit + + options.append(option) appliance = _get_appliance_by_device_id(hass, device_id) await hass.async_add_executor_job(getattr(appliance, method), program, options) From e39672078d259e4366a409f6258f6a0ef61fb58a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:39:14 +0200 Subject: [PATCH 368/903] Use TriggerActionType [core, d-h] (#76804) --- .../components/device_automation/entity.py | 11 ++++------- .../components/device_automation/toggle_entity.py | 13 +++++-------- .../components/device_automation/trigger.py | 15 ++++++--------- homeassistant/components/fan/device_trigger.py | 13 ++++--------- homeassistant/components/geo_location/trigger.py | 13 +++++-------- .../components/humidifier/device_trigger.py | 15 +++++---------- 6 files changed, 29 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/device_automation/entity.py b/homeassistant/components/device_automation/entity.py index 4bc77370150..c7716f38712 100644 --- a/homeassistant/components/device_automation/entity.py +++ b/homeassistant/components/device_automation/entity.py @@ -3,14 +3,11 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DEVICE_TRIGGER_BASE_SCHEMA @@ -38,8 +35,8 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" to_state = None @@ -53,7 +50,7 @@ async def async_attach_trigger( state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index af97de85f70..f14c4de5c2f 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( ATTR_ENTITY_ID, @@ -23,6 +19,7 @@ from homeassistant.helpers import ( config_validation as cv, entity_registry as er, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DEVICE_TRIGGER_BASE_SCHEMA, entity @@ -155,12 +152,12 @@ def async_condition_from_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" if config[CONF_TYPE] not in [CONF_TURNED_ON, CONF_TURNED_OFF]: - return await entity.async_attach_trigger(hass, config, action, automation_info) + return await entity.async_attach_trigger(hass, config, action, trigger_info) if config[CONF_TYPE] == CONF_TURNED_ON: to_state = "on" @@ -176,7 +173,7 @@ async def async_attach_trigger( state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index eb39ec383af..bd72b24d844 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -5,12 +5,9 @@ from typing import Any, Protocol, cast import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import ( @@ -40,8 +37,8 @@ class DeviceAutomationTriggerProtocol(Protocol): self, hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" @@ -74,11 +71,11 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for trigger.""" platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER ) - return await platform.async_attach_trigger(hass, config, action, automation_info) + return await platform.async_attach_trigger(hass, config, action, trigger_info) diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index 35eb5a9f50c..cc10e9cbeca 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -37,10 +34,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 9f2b56a31c6..bc04490e76c 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -3,15 +3,12 @@ import logging import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -44,11 +41,11 @@ def source_match(state, source): async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] source: str = config[CONF_SOURCE].lower() zone_entity_id = config.get(CONF_ZONE) trigger_event = config.get(CONF_EVENT) @@ -66,7 +63,7 @@ async def async_attach_trigger( if (zone_state := hass.states.get(zone_entity_id)) is None: _LOGGER.warning( "Unable to execute automation %s: Zone %s not found", - automation_info["name"], + trigger_info["name"], zone_entity_id, ) return diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index e8f3a0ac446..b9abb231dfd 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import ( DEVICE_TRIGGER_BASE_SCHEMA, toggle_entity, @@ -27,6 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -82,8 +79,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "target_humidity_changed": @@ -106,12 +103,10 @@ async def async_attach_trigger( ) ) return await numeric_state_trigger.async_attach_trigger( - hass, numeric_state_config, action, automation_info, platform_type="device" + hass, numeric_state_config, action, trigger_info, platform_type="device" ) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_trigger_capabilities( From af002d9dc4536d641df41911f18e13cb3e273524 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:39:55 +0200 Subject: [PATCH 369/903] Use TriggerActionType [core, l-m] (#76806) --- .../components/light/device_trigger.py | 13 ++++-------- .../components/lock/device_trigger.py | 11 ++++------ .../components/media_player/device_trigger.py | 13 +++++------- .../components/mqtt/device_trigger.py | 21 ++++++++----------- homeassistant/components/mqtt/trigger.py | 15 ++++++------- 5 files changed, 28 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 6f5f5fd7f52..5ae5b12bf61 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -23,13 +20,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_triggers( diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index b55a2ac254b..9fc35fb1352 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( @@ -24,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -80,8 +77,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "jammed": @@ -104,5 +101,5 @@ async def async_attach_trigger( state_config[CONF_FOR] = config[CONF_FOR] state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py index e0c88489841..9b61c89dafb 100644 --- a/homeassistant/components/media_player/device_trigger.py +++ b/homeassistant/components/media_player/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import ( DEVICE_TRIGGER_BASE_SCHEMA, entity, @@ -28,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -94,12 +91,12 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] not in TRIGGER_TYPES: - return await entity.async_attach_trigger(hass, config, action, automation_info) + return await entity.async_attach_trigger(hass, config, action, trigger_info) if config[CONF_TYPE] == "buffering": to_state = STATE_BUFFERING elif config[CONF_TYPE] == "idle": @@ -122,5 +119,5 @@ async def async_attach_trigger( state_config[CONF_FOR] = config[CONF_FOR] state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 0b4bcbfcbc2..30d6fdea05f 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -8,10 +8,6 @@ from typing import cast import attr import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -26,6 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import debug_info, trigger as mqtt_trigger @@ -93,8 +90,8 @@ LOG_NAME = "Device trigger" class TriggerInstance: """Attached trigger settings.""" - action: AutomationActionType = attr.ib() - automation_info: AutomationTriggerInfo = attr.ib() + action: TriggerActionType = attr.ib() + trigger_info: TriggerInfo = attr.ib() trigger: Trigger = attr.ib() remove: CALLBACK_TYPE | None = attr.ib(default=None) @@ -118,7 +115,7 @@ class TriggerInstance: self.trigger.hass, mqtt_config, self.action, - self.automation_info, + self.trigger_info, ) @@ -138,10 +135,10 @@ class Trigger: trigger_instances: list[TriggerInstance] = attr.ib(factory=list) async def add_trigger( - self, action: AutomationActionType, automation_info: AutomationTriggerInfo + self, action: TriggerActionType, trigger_info: TriggerInfo ) -> Callable: """Add MQTT trigger.""" - instance = TriggerInstance(action, automation_info, self) + instance = TriggerInstance(action, trigger_info, self) self.trigger_instances.append(instance) if self.topic is not None: @@ -323,8 +320,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" hass.data.setdefault(DEVICE_TRIGGERS, {}) @@ -344,5 +341,5 @@ async def async_attach_trigger( value_template=None, ) return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger( - action, automation_info + action, trigger_info ) diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index aca4cf0a480..3530538122d 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -4,14 +4,11 @@ import logging import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.json import json_loads +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .. import mqtt @@ -39,11 +36,11 @@ _LOGGER = logging.getLogger(__name__) async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] topic = config[CONF_TOPIC] wanted_payload = config.get(CONF_PAYLOAD) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -51,8 +48,8 @@ async def async_attach_trigger( qos = config[CONF_QOS] job = HassJob(action) variables = None - if automation_info: - variables = automation_info.get("variables") + if trigger_info: + variables = trigger_info.get("variables") template.attach(hass, wanted_payload) if wanted_payload: From 1ebac6c7cacc41c9d706da993a0c504810e12522 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:40:16 +0200 Subject: [PATCH 370/903] Use TriggerActionType [core, r-t] (#76807) --- homeassistant/components/remote/device_trigger.py | 13 ++++--------- homeassistant/components/select/device_trigger.py | 11 ++++------- homeassistant/components/sensor/device_trigger.py | 11 ++++------- homeassistant/components/sun/trigger.py | 11 ++++------- homeassistant/components/switch/device_trigger.py | 13 ++++--------- homeassistant/components/tag/trigger.py | 11 ++++------- 6 files changed, 24 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/remote/device_trigger.py b/homeassistant/components/remote/device_trigger.py index 127f07827e2..f2d5c54e3c3 100644 --- a/homeassistant/components/remote/device_trigger.py +++ b/homeassistant/components/remote/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -23,13 +20,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_triggers( diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py index 574acfc6893..897ed855a5e 100644 --- a/homeassistant/components/select/device_trigger.py +++ b/homeassistant/components/select/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers.state import ( CONF_FOR, @@ -26,6 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.entity import get_capability +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import ATTR_OPTIONS, DOMAIN @@ -64,8 +61,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" state_config = { @@ -84,7 +81,7 @@ async def async_attach_trigger( state_config = await async_validate_state_trigger_config(hass, state_config) return await async_attach_state_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 741c0281e0d..5c815d3c546 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -1,10 +1,6 @@ """Provides device triggers for sensors.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -27,6 +23,7 @@ from homeassistant.helpers.entity import ( get_device_class, get_unit_of_measurement, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass @@ -143,8 +140,8 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" numeric_state_config = { @@ -162,7 +159,7 @@ async def async_attach_trigger( hass, numeric_state_config ) return await numeric_state_trigger.async_attach_trigger( - hass, numeric_state_config, action, automation_info, platform_type="device" + hass, numeric_state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py index 75f5b36f8f5..86af51f0283 100644 --- a/homeassistant/components/sun/trigger.py +++ b/homeassistant/components/sun/trigger.py @@ -3,10 +3,6 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ( CONF_EVENT, CONF_OFFSET, @@ -16,6 +12,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_sunrise, async_track_sunset +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -32,11 +29,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for events based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) description = event diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 9f56d7a09d2..499b04bbaf3 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -23,13 +20,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_triggers( diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py index b844ee260a2..146521dfba9 100644 --- a/homeassistant/components/tag/trigger.py +++ b/homeassistant/components/tag/trigger.py @@ -1,13 +1,10 @@ """Support for tag triggers.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID @@ -24,11 +21,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for tag_scanned events based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] tag_ids = set(config[TAG_ID]) device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None From 19cf6089d605ac1bdc015625e95387a23d355f25 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:44:12 +0200 Subject: [PATCH 371/903] Use TriggerActionType [core, a-d] (#76803) --- .../alarm_control_panel/device_trigger.py | 11 ++++------- homeassistant/components/button/device_trigger.py | 11 ++++------- homeassistant/components/calendar/trigger.py | 11 ++++------- homeassistant/components/climate/device_trigger.py | 13 +++++-------- homeassistant/components/cover/device_trigger.py | 13 +++++-------- .../components/device_tracker/device_trigger.py | 11 ++++------- 6 files changed, 26 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index 840b5eba6f3..303243d66cb 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -5,10 +5,6 @@ from typing import Final import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( @@ -29,6 +25,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.entity import get_supported_features +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -131,8 +128,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "triggered": @@ -159,5 +156,5 @@ async def async_attach_trigger( state_config[CONF_FOR] = config[CONF_FOR] state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/button/device_trigger.py b/homeassistant/components/button/device_trigger.py index 1418039b2e8..673806be7d2 100644 --- a/homeassistant/components/button/device_trigger.py +++ b/homeassistant/components/button/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers.state import ( async_attach_trigger as async_attach_state_trigger, @@ -21,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -56,8 +53,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" state_config = { @@ -67,5 +64,5 @@ async def async_attach_trigger( state_config = await async_validate_state_trigger_config(hass, state_config) return await async_attach_state_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 7845037f896..74be0f7e71d 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -8,10 +8,6 @@ from typing import Any import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_OFFSET, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -21,6 +17,7 @@ from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_time_interval, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -167,8 +164,8 @@ class CalendarEventListener: async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach trigger for the specified calendar.""" entity_id = config[CONF_ENTITY_ID] @@ -184,7 +181,7 @@ async def async_attach_trigger( ) trigger_data = { - **automation_info["trigger_data"], + **trigger_info["trigger_data"], "platform": DOMAIN, "event": event_type, "offset": offset, diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index d8d46342603..a7b723eb41a 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, @@ -25,6 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN, const @@ -112,8 +109,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if (trigger_type := config[CONF_TYPE]) == "hvac_mode_changed": @@ -133,7 +130,7 @@ async def async_attach_trigger( hass, state_config ) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) numeric_state_config = { @@ -161,7 +158,7 @@ async def async_attach_trigger( hass, numeric_state_config ) return await numeric_state_trigger.async_attach_trigger( - hass, numeric_state_config, action, automation_info, platform_type="device" + hass, numeric_state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index a6ed7785486..b0be418f312 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, @@ -30,6 +26,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.entity import get_supported_features +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import ( @@ -147,8 +144,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] in STATE_TRIGGER_TYPES: @@ -172,7 +169,7 @@ async def async_attach_trigger( hass, state_config ) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) if config[CONF_TYPE] == "position": @@ -194,5 +191,5 @@ async def async_attach_trigger( hass, numeric_state_config ) return await numeric_state_trigger.async_attach_trigger( - hass, numeric_state_config, action, automation_info, platform_type="device" + hass, numeric_state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index 1d9dc4548b3..231fab65d35 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -5,10 +5,6 @@ from typing import Final import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.zone import DOMAIN as DOMAIN_ZONE, trigger as zone from homeassistant.const import ( @@ -22,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -74,8 +71,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "enters": @@ -91,7 +88,7 @@ async def async_attach_trigger( } zone_config = await zone.async_validate_trigger_config(hass, zone_config) return await zone.async_attach_trigger( - hass, zone_config, action, automation_info, platform_type="device" + hass, zone_config, action, trigger_info, platform_type="device" ) From 7360cce1c2d1859a45ee350eee83f78dda71308d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 18:06:16 +0200 Subject: [PATCH 372/903] Use TriggerActionType [core, homeassistant] (#76805) --- .../components/homeassistant/trigger.py | 11 ++++------- .../homeassistant/triggers/event.py | 13 +++++-------- .../homeassistant/triggers/homeassistant.py | 13 +++++-------- .../homeassistant/triggers/numeric_state.py | 19 ++++++++----------- .../homeassistant/triggers/state.py | 19 ++++++++----------- .../components/homeassistant/triggers/time.py | 11 ++++------- .../homeassistant/triggers/time_pattern.py | 11 ++++------- 7 files changed, 38 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/homeassistant/trigger.py b/homeassistant/components/homeassistant/trigger.py index 42b0e30af1d..588b6713007 100644 --- a/homeassistant/components/homeassistant/trigger.py +++ b/homeassistant/components/homeassistant/trigger.py @@ -1,15 +1,12 @@ """Home Assistant trigger dispatcher.""" import importlib -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation.trigger import ( DeviceAutomationTriggerProtocol, ) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -31,9 +28,9 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach trigger of specified platform.""" platform = _get_trigger_platform(config) - return await platform.async_attach_trigger(hass, config, action, automation_info) + return await platform.async_attach_trigger(hass, config, action, trigger_info) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index b0d817478dc..0796d49d770 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -5,13 +5,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType CONF_EVENT_TYPE = "event_type" @@ -37,14 +34,14 @@ def _schema_value(value: Any) -> Any: async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "event", ) -> CALLBACK_TYPE: """Listen for events based on configuration.""" - trigger_data = automation_info["trigger_data"] - variables = automation_info["variables"] + trigger_data = trigger_info["trigger_data"] + variables = trigger_info["variables"] template.attach(hass, config[CONF_EVENT_TYPE]) event_types = template.render_complex( diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index c9a5a780e88..8749c47861a 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -1,13 +1,10 @@ """Offer Home Assistant core automation rules.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_EVENT, CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs @@ -26,11 +23,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for events based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] event = config.get(CONF_EVENT) job = HassJob(action) @@ -56,7 +53,7 @@ async def async_attach_trigger( # Automation are enabled while hass is starting up, fire right away # Check state because a config reload shouldn't trigger it. - if automation_info["home_assistant_start"]: + if trigger_info["home_assistant_start"]: hass.async_run_hass_job( job, { diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 934cc99993a..10971a03781 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -4,10 +4,6 @@ import logging import voluptuous as vol from homeassistant import exceptions -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ( CONF_ABOVE, CONF_ATTRIBUTE, @@ -28,6 +24,7 @@ from homeassistant.helpers.event import ( async_track_same_state, async_track_state_change_event, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs @@ -87,8 +84,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "numeric_state", ) -> CALLBACK_TYPE: @@ -105,8 +102,8 @@ async def async_attach_trigger( attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) - trigger_data = automation_info["trigger_data"] - _variables = automation_info["variables"] or {} + trigger_data = trigger_info["trigger_data"] + _variables = trigger_info["variables"] or {} if value_template is not None: value_template.hass = hass @@ -139,7 +136,7 @@ async def async_attach_trigger( except exceptions.ConditionError as ex: _LOGGER.warning( "Error initializing '%s' trigger: %s", - automation_info["name"], + trigger_info["name"], ex, ) @@ -185,7 +182,7 @@ async def async_attach_trigger( 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) + _LOGGER.warning("Error in '%s' trigger: %s", trigger_info["name"], ex) return if not matching: @@ -201,7 +198,7 @@ async def async_attach_trigger( except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", - automation_info["name"], + trigger_info["name"], ex, ) return diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 4f1e823c90f..8514000de07 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -7,10 +7,6 @@ import logging import voluptuous as vol from homeassistant import exceptions -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_ALL from homeassistant.core import ( CALLBACK_TYPE, @@ -30,6 +26,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, process_state_match, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs @@ -97,8 +94,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "state", ) -> CALLBACK_TYPE: @@ -131,8 +128,8 @@ async def async_attach_trigger( attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) - trigger_data = automation_info["trigger_data"] - _variables = automation_info["variables"] or {} + trigger_data = trigger_info["trigger_data"] + _variables = trigger_info["variables"] or {} @callback def state_automation_listener(event: Event): @@ -193,7 +190,7 @@ async def async_attach_trigger( call_action() return - trigger_info = { + data = { "trigger": { "platform": "state", "entity_id": entity, @@ -201,7 +198,7 @@ async def async_attach_trigger( "to_state": to_s, } } - variables = {**_variables, **trigger_info} + variables = {**_variables, **data} try: period[entity] = cv.positive_time_period( @@ -209,7 +206,7 @@ async def async_attach_trigger( ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( - "Error rendering '%s' for template: %s", automation_info["name"], ex + "Error rendering '%s' for template: %s", trigger_info["name"], ex ) return diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 619ef0e207c..a81afa1323a 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -5,10 +5,6 @@ from functools import partial import voluptuous as vol from homeassistant.components import sensor -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_AT, @@ -23,6 +19,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_change, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -45,11 +42,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] entities: dict[str, CALLBACK_TYPE] = {} removes = [] job = HassJob(action) diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index 7ee1d218171..3c2cf58bca8 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -1,14 +1,11 @@ """Offer time listening automation rules.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -63,11 +60,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) seconds = config.get(CONF_SECONDS) From 453cbc3e14e7d546d937d0848d5a62308a226494 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 18:15:20 +0200 Subject: [PATCH 373/903] Use TriggerActionType [core, t-z] (#76808) --- homeassistant/components/template/trigger.py | 21 ++++++++----------- .../components/update/device_trigger.py | 13 ++++-------- .../components/vacuum/device_trigger.py | 11 ++++------ homeassistant/components/webhook/trigger.py | 19 +++++++---------- homeassistant/components/zone/trigger.py | 13 +++++------- 5 files changed, 30 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 33ac90079b7..7c25e7090a6 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -4,10 +4,6 @@ import logging import voluptuous as vol from homeassistant import exceptions -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template @@ -17,6 +13,7 @@ from homeassistant.helpers.event import ( async_track_template_result, ) from homeassistant.helpers.template import Template, result_as_boolean +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -35,13 +32,13 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "template", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] value_template: Template = config[CONF_VALUE_TEMPLATE] value_template.hass = hass time_delta = config.get(CONF_FOR) @@ -53,13 +50,13 @@ async def async_attach_trigger( # Arm at setup if the template is already false. try: if not result_as_boolean( - value_template.async_render(automation_info["variables"]) + value_template.async_render(trigger_info["variables"]) ): armed = True except exceptions.TemplateError as ex: _LOGGER.warning( "Error initializing 'template' trigger for '%s': %s", - automation_info["name"], + trigger_info["name"], ex, ) @@ -72,7 +69,7 @@ async def async_attach_trigger( if isinstance(result, exceptions.TemplateError): _LOGGER.warning( "Error evaluating 'template' trigger for '%s': %s", - automation_info["name"], + trigger_info["name"], result, ) return @@ -134,7 +131,7 @@ async def async_attach_trigger( ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( - "Error rendering '%s' for template: %s", automation_info["name"], ex + "Error rendering '%s' for template: %s", trigger_info["name"], ex ) return @@ -144,7 +141,7 @@ async def async_attach_trigger( info = async_track_template_result( hass, - [TrackTemplate(value_template, automation_info["variables"])], + [TrackTemplate(value_template, trigger_info["variables"])], template_listener, ) unsub = info.async_remove diff --git a/homeassistant/components/update/device_trigger.py b/homeassistant/components/update/device_trigger.py index ac8113d5708..bd0e1a6e1b7 100644 --- a/homeassistant/components/update/device_trigger.py +++ b/homeassistant/components/update/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -23,13 +20,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_triggers( diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 4b8ec2fc08d..c90aa1756e4 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( @@ -19,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN, STATE_CLEANING, STATE_DOCKED @@ -74,8 +71,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "cleaning": @@ -92,5 +89,5 @@ async def async_attach_trigger( state_config[CONF_FOR] = config[CONF_FOR] state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 498a7363a61..262d77e61f7 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -6,13 +6,10 @@ from dataclasses import dataclass from aiohttp import hdrs import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN, async_register, async_unregister @@ -35,7 +32,7 @@ WEBHOOK_TRIGGERS = f"{DOMAIN}_triggers" class TriggerInstance: """Attached trigger settings.""" - automation_info: AutomationTriggerInfo + trigger_info: TriggerInfo job: HassJob @@ -55,15 +52,15 @@ async def _handle_webhook(hass, webhook_id, request): WEBHOOK_TRIGGERS, {} ) for trigger in triggers[webhook_id]: - result = {**base_result, **trigger.automation_info["trigger_data"]} + result = {**base_result, **trigger.trigger_info["trigger_data"]} hass.async_run_hass_job(trigger.job, {"trigger": result}) async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" webhook_id: str = config[CONF_WEBHOOK_ID] @@ -76,14 +73,14 @@ async def async_attach_trigger( if webhook_id not in triggers: async_register( hass, - automation_info["domain"], - automation_info["name"], + trigger_info["domain"], + trigger_info["name"], webhook_id, _handle_webhook, ) triggers[webhook_id] = [] - trigger_instance = TriggerInstance(automation_info, job) + trigger_instance = TriggerInstance(trigger_info, job) triggers[webhook_id].append(trigger_instance) @callback diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 0865182df80..4958ec102d1 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -3,10 +3,6 @@ import logging import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_ENTITY_ID, @@ -22,6 +18,7 @@ from homeassistant.helpers import ( location, ) from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-defs @@ -62,13 +59,13 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "zone", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] entity_id: list[str] = config[CONF_ENTITY_ID] zone_entity_id = config.get(CONF_ZONE) event = config.get(CONF_EVENT) @@ -91,7 +88,7 @@ async def async_attach_trigger( if not (zone_state := hass.states.get(zone_entity_id)): _LOGGER.warning( "Automation '%s' is referencing non-existing zone '%s' in a zone trigger", - automation_info["name"], + trigger_info["name"], zone_entity_id, ) return From 223ea034926d63560c51dc7ca403d6aa65f57abb Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 15 Aug 2022 19:32:44 +0200 Subject: [PATCH 374/903] Fix Hue events for relative_rotary devices (such as Hue Tap Dial) (#76758) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hue/logbook.py | 3 + homeassistant/components/hue/strings.json | 21 +++-- .../components/hue/translations/en.json | 19 ++-- .../components/hue/v2/device_trigger.py | 88 +++++++++---------- homeassistant/components/hue/v2/hue_event.py | 33 ++++++- 5 files changed, 98 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/hue/logbook.py b/homeassistant/components/hue/logbook.py index e98a99f1861..412ca044b58 100644 --- a/homeassistant/components/hue/logbook.py +++ b/homeassistant/components/hue/logbook.py @@ -28,6 +28,8 @@ TRIGGER_SUBTYPE = { "2": "second button", "3": "third button", "4": "fourth button", + "clock_wise": "Rotation clockwise", + "counter_clock_wise": "Rotation counter-clockwise", } TRIGGER_TYPE = { "remote_button_long_release": "{subtype} released after long press", @@ -40,6 +42,7 @@ TRIGGER_TYPE = { "short_release": "{subtype} released after short press", "long_release": "{subtype} released after long press", "double_short_release": "both {subtype} released", + "start": '"{subtype}" pressed initially', } UNKNOWN_TYPE = "unknown type" diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 0d7c67ec84b..a44eea0fe33 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -49,20 +49,23 @@ "1": "First button", "2": "Second button", "3": "Third button", - "4": "Fourth button" + "4": "Fourth button", + "clock_wise": "Rotation clockwise", + "counter_clock_wise": "Rotation counter-clockwise" }, "trigger_type": { - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", "remote_double_button_long_press": "Both \"{subtype}\" released after long press", "remote_double_button_short_press": "Both \"{subtype}\" released", - "initial_press": "Button \"{subtype}\" pressed initially", - "repeat": "Button \"{subtype}\" held down", - "short_release": "Button \"{subtype}\" released after short press", - "long_release": "Button \"{subtype}\" released after long press", - "double_short_release": "Both \"{subtype}\" released" + "initial_press": "\"{subtype}\" pressed initially", + "repeat": "\"{subtype}\" held down", + "short_release": "\"{subtype}\" released after short press", + "long_release": "\"{subtype}\" released after long press", + "double_short_release": "Both \"{subtype}\" released", + "start": "\"{subtype}\" pressed initially" } }, "options": { diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index f617be431b9..7a54fc5ce03 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -49,19 +49,22 @@ "double_buttons_1_3": "First and Third buttons", "double_buttons_2_4": "Second and Fourth buttons", "turn_off": "Turn off", - "turn_on": "Turn on" + "turn_on": "Turn on", + "clock_wise": "Rotation clockwise", + "counter_clock_wise": "Rotation counter-clockwise" }, "trigger_type": { "double_short_release": "Both \"{subtype}\" released", - "initial_press": "Button \"{subtype}\" pressed initially", - "long_release": "Button \"{subtype}\" released after long press", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", + "initial_press": "\"{subtype}\" pressed initially", + "long_release": "\"{subtype}\" released after long press", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", "remote_double_button_long_press": "Both \"{subtype}\" released after long press", "remote_double_button_short_press": "Both \"{subtype}\" released", - "repeat": "Button \"{subtype}\" held down", - "short_release": "Button \"{subtype}\" released after short press" + "repeat": "\"{subtype}\" held down", + "short_release": "\"{subtype}\" released after short press", + "start": "\"{subtype}\" pressed initially" } }, "options": { diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 2b868a4685c..0fa9e37569d 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -4,10 +4,13 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any from aiohue.v2.models.button import ButtonEvent +from aiohue.v2.models.relative_rotary import ( + RelativeRotaryAction, + RelativeRotaryDirection, +) from aiohue.v2.models.resource import ResourceTypes import voluptuous as vol -from homeassistant.components import persistent_notification from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import ( @@ -49,48 +52,25 @@ DEFAULT_BUTTON_EVENT_TYPES = ( ButtonEvent.LONG_RELEASE, ) +DEFAULT_ROTARY_EVENT_TYPES = (RelativeRotaryAction.START, RelativeRotaryAction.REPEAT) +DEFAULT_ROTARY_EVENT_SUBTYPES = ( + RelativeRotaryDirection.CLOCK_WISE, + RelativeRotaryDirection.COUNTER_CLOCK_WISE, +) + DEVICE_SPECIFIC_EVENT_TYPES = { # device specific overrides of specific supported button events "Hue tap switch": (ButtonEvent.INITIAL_PRESS,), } -def check_invalid_device_trigger( - bridge: HueBridge, - config: ConfigType, - device_entry: DeviceEntry, - automation_info: AutomationTriggerInfo | None = None, -): - """Check automation config for deprecated format.""" - # NOTE: Remove this check after 2022.6 - if isinstance(config["subtype"], int): - return - # found deprecated V1 style trigger, notify the user that it should be adjusted - msg = ( - f"Incompatible device trigger detected for " - f"[{device_entry.name}](/config/devices/device/{device_entry.id}) " - "Please manually fix the outdated automation(s) once to fix this issue." - ) - if automation_info: - automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore[index] - msg += f"\n\n[Check it out](/config/automation/edit/{automation_id})." - persistent_notification.async_create( - bridge.hass, - msg, - title="Outdated device trigger found", - notification_id=f"hue_trigger_{device_entry.id}", - ) - - async def async_validate_trigger_config( bridge: "HueBridge", device_entry: DeviceEntry, config: ConfigType, ) -> ConfigType: """Validate config.""" - config = TRIGGER_SCHEMA(config) - check_invalid_device_trigger(bridge, config, device_entry) - return config + return TRIGGER_SCHEMA(config) async def async_attach_trigger( @@ -113,7 +93,6 @@ async def async_attach_trigger( }, } ) - check_invalid_device_trigger(bridge, config, device_entry, automation_info) return await event_trigger.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" ) @@ -131,22 +110,37 @@ def async_get_triggers( # extract triggers from all button resources of this Hue device triggers = [] model_id = api.devices[hue_dev_id].product_data.product_name + for resource in api.devices.get_sensors(hue_dev_id): - if resource.type != ResourceTypes.BUTTON: - continue - for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( - model_id, DEFAULT_BUTTON_EVENT_TYPES - ): - triggers.append( - { - CONF_DEVICE_ID: device_entry.id, - CONF_DOMAIN: DOMAIN, - CONF_PLATFORM: "device", - CONF_TYPE: event_type.value, - CONF_SUBTYPE: resource.metadata.control_id, - CONF_UNIQUE_ID: resource.id, - } - ) + # button triggers + if resource.type == ResourceTypes.BUTTON: + for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( + model_id, DEFAULT_BUTTON_EVENT_TYPES + ): + triggers.append( + { + CONF_DEVICE_ID: device_entry.id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: event_type.value, + CONF_SUBTYPE: resource.metadata.control_id, + CONF_UNIQUE_ID: resource.id, + } + ) + # relative_rotary triggers + elif resource.type == ResourceTypes.RELATIVE_ROTARY: + for event_type in DEFAULT_ROTARY_EVENT_TYPES: + for sub_type in DEFAULT_ROTARY_EVENT_SUBTYPES: + triggers.append( + { + CONF_DEVICE_ID: device_entry.id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: event_type.value, + CONF_SUBTYPE: sub_type.value, + CONF_UNIQUE_ID: resource.id, + } + ) return triggers diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index 4b9adf16226..8e85288eee0 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.models.button import Button +from aiohue.v2.models.relative_rotary import RelativeRotary from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID from homeassistant.core import callback @@ -14,6 +15,8 @@ from homeassistant.util import slugify from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN as DOMAIN CONF_CONTROL_ID = "control_id" +CONF_DURATION = "duration" +CONF_STEPS = "steps" if TYPE_CHECKING: from ..bridge import HueBridge @@ -28,12 +31,12 @@ async def async_setup_hue_events(bridge: "HueBridge"): conf_entry = bridge.config_entry dev_reg = device_registry.async_get(hass) - # at this time the `button` resource is the only source of hue events btn_controller = api.sensors.button + rotary_controller = api.sensors.relative_rotary @callback def handle_button_event(evt_type: EventType, hue_resource: Button) -> None: - """Handle event from Hue devices controller.""" + """Handle event from Hue button resource controller.""" LOGGER.debug("Received button event: %s", hue_resource) # guard for missing button object on the resource @@ -60,3 +63,29 @@ async def async_setup_hue_events(bridge: "HueBridge"): handle_button_event, event_filter=EventType.RESOURCE_UPDATED ) ) + + @callback + def handle_rotary_event(evt_type: EventType, hue_resource: RelativeRotary) -> None: + """Handle event from Hue relative_rotary resource controller.""" + LOGGER.debug("Received relative_rotary event: %s", hue_resource) + + hue_device = btn_controller.get_device(hue_resource.id) + device = dev_reg.async_get_device({(DOMAIN, hue_device.id)}) + + # Fire event + data = { + CONF_DEVICE_ID: device.id, # type: ignore[union-attr] + CONF_UNIQUE_ID: hue_resource.id, + CONF_TYPE: hue_resource.relative_rotary.last_event.action.value, + CONF_SUBTYPE: hue_resource.relative_rotary.last_event.rotation.direction.value, + CONF_DURATION: hue_resource.relative_rotary.last_event.rotation.duration, + CONF_STEPS: hue_resource.relative_rotary.last_event.rotation.steps, + } + hass.bus.async_fire(ATTR_HUE_EVENT, data) + + # add listener for updates from `relative_rotary` resource + conf_entry.async_on_unload( + rotary_controller.subscribe( + handle_rotary_event, event_filter=EventType.RESOURCE_UPDATED + ) + ) From 702f8180a694cfeb4630e0c9432eb937f6821d6f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 20:00:42 +0200 Subject: [PATCH 375/903] Use TriggerActionType [l-t] (#76813) --- .../components/lcn/device_trigger.py | 11 ++++------ homeassistant/components/litejet/trigger.py | 11 ++++------ .../lutron_caseta/device_trigger.py | 11 ++++------ .../components/nanoleaf/device_trigger.py | 11 ++++------ .../components/nest/device_trigger.py | 11 ++++------ .../components/netatmo/device_trigger.py | 11 ++++------ .../components/philips_js/__init__.py | 4 ++-- .../components/philips_js/device_trigger.py | 11 ++++------ .../components/rfxtrx/device_trigger.py | 11 ++++------ .../components/shelly/device_trigger.py | 11 ++++------ .../components/tasmota/device_trigger.py | 21 ++++++++----------- 11 files changed, 47 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/lcn/device_trigger.py b/homeassistant/components/lcn/device_trigger.py index 8ae640cf6c2..46a94929d0b 100644 --- a/homeassistant/components/lcn/device_trigger.py +++ b/homeassistant/components/lcn/device_trigger.py @@ -3,15 +3,12 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KEY_ACTIONS, SENDKEYS @@ -75,8 +72,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_data = { @@ -97,7 +94,7 @@ async def async_attach_trigger( ) return await event.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index 5aff5dbc66c..a0cdeaf9a01 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -5,14 +5,11 @@ from collections.abc import Callable import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -39,11 +36,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for events based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) held_less_than = config.get(CONF_HELD_LESS_THAN) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 27227619d45..77cad154c06 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -22,6 +18,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import ( @@ -425,8 +422,8 @@ def _device_model_to_type(model: str) -> str: async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" device_registry = dr.async_get(hass) @@ -453,7 +450,7 @@ async def async_attach_trigger( } event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/nanoleaf/device_trigger.py b/homeassistant/components/nanoleaf/device_trigger.py index 68dc6326719..5de093a4a17 100644 --- a/homeassistant/components/nanoleaf/device_trigger.py +++ b/homeassistant/components/nanoleaf/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import DeviceNotFound from homeassistant.components.homeassistant.triggers import event as event_trigger @@ -19,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, NANOLEAF_EVENT, TOUCH_GESTURE_TRIGGER_MAP, TOUCH_MODELS @@ -58,8 +55,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = event_trigger.TRIGGER_SCHEMA( @@ -73,5 +70,5 @@ async def async_attach_trigger( } ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index cb546c87ee4..f7369489e22 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -14,6 +10,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -57,8 +54,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = event_trigger.TRIGGER_SCHEMA( @@ -72,5 +69,5 @@ async def async_attach_trigger( } ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index 25f76307b5f..955671e3dc1 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -26,6 +22,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .climate import STATE_NETATMO_AWAY, STATE_NETATMO_HG, STATE_NETATMO_SCHEDULE @@ -140,8 +137,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" device_registry = dr.async_get(hass) @@ -169,5 +166,5 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 154df3ed214..29c8ab36ba2 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -10,7 +10,6 @@ from typing import Any from haphilipsjs import ConnectionFailure, PhilipsTV from haphilipsjs.typing import SystemType -from homeassistant.components.automation import AutomationActionType from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_VERSION, @@ -21,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, HassJob, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.trigger import TriggerActionType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN @@ -93,7 +93,7 @@ class PluggableAction: return bool(self._actions) @callback - def async_attach(self, action: AutomationActionType, variables: dict[str, Any]): + def async_attach(self, action: TriggerActionType, variables: dict[str, Any]): """Attach a device trigger for turn on.""" @callback diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index eca3158fb15..d7ce9807d64 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -3,15 +3,12 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_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.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import PhilipsTVDataUpdateCoordinator @@ -47,11 +44,11 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] registry: dr.DeviceRegistry = dr.async_get(hass) if (trigger_type := config[CONF_TYPE]) == TRIGGER_TYPE_TURN_ON: variables = { diff --git a/homeassistant/components/rfxtrx/device_trigger.py b/homeassistant/components/rfxtrx/device_trigger.py index 196377dd60f..a2f32395572 100644 --- a/homeassistant/components/rfxtrx/device_trigger.py +++ b/homeassistant/components/rfxtrx/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -20,6 +16,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -91,8 +88,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" config = TRIGGER_SCHEMA(config) @@ -113,5 +110,5 @@ async def async_attach_trigger( ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 0f9fc55ed71..fe253ebacb6 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -5,10 +5,6 @@ from typing import Final import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -23,6 +19,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import get_block_device_wrapper, get_rpc_device_wrapper @@ -140,8 +137,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = { @@ -156,5 +153,5 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index 69cb3a0bd72..79637e90424 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -9,10 +9,6 @@ from hatasmota.models import DiscoveryHashType from hatasmota.trigger import TasmotaTrigger, TasmotaTriggerConfig import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.config_entries import ConfigEntry @@ -22,6 +18,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, TASMOTA_EVENT @@ -51,8 +48,8 @@ DEVICE_TRIGGERS = "tasmota_device_triggers" class TriggerInstance: """Attached trigger settings.""" - action: AutomationActionType = attr.ib() - automation_info: AutomationTriggerInfo = attr.ib() + action: TriggerActionType = attr.ib() + trigger_info: TriggerInfo = attr.ib() trigger: Trigger = attr.ib() remove: CALLBACK_TYPE | None = attr.ib(default=None) @@ -77,7 +74,7 @@ class TriggerInstance: self.trigger.hass, event_config, self.action, - self.automation_info, + self.trigger_info, platform_type="device", ) @@ -96,10 +93,10 @@ class Trigger: trigger_instances: list[TriggerInstance] = attr.ib(factory=list) async def add_trigger( - self, action: AutomationActionType, automation_info: AutomationTriggerInfo + self, action: TriggerActionType, trigger_info: TriggerInfo ) -> Callable[[], None]: """Add Tasmota trigger.""" - instance = TriggerInstance(action, automation_info, self) + instance = TriggerInstance(action, trigger_info, self) self.trigger_instances.append(instance) if self.tasmota_trigger is not None: @@ -303,8 +300,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a device trigger.""" if DEVICE_TRIGGERS not in hass.data: @@ -325,4 +322,4 @@ async def async_attach_trigger( tasmota_trigger=None, ) trigger: Trigger = device_triggers[discovery_id] - return await trigger.add_trigger(action, automation_info) + return await trigger.add_trigger(action, trigger_info) From badbc414fb88f2129258525b803ce7785905d141 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 20:15:57 +0200 Subject: [PATCH 376/903] Use TriggerActionType [w-z] (#76814) --- homeassistant/components/webostv/__init__.py | 4 ++-- .../components/webostv/device_trigger.py | 11 ++++------- homeassistant/components/webostv/trigger.py | 11 ++++------- .../components/webostv/triggers/turn_on.py | 11 ++++------- homeassistant/components/wemo/device_trigger.py | 11 ++++------- homeassistant/components/zha/device_trigger.py | 11 ++++------- .../components/zwave_js/device_trigger.py | 15 ++++++--------- homeassistant/components/zwave_js/trigger.py | 11 ++++------- .../components/zwave_js/triggers/event.py | 11 ++++------- .../components/zwave_js/triggers/value_updated.py | 11 ++++------- 10 files changed, 40 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 32161e6bad6..8b023990590 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -10,7 +10,6 @@ from aiowebostv import WebOsClient, WebOsTvPairError import voluptuous as vol from homeassistant.components import notify as hass_notify -from homeassistant.components.automation import AutomationActionType from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_COMMAND, @@ -30,6 +29,7 @@ from homeassistant.core import ( ) from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.trigger import TriggerActionType from homeassistant.helpers.typing import ConfigType from .const import ( @@ -181,7 +181,7 @@ class PluggableAction: @callback def async_attach( - self, action: AutomationActionType, variables: dict[str, Any] + self, action: TriggerActionType, variables: dict[str, Any] ) -> Callable[[], None]: """Attach a device trigger for turn on.""" diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 9ce49bbe79e..859accc86e6 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -14,6 +10,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import trigger @@ -75,8 +72,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if (trigger_type := config[CONF_TYPE]) == TURN_ON_PLATFORM_TYPE: @@ -88,7 +85,7 @@ async def async_attach_trigger( hass, trigger_config ) return await trigger.async_attach_trigger( - hass, trigger_config, action, automation_info + hass, trigger_config, action, trigger_info ) raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") diff --git a/homeassistant/components/webostv/trigger.py b/homeassistant/components/webostv/trigger.py index 1ad7058e1de..5441917cc31 100644 --- a/homeassistant/components/webostv/trigger.py +++ b/homeassistant/components/webostv/trigger.py @@ -3,12 +3,9 @@ from __future__ import annotations from typing import cast -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .triggers import TriggersPlatformModule, turn_on @@ -39,8 +36,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach trigger of specified platform.""" platform = _get_trigger_platform(config) @@ -48,6 +45,6 @@ async def async_attach_trigger( return cast( CALLBACK_TYPE, await getattr(platform, "async_attach_trigger")( - hass, config, action, automation_info + hass, config, action, trigger_info ), ) diff --git a/homeassistant/components/webostv/triggers/turn_on.py b/homeassistant/components/webostv/triggers/turn_on.py index 71949ce58ce..806b0b4b964 100644 --- a/homeassistant/components/webostv/triggers/turn_on.py +++ b/homeassistant/components/webostv/triggers/turn_on.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from ..const import DOMAIN @@ -39,8 +36,8 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE | None: @@ -57,7 +54,7 @@ async def async_attach_trigger( } ) - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] unsubs = [] diff --git a/homeassistant/components/wemo/device_trigger.py b/homeassistant/components/wemo/device_trigger.py index 973a3358b1b..077c32fb1ad 100644 --- a/homeassistant/components/wemo/device_trigger.py +++ b/homeassistant/components/wemo/device_trigger.py @@ -4,14 +4,11 @@ from __future__ import annotations from pywemo.subscribe import EVENT_TYPE_LONG_PRESS import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN as WEMO_DOMAIN, WEMO_SUBSCRIPTION_EVENT @@ -57,8 +54,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = event_trigger.TRIGGER_SCHEMA( @@ -72,5 +69,5 @@ async def async_attach_trigger( } ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 4ad8eccea1d..94b94b89e40 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -2,10 +2,6 @@ import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -14,6 +10,7 @@ from homeassistant.components.homeassistant.triggers import event as event_trigg from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError, IntegrationError +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN as ZHA_DOMAIN @@ -54,8 +51,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_key: tuple[str, str] = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -79,7 +76,7 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index dfca2eb4b27..76a7f134d17 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -6,10 +6,6 @@ from typing import Any import voluptuous as vol from zwave_js_server.const import CommandClass -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -29,6 +25,7 @@ from homeassistant.helpers import ( device_registry, entity_registry, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import trigger @@ -366,8 +363,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_type = config[CONF_TYPE] @@ -411,7 +408,7 @@ async def async_attach_trigger( event_config = event.TRIGGER_SCHEMA(event_config) return await event.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) if trigger_platform == "state": @@ -427,7 +424,7 @@ async def async_attach_trigger( state_config = await state.async_validate_trigger_config(hass, state_config) return await state.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) if trigger_platform == VALUE_UPDATED_PLATFORM_TYPE: @@ -451,7 +448,7 @@ async def async_attach_trigger( hass, zwave_js_config ) return await trigger.async_attach_trigger( - hass, zwave_js_config, action, automation_info + hass, zwave_js_config, action, trigger_info ) raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") diff --git a/homeassistant/components/zwave_js/trigger.py b/homeassistant/components/zwave_js/trigger.py index 07f89388e67..d1751dc4f4f 100644 --- a/homeassistant/components/zwave_js/trigger.py +++ b/homeassistant/components/zwave_js/trigger.py @@ -4,12 +4,9 @@ from __future__ import annotations from types import ModuleType from typing import cast -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .triggers import event, value_updated @@ -45,8 +42,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach trigger of specified platform.""" platform = _get_trigger_platform(config) @@ -54,6 +51,6 @@ async def async_attach_trigger( return cast( CALLBACK_TYPE, await getattr(platform, "async_attach_trigger")( - hass, config, action, automation_info + hass, config, action, trigger_info ), ) diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 784ae74777b..eecd685cc1b 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -10,10 +10,6 @@ from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.zwave_js.const import ( ATTR_CONFIG_ENTRY_ID, ATTR_EVENT, @@ -32,6 +28,7 @@ from homeassistant.components.zwave_js.helpers import ( from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .helpers import async_bypass_dynamic_config_validation @@ -136,8 +133,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: @@ -156,7 +153,7 @@ async def async_attach_trigger( unsubs = [] job = HassJob(action) - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] @callback def async_on_event(event_data: dict, device: dr.DeviceEntry | None = None) -> None: diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 29b4b4d06d6..6a94ab0577b 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -7,10 +7,6 @@ import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.model.value import Value, get_value_id -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.zwave_js.config_validation import VALUE_SCHEMA from homeassistant.components.zwave_js.const import ( ATTR_COMMAND_CLASS, @@ -34,6 +30,7 @@ from homeassistant.components.zwave_js.helpers import ( from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .helpers import async_bypass_dynamic_config_validation @@ -87,8 +84,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: @@ -108,7 +105,7 @@ async def async_attach_trigger( unsubs = [] job = HassJob(action) - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] @callback def async_on_value_updated( From d8916c3e47d02f9bae08111b033d230529c78d00 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 20:26:49 +0200 Subject: [PATCH 377/903] Use TriggerActionType [a-k] (#76812) --- .../components/arcam_fmj/device_trigger.py | 11 ++++------- .../components/deconz/device_trigger.py | 11 ++++------- .../homekit_controller/device_trigger.py | 17 +++++++---------- .../components/hue/device_trigger.py | 13 +++++-------- .../components/hue/v1/device_trigger.py | 11 ++++------- .../components/hue/v2/device_trigger.py | 11 ++++------- .../components/kodi/device_trigger.py | 19 ++++++++----------- 7 files changed, 36 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 593250e4983..13f1acc7244 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, @@ -18,6 +14,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, EVENT_TURN_ON @@ -57,11 +54,11 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] job = HassJob(action) if config[CONF_TYPE] == "turn_on": diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index e15513bddbf..601fe95616b 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -22,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -668,8 +665,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" event_data: dict[str, int | str] = {} @@ -693,7 +690,7 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(raw_event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 700ab60c47f..ecffb902928 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -10,14 +10,11 @@ from aiohomekit.model.services import ServicesTypes from aiohomekit.utils import clamp_enum_to_char import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS @@ -84,11 +81,11 @@ class TriggerSource: async def async_attach_trigger( self, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] job = HassJob(action) @callback @@ -269,10 +266,10 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" device_id = config[CONF_DEVICE_ID] device = hass.data[TRIGGERS][device_id] - return await device.async_attach_trigger(config, action, automation_info) + return await device.async_attach_trigger(config, action, trigger_info) diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index e8eb7695ed9..069f0d42d8d 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -24,11 +24,8 @@ from .v2.device_trigger import ( ) if TYPE_CHECKING: - from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, - ) from homeassistant.core import HomeAssistant + from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from .bridge import HueBridge @@ -59,8 +56,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" device_id = config[CONF_DEVICE_ID] @@ -75,10 +72,10 @@ async def async_attach_trigger( bridge: HueBridge = hass.data[DOMAIN][conf_entry_id] if bridge.api_version == 1: return await async_attach_trigger_v1( - bridge, device_entry, config, action, automation_info + bridge, device_entry, config, action, trigger_info ) return await async_attach_trigger_v2( - bridge, device_entry, config, action, automation_info + bridge, device_entry, config, action, trigger_info ) raise InvalidDeviceAutomationConfig( f"Device ID {device_id} is not found on any Hue bridge" diff --git a/homeassistant/components/hue/v1/device_trigger.py b/homeassistant/components/hue/v1/device_trigger.py index 579f4b71efb..4316ea65406 100644 --- a/homeassistant/components/hue/v1/device_trigger.py +++ b/homeassistant/components/hue/v1/device_trigger.py @@ -3,10 +3,6 @@ from typing import TYPE_CHECKING import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -22,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN @@ -148,8 +145,8 @@ async def async_attach_trigger( bridge: "HueBridge", device_entry: DeviceEntry, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" hass = bridge.hass @@ -171,7 +168,7 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 0fa9e37569d..62ce0ec1edf 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -29,10 +29,7 @@ from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN if TYPE_CHECKING: from aiohue.v2 import HueBridgeV2 - from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, - ) + from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from ..bridge import HueBridge @@ -77,8 +74,8 @@ async def async_attach_trigger( bridge: "HueBridge", device_entry: DeviceEntry, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" hass = bridge.hass @@ -94,7 +91,7 @@ async def async_attach_trigger( } ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py index 11d0b1567f9..07fcf11c077 100644 --- a/homeassistant/components/kodi/device_trigger.py +++ b/homeassistant/components/kodi/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, @@ -18,6 +14,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, EVENT_TURN_OFF, EVENT_TURN_ON @@ -68,11 +65,11 @@ async def async_get_triggers( def _attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, + action: TriggerActionType, event_type, - automation_info: AutomationTriggerInfo, + trigger_info: TriggerInfo, ): - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] job = HassJob(action) @callback @@ -90,14 +87,14 @@ def _attach_trigger( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "turn_on": - return _attach_trigger(hass, config, action, EVENT_TURN_ON, automation_info) + return _attach_trigger(hass, config, action, EVENT_TURN_ON, trigger_info) if config[CONF_TYPE] == "turn_off": - return _attach_trigger(hass, config, action, EVENT_TURN_OFF, automation_info) + return _attach_trigger(hass, config, action, EVENT_TURN_OFF, trigger_info) return lambda: None From 4890785299be7b7e7984e5162415a9bdbf9e1fac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Aug 2022 10:19:37 -1000 Subject: [PATCH 378/903] Fix bluetooth callback registration not surviving a reload (#76817) --- .../components/bluetooth/__init__.py | 21 ++-- tests/components/bluetooth/test_init.py | 97 +++++++++++++++++++ 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index a5204d50b68..19df484c4e1 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -364,13 +364,13 @@ class BluetoothManager: async with async_timeout.timeout(START_TIMEOUT): await self.scanner.start() # type: ignore[no-untyped-call] except InvalidMessageError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) raise ConfigEntryNotReady( f"Invalid DBus message received: {ex}; try restarting `dbus`" ) from ex except BrokenPipeError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) if is_docker_env(): raise ConfigEntryNotReady( @@ -380,7 +380,7 @@ class BluetoothManager: f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" ) from ex except FileNotFoundError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug( "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True ) @@ -392,12 +392,12 @@ class BluetoothManager: f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" ) from ex except asyncio.TimeoutError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() raise ConfigEntryNotReady( f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" ) from ex except BleakError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() @@ -579,15 +579,20 @@ class BluetoothManager: self._cancel_stop = None await self.async_stop() + @hass_callback + def _async_cancel_scanner_callback(self) -> None: + """Cancel the scanner callback.""" + if self._cancel_device_detected: + self._cancel_device_detected() + self._cancel_device_detected = None + async def async_stop(self) -> None: """Stop bluetooth discovery.""" _LOGGER.debug("Stopping bluetooth discovery") if self._cancel_watchdog: self._cancel_watchdog() self._cancel_watchdog = None - if self._cancel_device_detected: - self._cancel_device_detected() - self._cancel_device_detected = None + self._async_cancel_scanner_callback() if self._cancel_unavailable_tracking: self._cancel_unavailable_tracking() self._cancel_unavailable_tracking = None diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 45babd05748..796b3ffb469 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -165,6 +165,43 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): await hass.async_block_till_done() +async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): + """Test we can successfully reload when the entry is in a retry state.""" + mock_bt = [] + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BleakError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + + assert "Failed to start Bluetooth" in caplog.text + assert len(bluetooth.async_discovered_service_info(hass)) == 0 + assert entry.state == ConfigEntryState.SETUP_RETRY + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop", + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] @@ -868,6 +905,66 @@ async def test_register_callback_by_address( assert service_info.manufacturer_id == 89 +async def test_register_callback_survives_reload( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by address survives bluetooth being reloaded.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + assert len(callbacks) == 1 + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + assert len(callbacks) == 2 + service_info: BluetoothServiceInfo = callbacks[1][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + async def test_process_advertisements_bail_on_good_advertisement( hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth ): From f400a404cdc3c647fc136af3bc6001c54a2cfb7f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 23:27:08 +0200 Subject: [PATCH 379/903] Update pylint to 2.14.5 (#76821) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6685d06c3b0..63634bc4022 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.971 pre-commit==2.20.0 -pylint==2.14.4 +pylint==2.14.5 pipdeptree==2.2.1 pytest-aiohttp==0.3.0 pytest-cov==3.0.0 From 1c1b23ef69822850abc698b56576489403310087 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 15 Aug 2022 23:35:30 +0200 Subject: [PATCH 380/903] Correct referenced entities and devices for event triggers (#76818) --- .../components/automation/__init__.py | 8 +++-- tests/components/automation/test_init.py | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 43ff31dfab8..7421e95f293 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -36,6 +36,7 @@ from homeassistant.core import ( HomeAssistant, callback, split_entity_id, + valid_entity_id, ) from homeassistant.exceptions import ( ConditionError, @@ -355,7 +356,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): referenced |= condition.async_extract_devices(conf) for conf in self._trigger_config: - referenced |= set(_trigger_extract_device(conf)) + referenced |= set(_trigger_extract_devices(conf)) self._referenced_devices = referenced return referenced @@ -762,7 +763,7 @@ async def _async_process_if(hass, name, config, p_config): @callback -def _trigger_extract_device(trigger_conf: dict) -> list[str]: +def _trigger_extract_devices(trigger_conf: dict) -> list[str]: """Extract devices from a trigger config.""" if trigger_conf[CONF_PLATFORM] == "device": return [trigger_conf[CONF_DEVICE_ID]] @@ -771,6 +772,7 @@ def _trigger_extract_device(trigger_conf: dict) -> list[str]: trigger_conf[CONF_PLATFORM] == "event" and CONF_EVENT_DATA in trigger_conf and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA] + and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID], str) ): return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]] @@ -802,6 +804,8 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]: trigger_conf[CONF_PLATFORM] == "event" and CONF_EVENT_DATA in trigger_conf and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA] + and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID], str) + and valid_entity_id(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]) ): return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]] diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index bcbcf382892..cef553653de 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1103,6 +1103,24 @@ async def test_extraction_functions(hass): "event_type": "state_changed", "event_data": {"entity_id": "sensor.trigger_event"}, }, + # entity_id is a list of strings (not supported) + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": ["sensor.trigger_event2"]}, + }, + # entity_id is not a valid entity ID + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": "abc"}, + }, + # entity_id is not a string + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": 123}, + }, ], "condition": { "condition": "state", @@ -1151,6 +1169,18 @@ async def test_extraction_functions(hass): "event_type": "esphome.button_pressed", "event_data": {"device_id": "device-trigger-event"}, }, + # device_id is a list of strings (not supported) + { + "platform": "event", + "event_type": "esphome.button_pressed", + "event_data": {"device_id": ["device-trigger-event"]}, + }, + # device_id is not a string + { + "platform": "event", + "event_type": "esphome.button_pressed", + "event_data": {"device_id": 123}, + }, ], "condition": { "condition": "device", From ff3fd4c29d09b1cc5d3158643ff1dad3de5c9cbb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 16 Aug 2022 00:30:51 +0000 Subject: [PATCH 381/903] [ci skip] Translation update --- .../components/acmeda/translations/es.json | 2 +- .../components/airtouch4/translations/es.json | 2 +- .../components/airvisual/translations/es.json | 4 +- .../components/almond/translations/es.json | 2 +- .../android_ip_webcam/translations/fi.json | 12 +++ .../android_ip_webcam/translations/ja.json | 25 +++++ .../android_ip_webcam/translations/ru.json | 26 ++++++ .../components/androidtv/translations/es.json | 2 +- .../components/apple_tv/translations/es.json | 16 ++-- .../components/asuswrt/translations/es.json | 4 +- .../components/atag/translations/es.json | 2 +- .../aurora_abb_powerone/translations/es.json | 6 +- .../components/auth/translations/es.json | 6 +- .../components/awair/translations/ca.json | 9 +- .../components/awair/translations/el.json | 15 ++- .../components/awair/translations/fi.json | 16 ++++ .../components/awair/translations/ja.json | 31 ++++++- .../components/awair/translations/no.json | 31 ++++++- .../components/awair/translations/ru.json | 31 ++++++- .../components/bluetooth/translations/es.json | 2 +- .../components/bosch_shc/translations/es.json | 4 +- .../components/broadlink/translations/fr.json | 2 +- .../components/cast/translations/es.json | 4 +- .../components/climate/translations/es.json | 6 +- .../components/cover/translations/es.json | 4 +- .../components/demo/translations/ca.json | 2 +- .../components/demo/translations/ja.json | 6 +- .../deutsche_bahn/translations/ca.json | 1 + .../deutsche_bahn/translations/es.json | 2 +- .../device_tracker/translations/es.json | 2 +- .../components/doorbird/translations/es.json | 2 +- .../components/dsmr/translations/ja.json | 1 + .../components/eafm/translations/es.json | 2 +- .../components/elkm1/translations/es.json | 2 +- .../components/elmax/translations/es.json | 2 +- .../components/escea/translations/ja.json | 5 + .../components/esphome/translations/es.json | 4 +- .../components/fivem/translations/es.json | 2 +- .../flunearyou/translations/ca.json | 1 + .../flunearyou/translations/es.json | 2 +- .../flunearyou/translations/it.json | 1 + .../forecast_solar/translations/es.json | 2 +- .../forked_daapd/translations/es.json | 2 +- .../fritzbox_callmonitor/translations/es.json | 2 +- .../components/generic/translations/es.json | 4 +- .../geocaching/translations/es.json | 2 +- .../components/github/translations/es.json | 2 +- .../components/google/translations/es.json | 4 +- .../components/group/translations/es.json | 4 +- .../components/guardian/translations/ca.json | 13 +++ .../components/guardian/translations/es.json | 2 +- .../components/guardian/translations/ja.json | 12 +++ .../components/guardian/translations/ru.json | 13 +++ .../components/habitica/translations/es.json | 2 +- .../components/hassio/translations/es.json | 2 +- .../components/hive/translations/es.json | 2 +- .../home_connect/translations/es.json | 2 +- .../home_plus_control/translations/es.json | 2 +- .../components/homekit/translations/es.json | 2 +- .../homekit_controller/translations/es.json | 4 +- .../translations/sensor.it.json | 15 ++- .../homematicip_cloud/translations/es.json | 6 +- .../components/honeywell/translations/es.json | 2 +- .../huawei_lte/translations/es.json | 2 +- .../components/hue/translations/ca.json | 17 ++-- .../components/hue/translations/en.json | 6 +- .../components/hue/translations/es.json | 4 +- .../components/hue/translations/id.json | 17 ++-- .../components/hue/translations/pt-BR.json | 17 ++-- .../humidifier/translations/es.json | 2 +- .../components/hyperion/translations/es.json | 2 +- .../components/icloud/translations/es.json | 2 +- .../components/insteon/translations/es.json | 6 +- .../components/ipma/translations/es.json | 2 +- .../components/ipp/translations/es.json | 2 +- .../justnimbus/translations/ja.json | 19 ++++ .../components/knx/translations/es.json | 8 +- .../components/kodi/translations/es.json | 2 +- .../components/konnected/translations/es.json | 2 +- .../components/kraken/translations/ja.json | 3 + .../components/lifx/translations/es.json | 2 +- .../components/mailgun/translations/es.json | 2 +- .../components/mazda/translations/es.json | 4 +- .../components/meater/translations/es.json | 2 +- .../media_player/translations/es.json | 2 +- .../met_eireann/translations/es.json | 2 +- .../meteo_france/translations/es.json | 2 +- .../components/metoffice/translations/es.json | 2 +- .../components/miflora/translations/es.json | 2 +- .../minecraft_server/translations/es.json | 6 +- .../components/mitemp_bt/translations/ca.json | 2 +- .../components/mitemp_bt/translations/es.json | 2 +- .../motion_blinds/translations/es.json | 4 +- .../components/mysensors/translations/ja.json | 2 + .../components/nam/translations/es.json | 2 +- .../components/neato/translations/es.json | 2 +- .../components/nest/translations/ca.json | 2 + .../components/nest/translations/es.json | 14 +-- .../components/netatmo/translations/es.json | 4 +- .../components/netgear/translations/es.json | 2 +- .../components/nina/translations/es.json | 2 +- .../nmap_tracker/translations/es.json | 2 +- .../components/nws/translations/es.json | 2 +- .../components/onvif/translations/es.json | 6 +- .../openexchangerates/translations/ca.json | 1 + .../openexchangerates/translations/ja.json | 9 +- .../components/overkiz/translations/es.json | 4 +- .../ovo_energy/translations/es.json | 2 +- .../components/plaato/translations/es.json | 4 +- .../components/plugwise/translations/es.json | 2 +- .../components/point/translations/es.json | 8 +- .../components/qingping/translations/ca.json | 22 +++++ .../components/qingping/translations/de.json | 22 +++++ .../components/qingping/translations/el.json | 22 +++++ .../components/qingping/translations/es.json | 22 +++++ .../components/qingping/translations/et.json | 22 +++++ .../components/qingping/translations/fi.json | 7 ++ .../components/qingping/translations/fr.json | 22 +++++ .../components/qingping/translations/hu.json | 22 +++++ .../components/qingping/translations/id.json | 22 +++++ .../components/qingping/translations/it.json | 22 +++++ .../components/qingping/translations/ja.json | 22 +++++ .../components/qingping/translations/no.json | 22 +++++ .../qingping/translations/pt-BR.json | 22 +++++ .../components/qingping/translations/ru.json | 22 +++++ .../qingping/translations/zh-Hant.json | 22 +++++ .../components/roon/translations/ja.json | 3 + .../rtsp_to_webrtc/translations/es.json | 8 +- .../components/samsungtv/translations/es.json | 2 +- .../components/schedule/translations/el.json | 6 ++ .../components/schedule/translations/fi.json | 3 + .../components/schedule/translations/ja.json | 9 ++ .../components/schedule/translations/no.json | 9 ++ .../components/schedule/translations/ru.json | 9 ++ .../components/scrape/translations/es.json | 12 +-- .../components/select/translations/es.json | 2 +- .../components/sensor/translations/es.json | 92 +++++++++---------- .../components/senz/translations/es.json | 2 +- .../components/shelly/translations/es.json | 2 +- .../simplisafe/translations/ca.json | 2 +- .../simplisafe/translations/es.json | 2 +- .../simplisafe/translations/it.json | 2 +- .../simplisafe/translations/ja.json | 1 + .../components/smappee/translations/es.json | 4 +- .../smartthings/translations/es.json | 6 +- .../components/solarlog/translations/es.json | 2 +- .../components/spotify/translations/es.json | 2 +- .../components/sql/translations/es.json | 4 +- .../steam_online/translations/es.json | 2 +- .../components/subaru/translations/es.json | 6 +- .../switch_as_x/translations/es.json | 2 +- .../components/switchbot/translations/el.json | 3 + .../components/switchbot/translations/fi.json | 11 +++ .../components/switchbot/translations/ja.json | 12 +++ .../components/switchbot/translations/ru.json | 9 ++ .../tellduslive/translations/es.json | 2 +- .../tesla_wall_connector/translations/es.json | 2 +- .../tomorrowio/translations/es.json | 2 +- .../components/toon/translations/es.json | 2 +- .../totalconnect/translations/es.json | 2 +- .../components/traccar/translations/es.json | 2 +- .../components/tractive/translations/es.json | 2 +- .../transmission/translations/es.json | 2 +- .../components/twilio/translations/es.json | 2 +- .../components/unifi/translations/es.json | 12 +-- .../unifiprotect/translations/ca.json | 1 + .../unifiprotect/translations/es.json | 2 +- .../unifiprotect/translations/ja.json | 1 + .../components/upb/translations/es.json | 4 +- .../components/update/translations/es.json | 2 +- .../components/upnp/translations/ja.json | 3 + .../uptimerobot/translations/es.json | 2 +- .../components/uscis/translations/es.json | 2 +- .../components/version/translations/es.json | 4 +- .../components/vulcan/translations/es.json | 2 +- .../components/webostv/translations/es.json | 2 +- .../components/withings/translations/es.json | 2 +- .../components/wiz/translations/es.json | 2 +- .../xiaomi_aqara/translations/es.json | 6 +- .../xiaomi_ble/translations/ca.json | 6 ++ .../xiaomi_miio/translations/es.json | 10 +- .../yalexs_ble/translations/ca.json | 8 +- .../yalexs_ble/translations/el.json | 11 ++- .../yalexs_ble/translations/es.json | 2 +- .../yalexs_ble/translations/et.json | 3 +- .../yalexs_ble/translations/hu.json | 1 + .../yalexs_ble/translations/ja.json | 28 ++++++ .../yalexs_ble/translations/no.json | 3 +- .../yalexs_ble/translations/ru.json | 31 +++++++ .../yalexs_ble/translations/zh-Hant.json | 3 +- .../components/yeelight/translations/es.json | 4 +- .../components/yolink/translations/es.json | 2 +- .../components/zwave_js/translations/es.json | 10 +- 193 files changed, 1048 insertions(+), 302 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/translations/fi.json create mode 100644 homeassistant/components/android_ip_webcam/translations/ja.json create mode 100644 homeassistant/components/android_ip_webcam/translations/ru.json create mode 100644 homeassistant/components/awair/translations/fi.json create mode 100644 homeassistant/components/justnimbus/translations/ja.json create mode 100644 homeassistant/components/qingping/translations/ca.json create mode 100644 homeassistant/components/qingping/translations/de.json create mode 100644 homeassistant/components/qingping/translations/el.json create mode 100644 homeassistant/components/qingping/translations/es.json create mode 100644 homeassistant/components/qingping/translations/et.json create mode 100644 homeassistant/components/qingping/translations/fi.json create mode 100644 homeassistant/components/qingping/translations/fr.json create mode 100644 homeassistant/components/qingping/translations/hu.json create mode 100644 homeassistant/components/qingping/translations/id.json create mode 100644 homeassistant/components/qingping/translations/it.json create mode 100644 homeassistant/components/qingping/translations/ja.json create mode 100644 homeassistant/components/qingping/translations/no.json create mode 100644 homeassistant/components/qingping/translations/pt-BR.json create mode 100644 homeassistant/components/qingping/translations/ru.json create mode 100644 homeassistant/components/qingping/translations/zh-Hant.json create mode 100644 homeassistant/components/schedule/translations/fi.json create mode 100644 homeassistant/components/schedule/translations/ja.json create mode 100644 homeassistant/components/schedule/translations/no.json create mode 100644 homeassistant/components/schedule/translations/ru.json create mode 100644 homeassistant/components/switchbot/translations/fi.json create mode 100644 homeassistant/components/yalexs_ble/translations/ja.json create mode 100644 homeassistant/components/yalexs_ble/translations/ru.json diff --git a/homeassistant/components/acmeda/translations/es.json b/homeassistant/components/acmeda/translations/es.json index 6e336c0315b..0eb22f132bd 100644 --- a/homeassistant/components/acmeda/translations/es.json +++ b/homeassistant/components/acmeda/translations/es.json @@ -8,7 +8,7 @@ "data": { "id": "ID de host" }, - "title": "Elige un hub para a\u00f1adir" + "title": "Elige un concentrador para a\u00f1adir" } } } diff --git a/homeassistant/components/airtouch4/translations/es.json b/homeassistant/components/airtouch4/translations/es.json index 65616d2a2e9..dd7f1044b20 100644 --- a/homeassistant/components/airtouch4/translations/es.json +++ b/homeassistant/components/airtouch4/translations/es.json @@ -12,7 +12,7 @@ "data": { "host": "Host" }, - "title": "Configura los detalles de conexi\u00f3n de tu AirTouch 4." + "title": "Configurar los detalles de conexi\u00f3n de tu AirTouch 4." } } } diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 739aaa818ed..acffe47f3ca 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -17,7 +17,7 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "Utiliza la API de nube de AirVisual para supervisar una latitud/longitud.", + "description": "Usar la API de la nube de AirVisual para supervisar una latitud/longitud.", "title": "Configurar una geograf\u00eda" }, "geography_by_name": { @@ -27,7 +27,7 @@ "country": "Pa\u00eds", "state": "estado" }, - "description": "Utiliza la API en la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", + "description": "Usar la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", "title": "Configurar una geograf\u00eda" }, "node_pro": { diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index 1a5b3ddf074..7c0a80ef444 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "No se pudo conectar", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { diff --git a/homeassistant/components/android_ip_webcam/translations/fi.json b/homeassistant/components/android_ip_webcam/translations/fi.json new file mode 100644 index 00000000000..61febe9dd9c --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/fi.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Salasana", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/ja.json b/homeassistant/components/android_ip_webcam/translations/ja.json new file mode 100644 index 00000000000..832d6b9c71c --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "Android IP Webcam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/ru.json b/homeassistant/components/android_ip_webcam/translations/ru.json new file mode 100644 index 00000000000..deeac93856b --- /dev/null +++ b/homeassistant/components/android_ip_webcam/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." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Android IP Webcam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Android IP Webcam \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/es.json b/homeassistant/components/androidtv/translations/es.json index b41827566ab..bc6f9f6996f 100644 --- a/homeassistant/components/androidtv/translations/es.json +++ b/homeassistant/components/androidtv/translations/es.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "adb_server_ip": "Direcci\u00f3n IP del servidor ADB (d\u00e9jalo vac\u00edo para no utilizarlo)", + "adb_server_ip": "Direcci\u00f3n IP del servidor ADB (d\u00e9jalo vac\u00edo para no usarlo)", "adb_server_port": "Puerto del servidor ADB", "adbkey": "Ruta a tu archivo de clave ADB (d\u00e9jalo en blanco para generarlo autom\u00e1ticamente)", "device_class": "Tipo de dispositivo", diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index d1654272818..3881692d0be 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -3,10 +3,10 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), intenta de nuevo m\u00e1s tarde.", + "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), vuelve a intentarlo m\u00e1s tarde.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", - "device_not_found": "No se encontr\u00f3 el dispositivo durante el descubrimiento, por favor intenta a\u00f1adirlo nuevamente.", - "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo nuevamente.", + "device_not_found": "No se encontr\u00f3 el dispositivo durante el descubrimiento, por favor, intenta a\u00f1adi\u00e9ndolo de nuevo.", + "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo de nuevo.", "ipv6_not_supported": "IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", @@ -22,8 +22,8 @@ "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios en Home Assistant!", - "title": "Confirma para a\u00f1adir Apple TV" + "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nPor favor, ten en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios en Home Assistant!", + "title": "Confirmar la adici\u00f3n de Apple TV" }, "pair_no_pin": { "description": "Se requiere emparejamiento para el servicio `{protocol}`. Por favor, introduce el PIN {pin} en tu dispositivo para continuar.", @@ -33,15 +33,15 @@ "data": { "pin": "C\u00f3digo PIN" }, - "description": "El emparejamiento es necesario para el protocolo `{protocol}`. Introduce el c\u00f3digo PIN que aparece en la pantalla. Los ceros iniciales deben ser omitidos, es decir, introduce 123 si el c\u00f3digo mostrado es 0123.", + "description": "El emparejamiento es necesario para el protocolo `{protocol}`. Por favor, introduce el c\u00f3digo PIN que aparece en la pantalla. Los ceros iniciales deben ser omitidos, es decir, introduce 123 si el c\u00f3digo mostrado es 0123.", "title": "Emparejamiento" }, "password": { - "description": "Se requiere una contrase\u00f1a por `{protocol}`. Esto a\u00fan no es compatible, por favor deshabilita la contrase\u00f1a para continuar.", + "description": "Se requiere una contrase\u00f1a por `{protocol}`. Esto a\u00fan no es compatible, por favor, deshabilita la contrase\u00f1a para continuar.", "title": "Se requiere contrase\u00f1a" }, "protocol_disabled": { - "description": "Se requiere emparejamiento para `{protocol}` pero est\u00e1 deshabilitado en el dispositivo. Revisa las posibles restricciones de acceso (p. ej., permitir que todos los dispositivos de la red local se conecten) en el dispositivo. \n\nPuedes continuar sin emparejar este protocolo, pero algunas funciones estar\u00e1n limitadas.", + "description": "Se requiere emparejamiento para `{protocol}` pero est\u00e1 deshabilitado en el dispositivo. Por favor, revisa las posibles restricciones de acceso (p. ej., permitir que todos los dispositivos de la red local se conecten) en el dispositivo. \n\nPuedes continuar sin emparejar este protocolo, pero algunas funciones estar\u00e1n limitadas.", "title": "No es posible el emparejamiento" }, "reconfigure": { diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index 57e7bb4cde6..3b6871093c4 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -20,7 +20,7 @@ "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto (dejar vac\u00edo para el predeterminado del protocolo)", - "protocol": "Protocolo de comunicaci\u00f3n a utilizar", + "protocol": "Protocolo de comunicaci\u00f3n a usar", "ssh_key": "Ruta a tu archivo de clave SSH (en lugar de contrase\u00f1a)", "username": "Nombre de usuario" }, @@ -37,7 +37,7 @@ "dnsmasq": "La ubicaci\u00f3n en el router de los archivos dnsmasq.leases", "interface": "La interfaz de la que quieres 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" + "track_unknown": "Rastrear dispositivos desconocidos/sin nombre" }, "title": "Opciones de AsusWRT" } diff --git a/homeassistant/components/atag/translations/es.json b/homeassistant/components/atag/translations/es.json index c1bc879f64d..c65d1b536c1 100644 --- a/homeassistant/components/atag/translations/es.json +++ b/homeassistant/components/atag/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "unauthorized": "Emparejamiento denegado, verifica el dispositivo para la solicitud de autenticaci\u00f3n" + "unauthorized": "Emparejamiento denegado, comprueba el dispositivo para la solicitud de autenticaci\u00f3n" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/es.json b/homeassistant/components/aurora_abb_powerone/translations/es.json index 537ee81a30e..e14da3c18f7 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/es.json +++ b/homeassistant/components/aurora_abb_powerone/translations/es.json @@ -5,8 +5,8 @@ "no_serial_ports": "No se encontraron puertos de comunicaciones. Necesitas un dispositivo RS485 v\u00e1lido para comunicarse." }, "error": { - "cannot_connect": "No se puede conectar, por favor, verifica el puerto serie, la direcci\u00f3n, la conexi\u00f3n el\u00e9ctrica y que el inversor est\u00e9 encendido (durante la luz del d\u00eda)", - "cannot_open_serial_port": "No se puede abrir el puerto serie, por favor, verif\u00edcalo e int\u00e9ntalo de nuevo", + "cannot_connect": "No se puede conectar, por favor, comprueba el puerto serie, la direcci\u00f3n, la conexi\u00f3n el\u00e9ctrica y que el inversor est\u00e9 encendido (durante la luz del d\u00eda)", + "cannot_open_serial_port": "No se puede abrir el puerto serie, por favor, compru\u00e9balo e int\u00e9ntalo de nuevo", "invalid_serial_port": "El puerto serie no es un dispositivo v\u00e1lido o no se pudo abrir" }, "step": { @@ -15,7 +15,7 @@ "address": "Direcci\u00f3n del inversor", "port": "Puerto adaptador RS485 o USB-RS485" }, - "description": "El inversor debe estar conectado a trav\u00e9s de un adaptador RS485, por favor, selecciona el puerto serie y la direcci\u00f3n del inversor seg\u00fan lo configurado en el panel LCD" + "description": "El inversor debe estar conectado a trav\u00e9s de un adaptador RS485. Por favor, selecciona el puerto serie y la direcci\u00f3n del inversor seg\u00fan lo configurado en el panel LCD" } } } diff --git a/homeassistant/components/auth/translations/es.json b/homeassistant/components/auth/translations/es.json index 85f412f0814..f0c70702847 100644 --- a/homeassistant/components/auth/translations/es.json +++ b/homeassistant/components/auth/translations/es.json @@ -5,11 +5,11 @@ "no_available_service": "No hay servicios de notificaci\u00f3n disponibles." }, "error": { - "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, vuelve a intentarlo." + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "step": { "init": { - "description": "Selecciona uno de los servicios de notificaci\u00f3n:", + "description": "Por favor, selecciona uno de los servicios de notificaci\u00f3n:", "title": "Configurar una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" }, "setup": { @@ -21,7 +21,7 @@ }, "totp": { "error": { - "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, vuelve a intentarlo. Si recibes este error constantemente, aseg\u00farate de que el reloj de tu sistema Home Assistant sea exacto." + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, int\u00e9ntalo de nuevo. Si recibes este error constantemente, aseg\u00farate de que el reloj de tu sistema Home Assistant sea exacto." }, "step": { "init": { diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index eaf255a0532..b5c16826078 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -19,7 +19,8 @@ "data": { "access_token": "Token d'acc\u00e9s", "email": "Correu electr\u00f2nic" - } + }, + "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a: {url}" }, "discovery_confirm": { "description": "Vols configurar {model} ({device_id})?" @@ -27,7 +28,8 @@ "local": { "data": { "host": "Adre\u00e7a IP" - } + }, + "description": "L'API local d'Awair s'ha d'activar seguint aquests passos: {url}" }, "reauth": { "data": { @@ -48,8 +50,9 @@ "access_token": "Token d'acc\u00e9s", "email": "Correu electr\u00f2nic" }, - "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a trav\u00e9s de l'enlla\u00e7 seg\u00fcent: https://developer.getawair.com/onboard/login", + "description": "Tria local per a la millor experi\u00e8ncia. Utilitza 'al n\u00favol' si el teu dispositiu no est\u00e0 connectat a la mateixa xarxa que Home Assistant, o si tens un dispositiu antic.", "menu_options": { + "cloud": "Connecta't a trav\u00e9s del n\u00favol", "local": "Connecta't localment (preferit)" } } diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 4cd1ac93d0a..75447f29c15 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -2,22 +2,33 @@ "config": { "abort": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_configured_account": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_configured_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "unreachable": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "error": { "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unreachable": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "flow_title": "{model} ({device_id})", "step": { "cloud": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "email": "Email" + }, "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: {url}" }, "discovery_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} ({device_id});" }, "local": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, "description": "\u03a4\u03bf Awair Local API \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1: {url}" }, "reauth": { diff --git a/homeassistant/components/awair/translations/fi.json b/homeassistant/components/awair/translations/fi.json new file mode 100644 index 00000000000..edbdcb1b086 --- /dev/null +++ b/homeassistant/components/awair/translations/fi.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "cloud": { + "data": { + "email": "S\u00e4hk\u00f6posti" + } + }, + "local": { + "data": { + "host": "IP Osoite" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 7c7b73b312f..6599af1bd14 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "unreachable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unreachable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "email": "E\u30e1\u30fc\u30eb" + }, + "description": "Awair\u958b\u767a\u8005\u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3\u3092\u767b\u9332\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {url}" + }, + "discovery_confirm": { + "description": "{model} ({device_id}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "local": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9" + }, + "description": "\u6b21\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001Awair Local API\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {url}" + }, "reauth": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", @@ -29,7 +50,11 @@ "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "email": "E\u30e1\u30fc\u30eb" }, - "description": "Awair developer access token\u306e\u767b\u9332\u306f\u4ee5\u4e0b\u306e\u30b5\u30a4\u30c8\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://developer.getawair.com/onboard/login" + "description": "Awair developer access token\u306e\u767b\u9332\u306f\u4ee5\u4e0b\u306e\u30b5\u30a4\u30c8\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "\u30af\u30e9\u30a6\u30c9\u7d4c\u7531\u3067\u63a5\u7d9a", + "local": "\u30ed\u30fc\u30ab\u30eb\u306b\u63a5\u7d9a(\u63a8\u5968)" + } } } } diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index 13232ca37df..9ffbf544909 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", + "already_configured_account": "Kontoen er allerede konfigurert", + "already_configured_device": "Enheten er allerede konfigurert", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "unreachable": "Tilkobling mislyktes" }, "error": { "invalid_access_token": "Ugyldig tilgangstoken", - "unknown": "Uventet feil" + "unknown": "Uventet feil", + "unreachable": "Tilkobling mislyktes" }, + "flow_title": "{model} ( {device_id} )", "step": { + "cloud": { + "data": { + "access_token": "Tilgangstoken", + "email": "E-post" + }, + "description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: {url}" + }, + "discovery_confirm": { + "description": "Vil du konfigurere {model} ( {device_id} )?" + }, + "local": { + "data": { + "host": "IP adresse" + }, + "description": "Awair Local API m\u00e5 aktiveres ved \u00e5 f\u00f8lge disse trinnene: {url}" + }, "reauth": { "data": { "access_token": "Tilgangstoken", @@ -29,7 +50,11 @@ "access_token": "Tilgangstoken", "email": "E-post" }, - "description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: https://developer.getawair.com/onboard/login" + "description": "Velg lokal for den beste opplevelsen. Bruk bare sky hvis enheten ikke er koblet til samme nettverk som Home Assistant, eller hvis du har en eldre enhet.", + "menu_options": { + "cloud": "Koble til via skyen", + "local": "Koble til lokalt (foretrukket)" + } } } } diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index 23424091565..c0be2df4b0a 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -2,14 +2,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.", + "already_configured_account": "\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_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "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.", - "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." + "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.", + "unreachable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "error": { "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unreachable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a Awair \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {url}" + }, + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 API Awair, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0437\u0434\u0435\u0441\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f: {url}" + }, "reauth": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", @@ -29,7 +50,11 @@ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, - "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a Awair \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: https://developer.getawair.com/onboard/login" + "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u043a\u043e, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0438\u043b\u0438 \u0435\u0441\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u0442\u043e\u0439 \u0436\u0435 \u0441\u0435\u0442\u0438, \u0447\u0442\u043e \u0438 Home Assistant. \u0412 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445 \u043e\u0442\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u043c\u0443 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044e.", + "menu_options": { + "cloud": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u043e\u0431\u043b\u0430\u043a\u043e", + "local": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e (\u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0438\u0442\u0435\u043b\u044c\u043d\u043e)" + } } } } diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json index ec970720228..1dc8669a067 100644 --- a/homeassistant/components/bluetooth/translations/es.json +++ b/homeassistant/components/bluetooth/translations/es.json @@ -24,7 +24,7 @@ "step": { "init": { "data": { - "adapter": "El adaptador Bluetooth que se utilizar\u00e1 para escanear" + "adapter": "El adaptador Bluetooth que se usar\u00e1 para escanear" } } } diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json index 7a019e277a4..b2934bca747 100644 --- a/homeassistant/components/bosch_shc/translations/es.json +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "pairing_failed": "Emparejamiento fallido; Verifica que el Smart Home Controller de Bosch est\u00e9 en modo de emparejamiento (el LED parpadea) y que tu contrase\u00f1a sea correcta.", + "pairing_failed": "Emparejamiento fallido; por favor, comprueba que el Smart Home Controller de Bosch est\u00e9 en modo de emparejamiento (el LED parpadea) y que tu contrase\u00f1a sea correcta.", "session_error": "Error de sesi\u00f3n: la API devuelve un resultado No-OK.", "unknown": "Error inesperado" }, "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Pulsa el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?" + "description": "Por favor, pulsa el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?" }, "credentials": { "data": { diff --git a/homeassistant/components/broadlink/translations/fr.json b/homeassistant/components/broadlink/translations/fr.json index e39b722d8c9..7c9d22fd52f 100644 --- a/homeassistant/components/broadlink/translations/fr.json +++ b/homeassistant/components/broadlink/translations/fr.json @@ -5,7 +5,7 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", - "not_supported": "Dispositif non pris en charge", + "not_supported": "Appareil non pris en charge", "unknown": "Erreur inattendue" }, "error": { diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 9c0847b52ce..8f6bc774098 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -11,7 +11,7 @@ "data": { "known_hosts": "Hosts conocidos" }, - "description": "Hosts conocidos: una lista separada por comas de nombres de host o direcciones IP de dispositivos de transmisi\u00f3n, se usa si el descubrimiento de mDNS no funciona.", + "description": "Hosts conocidos: una lista separada por comas de nombres de host o direcciones IP de dispositivos de transmisi\u00f3n, se usa si el descubrimiento mDNS no funciona.", "title": "Configuraci\u00f3n de Google Cast" }, "confirm": { @@ -29,7 +29,7 @@ "ignore_cec": "Ignorar CEC", "uuid": "UUIDs permitidos" }, - "description": "UUID permitidos: una lista separada por comas de UUID de dispositivos Cast para a\u00f1adir a Home Assistant. \u00dasalo solo si no deseas a\u00f1adir todos los dispositivos de transmisi\u00f3n disponibles.\nIgnorar CEC: una lista separada por comas de Chromecasts que deben ignorar los datos de CEC para determinar la entrada activa. Esto se pasar\u00e1 a pychromecast.IGNORE_CEC.", + "description": "UUID permitidos: una lista separada por comas de UUIDs de dispositivos Cast para a\u00f1adir a Home Assistant. \u00dasalo solo si no deseas a\u00f1adir todos los dispositivos de transmisi\u00f3n disponibles.\nIgnorar CEC: una lista separada por comas de Chromecasts que deben ignorar los datos de CEC para determinar la entrada activa. Esto se pasar\u00e1 a pychromecast.IGNORE_CEC.", "title": "Configuraci\u00f3n avanzada de Google Cast" }, "basic_options": { diff --git a/homeassistant/components/climate/translations/es.json b/homeassistant/components/climate/translations/es.json index bf7e37c71ff..188adec66c5 100644 --- a/homeassistant/components/climate/translations/es.json +++ b/homeassistant/components/climate/translations/es.json @@ -9,9 +9,9 @@ "is_preset_mode": "{entity_name} se establece en un modo preestablecido espec\u00edfico" }, "trigger_type": { - "current_humidity_changed": "{entity_name} cambi\u00f3 la humedad medida", - "current_temperature_changed": "{entity_name} cambi\u00f3 la temperatura medida", - "hvac_mode_changed": "{entity_name} cambi\u00f3 el modo HVAC" + "current_humidity_changed": "La humedad medida por {entity_name} cambi\u00f3", + "current_temperature_changed": "La temperatura medida por {entity_name} cambi\u00f3", + "hvac_mode_changed": "El modo HVAC de {entity_name} cambi\u00f3" } }, "state": { diff --git a/homeassistant/components/cover/translations/es.json b/homeassistant/components/cover/translations/es.json index 708c4a2dc7d..327a41bc24c 100644 --- a/homeassistant/components/cover/translations/es.json +++ b/homeassistant/components/cover/translations/es.json @@ -22,8 +22,8 @@ "closing": "{entity_name} cerr\u00e1ndose", "opened": "{entity_name} abierto", "opening": "{entity_name} abri\u00e9ndose", - "position": "{entity_name} cambi\u00f3 de posici\u00f3n", - "tilt_position": "{entity_name} cambi\u00f3 de posici\u00f3n de inclinaci\u00f3n" + "position": "La posici\u00f3n de {entity_name} cambia", + "tilt_position": "La posici\u00f3n de inclinaci\u00f3n de {entity_name} cambia" } }, "state": { diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index 19fc5a86e0b..cf6055bfda4 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -15,7 +15,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Prem D'acord quan s'hagi omplert el l\u00edquid d'intermitents", + "description": "Prem ENVIA quan s'hagi omplert el l\u00edquid d'intermitents", "title": "Cal omplir el l\u00edquid d'intermitents" } } diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index 97eb4866bfa..bd4d650de1c 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -4,10 +4,12 @@ "fix_flow": { "step": { "confirm": { - "description": "SUBMIT(\u9001\u4fe1)\u3092\u62bc\u3057\u3066\u3001\u96fb\u6e90\u304c\u4ea4\u63db\u3055\u308c\u305f\u3053\u3068\u3092\u78ba\u8a8d\u3057\u307e\u3059" + "description": "SUBMIT(\u9001\u4fe1)\u3092\u62bc\u3057\u3066\u3001\u96fb\u6e90\u304c\u4ea4\u63db\u3055\u308c\u305f\u3053\u3068\u3092\u78ba\u8a8d\u3057\u307e\u3059", + "title": "\u96fb\u6e90\u3092\u4ea4\u63db\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" } } - } + }, + "title": "\u96fb\u6e90\u304c\u4e0d\u5b89\u5b9a" }, "out_of_blinker_fluid": { "fix_flow": { diff --git a/homeassistant/components/deutsche_bahn/translations/ca.json b/homeassistant/components/deutsche_bahn/translations/ca.json index a4dc0724423..406d134c92f 100644 --- a/homeassistant/components/deutsche_bahn/translations/ca.json +++ b/homeassistant/components/deutsche_bahn/translations/ca.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "La integraci\u00f3 de Deutsche Bahn est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 es basa en el 'webscraping', que no est\u00e0 adm\u00e8s. \n\nElimina la configuraci\u00f3 YAML de Deutsche Bahn del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", "title": "La integraci\u00f3 Deutsche Bahn est\u00e0 sent eliminada" } } diff --git a/homeassistant/components/deutsche_bahn/translations/es.json b/homeassistant/components/deutsche_bahn/translations/es.json index 32aaf5a3893..2572474f2bc 100644 --- a/homeassistant/components/deutsche_bahn/translations/es.json +++ b/homeassistant/components/deutsche_bahn/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n Deutsche Bahn" } } diff --git a/homeassistant/components/device_tracker/translations/es.json b/homeassistant/components/device_tracker/translations/es.json index 47948da66ec..e68dbcf4087 100644 --- a/homeassistant/components/device_tracker/translations/es.json +++ b/homeassistant/components/device_tracker/translations/es.json @@ -15,5 +15,5 @@ "not_home": "Fuera" } }, - "title": "Seguimiento de dispositivos" + "title": "Rastreador de dispositivos" } \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 509b6e7d8c7..538178301cc 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -29,7 +29,7 @@ "events": "Lista de eventos separados por comas." }, "data_description": { - "events": "A\u00f1ade un nombre de evento separado por comas para cada evento que desees rastrear. Despu\u00e9s de introducirlos aqu\u00ed, usa la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. \n\nEjemplo: alguien_puls\u00f3_el_bot\u00f3n, movimiento" + "events": "A\u00f1ade un nombre de evento separado por comas para cada evento que desees rastrear. Despu\u00e9s de introducirlos aqu\u00ed, usa la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. \n\nEjemplo: somebody_pressed_the_button, motion" } } } diff --git a/homeassistant/components/dsmr/translations/ja.json b/homeassistant/components/dsmr/translations/ja.json index 53c7d5c1050..edabe226727 100644 --- a/homeassistant/components/dsmr/translations/ja.json +++ b/homeassistant/components/dsmr/translations/ja.json @@ -11,6 +11,7 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { + "other": "\u7a7a", "setup_network": { "data": { "dsmr_version": "DSMR\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u9078\u629e", diff --git a/homeassistant/components/eafm/translations/es.json b/homeassistant/components/eafm/translations/es.json index 8e38ac23e0a..a846a0bc04c 100644 --- a/homeassistant/components/eafm/translations/es.json +++ b/homeassistant/components/eafm/translations/es.json @@ -10,7 +10,7 @@ "station": "Estaci\u00f3n" }, "description": "Selecciona la estaci\u00f3n que deseas supervisar", - "title": "Seguimiento de una estaci\u00f3n de supervisi\u00f3n de inundaciones" + "title": "Rastrear una estaci\u00f3n de supervisi\u00f3n de inundaciones" } } } diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index 1cbae31550f..25d07dcfef8 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -34,7 +34,7 @@ "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", "username": "Nombre de usuario" }, - "description": "La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no seguro' y 2601 para 'seguro'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baud]'. Ejemplo: '/dev/ttyS1'. El baudio es opcional y el valor predeterminado es 115200.", + "description": "La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no seguro' y 2601 para 'seguro'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. Los baudios son opcionales y el valor predeterminado es 115200.", "title": "Conectar con Control Elk-M1" }, "user": { diff --git a/homeassistant/components/elmax/translations/es.json b/homeassistant/components/elmax/translations/es.json index 8d6bccd03ab..f2ae2d7ef83 100644 --- a/homeassistant/components/elmax/translations/es.json +++ b/homeassistant/components/elmax/translations/es.json @@ -17,7 +17,7 @@ "panel_name": "Nombre del panel", "panel_pin": "C\u00f3digo PIN" }, - "description": "Selecciona qu\u00e9 panel te gustar\u00eda controlar con esta integraci\u00f3n. Ten en cuenta que el panel debe estar ENCENDIDO para poder configurarlo." + "description": "Selecciona qu\u00e9 panel te gustar\u00eda controlar con esta integraci\u00f3n. Por favor, ten en cuenta que el panel debe estar ENCENDIDO para poder configurarlo." }, "user": { "data": { diff --git a/homeassistant/components/escea/translations/ja.json b/homeassistant/components/escea/translations/ja.json index a64c00ee212..73e0c4d503e 100644 --- a/homeassistant/components/escea/translations/ja.json +++ b/homeassistant/components/escea/translations/ja.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + }, + "step": { + "confirm": { + "description": "Escea fireplace\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + } } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index f8e933f7711..87c8dc6ddad 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -17,7 +17,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Escribe la contrase\u00f1a que hayas puesto en la configuraci\u00f3n para {name}." + "description": "Por favor, introduce la contrase\u00f1a que hayas puesto en la configuraci\u00f3n para {name}." }, "discovery_confirm": { "description": "\u00bfQuieres a\u00f1adir el nodo de ESPHome `{name}` a Home Assistant?", @@ -33,7 +33,7 @@ "data": { "noise_psk": "Clave de cifrado" }, - "description": "El dispositivo ESPHome {name} habilit\u00f3 el cifrado de transporte o cambi\u00f3 la clave de cifrado. Introduce la clave actualizada." + "description": "El dispositivo ESPHome {name} habilit\u00f3 el cifrado de transporte o cambi\u00f3 la clave de cifrado. Por favor, introduce la clave actualizada." }, "user": { "data": { diff --git a/homeassistant/components/fivem/translations/es.json b/homeassistant/components/fivem/translations/es.json index 3264888e71e..4ec4b4bd295 100644 --- a/homeassistant/components/fivem/translations/es.json +++ b/homeassistant/components/fivem/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar. Por favor, verifica el host y el puerto y vuelve a intentarlo. Tambi\u00e9n aseg\u00farate de estar ejecutando el servidor FiveM m\u00e1s reciente.", + "cannot_connect": "No se pudo conectar. Por favor, comprueba el host y el puerto e int\u00e9ntalo de nuevo. Tambi\u00e9n aseg\u00farate de estar ejecutando el servidor FiveM m\u00e1s reciente.", "invalid_game_name": "La API del juego al que intentas conectarte no es un juego de FiveM.", "unknown_error": "Error inesperado" }, diff --git a/homeassistant/components/flunearyou/translations/ca.json b/homeassistant/components/flunearyou/translations/ca.json index 912f88d8b28..9c4e55f8b54 100644 --- a/homeassistant/components/flunearyou/translations/ca.json +++ b/homeassistant/components/flunearyou/translations/ca.json @@ -22,6 +22,7 @@ "fix_flow": { "step": { "confirm": { + "description": "La font de dades externa que alimenta la integraci\u00f3 Flu Near You ja no est\u00e0 disponible; per tant, la integraci\u00f3 ja no funciona. \n\nPrem ENVIAR per eliminar Flu Near You de Home Assistant.", "title": "Elimina Flu Near You" } } diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json index 3a2c41cc735..a7d9cf89f6e 100644 --- a/homeassistant/components/flunearyou/translations/es.json +++ b/homeassistant/components/flunearyou/translations/es.json @@ -22,7 +22,7 @@ "fix_flow": { "step": { "confirm": { - "description": "La fuente de datos externa que alimenta la integraci\u00f3n Flu Near You ya no est\u00e1 disponible; por lo tanto, la integraci\u00f3n ya no funciona. \n\nPulsar ENVIAR para eliminar Flu Near You de tu instancia Home Assistant.", + "description": "La fuente de datos externa que alimenta la integraci\u00f3n Flu Near You ya no est\u00e1 disponible; por lo tanto, la integraci\u00f3n ya no funciona. \n\nPulsa ENVIAR para eliminar Flu Near You de tu instancia Home Assistant.", "title": "Eliminar Flu Near You" } } diff --git a/homeassistant/components/flunearyou/translations/it.json b/homeassistant/components/flunearyou/translations/it.json index 4b3bd589325..a8939f2d18c 100644 --- a/homeassistant/components/flunearyou/translations/it.json +++ b/homeassistant/components/flunearyou/translations/it.json @@ -22,6 +22,7 @@ "fix_flow": { "step": { "confirm": { + "description": "L'origine dati esterna che alimenta l'integrazione Flu Near You non \u00e8 pi\u00f9 disponibile; quindi, l'integrazione non funziona pi\u00f9. \n\nPremi INVIA per rimuovere Flu Near You dall'istanza di Home Assistant.", "title": "Rimuovi Flu Near You" } } diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index 5567fa63ee2..08e67ec95d1 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -10,7 +10,7 @@ "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares", "name": "Nombre" }, - "description": "Rellena los datos de tus placas solares. Consulta la documentaci\u00f3n si un campo no est\u00e1 claro." + "description": "Rellena los datos de tus placas solares. Por favor, consulta la documentaci\u00f3n si un campo no est\u00e1 claro." } } }, diff --git a/homeassistant/components/forked_daapd/translations/es.json b/homeassistant/components/forked_daapd/translations/es.json index 2e6c986a61e..999ec0846d5 100644 --- a/homeassistant/components/forked_daapd/translations/es.json +++ b/homeassistant/components/forked_daapd/translations/es.json @@ -5,7 +5,7 @@ "not_forked_daapd": "El dispositivo no es un servidor forked-daapd." }, "error": { - "forbidden": "No se puede conectar. Comprueba los permisos de tu forked-daapd.", + "forbidden": "No se puede conectar. Por favor, comprueba los permisos de red de tu forked-daapd.", "unknown_error": "Error inesperado", "websocket_not_enabled": "Websocket del servidor forked-daapd no habilitado.", "wrong_host_or_port": "No se ha podido conectar. Por favor, comprueba host y puerto.", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json index 8e7dbbc1b8e..d8203394eba 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -27,7 +27,7 @@ }, "options": { "error": { - "malformed_prefixes": "Los prefijos tienen un formato incorrecto, por favor, verifica el formato." + "malformed_prefixes": "Los prefijos tienen un formato incorrecto, por favor, comprueba su formato." }, "step": { "init": { diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json index ac5c16c3120..ff3ce5d4a91 100644 --- a/homeassistant/components/generic/translations/es.json +++ b/homeassistant/components/generic/translations/es.json @@ -36,7 +36,7 @@ "data": { "authentication": "Autenticaci\u00f3n", "framerate": "Velocidad de fotogramas (Hz)", - "limit_refetch_to_url_change": "Limitar recuperaci\u00f3n al cambio de URL", + "limit_refetch_to_url_change": "Limitar la reobtenci\u00f3n de cambios de URL", "password": "Contrase\u00f1a", "rtsp_transport": "Protocolo de transporte RTSP", "still_image_url": "URL de la imagen fija (p. ej., http://...)", @@ -78,7 +78,7 @@ "data": { "authentication": "Autenticaci\u00f3n", "framerate": "Velocidad de fotogramas (Hz)", - "limit_refetch_to_url_change": "Limitar recuperaci\u00f3n al cambio de URL", + "limit_refetch_to_url_change": "Limitar la reobtenci\u00f3n de cambios de URL", "password": "Contrase\u00f1a", "rtsp_transport": "Protocolo de transporte RTSP", "still_image_url": "URL de la imagen fija (p. ej., http://...)", diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json index 14b534271f9..357eeba9000 100644 --- a/homeassistant/components/geocaching/translations/es.json +++ b/homeassistant/components/geocaching/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/github/translations/es.json b/homeassistant/components/github/translations/es.json index bf1dcf64ae4..02006e7f7b3 100644 --- a/homeassistant/components/github/translations/es.json +++ b/homeassistant/components/github/translations/es.json @@ -10,7 +10,7 @@ "step": { "repositories": { "data": { - "repositories": "Selecciona los repositorios a seguir." + "repositories": "Selecciona repositorios para rastrear." }, "title": "Configura los repositorios" } diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index be94117d1bd..4b36c5006c0 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -39,8 +39,8 @@ "title": "Se va a eliminar la configuraci\u00f3n YAML de Google Calendar" }, "removed_track_new_yaml": { - "description": "Has inhabilitado el seguimiento de entidades para Google Calendar en configuration.yaml, que ya no es compatible. Debes cambiar manualmente las opciones del sistema de integraci\u00f3n en la IU para deshabilitar las entidades reci\u00e9n descubiertas en el futuro. Elimina la configuraci\u00f3n track_new de configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "El seguimiento de entidades de Google Calendar ha cambiado" + "description": "Has deshabilitado el rastreo de entidades para Google Calendar en configuration.yaml, que ya no es compatible. Debes cambiar manualmente las opciones de sistema de la integraci\u00f3n en la IU para deshabilitar las entidades reci\u00e9n descubiertas en el futuro. Elimina la configuraci\u00f3n track_new de configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "El rastreo de entidades de Google Calendar ha cambiado" } }, "options": { diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index c5fe0ede852..d80fea1008d 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -66,9 +66,9 @@ "cover": "Grupo de persianas/cortinas", "fan": "Grupo de ventiladores", "light": "Grupo de luces", - "lock": "Bloquear el grupo", + "lock": "Grupo de cerraduras", "media_player": "Grupo de reproductores multimedia", - "switch": "Grupo de conmutadores" + "switch": "Grupo de interruptores" }, "title": "A\u00f1adir grupo" } diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json index 59d9f6d03b8..a338db67446 100644 --- a/homeassistant/components/guardian/translations/ca.json +++ b/homeassistant/components/guardian/translations/ca.json @@ -17,5 +17,18 @@ "description": "Configura un dispositiu Elexa Guardian local." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`. Despr\u00e9s, fes clic a ENVIAR per marcar aquest problema com a resolt.", + "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + } + } + }, + "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index a7f54c0a726..8df17ed3ff3 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -23,7 +23,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`. A continuaci\u00f3n, haz clic en ENVIAR m\u00e1s abajo para marcar este problema como resuelto.", + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", "title": "El servicio {deprecated_service} ser\u00e1 eliminado" } } diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json index 337223c5736..7e95869e071 100644 --- a/homeassistant/components/guardian/translations/ja.json +++ b/homeassistant/components/guardian/translations/ja.json @@ -17,5 +17,17 @@ "description": "\u30ed\u30fc\u30ab\u30eb\u306eElexa Guardian\u30c7\u30d0\u30a4\u30b9\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "{deprecated_service} \u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } + }, + "title": "{deprecated_service} \u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json index fe93ccb6369..068787b5e86 100644 --- a/homeassistant/components/guardian/translations/ru.json +++ b/homeassistant/components/guardian/translations/ru.json @@ -17,5 +17,18 @@ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0439 \u0441\u043b\u0443\u0436\u0431\u044b \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_target}`. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u043d\u0451\u043d\u043d\u0443\u044e.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } + }, + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/es.json b/homeassistant/components/habitica/translations/es.json index 681336e553f..55cf8eb7642 100644 --- a/homeassistant/components/habitica/translations/es.json +++ b/homeassistant/components/habitica/translations/es.json @@ -12,7 +12,7 @@ "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 el seguimiento 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" + "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" } } } diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 2d88b0d2252..102256ef117 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -13,7 +13,7 @@ "supervisor_version": "Versi\u00f3n del Supervisor", "supported": "Soportado", "update_channel": "Canal de actualizaci\u00f3n", - "version_api": "Versi\u00f3n del API" + "version_api": "Versi\u00f3n de la API" } } } \ No newline at end of file diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index ffba8558cac..0bece2f0e54 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -7,7 +7,7 @@ }, "error": { "invalid_code": "Error al iniciar sesi\u00f3n en Hive. Tu c\u00f3digo de autenticaci\u00f3n de dos factores era incorrecto.", - "invalid_password": "Error al iniciar sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, prueba de nuevo.", + "invalid_password": "Error al iniciar sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, int\u00e9ntalo de nuevo.", "invalid_username": "Error al iniciar sesi\u00f3n en Hive. No se reconoce tu direcci\u00f3n de correo electr\u00f3nico.", "no_internet_available": "Se requiere una conexi\u00f3n a Internet para conectarse a Hive.", "unknown": "Error inesperado" diff --git a/homeassistant/components/home_connect/translations/es.json b/homeassistant/components/home_connect/translations/es.json index a476741a8b1..70d052ee0e3 100644 --- a/homeassistant/components/home_connect/translations/es.json +++ b/homeassistant/components/home_connect/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json index 4796a9e1236..609a989aa84 100644 --- a/homeassistant/components/home_plus_control/translations/es.json +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 3bca7c0f253..f0b6a1ecb6d 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -37,7 +37,7 @@ "camera_audio": "C\u00e1maras que admiten audio", "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" }, - "description": "Verifica todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", + "description": "Marca todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", "title": "Configuraci\u00f3n de C\u00e1mara" }, "exclude": { diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index 6185c432007..a43373cd399 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -11,10 +11,10 @@ "no_devices": "No se encontraron dispositivos no emparejados" }, "error": { - "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor, rev\u00edsalo e int\u00e9ntalo de nuevo.", + "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado no es seguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", "max_peers_error": "El dispositivo se neg\u00f3 a a\u00f1adir el emparejamiento porque no tiene almacenamiento disponible para emparejamientos.", - "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no sea compatible actualmente.", + "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no sea compatible por el momento.", "unable_to_pair": "No se puede emparejar, por favor, int\u00e9ntalo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. El emparejamiento ha fallado." }, diff --git a/homeassistant/components/homekit_controller/translations/sensor.it.json b/homeassistant/components/homekit_controller/translations/sensor.it.json index acfff28f0b2..338c9e58009 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.it.json +++ b/homeassistant/components/homekit_controller/translations/sensor.it.json @@ -1,10 +1,21 @@ { "state": { "homekit_controller__thread_node_capabilities": { - "none": "Nessuna" + "border_router_capable": "Capacit\u00e0 di router di confine", + "full": "Dispositivo terminale completo", + "minimal": "Dispositivo terminale minimale", + "none": "Nessuna", + "router_eligible": "Dispositivo terminale idoneo al router", + "sleepy": "Dispositivo terminale dormiente" }, "homekit_controller__thread_status": { - "disabled": "Disabilitato" + "border_router": "Router di confine", + "child": "Figlio", + "detached": "Separato", + "disabled": "Disabilitato", + "joining": "Unendosi", + "leader": "Capo", + "router": "Router" } } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json index afc637bbaa7..0a6096c644d 100644 --- a/homeassistant/components/homematicip_cloud/translations/es.json +++ b/homeassistant/components/homematicip_cloud/translations/es.json @@ -6,10 +6,10 @@ "unknown": "Error inesperado" }, "error": { - "invalid_sgtin_or_pin": "SGTIN o C\u00f3digo PIN no v\u00e1lido, int\u00e9ntalo de nuevo.", + "invalid_sgtin_or_pin": "SGTIN o C\u00f3digo PIN no v\u00e1lido, por favor, int\u00e9ntalo de nuevo.", "press_the_button": "Por favor, pulsa el bot\u00f3n azul", - "register_failed": "No se pudo registrar, por favor int\u00e9ntalo de nuevo.", - "timeout_button": "Se agot\u00f3 el tiempo de espera para presionar el bot\u00f3n azul. Vuelve a intentarlo." + "register_failed": "No se pudo registrar, por favor, int\u00e9ntalo de nuevo.", + "timeout_button": "Se agot\u00f3 el tiempo de espera para presionar el bot\u00f3n azul, por favor, int\u00e9ntalo de nuevo." }, "step": { "init": { diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index c97e5aa3ddf..ef261013844 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -9,7 +9,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduce las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." + "description": "Por favor, introduce las credenciales usadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." } } }, diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 03b159989f2..a88f35ba3d5 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -9,7 +9,7 @@ "incorrect_username": "Nombre de usuario incorrecto", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_url": "URL no v\u00e1lida", - "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, por favor, int\u00e9ntalo de nuevo m\u00e1s tarde.", + "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, por favor, vuelve a intentarlo m\u00e1s tarde.", "response_error": "Error desconocido del dispositivo", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/hue/translations/ca.json b/homeassistant/components/hue/translations/ca.json index 3b192e80b00..11f66de23b9 100644 --- a/homeassistant/components/hue/translations/ca.json +++ b/homeassistant/components/hue/translations/ca.json @@ -44,6 +44,8 @@ "button_2": "Segon bot\u00f3", "button_3": "Tercer bot\u00f3", "button_4": "Quart bot\u00f3", + "clock_wise": "Rotaci\u00f3 en sentit horari", + "counter_clock_wise": "Rotaci\u00f3 en sentit antihorari", "dim_down": "Atenua la brillantor", "dim_up": "Augmenta la brillantor", "double_buttons_1_3": "primer i tercer botons", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Ambd\u00f3s \"{subtype}\" alliberats", - "initial_press": "Bot\u00f3 \"{subtype}\" premut inicialment", - "long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", - "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", + "initial_press": "\"{subtype}\" premut inicialment", + "long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_short_press": "\"{subtype}\" premut", + "remote_button_short_release": "\"{subtype}\" alliberat", "remote_double_button_long_press": "Ambd\u00f3s \"{subtype}\" alliberats despr\u00e9s d'una estona premuts", "remote_double_button_short_press": "Ambd\u00f3s \"{subtype}\" alliberats", - "repeat": "Bot\u00f3 \"{subtype}\" mantingut premut", - "short_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s de pr\u00e9mer breument" + "repeat": "\"{subtype}\" mantingut premut", + "short_release": "\"{subtype}\" alliberat despr\u00e9s de pr\u00e9mer breument", + "start": "\"{subtype}\" premut inicialment" } }, "options": { diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index 7a54fc5ce03..ca50dec4715 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -44,14 +44,14 @@ "button_2": "Second button", "button_3": "Third button", "button_4": "Fourth button", + "clock_wise": "Rotation clockwise", + "counter_clock_wise": "Rotation counter-clockwise", "dim_down": "Dim down", "dim_up": "Dim up", "double_buttons_1_3": "First and Third buttons", "double_buttons_2_4": "Second and Fourth buttons", "turn_off": "Turn off", - "turn_on": "Turn on", - "clock_wise": "Rotation clockwise", - "counter_clock_wise": "Rotation counter-clockwise" + "turn_on": "Turn on" }, "trigger_type": { "double_short_release": "Both \"{subtype}\" released", diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index e49438144b7..96f3388f2d3 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -13,7 +13,7 @@ }, "error": { "linking": "Error inesperado", - "register_failed": "No se pudo registrar, int\u00e9ntalo de nuevo" + "register_failed": "No se pudo registrar, por favor, int\u00e9ntalo de nuevo" }, "step": { "init": { @@ -24,7 +24,7 @@ }, "link": { "description": "Presiona el bot\u00f3n en la pasarela para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_philips_hue.jpg)", - "title": "Vincular pasarela" + "title": "Vincular concentrador" }, "manual": { "data": { diff --git a/homeassistant/components/hue/translations/id.json b/homeassistant/components/hue/translations/id.json index d450adfb3c4..0b81e1093df 100644 --- a/homeassistant/components/hue/translations/id.json +++ b/homeassistant/components/hue/translations/id.json @@ -44,6 +44,8 @@ "button_2": "Tombol kedua", "button_3": "Tombol ketiga", "button_4": "Tombol keempat", + "clock_wise": "Rotasi searah jarum jam", + "counter_clock_wise": "Rotasi berlawanan arah jarum jam", "dim_down": "Redupkan", "dim_up": "Terangkan", "double_buttons_1_3": "Tombol Pertama dan Ketiga", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Kedua \"{subtype}\" dilepaskan", - "initial_press": "Tombol \"{subtype}\" awalnya ditekan", - "long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", - "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", - "remote_button_short_press": "Tombol \"{subtype}\" ditekan", - "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", + "initial_press": "\"{subtype}\" awalnya ditekan", + "long_release": "\"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_short_press": "\"{subtype}\" ditekan", + "remote_button_short_release": "\"{subtype}\" dilepaskan", "remote_double_button_long_press": "Kedua \"{subtype}\" dilepaskan setelah ditekan lama", "remote_double_button_short_press": "Kedua \"{subtype}\" dilepas", - "repeat": "Tombol \"{subtype}\" ditekan terus", - "short_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan sebentar" + "repeat": "\"{subtype}\" ditekan terus", + "short_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan sebentar", + "start": "\"{subtype}\" awalnya ditekan" } }, "options": { diff --git a/homeassistant/components/hue/translations/pt-BR.json b/homeassistant/components/hue/translations/pt-BR.json index cc97e5e055e..12a609034dc 100644 --- a/homeassistant/components/hue/translations/pt-BR.json +++ b/homeassistant/components/hue/translations/pt-BR.json @@ -44,6 +44,8 @@ "button_2": "Segundo bot\u00e3o", "button_3": "Terceiro bot\u00e3o", "button_4": "Quarto bot\u00e3o", + "clock_wise": "Rota\u00e7\u00e3o no sentido hor\u00e1rio", + "counter_clock_wise": "Rota\u00e7\u00e3o no sentido anti-hor\u00e1rio", "dim_down": "Diminuir a luminosidade", "dim_up": "Aumentar a luminosidade", "double_buttons_1_3": "Primeiro e terceiro bot\u00f5es", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Ambos \"{subtype}\" liberados", - "initial_press": "Bot\u00e3o \" {subtype} \" pressionado inicialmente", - "long_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s press\u00e3o longa", - "remote_button_long_release": "Bot\u00e3o \"{subtype}\" liberado ap\u00f3s longa press\u00e3o", - "remote_button_short_press": "Bot\u00e3o \"{subtype}\" pressionado", - "remote_button_short_release": "Bot\u00e3o \"{subtype}\" liberado", + "initial_press": "\"{subtype}\" pressionado inicialmente", + "long_release": "\"{subtype}\" liberado ap\u00f3s pressionar longamente", + "remote_button_long_release": "\"{subtype}\" liberado ap\u00f3s pressionar longamente", + "remote_button_short_press": "\"{subtype}\" pressionado", + "remote_button_short_release": "\"{subtype}\" lan\u00e7ado", "remote_double_button_long_press": "Ambos \"{subtype}\" lan\u00e7ados ap\u00f3s longa imprensa", "remote_double_button_short_press": "Ambos \"{subtype}\" lan\u00e7ados", - "repeat": "Bot\u00e3o \" {subtype} \" pressionado", - "short_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s pressionamento curto" + "repeat": "\"{subtype}\" pressionado", + "short_release": "\"{subtype}\" liberado ap\u00f3s pressionamento curto", + "start": "\"{subtype}\" pressionado inicialmente" } }, "options": { diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json index 7a03cf901fc..c7331938aef 100644 --- a/homeassistant/components/humidifier/translations/es.json +++ b/homeassistant/components/humidifier/translations/es.json @@ -14,7 +14,7 @@ }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", - "target_humidity_changed": "{entity_name} cambi\u00f3 su humedad objetivo", + "target_humidity_changed": "La humedad objetivo de {entity_name} cambi\u00f3", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido" } diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index 91143ec96a8..3220866ab16 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -27,7 +27,7 @@ "title": "Confirmar para a\u00f1adir el servicio Hyperion Ambilight" }, "create_token": { - "description": "Elige **Enviar** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Ser\u00e1s redirigido a la IU de Hyperion para aprobar la solicitud. Verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"", + "description": "Elige **Enviar** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Ser\u00e1s redirigido a la IU de Hyperion para aprobar la solicitud. Por favor, verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"", "title": "Crear autom\u00e1ticamente un nuevo token de autenticaci\u00f3n" }, "create_token_external": { diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index e5e6a8927a9..5f0f4ac6495 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "send_verification_code": "No se pudo enviar el c\u00f3digo de verificaci\u00f3n", - "validate_verification_code": "No se ha podido verificar el c\u00f3digo de verificaci\u00f3n, vuelve a intentarlo" + "validate_verification_code": "No se ha podido verificar el c\u00f3digo de verificaci\u00f3n, int\u00e9ntalo de nuevo" }, "step": { "reauth": { diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 8dbe09d635c..855349ab9cf 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -50,7 +50,7 @@ "options": { "error": { "cannot_connect": "No se pudo conectar", - "input_error": "Entradas no v\u00e1lidas, por favor, verifica tus valores.", + "input_error": "Entradas no v\u00e1lidas, por favor, comprueba tus valores.", "select_single": "Selecciona una opci\u00f3n." }, "step": { @@ -69,7 +69,7 @@ "steps": "Pasos de atenuaci\u00f3n (s\u00f3lo para dispositivos de luz, por defecto 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "Cambia la contrase\u00f1a del Hub Insteon." + "description": "Cambia la contrase\u00f1a del Insteon Hub." }, "change_hub_config": { "data": { @@ -78,7 +78,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Cambia la informaci\u00f3n de conexi\u00f3n del Hub Insteon. Debes reiniciar Home Assistant despu\u00e9s de realizar este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n en el Hub, usa la aplicaci\u00f3n Hub." + "description": "Cambia la informaci\u00f3n de conexi\u00f3n del Insteon Hub. Debes reiniciar Home Assistant despu\u00e9s de realizar este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n en el Hub, usa la aplicaci\u00f3n Hub." }, "init": { "data": { diff --git a/homeassistant/components/ipma/translations/es.json b/homeassistant/components/ipma/translations/es.json index 089a58903b4..078dd7546d9 100644 --- a/homeassistant/components/ipma/translations/es.json +++ b/homeassistant/components/ipma/translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nombre ya existe" + "name_exists": "El nombre ya existe" }, "step": { "user": { diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index 845ba0b2b7b..740462de0f4 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -11,7 +11,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "connection_upgrade": "No se pudo conectar con la impresora. Int\u00e9ntalo de nuevo con la opci\u00f3n SSL/TLS marcada." + "connection_upgrade": "No se pudo conectar con la impresora. Por favor, int\u00e9ntalo de nuevo con la opci\u00f3n SSL/TLS marcada." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/justnimbus/translations/ja.json b/homeassistant/components/justnimbus/translations/ja.json new file mode 100644 index 00000000000..93f2481120e --- /dev/null +++ b/homeassistant/components/justnimbus/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "client_id": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index cad83301d83..19de37aaf56 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -21,7 +21,7 @@ }, "data_description": { "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", - "local_ip": "D\u00e9jalo en blanco para utilizar el descubrimiento autom\u00e1tico.", + "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico.", "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." }, "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu dispositivo de t\u00fanel." @@ -61,7 +61,7 @@ "user_id": "Este suele ser el n\u00famero de t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda ID de usuario '3'.", "user_password": "Contrase\u00f1a para la conexi\u00f3n de t\u00fanel espec\u00edfica establecida en el panel 'Propiedades' del t\u00fanel en ETS." }, - "description": "Introduce tu informaci\u00f3n de IP segura." + "description": "Por favor, introduce tu informaci\u00f3n de IP segura." }, "secure_tunneling": { "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", @@ -74,7 +74,7 @@ "data": { "gateway": "Conexi\u00f3n de t\u00fanel KNX" }, - "description": "Selecciona una puerta de enlace de la lista." + "description": "Por favor, selecciona una puerta de enlace de la lista." }, "type": { "data": { @@ -98,7 +98,7 @@ }, "data_description": { "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", - "local_ip": "Usa `0.0.0.0` para el descubrimiento autom\u00e1tico.", + "local_ip": "Usar `0.0.0.0` para el descubrimiento autom\u00e1tico.", "multicast_group": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `224.0.23.12`", "multicast_port": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `3671`", "rate_limit": "N\u00famero m\u00e1ximo de telegramas salientes por segundo.\nRecomendado: 20 a 40", diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json index 5ea61e0e0c9..b0599290789 100644 --- a/homeassistant/components/kodi/translations/es.json +++ b/homeassistant/components/kodi/translations/es.json @@ -31,7 +31,7 @@ "port": "Puerto", "ssl": "Utiliza un certificado SSL" }, - "description": "Informaci\u00f3n de conexi\u00f3n de Kodi. Aseg\u00farate de habilitar \"Permitir el control de Kodi a trav\u00e9s de HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." + "description": "Informaci\u00f3n de conexi\u00f3n de Kodi. Por favor, aseg\u00farate de habilitar \"Permitir el control de Kodi a trav\u00e9s de HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." }, "ws_port": { "data": { diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 47a4d1dc5b4..b99cbcde3f5 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -89,7 +89,7 @@ "discovery": "Responder a las solicitudes de descubrimiento en tu red", "override_api_host": "Anular la URL predeterminada del panel de host de la API de Home Assistant" }, - "description": "Selecciona el comportamiento deseado para tu panel", + "description": "Por favor, selecciona el comportamiento deseado para tu panel", "title": "Configurar miscel\u00e1neos" }, "options_switch": { diff --git a/homeassistant/components/kraken/translations/ja.json b/homeassistant/components/kraken/translations/ja.json index 40a1ac53232..97ae0c9737e 100644 --- a/homeassistant/components/kraken/translations/ja.json +++ b/homeassistant/components/kraken/translations/ja.json @@ -5,6 +5,9 @@ }, "step": { "user": { + "data": { + "other": "\u7a7a" + }, "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" } } diff --git a/homeassistant/components/lifx/translations/es.json b/homeassistant/components/lifx/translations/es.json index a308d5fbb9e..6bc2249182e 100644 --- a/homeassistant/components/lifx/translations/es.json +++ b/homeassistant/components/lifx/translations/es.json @@ -26,7 +26,7 @@ "data": { "host": "Host" }, - "description": "Si deja el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/mailgun/translations/es.json b/homeassistant/components/mailgun/translations/es.json index 85007655134..bf632ad9700 100644 --- a/homeassistant/components/mailgun/translations/es.json +++ b/homeassistant/components/mailgun/translations/es.json @@ -6,7 +6,7 @@ "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 debes configurar los [Webhooks en Mailgun]({mailgun_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/json \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks con Mailgun]({mailgun_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/json \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." }, "step": { "user": { diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index 009385a5bc1..53dec475cad 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -5,7 +5,7 @@ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntalo de nuevo m\u00e1s tarde.", + "account_locked": "Cuenta bloqueada. Por favor, vuelve a intentarlo m\u00e1s tarde.", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" @@ -17,7 +17,7 @@ "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." + "description": "Por favor, introduce la direcci\u00f3n de correo electr\u00f3nico y la contrase\u00f1a que usas para iniciar sesi\u00f3n en la aplicaci\u00f3n m\u00f3vil MyMazda." } } } diff --git a/homeassistant/components/meater/translations/es.json b/homeassistant/components/meater/translations/es.json index 8c4be00c610..1fc556b8ee2 100644 --- a/homeassistant/components/meater/translations/es.json +++ b/homeassistant/components/meater/translations/es.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "service_unavailable_error": "La API no est\u00e1 disponible en este momento, por favor int\u00e9ntalo m\u00e1s tarde.", + "service_unavailable_error": "La API no est\u00e1 disponible en este momento, por favor, vuelve a intentarlo m\u00e1s tarde.", "unknown_auth_error": "Error inesperado" }, "step": { diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index 946062acdc7..fe056b81eb1 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -20,7 +20,7 @@ }, "state": { "_": { - "buffering": "almacenamiento en b\u00fafer", + "buffering": "almacenando en b\u00fafer", "idle": "Inactivo", "off": "Apagado", "on": "Encendido", diff --git a/homeassistant/components/met_eireann/translations/es.json b/homeassistant/components/met_eireann/translations/es.json index 5448f05a9bb..d476d15366e 100644 --- a/homeassistant/components/met_eireann/translations/es.json +++ b/homeassistant/components/met_eireann/translations/es.json @@ -11,7 +11,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Introduce tu ubicaci\u00f3n para utilizar los datos meteorol\u00f3gicos de la API del pron\u00f3stico meteorol\u00f3gico p\u00fablico de Met \u00c9ireann", + "description": "Introduce tu ubicaci\u00f3n para usar los datos meteorol\u00f3gicos de la API del pron\u00f3stico meteorol\u00f3gico p\u00fablico de Met \u00c9ireann", "title": "Ubicaci\u00f3n" } } diff --git a/homeassistant/components/meteo_france/translations/es.json b/homeassistant/components/meteo_france/translations/es.json index b8363d01868..d0a713119af 100644 --- a/homeassistant/components/meteo_france/translations/es.json +++ b/homeassistant/components/meteo_france/translations/es.json @@ -5,7 +5,7 @@ "unknown": "Error inesperado" }, "error": { - "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, verifica el campo de la ciudad" + "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, comprueba el campo de la ciudad" }, "step": { "cities": { diff --git a/homeassistant/components/metoffice/translations/es.json b/homeassistant/components/metoffice/translations/es.json index 0a533ecd130..9d984f9841a 100644 --- a/homeassistant/components/metoffice/translations/es.json +++ b/homeassistant/components/metoffice/translations/es.json @@ -14,7 +14,7 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "La latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana.", + "description": "La latitud y la longitud se usar\u00e1n para encontrar la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana.", "title": "Conectar con la UK Met Office" } } diff --git a/homeassistant/components/miflora/translations/es.json b/homeassistant/components/miflora/translations/es.json index 93a1e82aed6..c16bf2bae56 100644 --- a/homeassistant/components/miflora/translations/es.json +++ b/homeassistant/components/miflora/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "La integraci\u00f3n Mi Flora dej\u00f3 de funcionar en Home Assistant 2022.7 y se reemplaz\u00f3 por la integraci\u00f3n de Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes agregar tu dispositivo Mi Flora usando la nueva integraci\u00f3n manualmente. \n\nHome Assistant ya no usa la configuraci\u00f3n YAML existente de Mi Flora. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n Mi Flora dej\u00f3 de funcionar en Home Assistant 2022.7 y se reemplaz\u00f3 por la integraci\u00f3n de Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes a\u00f1adir manualmente tu dispositivo Mi Flora usando la nueva integraci\u00f3n. \n\nHome Assistant ya no usa la configuraci\u00f3n YAML existente de Mi Flora. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "La integraci\u00f3n Mi Flora ha sido reemplazada" } } diff --git a/homeassistant/components/minecraft_server/translations/es.json b/homeassistant/components/minecraft_server/translations/es.json index 8fa95f0dd10..34fa86dc45d 100644 --- a/homeassistant/components/minecraft_server/translations/es.json +++ b/homeassistant/components/minecraft_server/translations/es.json @@ -4,9 +4,9 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar con el servidor. Verifica el host y el puerto y vuelve a intentarlo. Tambi\u00e9n aseg\u00farate de estar ejecutando al menos la versi\u00f3n 1.7 de Minecraft en tu servidor.", - "invalid_ip": "La direcci\u00f3n IP no es v\u00e1lida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo y vuelve a intentarlo.", - "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo y vuelve a intentarlo." + "cannot_connect": "Error al conectar con el servidor. Por favor, comprueba el host y el puerto e int\u00e9ntalo de nuevo. Tambi\u00e9n aseg\u00farate de estar ejecutando al menos la versi\u00f3n 1.7 de Minecraft en tu servidor.", + "invalid_ip": "La direcci\u00f3n IP no es v\u00e1lida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo.", + "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo." }, "step": { "user": { diff --git a/homeassistant/components/mitemp_bt/translations/ca.json b/homeassistant/components/mitemp_bt/translations/ca.json index a567bceb950..c46a8d3ff3d 100644 --- a/homeassistant/components/mitemp_bt/translations/ca.json +++ b/homeassistant/components/mitemp_bt/translations/ca.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE va deixar de funcionar a Home Assistant 2022.7 i ha estat substitu\u00efda per la integraci\u00f3 Xiaomi BLE a la versi\u00f3 Home Assistant 2022.8.\n\nNo hi ha cap migraci\u00f3 possible, per tant, has d'afegir els teus dispositius Xiaomi Mijia BLE manualment utilitzant la nova integraci\u00f3.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML de l'antiga integraci\u00f3. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "description": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE va deixar de funcionar a Home Assistant 2022.7 i ha estat substitu\u00efda per la integraci\u00f3 Xiaomi BLE a la versi\u00f3 de Home Assistant 2022.8.\n\nNo hi ha cap migraci\u00f3 possible, per tant, has d'afegir els teus dispositius Xiaomi Mijia BLE manualment utilitzant la nova integraci\u00f3.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML de l'antiga integraci\u00f3. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE ha estat substitu\u00efda" } } diff --git a/homeassistant/components/mitemp_bt/translations/es.json b/homeassistant/components/mitemp_bt/translations/es.json index 6ae1e300961..544bc32943a 100644 --- a/homeassistant/components/mitemp_bt/translations/es.json +++ b/homeassistant/components/mitemp_bt/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE dej\u00f3 de funcionar en Home Assistant 2022.7 y fue reemplazada por la integraci\u00f3n Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes agregar tu dispositivo Xiaomi Mijia BLE utilizando la nueva integraci\u00f3n manualmente. \n\nHome Assistant ya no utiliza la configuraci\u00f3n YAML del sensor de temperatura y humedad BLE de Xiaomi Mijia. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE dej\u00f3 de funcionar en Home Assistant 2022.7 y fue reemplazada por la integraci\u00f3n Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes a\u00f1adir manualmente tu dispositivo Xiaomi Mijia BLE usando la nueva integraci\u00f3n. \n\nHome Assistant ya no utiliza la configuraci\u00f3n YAML del sensor de temperatura y humedad BLE de Xiaomi Mijia. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE ha sido reemplazada" } } diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index 585a7c0a8ed..d2f6235e9d5 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -20,14 +20,14 @@ "data": { "select_ip": "Direcci\u00f3n IP" }, - "description": "Vuelve a ejecutar la configuraci\u00f3n si deseas conectar Motion Gateways adicionales", + "description": "Ejecuta la configuraci\u00f3n de nuevo si quieres conectar Motion Gateways adicionales", "title": "Selecciona el Motion Gateway que deseas conectar" }, "user": { "data": { "host": "Direcci\u00f3n IP" }, - "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 configurada, se utiliza la detecci\u00f3n autom\u00e1tica" + "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 configurada, se usar\u00e1 la detecci\u00f3n autom\u00e1tica" } } }, diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index 2732a622511..fd2294ab820 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -14,6 +14,7 @@ "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", "invalid_subscribe_topic": "\u7121\u52b9\u306a\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6 \u30c8\u30d4\u30c3\u30af", "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", + "mqtt_required": "MQTT\u7d71\u5408\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "same_topic": "\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6\u3068\u30d1\u30d6\u30ea\u30c3\u30b7\u30e5\u306e\u30c8\u30d4\u30c3\u30af\u304c\u540c\u3058\u3067\u3059", @@ -72,6 +73,7 @@ "description": "\u8a2d\u5b9a\u3059\u308b\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "menu_options": { "gw_mqtt": "MQTT\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a", + "gw_serial": "\u30b7\u30ea\u30a2\u30eb\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a", "gw_tcp": "TCP\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a" } }, diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index 0ba92e457ca..c0ac0958d24 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "device_unsupported": "El dispositivo no es compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", - "reauth_unsuccessful": "No se pudo volver a autenticar, elimina la integraci\u00f3n y vuelve a configurarla." + "reauth_unsuccessful": "No se pudo volver a autenticar, por favor, elimina la integraci\u00f3n y vuelve a configurarla." }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index 81f11ec51ff..4dcab62da43 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 33d0179208b..20fc09c1af3 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -99,9 +99,11 @@ }, "issues": { "deprecated_yaml": { + "description": "La configuraci\u00f3 de Nest mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.10. \n\nLa configuraci\u00f3 existent de credencials d'aplicaci\u00f3 OAuth i d'acc\u00e9s s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de Nest est\u00e0 sent eliminada" }, "removed_app_auth": { + "description": "Per millorar la seguretat i reduir riscos, Google ha fet obsolet el m\u00e8tode d'autenticaci\u00f3 que utilitza Home Assistant. \n\n **Per resoldre-ho, has de dur a terme una acci\u00f3** ([m\u00e9s informaci\u00f3]({more_info_url})) \n\n 1. Visita la p\u00e0gina d'integracions.\n 2. Fes clic a Reconfigura a la integraci\u00f3 Nest.\n 3. Home Assistant et guiar\u00e0 a trav\u00e9s dels passos per actualitzar a l'autenticaci\u00f3 web. \n\nConsulta les [instruccions de la integraci\u00f3] Nest ({documentation_url}) per m\u00e9s informaci\u00f3 sobre resoluci\u00f3 de problemes.", "title": "Les credencials d'autenticaci\u00f3 de Nest s'han d'actualitzar" } } diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 7715f1f99bd..535482b74da 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -8,7 +8,7 @@ "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." @@ -17,13 +17,13 @@ "default": "Autenticado correctamente" }, "error": { - "bad_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (verifica la Cloud Console)", + "bad_project_id": "Por favor, introduce un ID de proyecto en la nube v\u00e1lido (comprueba la Consola Cloud)", "internal_error": "Error interno validando el c\u00f3digo", "invalid_pin": "C\u00f3digo PIN no v\u00e1lido", "subscriber_error": "Error de suscriptor desconocido, mira los registros", "timeout": "Tiempo de espera agotado validando el c\u00f3digo", "unknown": "Error inesperado", - "wrong_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (era el mismo que el ID del proyecto de acceso al dispositivo)" + "wrong_project_id": "Por favor, introduce un ID de proyecto en la nube v\u00e1lido (era el mismo que el ID del proyecto de acceso al dispositivo)" }, "step": { "auth": { @@ -45,14 +45,14 @@ "title": "Nest: Introduce el ID del Cloud Project" }, "create_cloud_project": { - "description": "La integraci\u00f3n Nest te permite integrar tus termostatos, c\u00e1maras y timbres Nest mediante la API de administraci\u00f3n de dispositivos inteligentes (SDM). La API de SDM **requiere una tarifa de configuraci\u00f3n \u00fanica de 5$**. Consulta la documentaci\u00f3n para obtener [m\u00e1s informaci\u00f3n]({more_info_url}).\n\n1. Ve a [Google Cloud Console]({cloud_console_url}).\n1. Si este es tu primer proyecto, haz clic en **Crear proyecto** y luego en **Nuevo proyecto**.\n1. Asigna un nombre a tu Cloud Project y, a continuaci\u00f3n, haz clic en **Crear**.\n1. Guarda el ID del Cloud Project, por ejemplo, *example-project-12345* ya que lo necesitar\u00e1s m\u00e1s adelante\n1. Ve a la Biblioteca de API de [API de administraci\u00f3n de dispositivos inteligentes]({sdm_api_url}) y haz clic en **Habilitar**.\n1. Ve a la biblioteca de API de [Cloud Pub/Sub API]({pubsub_api_url}) y haz clic en **Habilitar**.\n\nContin\u00faa cuando tu proyecto en la nube est\u00e9 configurado.", + "description": "La integraci\u00f3n Nest te permite integrar tus termostatos, c\u00e1maras y timbres Nest mediante la API de administraci\u00f3n de dispositivos inteligentes (SDM). La API de SDM **requiere una cuota \u00fanica de configuraci\u00f3n de 5$**. Consulta la documentaci\u00f3n para obtener [m\u00e1s informaci\u00f3n]({more_info_url}).\n\n1. Ve a [Google Cloud Console]({cloud_console_url}).\n1. Si este es tu primer proyecto, haz clic en **Crear proyecto** y luego en **Nuevo proyecto**.\n1. Asigna un nombre a tu Cloud Project y, a continuaci\u00f3n, haz clic en **Crear**.\n1. Guarda el ID del Cloud Project, por ejemplo, *example-project-12345* ya que lo necesitar\u00e1s m\u00e1s adelante\n1. Ve a la Biblioteca de API de [API de administraci\u00f3n de dispositivos inteligentes]({sdm_api_url}) y haz clic en **Habilitar**.\n1. Ve a la biblioteca de API de [Cloud Pub/Sub API]({pubsub_api_url}) y haz clic en **Habilitar**.\n\nContin\u00faa cuando tu proyecto en la nube est\u00e9 configurado.", "title": "Nest: Crear y configurar un Cloud Project" }, "device_project": { "data": { "project_id": "ID de proyecto de acceso a dispositivos" }, - "description": "Crea un proyecto de acceso a dispositivos Nest que **requiere una tarifa de 5$** para configurarlo.\n 1. Ve a la [Consola de acceso al dispositivo] ({device_access_console_url}) y sigue el flujo de pago.\n 1. Haz clic en **Crear proyecto**\n 1. Asigna un nombre a tu proyecto de acceso a dispositivos y haz clic en **Siguiente**.\n 1. Introduce tu ID de cliente de OAuth\n 1. Habilita los eventos haciendo clic en **Habilitar** y **Crear proyecto**. \n\n Introduce tu ID de proyecto de acceso a dispositivos a continuaci\u00f3n ([m\u00e1s informaci\u00f3n]({more_info_url})).", + "description": "Crea un proyecto de acceso a dispositivos Nest que **requiere una cuota de 5$** para configurarlo.\n 1. Ve a la [Consola de acceso al dispositivo]({device_access_console_url}) y sigue el flujo de pago.\n 1. Haz clic en **Crear proyecto**\n 1. Asigna un nombre a tu proyecto de acceso a dispositivos y haz clic en **Siguiente**.\n 1. Introduce tu ID de cliente de OAuth\n 1. Habilita los eventos haciendo clic en **Habilitar** y **Crear proyecto**. \n\n Introduce tu ID de proyecto de acceso a dispositivos a continuaci\u00f3n ([m\u00e1s informaci\u00f3n]({more_info_url})).", "title": "Nest: Crear un proyecto de acceso a dispositivos" }, "device_project_upgrade": { @@ -71,7 +71,7 @@ "code": "C\u00f3digo PIN" }, "description": "Para vincular tu cuenta Nest, [autoriza tu cuenta]({url}). \n\nDespu\u00e9s de la autorizaci\u00f3n, copia y pega el c\u00f3digo PIN proporcionado a continuaci\u00f3n.", - "title": "Vincular cuenta de Nest" + "title": "Vincular cuenta Nest" }, "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" @@ -103,7 +103,7 @@ "title": "Se va a eliminar la configuraci\u00f3n YAML de Nest" }, "removed_app_auth": { - "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de utilizar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n por tu parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de integraci\u00f3n]({documentation_url}) de Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", + "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de usar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n por tu parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de la integraci\u00f3n]({documentation_url}) Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", "title": "Las credenciales de autenticaci\u00f3n de Nest deben actualizarse" } } diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index 3a8350932be..68f4160beb9 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, @@ -36,7 +36,7 @@ "person": "{entity_name} ha detectado una persona", "person_away": "{entity_name} ha detectado que una persona se ha ido", "set_point": "Temperatura objetivo de {entity_name} configurada manualmente", - "therm_mode": "{entity_name} cambi\u00f3 a \" {subtype} \"", + "therm_mode": "{entity_name} cambi\u00f3 a \"{subtype}\"", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido", "vehicle": "{entity_name} ha detectado un veh\u00edculo" diff --git a/homeassistant/components/netgear/translations/es.json b/homeassistant/components/netgear/translations/es.json index 2ad6d0e6d16..740e45a302d 100644 --- a/homeassistant/components/netgear/translations/es.json +++ b/homeassistant/components/netgear/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "config": "Error de conexi\u00f3n o de inicio de sesi\u00f3n: verifica tu configuraci\u00f3n" + "config": "Error de conexi\u00f3n o de inicio de sesi\u00f3n: por favor, comprueba tu configuraci\u00f3n" }, "step": { "user": { diff --git a/homeassistant/components/nina/translations/es.json b/homeassistant/components/nina/translations/es.json index a4cabb669d8..8cc397794ba 100644 --- a/homeassistant/components/nina/translations/es.json +++ b/homeassistant/components/nina/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "no_selection": "Por favor selecciona al menos una ciudad/condado", + "no_selection": "Por favor, selecciona al menos una ciudad/condado", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/nmap_tracker/translations/es.json b/homeassistant/components/nmap_tracker/translations/es.json index f978f0d18ad..6491f3dc68a 100644 --- a/homeassistant/components/nmap_tracker/translations/es.json +++ b/homeassistant/components/nmap_tracker/translations/es.json @@ -36,5 +36,5 @@ } } }, - "title": "Rastreador de Nmap" + "title": "Rastreador Nmap" } \ No newline at end of file diff --git a/homeassistant/components/nws/translations/es.json b/homeassistant/components/nws/translations/es.json index 522e601d050..56fe3d4db01 100644 --- a/homeassistant/components/nws/translations/es.json +++ b/homeassistant/components/nws/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "station": "C\u00f3digo de estaci\u00f3n METAR" }, - "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, la latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n m\u00e1s cercana. Por ahora, una clave API puede ser cualquier cosa. Se recomienda utilizar una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida.", + "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, la latitud y la longitud se usar\u00e1n para encontrar la estaci\u00f3n m\u00e1s cercana. Por ahora, una clave API puede ser cualquier cosa. Se recomienda utilizar una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida.", "title": "Conectar con el National Weather Service" } } diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 0b450da3d46..ce828b53cd5 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -3,9 +3,9 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifica la configuraci\u00f3n del perfil en tu dispositivo.", + "no_h264": "No hab\u00eda transmisiones H264 disponibles. Comprueba la configuraci\u00f3n del perfil en tu dispositivo.", "no_mac": "No se pudo configurar un ID \u00fanico para el dispositivo ONVIF.", - "onvif_error": "Error al configurar el dispositivo ONVIF. Consulta los registros para obtener m\u00e1s informaci\u00f3n." + "onvif_error": "Error al configurar el dispositivo ONVIF. Revisa los registros para obtener m\u00e1s informaci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar" @@ -38,7 +38,7 @@ "data": { "auto": "Buscar autom\u00e1ticamente" }, - "description": "Al hacer clic en enviar, buscaremos en tu red dispositivos ONVIF compatibles con el perfil S. \n\nAlgunos fabricantes han comenzado a deshabilitar ONVIF por defecto. Aseg\u00farate de que ONVIF est\u00e9 habilitado en la configuraci\u00f3n de tu c\u00e1mara.", + "description": "Al hacer clic en enviar, buscaremos en tu red dispositivos ONVIF compatibles con el perfil S. \n\nAlgunos fabricantes han comenzado a deshabilitar ONVIF por defecto. Por favor, aseg\u00farate de que ONVIF est\u00e9 habilitado en la configuraci\u00f3n de tu c\u00e1mara.", "title": "Configuraci\u00f3n del dispositivo ONVIF" } } diff --git a/homeassistant/components/openexchangerates/translations/ca.json b/homeassistant/components/openexchangerates/translations/ca.json index 98c74ae55b3..634ea7578e7 100644 --- a/homeassistant/components/openexchangerates/translations/ca.json +++ b/homeassistant/components/openexchangerates/translations/ca.json @@ -26,6 +26,7 @@ }, "issues": { "deprecated_yaml": { + "description": "La configuraci\u00f3 d'Open Exchange Rates mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'Open Exchange Rates del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML d'Open Exchange Rates est\u00e0 sent eliminada" } } diff --git a/homeassistant/components/openexchangerates/translations/ja.json b/homeassistant/components/openexchangerates/translations/ja.json index 35a212e6d82..b4299af9207 100644 --- a/homeassistant/components/openexchangerates/translations/ja.json +++ b/homeassistant/components/openexchangerates/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, @@ -14,12 +15,18 @@ "step": { "user": { "data": { - "api_key": "API\u30ad\u30fc" + "api_key": "API\u30ad\u30fc", + "base": "\u57fa\u672c\u901a\u8ca8" }, "data_description": { "base": "\u7c73\u30c9\u30eb\u4ee5\u5916\u306e\u57fa\u672c\u901a\u8ca8\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001[\u6709\u6599\u30d7\u30e9\u30f3] ({signup}) \u304c\u5fc5\u8981\u3067\u3059\u3002" } } } + }, + "issues": { + "deprecated_yaml": { + "title": "Open Exchange Rates YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index 5503525ac58..a2b04f05cf9 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "server_in_maintenance": "El servidor est\u00e1 fuera de servicio por mantenimiento", + "server_in_maintenance": "El servidor est\u00e1 ca\u00eddo por mantenimiento", "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", "too_many_requests": "Demasiadas solicitudes, vuelve a intentarlo m\u00e1s tarde", "unknown": "Error inesperado" @@ -22,7 +22,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "La plataforma Overkiz es utilizada por varios proveedores como Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) y Atlantic (Cozytouch). Introduce las credenciales de tu aplicaci\u00f3n y selecciona tu concentrador." + "description": "La plataforma Overkiz es usada por varios proveedores como Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) y Atlantic (Cozytouch). Introduce las credenciales de tu aplicaci\u00f3n y selecciona tu concentrador." } } } diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index 0af57427980..a28e441d9d8 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -11,7 +11,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "La autenticaci\u00f3n fall\u00f3 para OVO Energy. Introduce tus credenciales actuales.", + "description": "La autenticaci\u00f3n fall\u00f3 para OVO Energy. Por favor, introduce tus credenciales actuales.", "title": "Reautenticaci\u00f3n" }, "user": { diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index 23acbb80c6c..e65cf7e82d5 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/translations/es.json @@ -20,7 +20,7 @@ "token": "Pega el token de autenticaci\u00f3n aqu\u00ed", "use_webhook": "Usar webhook" }, - "description": "Para poder consultar la API, se requiere un `auth_token` que se puede obtener siguiendo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrucciones \n\nDispositivo seleccionado: ** {device_type} ** \n\nSi prefieres utilizar el m\u00e9todo de webhook incorporado (solo Airlock), marca la casilla a continuaci\u00f3n y deja el token de autenticaci\u00f3n en blanco", + "description": "Para poder consultar la API, se requiere un `auth_token` que se puede obtener siguiendo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrucciones \n\nDispositivo seleccionado: **{device_type}** \n\nSi prefieres usar el m\u00e9todo de webhook incorporado (solo Airlock), por favor, marca la casilla a continuaci\u00f3n y deja el token de autenticaci\u00f3n en blanco", "title": "Seleccionar el m\u00e9todo API" }, "user": { @@ -33,7 +33,7 @@ }, "webhook": { "description": "Para enviar eventos a Home Assistant, deber\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\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles.", - "title": "Webhook a utilizar" + "title": "Webhook a usar" } } }, diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 50e440f6d6a..fed26040384 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -30,7 +30,7 @@ "port": "Puerto", "username": "Nombre de usuario Smile" }, - "description": "Por favor, introduce:", + "description": "Por favor, introduce", "title": "Conectar a Smile" } } diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index c0a86f70d55..dcde44ad273 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", - "external_setup": "Point se ha configurado correctamente a partir de otro flujo.", + "external_setup": "Point se ha configurado correctamente desde otro flujo.", "no_flows": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, @@ -11,13 +11,13 @@ "default": "Autenticado correctamente" }, "error": { - "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de presionar Enviar", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de pulsar Enviar", "no_token": "Token de acceso no v\u00e1lido" }, "step": { "auth": { - "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Minut, luego regresa y presiona **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", - "title": "Autenticaci\u00f3n con Point" + "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Minut, luego regresa y pulsa **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", + "title": "Autenticar Point" }, "user": { "data": { diff --git a/homeassistant/components/qingping/translations/ca.json b/homeassistant/components/qingping/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/qingping/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Dispositiu no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/de.json b/homeassistant/components/qingping/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/qingping/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/el.json b/homeassistant/components/qingping/translations/el.json new file mode 100644 index 00000000000..cdb57c8ac1b --- /dev/null +++ b/homeassistant/components/qingping/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/es.json b/homeassistant/components/qingping/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/qingping/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_supported": "Dispositivo no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/et.json b/homeassistant/components/qingping/translations/et.json new file mode 100644 index 00000000000..9d3874dde88 --- /dev/null +++ b/homeassistant/components/qingping/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "not_supported": "Seda seadet ei toetata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadaistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/fi.json b/homeassistant/components/qingping/translations/fi.json new file mode 100644 index 00000000000..5c7f611a2cd --- /dev/null +++ b/homeassistant/components/qingping/translations/fi.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "Laitetta ei tueta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/fr.json b/homeassistant/components/qingping/translations/fr.json new file mode 100644 index 00000000000..8ddb4af4dbc --- /dev/null +++ b/homeassistant/components/qingping/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "not_supported": "Appareil non pris en charge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/hu.json b/homeassistant/components/qingping/translations/hu.json new file mode 100644 index 00000000000..8a1bc9a1c42 --- /dev/null +++ b/homeassistant/components/qingping/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r be van \u00e1ll\u00edtva", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nincs felder\u00edtett eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Be\u00e1ll\u00edtja a k\u00f6vetkez\u0151t: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lasszon ki egy eszk\u00f6zt a be\u00e1ll\u00edt\u00e1shoz" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/id.json b/homeassistant/components/qingping/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/qingping/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_supported": "Perangkat tidak didukung" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/it.json b/homeassistant/components/qingping/translations/it.json new file mode 100644 index 00000000000..7784ed3a240 --- /dev/null +++ b/homeassistant/components/qingping/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Dispositivo non supportato" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/ja.json b/homeassistant/components/qingping/translations/ja.json new file mode 100644 index 00000000000..fe1c5746cda --- /dev/null +++ b/homeassistant/components/qingping/translations/ja.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/no.json b/homeassistant/components/qingping/translations/no.json new file mode 100644 index 00000000000..0bf8b1695ec --- /dev/null +++ b/homeassistant/components/qingping/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Enheten st\u00f8ttes ikke" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/pt-BR.json b/homeassistant/components/qingping/translations/pt-BR.json new file mode 100644 index 00000000000..0da7639fa2a --- /dev/null +++ b/homeassistant/components/qingping/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/ru.json b/homeassistant/components/qingping/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/qingping/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.", + "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": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/zh-Hant.json b/homeassistant/components/qingping/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/qingping/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\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": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/ja.json b/homeassistant/components/roon/translations/ja.json index 12a1b074916..7f6adbd1f04 100644 --- a/homeassistant/components/roon/translations/ja.json +++ b/homeassistant/components/roon/translations/ja.json @@ -18,6 +18,9 @@ "link": { "description": "Roon\u3067Home Assistant\u3092\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u305f\u5f8c\u3001Roon Core\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u8a2d\u5b9a(Settings )\u3092\u958b\u304d\u3001\u6a5f\u80fd\u62e1\u5f35\u30bf\u30d6(extensions tab)\u3067Home Assistant\u3092\u6709\u52b9(enable )\u306b\u3057\u307e\u3059\u3002", "title": "Roon\u3067HomeAssistant\u3092\u8a8d\u8a3c\u3059\u308b" + }, + "user": { + "other": "\u7a7a" } } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/es.json b/homeassistant/components/rtsp_to_webrtc/translations/es.json index 5a066d6645e..ea3e8c4afc1 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/es.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/es.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", - "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", + "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Revisa los registros para obtener m\u00e1s informaci\u00f3n.", + "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Revisa los registros para obtener m\u00e1s informaci\u00f3n.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "invalid_url": "Debe ser una URL de servidor RTSPtoWebRTC v\u00e1lida, por ejemplo, https://example.com", - "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", - "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulta los registros para obtener m\u00e1s informaci\u00f3n." + "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Revisa los registros para obtener m\u00e1s informaci\u00f3n.", + "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Revisa los registros para obtener m\u00e1s informaci\u00f3n." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 6a5487175e7..b102c85b2f3 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -6,7 +6,7 @@ "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a esta TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", - "not_supported": "Este dispositivo Samsung no es compatible actualmente.", + "not_supported": "Este dispositivo Samsung no es compatible por el momento.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/schedule/translations/el.json b/homeassistant/components/schedule/translations/el.json index 2c6a17ab298..b0a7c066a53 100644 --- a/homeassistant/components/schedule/translations/el.json +++ b/homeassistant/components/schedule/translations/el.json @@ -1,3 +1,9 @@ { + "state": { + "_": { + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" + } + }, "title": "\u03a0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1" } \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/fi.json b/homeassistant/components/schedule/translations/fi.json new file mode 100644 index 00000000000..927dd806b5c --- /dev/null +++ b/homeassistant/components/schedule/translations/fi.json @@ -0,0 +1,3 @@ +{ + "title": "Aikataulu" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/ja.json b/homeassistant/components/schedule/translations/ja.json new file mode 100644 index 00000000000..690284b8afa --- /dev/null +++ b/homeassistant/components/schedule/translations/ja.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u30aa\u30d5", + "on": "\u30aa\u30f3" + } + }, + "title": "\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/no.json b/homeassistant/components/schedule/translations/no.json new file mode 100644 index 00000000000..7dfb9cae4ea --- /dev/null +++ b/homeassistant/components/schedule/translations/no.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "Timeplan" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/ru.json b/homeassistant/components/schedule/translations/ru.json new file mode 100644 index 00000000000..26a5aeac364 --- /dev/null +++ b/homeassistant/components/schedule/translations/ru.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + }, + "title": "\u0420\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435" +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/es.json b/homeassistant/components/scrape/translations/es.json index f5c07aa30b4..d88197473d3 100644 --- a/homeassistant/components/scrape/translations/es.json +++ b/homeassistant/components/scrape/translations/es.json @@ -25,10 +25,10 @@ "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", - "headers": "Cabeceras a utilizar para la petici\u00f3n web", - "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", + "headers": "Cabeceras a usar para la petici\u00f3n web", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS se va a usar", "resource": "La URL del sitio web que contiene el valor.", - "select": "Define qu\u00e9 etiqueta buscar. Consulta los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", + "select": "Define qu\u00e9 etiqueta buscar. Revisa los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", "state_class": "El state_class del sensor", "value_template": "Define una plantilla para obtener el estado del sensor", "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" @@ -59,10 +59,10 @@ "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", - "headers": "Cabeceras a utilizar para la petici\u00f3n web", - "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", + "headers": "Cabeceras a usar para la petici\u00f3n web", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS se va a usar", "resource": "La URL del sitio web que contiene el valor.", - "select": "Define qu\u00e9 etiqueta buscar. Consulta los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", + "select": "Define qu\u00e9 etiqueta buscar. Revisa los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", "state_class": "El state_class del sensor", "value_template": "Define una plantilla para obtener el estado del sensor", "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" diff --git a/homeassistant/components/select/translations/es.json b/homeassistant/components/select/translations/es.json index b2066b4606d..a2927327151 100644 --- a/homeassistant/components/select/translations/es.json +++ b/homeassistant/components/select/translations/es.json @@ -7,7 +7,7 @@ "selected_option": "Opci\u00f3n de {entity_name} seleccionada actualmente" }, "trigger_type": { - "current_option_changed": "Opci\u00f3n de {entity_name} cambiada" + "current_option_changed": "Una opci\u00f3n de {entity_name} cambi\u00f3" } }, "title": "Seleccionar" diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index fd8a1581530..c61b0b7927a 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -1,62 +1,62 @@ { "device_automation": { "condition_type": { - "is_apparent_power": "Potencia aparente actual de {entity_name}", - "is_battery_level": "Nivel de bater\u00eda actual de {entity_name}", - "is_carbon_dioxide": "Nivel actual en {entity_name} de concentraci\u00f3n de di\u00f3xido de carbono", - "is_carbon_monoxide": "Nivel actual en {entity_name} de concentraci\u00f3n de mon\u00f3xido de carbono", - "is_current": "Corriente actual de {entity_name}", - "is_energy": "Energ\u00eda actual de {entity_name}", - "is_frequency": "Frecuencia de {entity_name} actual", - "is_gas": "Gas actual de {entity_name}", - "is_humidity": "Humedad actual de {entity_name}", - "is_illuminance": "Luminosidad actual de {entity_name}", - "is_nitrogen_dioxide": "Nivel actual de concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno de {entity_name}", - "is_nitrogen_monoxide": "Nivel actual de concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno de {entity_name}", - "is_nitrous_oxide": "Nivel actual de concentraci\u00f3n de \u00f3xido nitroso de {entity_name}", - "is_ozone": "Nivel actual de concentraci\u00f3n de ozono de {entity_name}", - "is_pm1": "Nivel actual de concentraci\u00f3n de PM1 de {entity_name}", - "is_pm10": "Nivel actual de concentraci\u00f3n de PM10 de {entity_name}", - "is_pm25": "Nivel actual de concentraci\u00f3n de PM2.5 de {entity_name}", - "is_power": "Potencia actual de {entity_name}", - "is_power_factor": "Factor de potencia actual de {entity_name}", - "is_pressure": "Presi\u00f3n actual de {entity_name}", - "is_reactive_power": "Potencia reactiva actual de {entity_name}", - "is_signal_strength": "Intensidad de la se\u00f1al actual de {entity_name}", - "is_sulphur_dioxide": "Nivel actual de concentraci\u00f3n de di\u00f3xido de azufre de {entity_name}", - "is_temperature": "Temperatura actual de {entity_name}", - "is_value": "Valor actual de {entity_name}", - "is_volatile_organic_compounds": "Nivel actual de concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles de {entity_name}", - "is_voltage": "Voltaje actual de {entity_name}" + "is_apparent_power": "La potencia aparente actual de {entity_name}", + "is_battery_level": "El nivel de bater\u00eda actual de {entity_name}", + "is_carbon_dioxide": "El nivel de la concentraci\u00f3n de di\u00f3xido de carbono actual de {entity_name}", + "is_carbon_monoxide": "El nivel de la concentraci\u00f3n de mon\u00f3xido de carbono actual de {entity_name}", + "is_current": "La intensidad de corriente actual de {entity_name}", + "is_energy": "La energ\u00eda actual de {entity_name}", + "is_frequency": "La frecuencia actual de {entity_name}", + "is_gas": "El gas actual de {entity_name}", + "is_humidity": "La humedad actual de {entity_name}", + "is_illuminance": "La luminosidad actual de {entity_name}", + "is_nitrogen_dioxide": "El nivel de la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno actual de {entity_name}", + "is_nitrogen_monoxide": "El nivel de la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno actual de {entity_name}", + "is_nitrous_oxide": "El nivel de la concentraci\u00f3n de \u00f3xido nitroso actual de {entity_name}", + "is_ozone": "El nivel de la concentraci\u00f3n de ozono actual de {entity_name}", + "is_pm1": "El nivel de la concentraci\u00f3n de PM1 actual de {entity_name}", + "is_pm10": "El nivel de la concentraci\u00f3n de PM10 actual de {entity_name}", + "is_pm25": "El nivel de la concentraci\u00f3n de PM2.5 actual de {entity_name}", + "is_power": "La potencia actual de {entity_name}", + "is_power_factor": "El factor de potencia actual de {entity_name}", + "is_pressure": "La presi\u00f3n actual de {entity_name}", + "is_reactive_power": "La potencia reactiva actual de {entity_name}", + "is_signal_strength": "La intensidad de la se\u00f1al actual de {entity_name}", + "is_sulphur_dioxide": "El nivel de la concentraci\u00f3n de di\u00f3xido de azufre actual de {entity_name}", + "is_temperature": "La temperatura actual de {entity_name}", + "is_value": "El valor actual de {entity_name}", + "is_volatile_organic_compounds": "El nivel de la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles actual de {entity_name}", + "is_voltage": "El voltaje actual de {entity_name}" }, "trigger_type": { - "apparent_power": "{entity_name} ha cambiado de potencia aparente", + "apparent_power": "La potencia aparente de {entity_name} cambia", "battery_level": "El nivel de bater\u00eda de {entity_name} cambia", - "carbon_dioxide": "{entity_name} cambios en la concentraci\u00f3n de di\u00f3xido de carbono", - "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono", - "current": "Cambio de corriente en {entity_name}", - "energy": "Cambio de energ\u00eda en {entity_name}", - "frequency": "{entity_name} ha cambiado de frecuencia", - "gas": "{entity_name} ha hambiado gas", + "carbon_dioxide": "La concentraci\u00f3n de di\u00f3xido de carbono de {entity_name} cambia", + "carbon_monoxide": "La concentraci\u00f3n de mon\u00f3xido de carbono de {entity_name} cambia", + "current": "La intensidad de corriente de {entity_name} cambia", + "energy": "La energ\u00eda de {entity_name} cambia", + "frequency": "La frecuencia de {entity_name} cambia", + "gas": "El gas de {entity_name} cambia", "humidity": "La humedad de {entity_name} cambia", "illuminance": "La luminosidad de {entity_name} cambia", - "nitrogen_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno", - "nitrogen_monoxide": "{entity_name} ha cambiado en la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno", - "nitrous_oxide": "{entity_name} ha cambiado en la concentraci\u00f3n de \u00f3xido nitroso", - "ozone": "{entity_name} ha cambiado en la concentraci\u00f3n de ozono", - "pm1": "{entity_name} ha cambiado en la concentraci\u00f3n de PM1", - "pm10": "{entity_name} ha cambiado en la concentraci\u00f3n de PM10", - "pm25": "{entity_name} ha cambiado en la concentraci\u00f3n de PM2.5", + "nitrogen_dioxide": "La concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno de {entity_name} cambia", + "nitrogen_monoxide": "La concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno de {entity_name} cambia", + "nitrous_oxide": "La concentraci\u00f3n de \u00f3xido nitroso de {entity_name} cambia", + "ozone": "La concentraci\u00f3n de ozono de {entity_name} cambia", + "pm1": "La concentraci\u00f3n de PM1 de {entity_name} cambia", + "pm10": "La concentraci\u00f3n de PM10 de {entity_name} cambia", + "pm25": "La concentraci\u00f3n de PM2.5 de {entity_name} cambia", "power": "La potencia de {entity_name} cambia", - "power_factor": "Cambio de factor de potencia en {entity_name}", + "power_factor": "El factor de potencia de {entity_name} cambia", "pressure": "La presi\u00f3n de {entity_name} cambia", - "reactive_power": "{entity_name} ha cambiado de potencia reactiva", + "reactive_power": "La potencia reactiva de {entity_name} cambia", "signal_strength": "La intensidad de se\u00f1al de {entity_name} cambia", - "sulphur_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de azufre", + "sulphur_dioxide": "La concentraci\u00f3n de di\u00f3xido de azufre de {entity_name} cambia", "temperature": "La temperatura de {entity_name} cambia", "value": "El valor de {entity_name} cambia", - "volatile_organic_compounds": "{entity_name} ha cambiado la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles", - "voltage": "Cambio de voltaje en {entity_name}" + "volatile_organic_compounds": "La concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles de {entity_name} cambia", + "voltage": "El voltaje de {entity_name} cambia" } }, "state": { diff --git a/homeassistant/components/senz/translations/es.json b/homeassistant/components/senz/translations/es.json index e08ed208ac6..406d6b64017 100644 --- a/homeassistant/components/senz/translations/es.json +++ b/homeassistant/components/senz/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos." }, "create_entry": { diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 585ec778f7c..f1ad9c7ffda 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -13,7 +13,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte. Puedes activar manualmente el dispositivo ahora con un bot\u00f3n del mismo o esperar a la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." + "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos por contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos por contrase\u00f1a se a\u00f1adir\u00e1n cuando el dispositivo se despierte. Puedes activar manualmente el dispositivo ahora con un bot\u00f3n del mismo o esperar a la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." }, "credentials": { "data": { diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 9f44ba9ade9..13ec9c79d38 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -35,7 +35,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "SimpliSafe autentica els seus usuaris a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) abans de comen\u00e7ar.\n\nQuan ja estiguis, fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials. Quan el proc\u00e9s s'hagi completat, torna aqu\u00ed i introdueix, a sota, el codi d'autoritzaci\u00f3 de l'URL de l'aplicaci\u00f3 web de SimpliSafe." + "description": "SimpliSafe autentica els seus usuaris a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) abans de comen\u00e7ar.\n\nQuan ja estiguis, fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials. Si ja has iniciat sessi\u00f3 a SimpliSafe a trav\u00e9s del navegador, potser hauries d'obrir una nova pestanya i copiar-hi l'URL de dalt.\n\nQuan el proc\u00e9s s'hagi completat, torna aqu\u00ed i introdueix, a sota, el codi d'autoritzaci\u00f3 de l'URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 842a2245fe3..408b595d329 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -35,7 +35,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\nCuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." + "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; por favor, aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\nCuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 6f2a61e2df4..997f376916d 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -35,7 +35,7 @@ "password": "Password", "username": "Nome utente" }, - "description": "SimpliSafe autentica gli utenti tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati, prima di iniziare, di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\nQuando sei pronto, fai clic [qui]({url}) per aprire l'app Web SimpliSafe e inserire le tue credenziali. Al termine del processo, ritorna qui e inserisci il codice di autorizzazione dall'URL dell'app Web SimpliSafe." + "description": "SimpliSafe autentica gli utenti tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati, prima di iniziare, di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\nQuando sei pronto, fai clic [qui]({url}) per aprire l'app web di SimpliSafe e inserisci le tue credenziali. Se hai gi\u00e0 effettuato l'accesso a SimpliSafe nel tuo browser, puoi aprire una nuova scheda e copiare/incollare l'URL sopra indicato in quella nuova scheda.\n\nAl termine del processo, ritorna qui e inserisci il codice di autorizzazione dall'URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 4e65fe8a057..38e383a75af 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_auth_code_length": "SimpliSafe\u306e\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306f45\u6587\u5b57\u3067\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "progress": { diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index ebffb459169..15e8ed37363 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", - "already_configured_local_device": "Los dispositivos locales ya est\u00e1n configurados. Elim\u00ednelos primero antes de configurar un dispositivo en la nube.", + "already_configured_local_device": "Los dispositivos locales ya est\u00e1n configurados. Por favor, elim\u00ednalos primero antes de configurar un dispositivo en la nube.", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "cannot_connect": "No se pudo conectar", "invalid_mdns": "Dispositivo no compatible con la integraci\u00f3n de Smappee.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/smartthings/translations/es.json b/homeassistant/components/smartthings/translations/es.json index b77cb803205..5e05cbf711e 100644 --- a/homeassistant/components/smartthings/translations/es.json +++ b/homeassistant/components/smartthings/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida:\n> {webhook_url} \n\nActualiza tu configuraci\u00f3n seg\u00fan las [instrucciones]({component_url}), reinicia Home Assistant y vuelve a intentarlo.", + "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida:\n> {webhook_url} \n\nPor favor, actualiza tu configuraci\u00f3n seg\u00fan las [instrucciones]({component_url}), reinicia Home Assistant e int\u00e9ntalo de nuevo.", "no_available_locations": "No hay Ubicaciones SmartThings disponibles para configurar en Home Assistant." }, "error": { @@ -9,7 +9,7 @@ "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", - "webhook_error": "SmartThings no pudo validar la URL del webhook. Aseg\u00farate de que la URL del webhook sea accesible desde Internet y vuelve a intentarlo." + "webhook_error": "SmartThings no pudo validar la URL del webhook. Por favor, aseg\u00farate de que la URL del webhook sea accesible desde Internet e int\u00e9ntalo de nuevo." }, "step": { "authorize": { @@ -26,7 +26,7 @@ "data": { "location_id": "Ubicaci\u00f3n" }, - "description": "Selecciona la Ubicaci\u00f3n SmartThings que quieres a\u00f1adir a Home Assistant. Se abrir\u00e1 una nueva ventana que te pedir\u00e1 que inicies sesi\u00f3n y autorices la instalaci\u00f3n de la integraci\u00f3n de Home Assistant en la ubicaci\u00f3n seleccionada.", + "description": "Por favor, selecciona la Ubicaci\u00f3n SmartThings que quieres a\u00f1adir a Home Assistant. Se abrir\u00e1 una nueva ventana que te pedir\u00e1 que inicies sesi\u00f3n y autorices la instalaci\u00f3n de la integraci\u00f3n de Home Assistant en la ubicaci\u00f3n seleccionada.", "title": "Seleccionar Ubicaci\u00f3n" }, "user": { diff --git a/homeassistant/components/solarlog/translations/es.json b/homeassistant/components/solarlog/translations/es.json index 0324b8ee5e6..195b623a1d2 100644 --- a/homeassistant/components/solarlog/translations/es.json +++ b/homeassistant/components/solarlog/translations/es.json @@ -11,7 +11,7 @@ "user": { "data": { "host": "Host", - "name": "El prefijo que se utilizar\u00e1 para tus sensores Solar-Log" + "name": "El prefijo que se usar\u00e1 para tus sensores Solar-Log" }, "title": "Define tu conexi\u00f3n Solar-Log" } diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index edb2ff4bf82..e2e3cf927a7 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "reauth_account_mismatch": "La cuenta de Spotify con la que est\u00e1s autenticado, no coincide con la cuenta necesaria para re-autenticaci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/sql/translations/es.json b/homeassistant/components/sql/translations/es.json index 427510a3f1d..aefea983a01 100644 --- a/homeassistant/components/sql/translations/es.json +++ b/homeassistant/components/sql/translations/es.json @@ -19,7 +19,7 @@ }, "data_description": { "column": "Columna de respuesta de la consulta para presentar como estado", - "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", + "db_url": "URL de la base de datos, d\u00e9jalo en blanco para usar la predeterminada de HA", "name": "Nombre que se usar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", "query": "Consulta a ejecutar, debe empezar por 'SELECT'", "unit_of_measurement": "Unidad de medida (opcional)", @@ -45,7 +45,7 @@ }, "data_description": { "column": "Columna de respuesta de la consulta para presentar como estado", - "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", + "db_url": "URL de la base de datos, d\u00e9jalo en blanco para usar la predeterminada de HA", "name": "Nombre que se usar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", "query": "Consulta a ejecutar, debe empezar por 'SELECT'", "unit_of_measurement": "Unidad de medida (opcional)", diff --git a/homeassistant/components/steam_online/translations/es.json b/homeassistant/components/steam_online/translations/es.json index 7aeca102cc4..e558ead9ff5 100644 --- a/homeassistant/components/steam_online/translations/es.json +++ b/homeassistant/components/steam_online/translations/es.json @@ -20,7 +20,7 @@ "account": "ID de la cuenta de Steam", "api_key": "Clave API" }, - "description": "Utiliza {account_id_url} para encontrar el ID de tu cuenta de Steam" + "description": "Usa {account_id_url} para encontrar el ID de tu cuenta de Steam" } } }, diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index be4121cf747..262e5b3186c 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -11,7 +11,7 @@ "incorrect_pin": "PIN incorrecto", "incorrect_validation_code": "C\u00f3digo de validaci\u00f3n incorrecto", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "two_factor_request_failed": "La solicitud del c\u00f3digo 2FA fall\u00f3, int\u00e9ntalo de nuevo" + "two_factor_request_failed": "La solicitud del c\u00f3digo 2FA fall\u00f3, por favor, int\u00e9ntalo de nuevo" }, "step": { "pin": { @@ -23,7 +23,7 @@ }, "two_factor": { "data": { - "contact_method": "Selecciona un m\u00e9todo de contacto:" + "contact_method": "Por favor, selecciona un m\u00e9todo de contacto:" }, "description": "Se requiere autenticaci\u00f3n de dos factores", "title": "Configuraci\u00f3n de Subaru Starlink" @@ -52,7 +52,7 @@ "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 tu 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).", + "description": "Cuando est\u00e1 habilitado, el sondeo del veh\u00edculo enviar\u00e1 un comando remoto a tu 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" } } diff --git a/homeassistant/components/switch_as_x/translations/es.json b/homeassistant/components/switch_as_x/translations/es.json index eeeae5ede55..014106acb4e 100644 --- a/homeassistant/components/switch_as_x/translations/es.json +++ b/homeassistant/components/switch_as_x/translations/es.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "entity_id": "Conmutador", + "entity_id": "Interruptor", "target_domain": "Nuevo tipo" }, "description": "Elige un interruptor que desees que aparezca en Home Assistant como luz, cubierta o cualquier otra cosa. El interruptor original se ocultar\u00e1." diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index d38179c0702..14a5af2818a 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -13,6 +13,9 @@ "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, "password": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae {name} \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" }, "user": { diff --git a/homeassistant/components/switchbot/translations/fi.json b/homeassistant/components/switchbot/translations/fi.json new file mode 100644 index 00000000000..33c3ca73dcc --- /dev/null +++ b/homeassistant/components/switchbot/translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "password": { + "data": { + "password": "Salasana" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index 3f9425d179a..8954b2397ab 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -7,8 +7,20 @@ "switchbot_unsupported_type": "\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u7a2e\u985e\u306eSwitchbot", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, + "error": { + "other": "\u7a7a" + }, "flow_title": "{name}", "step": { + "confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "password": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{name} \u30c7\u30d0\u30a4\u30b9\u306b\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u5fc5\u8981\u3067\u3059" + }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30c9\u30ec\u30b9", diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index 7b2f4f73cc2..ddf26f9a40e 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "password": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {name} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043f\u0430\u0440\u043e\u043b\u044c" + }, "user": { "data": { "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index 2fd67d46d01..cad0b303116 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -12,7 +12,7 @@ "step": { "auth": { "description": "Para vincular tu cuenta de Telldus Live:\n1. Pulsa el siguiente enlace\n2. Inicia sesi\u00f3n en Telldus Live\n3. Autoriza **{app_name}** (pulsa en **Yes**).\n4. Vuelve atr\u00e1s y pulsa **ENVIAR**.\n\n[Enlace a la cuenta TelldusLive]({auth_url})", - "title": "Autenticaci\u00f3n contra TelldusLive" + "title": "Autenticar contra TelldusLive" }, "user": { "data": { diff --git a/homeassistant/components/tesla_wall_connector/translations/es.json b/homeassistant/components/tesla_wall_connector/translations/es.json index c324ad42733..46f62664445 100644 --- a/homeassistant/components/tesla_wall_connector/translations/es.json +++ b/homeassistant/components/tesla_wall_connector/translations/es.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "title": "Configurar el Tesla Wall Connector" + "title": "Configurar Tesla Wall Connector" } } } diff --git a/homeassistant/components/tomorrowio/translations/es.json b/homeassistant/components/tomorrowio/translations/es.json index 48bac23d2ba..27eeea44515 100644 --- a/homeassistant/components/tomorrowio/translations/es.json +++ b/homeassistant/components/tomorrowio/translations/es.json @@ -3,7 +3,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_api_key": "Clave API no v\u00e1lida", - "rate_limited": "Actualmente el ritmo de consultas est\u00e1 limitado, por favor int\u00e9ntalo m\u00e1s tarde.", + "rate_limited": "La frecuencia de consulta est\u00e1 limitada en este momento, por favor, vuelve a intentarlo m\u00e1s tarde.", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index 89f85a222a1..2cab9e84513 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "step": { diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 61930b53fb0..079e0be5af0 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "no_locations": "No hay ubicaciones disponibles para este usuario, verifica la configuraci\u00f3n de TotalConnect", + "no_locations": "No hay ubicaciones disponibles para este usuario, comprueba la configuraci\u00f3n de TotalConnect", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/traccar/translations/es.json b/homeassistant/components/traccar/translations/es.json index 1177250f930..a3710a80a50 100644 --- a/homeassistant/components/traccar/translations/es.json +++ b/homeassistant/components/traccar/translations/es.json @@ -6,7 +6,7 @@ "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, deber\u00e1s configurar la funci\u00f3n de webhook en Traccar. \n\nUtiliza la siguiente URL: `{webhook_url}` \n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, deber\u00e1s configurar la funci\u00f3n de webhook en Traccar. \n\nUsa la siguiente URL: `{webhook_url}` \n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/tractive/translations/es.json b/homeassistant/components/tractive/translations/es.json index 0e242f535ee..41f5827ac1a 100644 --- a/homeassistant/components/tractive/translations/es.json +++ b/homeassistant/components/tractive/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimina la integraci\u00f3n y vuelve a configurarla.", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, por favor, elimina la integraci\u00f3n y vuelve a configurarla.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index d722d0a528a..e9d084d9758 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "name_exists": "Nombre ya existente" + "name_exists": "El nombre ya existe" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/twilio/translations/es.json b/homeassistant/components/twilio/translations/es.json index 3743b0ee163..8640618e8cd 100644 --- a/homeassistant/components/twilio/translations/es.json +++ b/homeassistant/components/twilio/translations/es.json @@ -6,7 +6,7 @@ "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 debes configurar los [Webhooks en Twilio]({twilio_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/x-www-form-urlencoded \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Twilio]({twilio_url}). \n\nCompleta la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/x-www-form-urlencoded \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." }, "step": { "user": { diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index 1bc3e90ac91..1c03e7b3617 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -43,19 +43,19 @@ "data": { "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta que se considera ausente", "ignore_wired_bug": "Deshabilitar la l\u00f3gica de errores cableada de UniFi Network", - "ssid_filter": "Selecciona los SSIDs para realizar el seguimiento de clientes inal\u00e1mbricos", - "track_clients": "Seguimiento de los clientes de red", - "track_devices": "Seguimiento de dispositivos de red (dispositivos Ubiquiti)", + "ssid_filter": "Selecciona los SSIDs en los que rastrear clientes inal\u00e1mbricos", + "track_clients": "Rastrear clientes de red", + "track_devices": "Rastrear dispositivos de red (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de red cableada" }, - "description": "Configurar dispositivo de seguimiento", + "description": "Configurar rastreo de dispositivo", "title": "Opciones de UniFi Network 1/3" }, "simple_options": { "data": { "block_client": "Clientes con acceso controlado a la red", - "track_clients": "Seguimiento de los clientes de red", - "track_devices": "Seguimiento de dispositivos de red (dispositivos Ubiquiti)" + "track_clients": "Rastrear clientes de red", + "track_devices": "Rastrear dispositivos de red (dispositivos Ubiquiti)" }, "description": "Configura la integraci\u00f3n UniFi Network" }, diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index a3d873ead5b..830b73d1eee 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -47,6 +47,7 @@ "data": { "all_updates": "M\u00e8triques en temps real (ALERTA: augmenta considerablement l'\u00fas de CPU)", "disable_rtsp": "Desactiva el flux RTSP", + "max_media": "Nombre m\u00e0xim d'esdeveniments a carregar al navegador multim\u00e8dia (augmenta l'\u00fas de RAM)", "override_connection_host": "Substitueix l'amfitri\u00f3 de connexi\u00f3" }, "description": "Les m\u00e8triques en temps real nom\u00e9s s'haurien d'activar si has habilitat sensors de diagn\u00f2stic i vols que s'actualitzin en temps real. Si no s'activa aquesta opci\u00f3, els sensors s'actualitzaran cada 15 minuts.", diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index 684ee3f817f..d12744bf841 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Actualiza UniFi Protect y vuelve a intentarlo." + "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Por favor, actualiza UniFi Protect y vuelve a intentarlo." }, "flow_title": "{name} ({ip_address})", "step": { diff --git a/homeassistant/components/unifiprotect/translations/ja.json b/homeassistant/components/unifiprotect/translations/ja.json index 12275699260..608601e2175 100644 --- a/homeassistant/components/unifiprotect/translations/ja.json +++ b/homeassistant/components/unifiprotect/translations/ja.json @@ -47,6 +47,7 @@ "data": { "all_updates": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30e1\u30c8\u30ea\u30c3\u30af(Realtime metrics)(\u8b66\u544a: CPU\u4f7f\u7528\u7387\u304c\u5927\u5e45\u306b\u5897\u52a0\u3057\u307e\u3059)", "disable_rtsp": "RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u7121\u52b9\u306b\u3059\u308b", + "max_media": "\u30e1\u30c7\u30a3\u30a2\u30d6\u30e9\u30a6\u30b6\u306b\u30ed\u30fc\u30c9\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u306e\u6700\u5927\u6570(RAM\u4f7f\u7528\u91cf\u304c\u5897\u52a0)", "override_connection_host": "\u63a5\u7d9a\u30db\u30b9\u30c8\u3092\u4e0a\u66f8\u304d" }, "description": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30e1\u30c8\u30ea\u30c3\u30af \u30aa\u30d7\u30b7\u30e7\u30f3(Realtime metrics option)\u306f\u3001\u8a3a\u65ad\u30bb\u30f3\u30b5\u30fc(diagnostics sensors)\u3092\u6709\u52b9\u306b\u3057\u3066\u3044\u3066\u3001\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u3067\u66f4\u65b0\u3057\u305f\u3044\u5834\u5408\u306b\u306e\u307f\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6709\u52b9\u306b\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u300115\u5206\u3054\u3068\u306b1\u56de\u3060\u3051\u66f4\u65b0\u3055\u308c\u307e\u3059\u3002", diff --git a/homeassistant/components/upb/translations/es.json b/homeassistant/components/upb/translations/es.json index 4b4a44ce0d3..6cef1f516a2 100644 --- a/homeassistant/components/upb/translations/es.json +++ b/homeassistant/components/upb/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_upb_file": "Archivo de exportaci\u00f3n UPB UPStart faltante o no v\u00e1lido, verifique el nombre y la ruta del archivo.", + "invalid_upb_file": "Falta el archivo de exportaci\u00f3n UPB UPStart o no es v\u00e1lido, comprueba el nombre y la ruta del archivo.", "unknown": "Error inesperado" }, "step": { @@ -15,7 +15,7 @@ "file_path": "Ruta y nombre del archivo de exportaci\u00f3n UPStart UPB.", "protocol": "Protocolo" }, - "description": "Conecta un Universal Powerline Bus Powerline Interface Module (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baudios]'. El par\u00e1metro baudios es opcional y el valor predeterminado es 4800. Ejemplo: '/dev/ttyS1'.", + "description": "Conecta un Universal Powerline Bus Powerline Interface Module (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baudios]'. Los baudios son opcionales y el valor predeterminado es 4800. Ejemplo: '/dev/ttyS1'.", "title": "Conectar con UPB PIM" } } diff --git a/homeassistant/components/update/translations/es.json b/homeassistant/components/update/translations/es.json index 49012350f47..f53c1247596 100644 --- a/homeassistant/components/update/translations/es.json +++ b/homeassistant/components/update/translations/es.json @@ -1,7 +1,7 @@ { "device_automation": { "trigger_type": { - "changed_states": "La disponibilidad de la actualizaci\u00f3n de {entity_name} cambie", + "changed_states": "La disponibilidad de la actualizaci\u00f3n de {entity_name} cambi\u00f3", "turned_off": "{entity_name} se actualiz\u00f3", "turned_on": "{entity_name} tiene una actualizaci\u00f3n disponible" } diff --git a/homeassistant/components/upnp/translations/ja.json b/homeassistant/components/upnp/translations/ja.json index 8c4d8d4695e..2beff286e3c 100644 --- a/homeassistant/components/upnp/translations/ja.json +++ b/homeassistant/components/upnp/translations/ja.json @@ -5,6 +5,9 @@ "incomplete_discovery": "\u4e0d\u5b8c\u5168\u306a\u691c\u51fa", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, + "error": { + "other": "\u7a7a" + }, "flow_title": "{name}", "step": { "ssdp_confirm": { diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json index e1ede36a55a..67e99d60ef4 100644 --- a/homeassistant/components/uptimerobot/translations/es.json +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, eliminq la integraci\u00f3n y vuelve a configurarla.", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, por favor, elimina la integraci\u00f3n y vuelve a configurarla.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/uscis/translations/es.json b/homeassistant/components/uscis/translations/es.json index 8dd3ad76874..f18acfff461 100644 --- a/homeassistant/components/uscis/translations/es.json +++ b/homeassistant/components/uscis/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n USCIS" } } diff --git a/homeassistant/components/version/translations/es.json b/homeassistant/components/version/translations/es.json index cbcd4bcd511..6433f5657b7 100644 --- a/homeassistant/components/version/translations/es.json +++ b/homeassistant/components/version/translations/es.json @@ -8,7 +8,7 @@ "data": { "version_source": "Fuente de la versi\u00f3n" }, - "description": "Selecciona la fuente de la que deseas realizar un seguimiento de las versiones", + "description": "Selecciona la fuente de la que deseas realizar un rastreo de las versiones", "title": "Selecciona el tipo de instalaci\u00f3n" }, "version_source": { @@ -18,7 +18,7 @@ "channel": "Qu\u00e9 canal debe ser rastreado", "image": "Qu\u00e9 imagen debe ser rastreada" }, - "description": "Configurar el seguimiento de versiones de {version_source}", + "description": "Configura el rastreo de versiones de {version_source}", "title": "Configurar" } } diff --git a/homeassistant/components/vulcan/translations/es.json b/homeassistant/components/vulcan/translations/es.json index 7e8b3b8b067..f98fa85f818 100644 --- a/homeassistant/components/vulcan/translations/es.json +++ b/homeassistant/components/vulcan/translations/es.json @@ -3,7 +3,7 @@ "abort": { "all_student_already_configured": "Todos los estudiantes ya han sido a\u00f1adidos.", "already_configured": "Ese estudiante ya ha sido a\u00f1adido.", - "no_matching_entries": "No se encontraron entradas que coincidan, usa una cuenta diferente o elimina la integraci\u00f3n con el estudiante obsoleto.", + "no_matching_entries": "No se encontraron entradas que coincidan, por favor, usa una cuenta diferente o elimina la integraci\u00f3n con el estudiante obsoleto.", "reauth_successful": "Re-autenticaci\u00f3n exitosa" }, "error": { diff --git a/homeassistant/components/webostv/translations/es.json b/homeassistant/components/webostv/translations/es.json index d4b3d2eecdc..c09d156bfb5 100644 --- a/homeassistant/components/webostv/translations/es.json +++ b/homeassistant/components/webostv/translations/es.json @@ -6,7 +6,7 @@ "error_pairing": "Conectado a LG webOS TV pero no emparejado" }, "error": { - "cannot_connect": "No se pudo conectar, por favor, enciende tu televisor o verifica la direcci\u00f3n IP" + "cannot_connect": "No se pudo conectar, por favor, enciende tu TV o comprueba la direcci\u00f3n IP" }, "flow_title": "LG webOS Smart TV", "step": { diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index 07734a7263f..e3a101b1892 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "Configuraci\u00f3n actualizada para el perfil.", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { "default": "Autenticado con \u00e9xito con Withings." diff --git a/homeassistant/components/wiz/translations/es.json b/homeassistant/components/wiz/translations/es.json index d3d0469cd0b..ef1292d1920 100644 --- a/homeassistant/components/wiz/translations/es.json +++ b/homeassistant/components/wiz/translations/es.json @@ -6,7 +6,7 @@ "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { - "bulb_time_out": "No se puede conectar a la bombilla. Tal vez la bombilla est\u00e1 desconectada o se ha introducido una IP incorrecta. \u00a1Por favor, enciende la luz y vuelve a intentarlo!", + "bulb_time_out": "No se puede conectar a la bombilla. Tal vez la bombilla est\u00e9 sin conexi\u00f3n o se ha introducido una IP incorrecta. \u00a1Por favor, enciende la luz e int\u00e9ntalo de nuevo!", "cannot_connect": "No se pudo conectar", "no_ip": "No es una direcci\u00f3n IP v\u00e1lida.", "no_wiz_light": "La bombilla no se puede conectar a trav\u00e9s de la integraci\u00f3n WiZ Platform.", diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index 6c0667af5d5..d58fbec1c58 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -10,7 +10,7 @@ "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos , consulta https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interfaz de red no v\u00e1lida", "invalid_key": "Clave de puerta de enlace no v\u00e1lida", - "invalid_mac": "Direcci\u00f3n Mac no v\u00e1lida" + "invalid_mac": "Direcci\u00f3n MAC no v\u00e1lida" }, "flow_title": "{name}", "step": { @@ -32,9 +32,9 @@ "data": { "host": "Direcci\u00f3n IP (opcional)", "interface": "La interfaz de la red a usar", - "mac": "Direcci\u00f3n Mac (opcional)" + "mac": "Direcci\u00f3n MAC (opcional)" }, - "description": "Si las direcciones IP y MAC se dejan vac\u00edas, se utiliza la detecci\u00f3n autom\u00e1tica" + "description": "Si las direcciones IP y MAC se dejan vac\u00edas, se usa la detecci\u00f3n autom\u00e1tica" } } } diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index 6dde2ede685..1411fc6b35e 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -19,6 +19,9 @@ "bluetooth_confirm": { "description": "Vols configurar {name}?" }, + "confirm_slow": { + "description": "No s'ha em\u00e8s cap 'broadcast' des d'aquest dispositiu durant l'\u00faltim minut, per tant no estem segurs de si aquest dispositiu utilitza encriptaci\u00f3 o no. Aix\u00f2 pot ser perqu\u00e8 el dispositiu utilitza un interval de 'broadcast' lent. Confirma per afegir aquest dispositiu de totes maneres, i la pr\u00f2xima vegada que rebi un 'broadcast' se't demanar\u00e0 que introdueixis la seva clau d'enlla\u00e7 si \u00e9s necessari." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -31,6 +34,9 @@ }, "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals." }, + "slow_confirm": { + "description": "No s'ha em\u00e8s cap 'broadcast' des d'aquest dispositiu durant l'\u00faltim minut, per tant no estem segurs de si aquest dispositiu utilitza encriptaci\u00f3 o no. Aix\u00f2 pot ser perqu\u00e8 el dispositiu utilitza un interval de 'broadcast' lent. Confirma per afegir aquest dispositiu de totes maneres, i la pr\u00f2xima vegada que rebi un 'broadcast' se't demanar\u00e0 que introdueixis la seva clau d'enlla\u00e7 si \u00e9s necessari." + }, "user": { "data": { "address": "Dispositiu" diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 8f692a87b2c..a93ca090955 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "No se pudo conectar", "cloud_credentials_incomplete": "Credenciales de la nube incompletas, por favor, completa el nombre de usuario, la contrase\u00f1a y el pa\u00eds", - "cloud_login_error": "No se pudo iniciar sesi\u00f3n en Xiaomi Miio Cloud, verifica las credenciales.", + "cloud_login_error": "No se pudo iniciar sesi\u00f3n en Xiaomi Miio Cloud, comprueba las credenciales.", "cloud_no_devices": "No se encontraron dispositivos en esta cuenta en la nube de Xiaomi Miio.", "unknown_device": "Se desconoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n.", "wrong_token": "Error de suma de comprobaci\u00f3n, token err\u00f3neo" @@ -24,7 +24,7 @@ "cloud_username": "Nombre de usuario de la nube", "manual": "Configurar manualmente (no recomendado)" }, - "description": "Inicia sesi\u00f3n en la nube de Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debes utilizar." + "description": "Inicia sesi\u00f3n en la nube de Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers para usar el servidor en la nube." }, "connect": { "data": { @@ -36,7 +36,7 @@ "host": "Direcci\u00f3n IP", "token": "Token API" }, - "description": "Necesitar\u00e1s la clave de 32 caracteres Token API, consulta https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obtener instrucciones. Ten en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara." + "description": "Necesitar\u00e1s la clave de 32 caracteres Token API, consulta https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obtener instrucciones. Por favor, ten en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara." }, "reauth_confirm": { "description": "La integraci\u00f3n de Xiaomi Miio necesita volver a autenticar tu cuenta para actualizar los tokens o a\u00f1adir las credenciales de la nube que faltan.", @@ -52,12 +52,12 @@ }, "options": { "error": { - "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellena el nombre de usuario, la contrase\u00f1a y el pa\u00eds" + "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, completa el nombre de usuario, la contrase\u00f1a y el pa\u00eds" }, "step": { "init": { "data": { - "cloud_subdevices": "Utiliza la nube para conectar subdispositivos" + "cloud_subdevices": "Usar la nube para obtener subdispositivos conectados" } } } diff --git a/homeassistant/components/yalexs_ble/translations/ca.json b/homeassistant/components/yalexs_ble/translations/ca.json index 5b4b014c4ac..881c5ef5d08 100644 --- a/homeassistant/components/yalexs_ble/translations/ca.json +++ b/homeassistant/components/yalexs_ble/translations/ca.json @@ -2,12 +2,15 @@ "config": { "abort": { "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", "no_unconfigured_devices": "No s'han trobat dispositius no configurats." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_key_format": "La clau fora de l\u00ednia ha de ser una cadena hexadecimal de 32 bytes.", + "invalid_key_index": "L'slot de clau fora de l\u00ednia ha de ser un nombre enter entre 0 i 255.", "unknown": "Error inesperat" }, "flow_title": "{name}", @@ -18,9 +21,10 @@ "user": { "data": { "address": "Adre\u00e7a Bluetooth", - "key": "Clau fora de l\u00ednia (cadena hexadecimal de 32 bytes)" + "key": "Clau fora de l\u00ednia (cadena hexadecimal de 32 bytes)", + "slot": "'Slot' de clau fora de l\u00ednia (nombre enter entre 0 i 255)" }, - "description": "Consulta la documentaci\u00f3 a {docs_url} per saber com trobar la clau de fora de l\u00ednia." + "description": "Consulta la documentaci\u00f3 per saber com trobar la clau de fora de l\u00ednia." } } } diff --git a/homeassistant/components/yalexs_ble/translations/el.json b/homeassistant/components/yalexs_ble/translations/el.json index 2a0243266be..3f6ae763e5e 100644 --- a/homeassistant/components/yalexs_ble/translations/el.json +++ b/homeassistant/components/yalexs_ble/translations/el.json @@ -1,11 +1,17 @@ { "config": { "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "no_unconfigured_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2." }, "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "invalid_key_format": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte.", - "invalid_key_index": "\u0397 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0 \u03ba\u03b1\u03b9 255." + "invalid_key_index": "\u0397 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0 \u03ba\u03b1\u03b9 255.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "{name}", "step": { @@ -15,7 +21,8 @@ "user": { "data": { "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 Bluetooth", - "key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte)" + "key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte)", + "slot": "\u03a5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u0391\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0 \u03ba\u03b1\u03b9 255)" }, "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {docs_url} \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2." } diff --git a/homeassistant/components/yalexs_ble/translations/es.json b/homeassistant/components/yalexs_ble/translations/es.json index adad8a18c40..56103ea2d3e 100644 --- a/homeassistant/components/yalexs_ble/translations/es.json +++ b/homeassistant/components/yalexs_ble/translations/es.json @@ -24,7 +24,7 @@ "key": "Clave sin conexi\u00f3n (cadena hexadecimal de 32 bytes)", "slot": "Ranura de clave sin conexi\u00f3n (entero entre 0 y 255)" }, - "description": "Consulta la documentaci\u00f3n para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." + "description": "Revisa la documentaci\u00f3n para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." } } } diff --git a/homeassistant/components/yalexs_ble/translations/et.json b/homeassistant/components/yalexs_ble/translations/et.json index 564b6c32dd5..dae8737aadc 100644 --- a/homeassistant/components/yalexs_ble/translations/et.json +++ b/homeassistant/components/yalexs_ble/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", "no_devices_found": "V\u00f5rgust seadmeid ei leitud", "no_unconfigured_devices": "H\u00e4\u00e4lestamata seadmeid ei leitud." }, @@ -23,7 +24,7 @@ "key": "V\u00f5rgu\u00fchenduseta v\u00f5ti (32-baidine kuueteistk\u00fcmnebaidine string)", "slot": "V\u00f5rgu\u00fchenduseta v\u00f5tmepesa (t\u00e4isarv vahemikus 0 kuni 255)" }, - "description": "V\u00f5rgu\u00fchenduseta v\u00f5tme leidmise kohta vaata dokumentatsiooni aadressil {docs_url} ." + "description": "V\u00f5rgu\u00fchenduseta v\u00f5tme leidmise kohta vaata dokumentatsiooni." } } } diff --git a/homeassistant/components/yalexs_ble/translations/hu.json b/homeassistant/components/yalexs_ble/translations/hu.json index 4208d98a1e6..fc957171d4f 100644 --- a/homeassistant/components/yalexs_ble/translations/hu.json +++ b/homeassistant/components/yalexs_ble/translations/hu.json @@ -13,6 +13,7 @@ "invalid_key_index": "Az offline kulcshelynek 0 \u00e9s 255 k\u00f6z\u00f6tti eg\u00e9sz sz\u00e1mnak kell lennie.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "{name}", "step": { "integration_discovery_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {nane}, Bluetooth-on kereszt\u00fcl, {address} c\u00edmmel?" diff --git a/homeassistant/components/yalexs_ble/translations/ja.json b/homeassistant/components/yalexs_ble/translations/ja.json new file mode 100644 index 00000000000..b847383ba9b --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/ja.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_key_format": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc\u306f\u300132\u30d0\u30a4\u30c8\u306e16\u9032\u6587\u5b57\u5217\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "invalid_key_index": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc\u30b9\u30ed\u30c3\u30c8\u306f\u30010\uff5e255\u306e\u6574\u6570\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth\u30a2\u30c9\u30ec\u30b9", + "key": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc(32\u30d0\u30a4\u30c8\u306e16\u9032\u6587\u5b57\u5217)", + "slot": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc\u30b9\u30ed\u30c3\u30c8(0\uff5e255\u306e\u6574\u6570)" + }, + "description": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/no.json b/homeassistant/components/yalexs_ble/translations/no.json index 6d2be535c73..99a1b78849d 100644 --- a/homeassistant/components/yalexs_ble/translations/no.json +++ b/homeassistant/components/yalexs_ble/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "no_unconfigured_devices": "Fant ingen ukonfigurerte enheter." }, @@ -23,7 +24,7 @@ "key": "Frakoblet n\u00f8kkel (32-byte sekskantstreng)", "slot": "Frakoblet n\u00f8kkelspor (heltall mellom 0 og 255)" }, - "description": "Se dokumentasjonen p\u00e5 {docs_url} for hvordan du finner frakoblet n\u00f8kkel." + "description": "Sjekk dokumentasjonen for hvordan du finner frakoblet n\u00f8kkel." } } } diff --git a/homeassistant/components/yalexs_ble/translations/ru.json b/homeassistant/components/yalexs_ble/translations/ru.json new file mode 100644 index 00000000000..15fed5d664b --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/ru.json @@ -0,0 +1,31 @@ +{ + "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.", + "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_unconfigured_devices": "\u041d\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_key_format": "\u0410\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0434\u043e\u043b\u0436\u0435\u043d \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u043e\u0431\u043e\u0439 32-\u0431\u0430\u0439\u0442\u043e\u0432\u0443\u044e \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u0443\u044e \u0441\u0442\u0440\u043e\u043a\u0443.", + "invalid_key_index": "\u0421\u043b\u043e\u0442 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u043e\u0442 0 \u0434\u043e 255.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} \u0441 \u0430\u0434\u0440\u0435\u0441\u043e\u043c {address}, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f Bluetooth?" + }, + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 Bluetooth", + "key": "\u0410\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 (32-\u0431\u0430\u0439\u0442\u043e\u0432\u0430\u044f \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430)", + "slot": "\u0421\u043b\u043e\u0442 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 (\u0446\u0435\u043b\u043e\u0435 \u0447\u0438\u0441\u043b\u043e \u043e\u0442 0 \u0434\u043e 255)" + }, + "description": "\u041a\u0430\u043a \u043d\u0430\u0439\u0442\u0438 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447, \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/zh-Hant.json b/homeassistant/components/yalexs_ble/translations/zh-Hant.json index f16fdcb0d07..94093fd8ef8 100644 --- a/homeassistant/components/yalexs_ble/translations/zh-Hant.json +++ b/homeassistant/components/yalexs_ble/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\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", "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002" }, @@ -23,7 +24,7 @@ "key": "\u96e2\u7dda\u91d1\u9470\uff0832 \u4f4d\u5143 16 \u9032\u4f4d\u5b57\u4e32\uff09", "slot": "\u96e2\u7dda\u91d1\u9470\uff080 \u81f3 255 \u9593\u7684\u6574\u6578\uff09" }, - "description": "\u8acb\u53c3\u8003\u6587\u4ef6 {docs_url} \u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" + "description": "\u8acb\u53c3\u8003\u6587\u4ef6\u4ee5\u7372\u5f97\u53d6\u7684\u96e2\u7dda\u91d1\u9470\u8a73\u7d30\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json index 6ec5b64a3fd..d8ff3f8adf4 100644 --- a/homeassistant/components/yeelight/translations/es.json +++ b/homeassistant/components/yeelight/translations/es.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Si dejas el host vac\u00edo, se usar\u00e1 descubrimiento para encontrar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } }, @@ -30,7 +30,7 @@ "init": { "data": { "model": "Modelo", - "nightlight_switch": "Usar interruptor de luz nocturna", + "nightlight_switch": "Usar interruptor de Nightlight", "save_on_change": "Guardar estado al cambiar", "transition": "Tiempo de transici\u00f3n (ms)", "use_music_mode": "Activar el Modo M\u00fasica" diff --git a/homeassistant/components/yolink/translations/es.json b/homeassistant/components/yolink/translations/es.json index 82f45972147..391bf1b1164 100644 --- a/homeassistant/components/yolink/translations/es.json +++ b/homeassistant/components/yolink/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 834c8a1116b..62b43a0bab6 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -13,7 +13,7 @@ "not_zwave_device": "El dispositivo descubierto no es un dispositivo Z-Wave." }, "error": { - "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Verifica la configuraci\u00f3n.", + "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" @@ -48,9 +48,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usar el complemento Z-Wave JS Supervisor" + "use_addon": "Utilizar el complemento del Supervisor Z-Wave JS" }, - "description": "\u00bfQuieres usar el complemento Z-Wave JS Supervisor?", + "description": "\u00bfQuieres usar el complemento del Supervisor Z-Wave JS?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { @@ -135,9 +135,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usar el complemento Supervisor Z-Wave JS" + "use_addon": "Utilizar el complemento del Supervisor Z-Wave JS" }, - "description": "\u00bfQuieres utilizar el complemento Supervisor Z-Wave JS ?", + "description": "\u00bfQuieres usar el complemento del Supervisor Z-Wave JS?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { From b43242ef0d7a89ecdbb2e36e7652fa6a83d17342 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Aug 2022 15:09:13 -1000 Subject: [PATCH 382/903] Fix lifx homekit discoveries not being ignorable or updating the IP (#76825) --- homeassistant/components/lifx/config_flow.py | 10 +++--- tests/components/lifx/test_config_flow.py | 33 +++++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index daa917dc847..4b2a5b0895e 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -119,18 +119,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm discovery.""" assert self._discovered_device is not None + discovered = self._discovered_device _LOGGER.debug( "Confirming discovery: %s with serial %s", - self._discovered_device.label, + discovered.label, self.unique_id, ) if user_input is not None or self._async_discovered_pending_migration(): - return self._async_create_entry_from_device(self._discovered_device) + return self._async_create_entry_from_device(discovered) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovered.ip_addr}) self._set_confirm_only() placeholders = { - "label": self._discovered_device.label, - "host": self._discovered_device.ip_addr, + "label": discovered.label, + "host": discovered.ip_addr, "serial": self.unique_id, } self.context["title_placeholders"] = placeholders diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index b346233874a..c9f069d8660 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -466,21 +466,38 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source assert result["reason"] == "cannot_connect" -async def test_discovered_by_dhcp_updates_ip(hass): +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname=LABEL, + name=LABEL, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + type="mock_type", + ), + ), + ], +) +async def test_discovered_by_dhcp_or_homekit_updates_ip(hass, source, data): """Update host from dhcp.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id=SERIAL ) config_entry.add_to_hass(hass) - with _patch_discovery(no_device=True), _patch_config_flow_try_connect( - no_device=True - ): + with _patch_discovery(), _patch_config_flow_try_connect(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL - ), + context={"source": source}, + data=data, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.ABORT From b8f83f6c70eaf5f734eb2527162084701cfef600 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 16 Aug 2022 07:40:33 +0200 Subject: [PATCH 383/903] Use BinarySensorDeviceClass instead of deprecated constants (#76830) --- homeassistant/components/devolo_home_network/binary_sensor.py | 4 ++-- homeassistant/components/zwave_me/binary_sensor.py | 4 ++-- pylint/plugins/hass_imports.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 6bc02d802f5..2e87bd180b1 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from devolo_plc_api.device import Device from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PLUG, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -47,7 +47,7 @@ class DevoloBinarySensorEntityDescription( SENSOR_TYPES: dict[str, DevoloBinarySensorEntityDescription] = { CONNECTED_TO_ROUTER: DevoloBinarySensorEntityDescription( key=CONNECTED_TO_ROUTER, - device_class=DEVICE_CLASS_PLUG, + device_class=BinarySensorDeviceClass.PLUG, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:router-network", diff --git a/homeassistant/components/zwave_me/binary_sensor.py b/homeassistant/components/zwave_me/binary_sensor.py index 40d850b8483..f1ee6896b25 100644 --- a/homeassistant/components/zwave_me/binary_sensor.py +++ b/homeassistant/components/zwave_me/binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from zwave_me_ws import ZWaveMeData from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -22,7 +22,7 @@ BINARY_SENSORS_MAP: dict[str, BinarySensorEntityDescription] = { ), "motion": BinarySensorEntityDescription( key="motion", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), } DEVICE_NAME = ZWaveMePlatform.BINARY_SENSOR diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 31fbe8f498e..de981ab6a6d 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -38,7 +38,7 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^FORMAT_(\w*)$"), ), ], - "homeassistant.components.binarysensor": [ + "homeassistant.components.binary_sensor": [ ObsoleteImportMatch( reason="replaced by BinarySensorDeviceClass enum", constant=re.compile(r"^DEVICE_CLASS_(\w*)$"), From 7ec54edc69c8f2563045b92a68370119d5b4aef2 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 16 Aug 2022 07:54:26 +0200 Subject: [PATCH 384/903] Fix Overkiz startup order to prevent unnamed device showing up (#76695) Gateways should be added first, before platform setup --- homeassistant/components/overkiz/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 9acdbfb9ec9..a4240bc0550 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -111,8 +111,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class): platforms[platform].append(device) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - device_registry = dr.async_get(hass) for gateway in setup.gateways: @@ -128,6 +126,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: configuration_url=server.configuration_url, ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True From 91005f4694aa706d9401e5858ff32f3e4eef1cb7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:16:33 +0200 Subject: [PATCH 385/903] Update pylint plugin to use TriggerActionType (#76819) --- pylint/plugins/hass_enforce_type_hints.py | 4 ++-- pylint/plugins/hass_imports.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 158848ba4d4..35862742b18 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -337,8 +337,8 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { arg_types={ 0: "HomeAssistant", 1: "ConfigType", - # 2: "AutomationActionType", # AutomationActionType is deprecated -> TriggerActionType - # 3: "AutomationTriggerInfo", # AutomationTriggerInfo is deprecated -> TriggerInfo + 2: "TriggerActionType", + 3: "TriggerInfo", }, return_type="CALLBACK_TYPE", ), diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index de981ab6a6d..caea0451f4d 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -38,6 +38,20 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^FORMAT_(\w*)$"), ), ], + "homeassistant.components.automation": [ + ObsoleteImportMatch( + reason="replaced by TriggerActionType from helpers.trigger", + constant=re.compile(r"^AutomationActionType$") + ), + ObsoleteImportMatch( + reason="replaced by TriggerData from helpers.trigger", + constant=re.compile(r"^AutomationTriggerData$") + ), + ObsoleteImportMatch( + reason="replaced by TriggerInfo from helpers.trigger", + constant=re.compile(r"^AutomationTriggerInfo$") + ), + ], "homeassistant.components.binary_sensor": [ ObsoleteImportMatch( reason="replaced by BinarySensorDeviceClass enum", From 93630cf1f84ea37a58e98c44a9ce377bd194f669 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:17:10 +0200 Subject: [PATCH 386/903] Add missing entry for `SOURCE_TYPE_*` to hass-imports plugin (#76829) --- pylint/plugins/hass_imports.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index caea0451f4d..c7abe0ad6ac 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -108,6 +108,18 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^SUPPORT_(\w*)$"), ), ], + "homeassistant.components.device_tracker": [ + ObsoleteImportMatch( + reason="replaced by SourceType enum", + constant=re.compile(r"^SOURCE_TYPE_\w+$") + ), + ], + "homeassistant.components.device_tracker.const": [ + ObsoleteImportMatch( + reason="replaced by SourceType enum", + constant=re.compile(r"^SOURCE_TYPE_\w+$") + ), + ], "homeassistant.components.fan": [ ObsoleteImportMatch( reason="replaced by FanEntityFeature enum", From a663445f25e42c01add7d8033952b11d3d582318 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 16 Aug 2022 10:34:17 +0100 Subject: [PATCH 387/903] Bump aiohomekit to 1.3.0 (#76841) --- homeassistant/components/homekit_controller/config_flow.py | 5 ----- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/test_config_flow.py | 1 - 5 files changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index eba531b917c..2ccdd557a5b 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -281,11 +281,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry, data={**existing_entry.data, **updated_ip_port} ) conn: HKDevice = self.hass.data[KNOWN_DEVICES][hkid] - # When we rediscover the device, let aiohomekit know - # that the device is available and we should not wait - # to retry connecting any longer. reconnect_soon - # will do nothing if the device is already connected - await conn.pairing.reconnect_soon() if config_num and conn.config_num != config_num: _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index ece53d29406..3f8d7828236 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.11"], + "requirements": ["aiohomekit==1.3.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 4bb2e474f03..5388db5998b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.11 +aiohomekit==1.3.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9909033875d..9baff4f1dbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.11 +aiohomekit==1.3.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index ff9b89473d6..3f545be8931 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -524,7 +524,6 @@ async def test_discovery_already_configured_update_csharp(hass, controller): entry.add_to_hass(hass) connection_mock = AsyncMock() - connection_mock.pairing.connect.reconnect_soon = AsyncMock() connection_mock.async_notify_config_changed = MagicMock() hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} From 3e1c9f1ac7f10133abd7967dee45555da915ec6e Mon Sep 17 00:00:00 2001 From: jonasrickert <46763066+jonasrickert@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:49:31 +0200 Subject: [PATCH 388/903] Add Rollotron DECT 1213 to fritzbox (#76386) --- homeassistant/components/fritzbox/const.py | 3 +- homeassistant/components/fritzbox/cover.py | 78 +++++++++++++++++ .../components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/fritzbox/__init__.py | 19 ++++ tests/components/fritzbox/test_cover.py | 86 +++++++++++++++++++ 7 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/fritzbox/cover.py create mode 100644 tests/components/fritzbox/test_cover.py diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 7b67bcd6cf8..4831b0f6ab2 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -27,7 +27,8 @@ LOGGER: Final[logging.Logger] = logging.getLogger(__package__) PLATFORMS: Final[list[Platform]] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.COVER, Platform.LIGHT, - Platform.SWITCH, Platform.SENSOR, + Platform.SWITCH, ] diff --git a/homeassistant/components/fritzbox/cover.py b/homeassistant/components/fritzbox/cover.py new file mode 100644 index 00000000000..d8fc8d4f3c3 --- /dev/null +++ b/homeassistant/components/fritzbox/cover.py @@ -0,0 +1,78 @@ +"""Support for AVM FRITZ!SmartHome cover devices.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import ( + ATTR_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import FritzBoxEntity +from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the FRITZ!SmartHome cover from ConfigEntry.""" + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + + async_add_entities( + FritzboxCover(coordinator, ain) + for ain, device in coordinator.data.items() + if device.has_blind + ) + + +class FritzboxCover(FritzBoxEntity, CoverEntity): + """The cover class for FRITZ!SmartHome covers.""" + + _attr_device_class = CoverDeviceClass.BLIND + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + ) + + @property + def current_cover_position(self) -> int | None: + """Return the current position.""" + position = None + if self.device.levelpercentage is not None: + position = 100 - self.device.levelpercentage + return position + + @property + def is_closed(self) -> bool | None: + """Return if the cover is closed.""" + if self.device.levelpercentage is None: + return None + return self.device.levelpercentage == 100 # type: ignore [no-any-return] + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.hass.async_add_executor_job(self.device.set_blind_open) + await self.coordinator.async_refresh() + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self.hass.async_add_executor_job(self.device.set_blind_close) + await self.coordinator.async_refresh() + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + await self.hass.async_add_executor_job( + self.device.set_level_percentage, 100 - kwargs[ATTR_POSITION] + ) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self.hass.async_add_executor_job(self.device.set_blind_stop) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 1dac4ddd78a..710f7e8f0c4 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox", "name": "AVM FRITZ!SmartHome", "documentation": "https://www.home-assistant.io/integrations/fritzbox", - "requirements": ["pyfritzhome==0.6.4"], + "requirements": ["pyfritzhome==0.6.5"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index 5388db5998b..184c6e4d77e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1536,7 +1536,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.4 +pyfritzhome==0.6.5 # homeassistant.components.fronius pyfronius==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9baff4f1dbd..e8e7774d313 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1061,7 +1061,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.4 +pyfritzhome==0.6.5 # homeassistant.components.fronius pyfronius==0.7.1 diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 05003ccdf51..05cd60059fa 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -61,6 +61,7 @@ class FritzDeviceBinarySensorMock(FritzDeviceBaseMock): has_lightbulb = False has_temperature_sensor = False has_thermostat = False + has_blind = False present = True @@ -82,6 +83,7 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock): has_switch = False has_temperature_sensor = True has_thermostat = True + has_blind = False holiday_active = "fake_holiday" lock = "fake_locked" present = True @@ -106,6 +108,7 @@ class FritzDeviceSensorMock(FritzDeviceBaseMock): has_switch = False has_temperature_sensor = True has_thermostat = False + has_blind = False lock = "fake_locked" present = True temperature = 1.23 @@ -126,6 +129,7 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock): has_switch = True has_temperature_sensor = True has_thermostat = False + has_blind = False switch_state = "fake_state" lock = "fake_locked" power = 5678 @@ -143,6 +147,21 @@ class FritzDeviceLightMock(FritzDeviceBaseMock): has_switch = False has_temperature_sensor = False has_thermostat = False + has_blind = False level = 100 present = True state = True + + +class FritzDeviceCoverMock(FritzDeviceBaseMock): + """Mock of a AVM Fritz!Box cover device.""" + + fw_version = "1.2.3" + has_alarm = False + has_powermeter = False + has_lightbulb = False + has_switch = False + has_temperature_sensor = False + has_thermostat = False + has_blind = True + levelpercentage = 0 diff --git a/tests/components/fritzbox/test_cover.py b/tests/components/fritzbox/test_cover.py new file mode 100644 index 00000000000..07b6ab5990e --- /dev/null +++ b/tests/components/fritzbox/test_cover.py @@ -0,0 +1,86 @@ +"""Tests for AVM Fritz!Box switch component.""" +from unittest.mock import Mock, call + +from homeassistant.components.cover import ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN +from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICES, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, +) +from homeassistant.core import HomeAssistant + +from . import FritzDeviceCoverMock, setup_config_entry +from .const import CONF_FAKE_NAME, MOCK_CONFIG + +ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}" + + +async def test_setup(hass: HomeAssistant, fritz: Mock): + """Test setup of platform.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_CURRENT_POSITION] == 100 + + +async def test_open_cover(hass: HomeAssistant, fritz: Mock): + """Test opening the cover.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + assert await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert device.set_blind_open.call_count == 1 + + +async def test_close_cover(hass: HomeAssistant, fritz: Mock): + """Test closing the device.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + assert await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert device.set_blind_close.call_count == 1 + + +async def test_set_position_cover(hass: HomeAssistant, fritz: Mock): + """Test stopping the device.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_POSITION: 50}, + True, + ) + assert device.set_level_percentage.call_args_list == [call(50)] + + +async def test_stop_cover(hass: HomeAssistant, fritz: Mock): + """Test stopping the device.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + assert await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert device.set_blind_stop.call_count == 1 From c7d46bc71971f0072e7aaa93e2b2ef219cebd92c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 16 Aug 2022 08:30:07 -0400 Subject: [PATCH 389/903] Improve Awair config flow (#76838) --- homeassistant/components/awair/config_flow.py | 79 +++++++++++++++---- homeassistant/components/awair/manifest.json | 5 ++ homeassistant/components/awair/strings.json | 7 +- homeassistant/generated/dhcp.py | 1 + homeassistant/strings.json | 1 + tests/components/awair/test_config_flow.py | 56 ++++++++++++- 6 files changed, 130 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 3fb822ab4fe..418413b690f 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -4,14 +4,15 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from aiohttp.client_exceptions import ClientConnectorError +from aiohttp.client_exceptions import ClientError from python_awair import Awair, AwairLocal, AwairLocalDevice from python_awair.exceptions import AuthError, AwairError import voluptuous as vol from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DEVICE, CONF_HOST +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -40,10 +41,11 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured(error="already_configured_device") self.context.update( { + "host": host, "title_placeholders": { "model": self._device.model, "device_id": self._device.device_id, - } + }, } ) else: @@ -109,31 +111,76 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_local(self, user_input: Mapping[str, Any]) -> FlowResult: + @callback + def _get_discovered_entries(self) -> dict[str, str]: + """Get discovered entries.""" + entries: dict[str, str] = {} + for flow in self._async_in_progress(): + if flow["context"]["source"] == SOURCE_ZEROCONF: + info = flow["context"]["title_placeholders"] + entries[ + flow["context"]["host"] + ] = f"{info['model']} ({info['device_id']})" + return entries + + async def async_step_local( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: + """Show how to enable local API.""" + if user_input is not None: + return await self.async_step_local_pick() + + return self.async_show_form( + step_id="local", + description_placeholders={ + "url": "https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Element-Local-API-Feature#h_01F40FBBW5323GBPV7D6XMG4J8" + }, + ) + + async def async_step_local_pick( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: """Handle collecting and verifying Awair Local API hosts.""" errors = {} - if user_input is not None: + # User input is either: + # 1. None if first time on this step + # 2. {device: manual} if picked manual entry option + # 3. {device: } if picked a device + # 4. {host: } if manually entered a host + # + # Option 1 and 2 will show the form again. + if user_input and user_input.get(CONF_DEVICE) != "manual": + if CONF_DEVICE in user_input: + user_input = {CONF_HOST: user_input[CONF_DEVICE]} + self._device, error = await self._check_local_connection( - user_input[CONF_HOST] + user_input.get(CONF_DEVICE) or user_input[CONF_HOST] ) if self._device is not None: - await self.async_set_unique_id(self._device.mac_address) - self._abort_if_unique_id_configured(error="already_configured_device") + await self.async_set_unique_id( + self._device.mac_address, raise_on_progress=False + ) title = f"{self._device.model} ({self._device.device_id})" return self.async_create_entry(title=title, data=user_input) if error is not None: - errors = {CONF_HOST: error} + errors = {"base": error} + + discovered = self._get_discovered_entries() + + if not discovered or (user_input and user_input.get(CONF_DEVICE) == "manual"): + data_schema = vol.Schema({vol.Required(CONF_HOST): str}) + + elif discovered: + discovered["manual"] = "Manual" + data_schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(discovered)}) return self.async_show_form( - step_id="local", - data_schema=vol.Schema({vol.Required(CONF_HOST): str}), - description_placeholders={ - "url": "https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Element-Local-API-Feature" - }, + step_id="local_pick", + data_schema=data_schema, errors=errors, ) @@ -177,7 +224,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): devices = await awair.devices() return (devices[0], None) - except ClientConnectorError as err: + except ClientError as err: LOGGER.error("Unable to connect error: %s", err) return (None, "unreachable") diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index cea5d01bfab..131a955a6eb 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -12,5 +12,10 @@ "type": "_http._tcp.local.", "name": "awair*" } + ], + "dhcp": [ + { + "macaddress": "70886B1*" + } ] } diff --git a/homeassistant/components/awair/strings.json b/homeassistant/components/awair/strings.json index fc95fc861f1..5b16f359403 100644 --- a/homeassistant/components/awair/strings.json +++ b/homeassistant/components/awair/strings.json @@ -9,10 +9,13 @@ } }, "local": { + "description": "Follow [these instructions]({url}) on how to enable the Awair Local API.\n\nClick submit when done." + }, + "local_pick": { "data": { + "device": "[%key:common::config_flow::data::device%]", "host": "[%key:common::config_flow::data::ip%]" - }, - "description": "Awair Local API must be enabled following these steps: {url}" + } }, "reauth_confirm": { "description": "Please re-enter your Awair developer access token.", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index fb8000f8393..9179a314215 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -11,6 +11,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'august', 'hostname': 'connect', 'macaddress': 'B8B7F1*'}, {'domain': 'august', 'hostname': 'connect', 'macaddress': '2C9FFB*'}, {'domain': 'august', 'hostname': 'august*', 'macaddress': 'E076D0*'}, + {'domain': 'awair', 'macaddress': '70886B1*'}, {'domain': 'axis', 'registered_devices': True}, {'domain': 'axis', 'hostname': 'axis-00408c*', 'macaddress': '00408C*'}, {'domain': 'axis', 'hostname': 'axis-accc8e*', 'macaddress': 'ACCC8E*'}, diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 9ae30becaee..86155be7b4d 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -26,6 +26,7 @@ "confirm_setup": "Do you want to start set up?" }, "data": { + "device": "Device", "name": "Name", "email": "Email", "username": "Username", diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index f6513321dfb..3fdf84f8260 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -240,6 +240,12 @@ async def test_create_local_entry(hass: HomeAssistant, local_devices): {"next_step_id": "local"}, ) + # We're being shown the local instructions + form_step = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + {}, + ) + result = await hass.config_entries.flow.async_configure( form_step["flow_id"], LOCAL_CONFIG, @@ -251,6 +257,48 @@ async def test_create_local_entry(hass: HomeAssistant, local_devices): assert result["result"].unique_id == LOCAL_UNIQUE_ID +async def test_create_local_entry_from_discovery(hass: HomeAssistant, local_devices): + """Test local API when device discovered after instructions shown.""" + + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=LOCAL_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "local"}, + ) + + # Create discovered entry in progress + with patch("python_awair.AwairClient.query", side_effect=[local_devices]): + await hass.config_entries.flow.async_init( + DOMAIN, + data=Mock(host=LOCAL_CONFIG[CONF_HOST]), + context={"source": SOURCE_ZEROCONF}, + ) + + # We're being shown the local instructions + form_step = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + {}, + ) + + with patch("python_awair.AwairClient.query", side_effect=[local_devices]), patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + {"device": LOCAL_CONFIG[CONF_HOST]}, + ) + + print(result) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Awair Element (24947)" + assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] + assert result["result"].unique_id == LOCAL_UNIQUE_ID + + async def test_create_local_entry_awair_error(hass: HomeAssistant): """Test overall flow when using local API and device is returns error.""" @@ -267,6 +315,12 @@ async def test_create_local_entry_awair_error(hass: HomeAssistant): {"next_step_id": "local"}, ) + # We're being shown the local instructions + form_step = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + {}, + ) + result = await hass.config_entries.flow.async_configure( form_step["flow_id"], LOCAL_CONFIG, @@ -274,7 +328,7 @@ async def test_create_local_entry_awair_error(hass: HomeAssistant): # User is returned to form to try again assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "local" + assert result["step_id"] == "local_pick" async def test_create_zeroconf_entry(hass: HomeAssistant, local_devices): From 45b253f65f701c685ecff0fe5bc9d924cd9ee370 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 16 Aug 2022 14:58:55 +0200 Subject: [PATCH 390/903] Clean awair debug print (#76864) --- tests/components/awair/test_config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 3fdf84f8260..16fca099d8c 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -292,7 +292,6 @@ async def test_create_local_entry_from_discovery(hass: HomeAssistant, local_devi {"device": LOCAL_CONFIG[CONF_HOST]}, ) - print(result) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Awair Element (24947)" assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] From 00c0ea8869dbe4241a4ad6dea562a62bedea7b68 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 15:33:33 +0200 Subject: [PATCH 391/903] Remove stale debug prints (#76865) --- homeassistant/components/unifiprotect/media_source.py | 1 - tests/components/flo/test_sensor.py | 2 -- tests/components/group/test_fan.py | 1 - 3 files changed, 4 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 17db4db5c3c..f9ea3620be3 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -839,7 +839,6 @@ class ProtectMediaSource(MediaSource): """Return all media source for all UniFi Protect NVRs.""" consoles: list[BrowseMediaSource] = [] - print(len(self.data_sources.values())) for data_source in self.data_sources.values(): if not data_source.api.bootstrap.has_media: continue diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index b5439241d33..c3266a84bd8 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -18,7 +18,6 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 # we should have 5 entities for the valve - print(hass.states) assert ( hass.states.get("sensor.smart_water_shutoff_current_system_mode").state == "home" @@ -59,7 +58,6 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture): ) # and 3 entities for the detector - print(hass.states) assert hass.states.get("sensor.kitchen_sink_temperature").state == "16" assert ( hass.states.get("sensor.kitchen_sink_temperature").attributes[ATTR_STATE_CLASS] diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index bb2cf311191..b8e47bfb289 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -148,7 +148,6 @@ async def test_state(hass, setup_comp): for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): - print("meh") hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) From 3cb062dc132b3c96c010d0b4a9caf40ccc4daf6f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 16 Aug 2022 14:48:01 +0100 Subject: [PATCH 392/903] Add System Bridge Media Source (#72865) --- .coveragerc | 1 + .../components/system_bridge/coordinator.py | 34 +++ .../components/system_bridge/manifest.json | 1 + .../components/system_bridge/media_source.py | 209 ++++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 homeassistant/components/system_bridge/media_source.py diff --git a/.coveragerc b/.coveragerc index 283adf0d3fc..8b632a524ff 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1223,6 +1223,7 @@ omit = homeassistant/components/system_bridge/binary_sensor.py homeassistant/components/system_bridge/const.py homeassistant/components/system_bridge/coordinator.py + homeassistant/components/system_bridge/media_source.py homeassistant/components/system_bridge/sensor.py homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/__init__.py diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 695dca44342..320c09a6f07 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -20,6 +20,10 @@ from systembridgeconnector.models.disk import Disk from systembridgeconnector.models.display import Display from systembridgeconnector.models.get_data import GetData from systembridgeconnector.models.gpu import Gpu +from systembridgeconnector.models.media_directories import MediaDirectories +from systembridgeconnector.models.media_files import File as MediaFile, MediaFiles +from systembridgeconnector.models.media_get_file import MediaGetFile +from systembridgeconnector.models.media_get_files import MediaGetFiles from systembridgeconnector.models.memory import Memory from systembridgeconnector.models.register_data_listener import RegisterDataListener from systembridgeconnector.models.system import System @@ -100,6 +104,36 @@ class SystemBridgeDataUpdateCoordinator( self.websocket_client.get_data(GetData(modules=modules)) ) + async def async_get_media_directories(self) -> MediaDirectories: + """Get media directories.""" + return await self.websocket_client.get_directories() + + async def async_get_media_files( + self, + base: str, + path: str | None = None, + ) -> MediaFiles: + """Get media files.""" + return await self.websocket_client.get_files( + MediaGetFiles( + base=base, + path=path, + ) + ) + + async def async_get_media_file( + self, + base: str, + path: str, + ) -> MediaFile: + """Get media file.""" + return await self.websocket_client.get_file( + MediaGetFile( + base=base, + path=path, + ) + ) + async def async_handle_module( self, module_name: str, diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 7968b588814..9370de70787 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -6,6 +6,7 @@ "requirements": ["systembridgeconnector==3.4.4"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._tcp.local."], + "dependencies": ["media_source"], "after_dependencies": ["zeroconf"], "quality_scale": "silver", "iot_class": "local_push", diff --git a/homeassistant/components/system_bridge/media_source.py b/homeassistant/components/system_bridge/media_source.py new file mode 100644 index 00000000000..6190cc1c5fe --- /dev/null +++ b/homeassistant/components/system_bridge/media_source.py @@ -0,0 +1,209 @@ +"""System Bridge Media Source Implementation.""" +from __future__ import annotations + +from systembridgeconnector.models.media_directories import MediaDirectories +from systembridgeconnector.models.media_files import File as MediaFile, MediaFiles + +from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY +from homeassistant.components.media_source.const import ( + MEDIA_CLASS_MAP, + MEDIA_MIME_TYPES, +) +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import SystemBridgeDataUpdateCoordinator + + +async def async_get_media_source(hass: HomeAssistant) -> MediaSource: + """Set up SystemBridge media source.""" + return SystemBridgeSource(hass) + + +class SystemBridgeSource(MediaSource): + """Provide System Bridge media files as a media source.""" + + def __init__( + self, + hass: HomeAssistant, + ) -> None: + """Initialize source.""" + super().__init__(DOMAIN) + self.name = "System Bridge" + self.hass: HomeAssistant = hass + + async def async_resolve_media( + self, + item: MediaSourceItem, + ) -> PlayMedia: + """Resolve media to a url.""" + entry_id, path, mime_type = item.identifier.split("~~", 2) + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry is None: + raise ValueError("Invalid entry") + path_split = path.split("/", 1) + return PlayMedia( + f"{_build_base_url(entry)}&base={path_split[0]}&path={path_split[1]}", + mime_type, + ) + + async def async_browse_media( + self, + item: MediaSourceItem, + ) -> BrowseMediaSource: + """Return media.""" + if not item.identifier: + return self._build_bridges() + + if "~~" not in item.identifier: + entry = self.hass.config_entries.async_get_entry(item.identifier) + if entry is None: + raise ValueError("Invalid entry") + coordinator: SystemBridgeDataUpdateCoordinator = self.hass.data[DOMAIN].get( + entry.entry_id + ) + directories = await coordinator.async_get_media_directories() + return _build_root_paths(entry, directories) + + entry_id, path = item.identifier.split("~~", 1) + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry is None: + raise ValueError("Invalid entry") + + coordinator = self.hass.data[DOMAIN].get(entry.entry_id) + + path_split = path.split("/", 1) + + files = await coordinator.async_get_media_files( + path_split[0], path_split[1] if len(path_split) > 1 else None + ) + + return _build_media_items(entry, files, path, item.identifier) + + def _build_bridges(self) -> BrowseMediaSource: + """Build bridges for System Bridge media.""" + children = [] + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.entry_id is not None: + children.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=entry.entry_id, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=entry.title, + can_play=False, + can_expand=True, + children=[], + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + ) + + return BrowseMediaSource( + domain=DOMAIN, + identifier="", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=self.name, + can_play=False, + can_expand=True, + children=children, + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + +def _build_base_url( + entry: ConfigEntry, +) -> str: + """Build base url for System Bridge media.""" + return f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}/api/media/file/data?apiKey={entry.data[CONF_API_KEY]}" + + +def _build_root_paths( + entry: ConfigEntry, + media_directories: MediaDirectories, +) -> BrowseMediaSource: + """Build base categories for System Bridge media.""" + return BrowseMediaSource( + domain=DOMAIN, + identifier="", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=entry.title, + can_play=False, + can_expand=True, + children=[ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{entry.entry_id}~~{directory.key}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=f"{directory.key[:1].capitalize()}{directory.key[1:]}", + can_play=False, + can_expand=True, + children=[], + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + for directory in media_directories.directories + ], + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + +def _build_media_items( + entry: ConfigEntry, + media_files: MediaFiles, + path: str, + identifier: str, +) -> BrowseMediaSource: + """Fetch requested files.""" + return BrowseMediaSource( + domain=DOMAIN, + identifier=identifier, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=f"{entry.title} - {path}", + can_play=False, + can_expand=True, + children=[ + _build_media_item(identifier, file) + for file in media_files.files + if file.is_directory + or ( + file.is_file + and file.mime_type is not None + and file.mime_type.startswith(MEDIA_MIME_TYPES) + ) + ], + ) + + +def _build_media_item( + path: str, + media_file: MediaFile, +) -> BrowseMediaSource: + """Build individual media item.""" + ext = ( + f"~~{media_file.mime_type}" + if media_file.is_file and media_file.mime_type is not None + else "" + ) + return BrowseMediaSource( + domain=DOMAIN, + identifier=f"{path}/{media_file.name}{ext}", + media_class=MEDIA_CLASS_DIRECTORY + if media_file.is_directory or media_file.mime_type is None + else MEDIA_CLASS_MAP[media_file.mime_type.split("/", 1)[0]], + media_content_type=media_file.mime_type, + title=media_file.name, + can_play=media_file.is_file, + can_expand=media_file.is_directory, + ) From 735dec8dde2be75363e1bf12bd5f5e1d0b3f0d5f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 15:53:16 +0200 Subject: [PATCH 393/903] Process UniFi Protect review comments (#76870) --- .../components/unifiprotect/media_source.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index f9ea3620be3..7e66f783ee0 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -344,11 +344,11 @@ class ProtectMediaSource(MediaSource): return await self._build_event(data, event, thumbnail_only) - async def get_registry(self) -> er.EntityRegistry: + @callback + def async_get_registry(self) -> er.EntityRegistry: """Get or return Entity Registry.""" - if self._registry is None: - self._registry = await er.async_get_registry(self.hass) + self._registry = er.async_get(self.hass) return self._registry def _breadcrumb( @@ -473,9 +473,8 @@ class ProtectMediaSource(MediaSource): continue # smart detect events have a paired motion event - if ( - event.get("type") == EventType.MOTION.value - and len(event.get("smartDetectEvents", [])) > 0 + if event.get("type") == EventType.MOTION.value and event.get( + "smartDetectEvents" ): continue @@ -717,7 +716,7 @@ class ProtectMediaSource(MediaSource): return None entity_id: str | None = None - entity_registry = await self.get_registry() + entity_registry = self.async_get_registry() for channel in camera.channels: # do not use the package camera if channel.id == 3: From 563ec67d39fcb8fc34df3344276147d5fc5acf0a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 16 Aug 2022 16:10:37 +0200 Subject: [PATCH 394/903] Add strict typing for auth (#75586) --- .strict-typing | 1 + homeassistant/components/auth/__init__.py | 86 ++++++++++++------- homeassistant/components/auth/indieauth.py | 30 ++++--- homeassistant/components/auth/login_flow.py | 64 +++++++++----- .../components/auth/mfa_setup_flow.py | 58 ++++++++----- homeassistant/data_entry_flow.py | 2 +- mypy.ini | 10 +++ tests/components/auth/test_mfa_setup_flow.py | 8 +- 8 files changed, 174 insertions(+), 85 deletions(-) diff --git a/.strict-typing b/.strict-typing index 1574ea09f2e..db51acc07df 100644 --- a/.strict-typing +++ b/.strict-typing @@ -59,6 +59,7 @@ homeassistant.components.ampio.* homeassistant.components.anthemav.* homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* +homeassistant.components.auth.* homeassistant.components.automation.* homeassistant.components.backup.* homeassistant.components.baf.* diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 897ca037c98..a8c7019030f 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -124,15 +124,22 @@ as part of a config flow. """ from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta from http import HTTPStatus +from typing import Any, Optional, cast import uuid from aiohttp import web +from multidict import MultiDictProxy import voluptuous as vol from homeassistant.auth import InvalidAuthError -from homeassistant.auth.models import TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, Credentials +from homeassistant.auth.models import ( + TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + Credentials, + User, +) from homeassistant.components import websocket_api from homeassistant.components.http.auth import ( async_sign_path, @@ -151,11 +158,16 @@ from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" +StoreResultType = Callable[[str, Credentials], str] +RetrieveResultType = Callable[[str, str], Optional[Credentials]] + @bind_hass -def create_auth_code(hass, client_id: str, credential: Credentials) -> str: +def create_auth_code( + hass: HomeAssistant, client_id: str, credential: Credentials +) -> str: """Create an authorization code to fetch tokens.""" - return hass.data[DOMAIN](client_id, credential) + return cast(StoreResultType, hass.data[DOMAIN])(client_id, credential) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -188,15 +200,15 @@ class TokenView(HomeAssistantView): requires_auth = False cors_allowed = True - def __init__(self, retrieve_auth): + def __init__(self, retrieve_auth: RetrieveResultType) -> None: """Initialize the token view.""" self._retrieve_auth = retrieve_auth @log_invalid_auth - async def post(self, request): + async def post(self, request: web.Request) -> web.Response: """Grant a token.""" - hass = request.app["hass"] - data = await request.post() + hass: HomeAssistant = request.app["hass"] + data = cast(MultiDictProxy[str], await request.post()) grant_type = data.get("grant_type") @@ -217,7 +229,11 @@ class TokenView(HomeAssistantView): {"error": "unsupported_grant_type"}, status_code=HTTPStatus.BAD_REQUEST ) - async def _async_handle_revoke_token(self, hass, data): + async def _async_handle_revoke_token( + self, + hass: HomeAssistant, + data: MultiDictProxy[str], + ) -> web.Response: """Handle revoke token request.""" # OAuth 2.0 Token Revocation [RFC7009] @@ -235,7 +251,12 @@ class TokenView(HomeAssistantView): await hass.auth.async_remove_refresh_token(refresh_token) return web.Response(status=HTTPStatus.OK) - async def _async_handle_auth_code(self, hass, data, remote_addr): + async def _async_handle_auth_code( + self, + hass: HomeAssistant, + data: MultiDictProxy[str], + remote_addr: str | None, + ) -> web.Response: """Handle authorization code request.""" client_id = data.get("client_id") if client_id is None or not indieauth.verify_client_id(client_id): @@ -298,7 +319,12 @@ class TokenView(HomeAssistantView): }, ) - async def _async_handle_refresh_token(self, hass, data, remote_addr): + async def _async_handle_refresh_token( + self, + hass: HomeAssistant, + data: MultiDictProxy[str], + remote_addr: str | None, + ) -> web.Response: """Handle authorization code request.""" client_id = data.get("client_id") if client_id is not None and not indieauth.verify_client_id(client_id): @@ -366,15 +392,15 @@ class LinkUserView(HomeAssistantView): url = "/auth/link_user" name = "api:auth:link_user" - def __init__(self, retrieve_credentials): + def __init__(self, retrieve_credentials: RetrieveResultType) -> None: """Initialize the link user view.""" self._retrieve_credentials = retrieve_credentials @RequestDataValidator(vol.Schema({"code": str, "client_id": str})) - async def post(self, request, data): + async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Link a user.""" - hass = request.app["hass"] - user = request["hass_user"] + hass: HomeAssistant = request.app["hass"] + user: User = request["hass_user"] credentials = self._retrieve_credentials(data["client_id"], data["code"]) @@ -394,12 +420,12 @@ class LinkUserView(HomeAssistantView): @callback -def _create_auth_code_store(): +def _create_auth_code_store() -> tuple[StoreResultType, RetrieveResultType]: """Create an in memory store.""" - temp_results = {} + temp_results: dict[tuple[str, str], tuple[datetime, Credentials]] = {} @callback - def store_result(client_id, result): + def store_result(client_id: str, result: Credentials) -> str: """Store flow result and return a code to retrieve it.""" if not isinstance(result, Credentials): raise ValueError("result has to be a Credentials instance") @@ -412,7 +438,7 @@ def _create_auth_code_store(): return code @callback - def retrieve_result(client_id, code): + def retrieve_result(client_id: str, code: str) -> Credentials | None: """Retrieve flow result.""" key = (client_id, code) @@ -437,8 +463,8 @@ def _create_auth_code_store(): @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_current_user( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Return the current user.""" user = connection.user enabled_modules = await hass.auth.async_get_enabled_mfa(user) @@ -482,8 +508,8 @@ async def websocket_current_user( @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_create_long_lived_access_token( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Create or a long-lived access token.""" refresh_token = await hass.auth.async_create_refresh_token( connection.user, @@ -506,12 +532,12 @@ async def websocket_create_long_lived_access_token( @websocket_api.ws_require_user() @callback def websocket_refresh_tokens( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Return metadata of users refresh tokens.""" current_id = connection.refresh_token_id - tokens = [] + tokens: list[dict[str, Any]] = [] for refresh in connection.user.refresh_tokens.values(): if refresh.credential: auth_provider_type = refresh.credential.auth_provider_type @@ -545,8 +571,8 @@ def websocket_refresh_tokens( @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_delete_refresh_token( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Handle a delete refresh token request.""" refresh_token = connection.user.refresh_tokens.get(msg["refresh_token_id"]) @@ -569,8 +595,8 @@ async def websocket_delete_refresh_token( @websocket_api.ws_require_user() @callback def websocket_sign_path( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Handle a sign path request.""" connection.send_message( websocket_api.result_message( diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index e823659f62b..fc4c298ca6c 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,18 +1,24 @@ """Helpers to resolve client ID/secret.""" +from __future__ import annotations + import asyncio from html.parser import HTMLParser from ipaddress import ip_address import logging -from urllib.parse import urljoin, urlparse +from urllib.parse import ParseResult, urljoin, urlparse import aiohttp +import aiohttp.client_exceptions +from homeassistant.core import HomeAssistant from homeassistant.util.network import is_local _LOGGER = logging.getLogger(__name__) -async def verify_redirect_uri(hass, client_id, redirect_uri): +async def verify_redirect_uri( + hass: HomeAssistant, client_id: str, redirect_uri: str +) -> bool: """Verify that the client and redirect uri match.""" try: client_id_parts = _parse_client_id(client_id) @@ -47,24 +53,24 @@ async def verify_redirect_uri(hass, client_id, redirect_uri): class LinkTagParser(HTMLParser): """Parser to find link tags.""" - def __init__(self, rel): + def __init__(self, rel: str) -> None: """Initialize a link tag parser.""" super().__init__() self.rel = rel - self.found = [] + self.found: list[str | None] = [] - def handle_starttag(self, tag, attrs): + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: """Handle finding a start tag.""" if tag != "link": return - attrs = dict(attrs) + attributes: dict[str, str | None] = dict(attrs) - if attrs.get("rel") == self.rel: - self.found.append(attrs.get("href")) + if attributes.get("rel") == self.rel: + self.found.append(attributes.get("href")) -async def fetch_redirect_uris(hass, url): +async def fetch_redirect_uris(hass: HomeAssistant, url: str) -> list[str]: """Find link tag with redirect_uri values. IndieAuth 4.2.2 @@ -108,7 +114,7 @@ async def fetch_redirect_uris(hass, url): return [urljoin(url, found) for found in parser.found] -def verify_client_id(client_id): +def verify_client_id(client_id: str) -> bool: """Verify that the client id is valid.""" try: _parse_client_id(client_id) @@ -117,7 +123,7 @@ def verify_client_id(client_id): return False -def _parse_url(url): +def _parse_url(url: str) -> ParseResult: """Parse a url in parts and canonicalize according to IndieAuth.""" parts = urlparse(url) @@ -134,7 +140,7 @@ def _parse_url(url): return parts -def _parse_client_id(client_id): +def _parse_client_id(client_id: str) -> ParseResult: """Test if client id is a valid URL according to IndieAuth section 3.2. https://indieauth.spec.indieweb.org/#client-identifier diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 6cc9d94c7a6..dc094cd581f 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -66,14 +66,19 @@ associate with an credential if "type" set to "link_user" in "version": 1 } """ +from __future__ import annotations + +from collections.abc import Callable from http import HTTPStatus from ipaddress import ip_address +from typing import TYPE_CHECKING, Any from aiohttp import web import voluptuous as vol import voluptuous_serialize from homeassistant import data_entry_flow +from homeassistant.auth import AuthManagerFlowManager from homeassistant.auth.models import Credentials from homeassistant.components import onboarding from homeassistant.components.http.auth import async_user_not_allowed_do_auth @@ -88,8 +93,13 @@ from homeassistant.core import HomeAssistant from . import indieauth +if TYPE_CHECKING: + from . import StoreResultType -async def async_setup(hass, store_result): + +async def async_setup( + hass: HomeAssistant, store_result: Callable[[str, Credentials], str] +) -> None: """Component to allow users to login.""" hass.http.register_view(AuthProvidersView) hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result)) @@ -103,9 +113,9 @@ class AuthProvidersView(HomeAssistantView): name = "api:auth:providers" requires_auth = False - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Get available auth providers.""" - hass = request.app["hass"] + hass: HomeAssistant = request.app["hass"] if not onboarding.async_is_user_onboarded(hass): return self.json_message( message="Onboarding not finished", @@ -121,7 +131,9 @@ class AuthProvidersView(HomeAssistantView): ) -def _prepare_result_json(result): +def _prepare_result_json( + result: data_entry_flow.FlowResult, +) -> data_entry_flow.FlowResult: """Convert result to JSON.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() @@ -147,12 +159,21 @@ class LoginFlowBaseView(HomeAssistantView): requires_auth = False - def __init__(self, flow_mgr, store_result): + def __init__( + self, + flow_mgr: AuthManagerFlowManager, + store_result: StoreResultType, + ) -> None: """Initialize the flow manager index view.""" self._flow_mgr = flow_mgr self._store_result = store_result - async def _async_flow_result_to_response(self, request, client_id, result): + async def _async_flow_result_to_response( + self, + request: web.Request, + client_id: str, + result: data_entry_flow.FlowResult, + ) -> web.Response: """Convert the flow result to a response.""" if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: # @log_invalid_auth does not work here since it returns HTTP 200. @@ -196,7 +217,7 @@ class LoginFlowIndexView(LoginFlowBaseView): url = "/auth/login_flow" name = "api:auth:login_flow" - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Do not allow index of flows in progress.""" return web.Response(status=HTTPStatus.METHOD_NOT_ALLOWED) @@ -211,15 +232,18 @@ class LoginFlowIndexView(LoginFlowBaseView): ) ) @log_invalid_auth - async def post(self, request, data): + async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Create a new login flow.""" - if not await indieauth.verify_redirect_uri( - request.app["hass"], data["client_id"], data["redirect_uri"] - ): + hass: HomeAssistant = request.app["hass"] + client_id: str = data["client_id"] + redirect_uri: str = data["redirect_uri"] + + if not await indieauth.verify_redirect_uri(hass, client_id, redirect_uri): return self.json_message( "invalid client id or redirect uri", HTTPStatus.BAD_REQUEST ) + handler: tuple[str, ...] | str if isinstance(data["handler"], list): handler = tuple(data["handler"]) else: @@ -227,9 +251,9 @@ class LoginFlowIndexView(LoginFlowBaseView): try: result = await self._flow_mgr.async_init( - handler, + handler, # type: ignore[arg-type] context={ - "ip_address": ip_address(request.remote), + "ip_address": ip_address(request.remote), # type: ignore[arg-type] "credential_only": data.get("type") == "link_user", }, ) @@ -240,9 +264,7 @@ class LoginFlowIndexView(LoginFlowBaseView): "Handler does not support init", HTTPStatus.BAD_REQUEST ) - return await self._async_flow_result_to_response( - request, data["client_id"], result - ) + return await self._async_flow_result_to_response(request, client_id, result) class LoginFlowResourceView(LoginFlowBaseView): @@ -251,13 +273,15 @@ class LoginFlowResourceView(LoginFlowBaseView): url = "/auth/login_flow/{flow_id}" name = "api:auth:login_flow:resource" - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Do not allow getting status of a flow in progress.""" return self.json_message("Invalid flow specified", HTTPStatus.NOT_FOUND) @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA)) @log_invalid_auth - async def post(self, request, data, flow_id): + async def post( + self, request: web.Request, data: dict[str, Any], flow_id: str + ) -> web.Response: """Handle progressing a login flow request.""" client_id = data.pop("client_id") @@ -267,7 +291,7 @@ class LoginFlowResourceView(LoginFlowBaseView): try: # do not allow change ip during login flow flow = self._flow_mgr.async_get(flow_id) - if flow["context"]["ip_address"] != ip_address(request.remote): + if flow["context"]["ip_address"] != ip_address(request.remote): # type: ignore[arg-type] return self.json_message("IP address changed", HTTPStatus.BAD_REQUEST) result = await self._flow_mgr.async_configure(flow_id, data) except data_entry_flow.UnknownFlow: @@ -277,7 +301,7 @@ class LoginFlowResourceView(LoginFlowBaseView): return await self._async_flow_result_to_response(request, client_id, result) - async def delete(self, request, flow_id): + async def delete(self, request: web.Request, flow_id: str) -> web.Response: """Cancel a flow in progress.""" try: self._flow_mgr.async_abort(flow_id) diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index e288fe33df7..d6a9282e089 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -1,5 +1,8 @@ """Helpers to setup multi-factor auth module.""" +from __future__ import annotations + import logging +from typing import Any import voluptuous as vol import voluptuous_serialize @@ -7,15 +10,19 @@ import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.config_validation as cv WS_TYPE_SETUP_MFA = "auth/setup_mfa" -SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_SETUP_MFA, - vol.Exclusive("mfa_module_id", "module_or_flow_id"): str, - vol.Exclusive("flow_id", "module_or_flow_id"): str, - vol.Optional("user_input"): object, - } +SCHEMA_WS_SETUP_MFA = vol.All( + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_SETUP_MFA, + vol.Exclusive("mfa_module_id", "module_or_flow_id"): str, + vol.Exclusive("flow_id", "module_or_flow_id"): str, + vol.Optional("user_input"): object, + } + ), + cv.has_at_least_one_key("mfa_module_id", "flow_id"), ) WS_TYPE_DEPOSE_MFA = "auth/depose_mfa" @@ -31,7 +38,13 @@ _LOGGER = logging.getLogger(__name__) class MfaFlowManager(data_entry_flow.FlowManager): """Manage multi factor authentication flows.""" - async def async_create_flow(self, handler_key, *, context, data): + async def async_create_flow( # type: ignore[override] + self, + handler_key: Any, + *, + context: dict[str, Any], + data: dict[str, Any], + ) -> data_entry_flow.FlowHandler: """Create a setup flow. handler is a mfa module.""" mfa_module = self.hass.auth.get_auth_mfa_module(handler_key) if mfa_module is None: @@ -40,13 +53,15 @@ class MfaFlowManager(data_entry_flow.FlowManager): user_id = data.pop("user_id") return await mfa_module.async_setup_flow(user_id) - async def async_finish_flow(self, flow, result): + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: """Complete an mfs setup flow.""" _LOGGER.debug("flow_result: %s", result) return result -async def async_setup(hass): +async def async_setup(hass: HomeAssistant) -> None: """Init mfa setup flow manager.""" hass.data[DATA_SETUP_FLOW_MGR] = MfaFlowManager(hass) @@ -62,13 +77,13 @@ async def async_setup(hass): @callback @websocket_api.ws_require_user(allow_system_user=False) def websocket_setup_mfa( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Return a setup flow for mfa auth module.""" - async def async_setup_flow(msg): + async def async_setup_flow(msg: dict[str, Any]) -> None: """Return a setup flow for mfa auth module.""" - flow_manager = hass.data[DATA_SETUP_FLOW_MGR] + flow_manager: MfaFlowManager = hass.data[DATA_SETUP_FLOW_MGR] if (flow_id := msg.get("flow_id")) is not None: result = await flow_manager.async_configure(flow_id, msg.get("user_input")) @@ -77,9 +92,8 @@ def websocket_setup_mfa( ) return - mfa_module_id = msg.get("mfa_module_id") - mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id) - if mfa_module is None: + mfa_module_id = msg["mfa_module_id"] + if hass.auth.get_auth_mfa_module(mfa_module_id) is None: connection.send_message( websocket_api.error_message( msg["id"], "no_module", f"MFA module {mfa_module_id} is not found" @@ -101,11 +115,11 @@ def websocket_setup_mfa( @callback @websocket_api.ws_require_user(allow_system_user=False) def websocket_depose_mfa( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Remove user from mfa module.""" - async def async_depose(msg): + async def async_depose(msg: dict[str, Any]) -> None: """Remove user from mfa auth module.""" mfa_module_id = msg["mfa_module_id"] try: @@ -127,7 +141,9 @@ def websocket_depose_mfa( hass.async_create_task(async_depose(msg)) -def _prepare_result_json(result): +def _prepare_result_json( + result: data_entry_flow.FlowResult, +) -> data_entry_flow.FlowResult: """Convert result to JSON.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 23b35138df7..64750b2ff50 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -175,7 +175,7 @@ class FlowManager(abc.ABC): ) @callback - def async_get(self, flow_id: str) -> FlowResult | None: + def async_get(self, flow_id: str) -> FlowResult: """Return a flow in progress as a partial FlowResult.""" if (flow := self._progress.get(flow_id)) is None: raise UnknownFlow diff --git a/mypy.ini b/mypy.ini index 7338d9a67f0..5d3b184880d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -349,6 +349,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.auth.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.automation.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index b0851a3cfe6..c2ff5a31d75 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -44,7 +44,13 @@ async def test_ws_setup_depose_mfa(hass, hass_ws_client): client = await hass_ws_client(hass, access_token) - await client.send_json({"id": 10, "type": mfa_setup_flow.WS_TYPE_SETUP_MFA}) + await client.send_json( + { + "id": 10, + "type": mfa_setup_flow.WS_TYPE_SETUP_MFA, + "mfa_module_id": "invalid_module", + } + ) result = await client.receive_json() assert result["id"] == 10 From 3d567d2c1b79caf321b291c26e28f88062886d0a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 16:18:40 +0200 Subject: [PATCH 395/903] Update numpy to 1.23.2 (#76855) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 5b1ff9715d1..a774541bd36 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.23.1"], + "requirements": ["numpy==1.23.2"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 561ebdb6e89..9da0af32da3 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.23.1", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.23.2", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index cada1199084..8a1658a5302 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.23.1", "opencv-python-headless==4.6.0.66"], + "requirements": ["numpy==1.23.2", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index c8a62791429..9f719b7e1b3 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.23.1", + "numpy==1.23.2", "pillow==9.2.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index e70056f207a..14bce1a0757 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.23.1"], + "requirements": ["numpy==1.23.2"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b7c2fd8cbb5..82c37466db7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -91,7 +91,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.1 +numpy==1.23.2 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 184c6e4d77e..5b34c0b0995 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.1 +numpy==1.23.2 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8e7774d313..3d79f5ff7d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -809,7 +809,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.1 +numpy==1.23.2 # homeassistant.components.google oauth2client==4.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3cb35eec147..3709d4cff08 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -105,7 +105,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.1 +numpy==1.23.2 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 From 2e191d6a60a8904644c536c4d3103f5e9bb6158f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 16:30:04 +0200 Subject: [PATCH 396/903] Update sentry-sdk to 1.9.5 (#76857) --- 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 2360180d1cf..849b40170f8 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.9.3"], + "requirements": ["sentry-sdk==1.9.5"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 5b34c0b0995..76ee358edb6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2176,7 +2176,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.9.3 +sentry-sdk==1.9.5 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d79f5ff7d8..3c6af75e928 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1473,7 +1473,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.9.3 +sentry-sdk==1.9.5 # homeassistant.components.sharkiq sharkiq==0.0.1 From 63d71457aacb56c90ff89852fb201f74562ebec2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 16:31:09 +0200 Subject: [PATCH 397/903] Type BrowseMedia children as a covariant (#76869) --- homeassistant/components/apple_tv/media_player.py | 2 +- homeassistant/components/jellyfin/media_source.py | 10 +++++----- .../components/media_player/browse_media.py | 3 ++- homeassistant/components/media_source/models.py | 2 -- .../components/unifiprotect/media_source.py | 4 ++-- homeassistant/components/xbox/browse_media.py | 13 ++++++------- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 362a09fb5fc..3a495e053eb 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -422,7 +422,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return cur_item # Add app item if we have one - if self._app_list and cur_item.children: + if self._app_list and cur_item.children and isinstance(cur_item.children, list): cur_item.children.insert(0, build_app_list(self._app_list)) return cur_item diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 8a09fd8d552..662c0f0040a 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -173,10 +173,10 @@ class JellyfinSource(MediaSource): if include_children: result.children_media_class = MEDIA_CLASS_ARTIST - result.children = await self._build_artists(library_id) # type: ignore[assignment] + result.children = await self._build_artists(library_id) if not result.children: result.children_media_class = MEDIA_CLASS_ALBUM - result.children = await self._build_albums(library_id) # type: ignore[assignment] + result.children = await self._build_albums(library_id) return result @@ -207,7 +207,7 @@ class JellyfinSource(MediaSource): if include_children: result.children_media_class = MEDIA_CLASS_ALBUM - result.children = await self._build_albums(artist_id) # type: ignore[assignment] + result.children = await self._build_albums(artist_id) return result @@ -238,7 +238,7 @@ class JellyfinSource(MediaSource): if include_children: result.children_media_class = MEDIA_CLASS_TRACK - result.children = await self._build_tracks(album_id) # type: ignore[assignment] + result.children = await self._build_tracks(album_id) return result @@ -293,7 +293,7 @@ class JellyfinSource(MediaSource): if include_children: result.children_media_class = MEDIA_CLASS_MOVIE - result.children = await self._build_movies(library_id) # type: ignore[assignment] + result.children = await self._build_movies(library_id) return result diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 9327bf68f9f..e3474eeb58e 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -1,6 +1,7 @@ """Browse media features for media player.""" from __future__ import annotations +from collections.abc import Sequence from datetime import timedelta import logging from typing import Any @@ -97,7 +98,7 @@ class BrowseMedia: title: str, can_play: bool, can_expand: bool, - children: list[BrowseMedia] | None = None, + children: Sequence[BrowseMedia] | None = None, children_media_class: str | None = None, thumbnail: str | None = None, not_shown: int = 0, diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 0aee6ad1330..f6772bc6ad9 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -27,8 +27,6 @@ class PlayMedia: class BrowseMediaSource(BrowseMedia): """Represent a browsable media file.""" - children: list[BrowseMediaSource | BrowseMedia] | None - def __init__( self, *, domain: str | None, identifier: str | None, **kwargs: Any ) -> None: diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 7e66f783ee0..104323eeaa2 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -528,7 +528,7 @@ class ProtectMediaSource(MediaSource): args["camera_id"] = camera_id events = await self._build_events(**args) # type: ignore[arg-type] - source.children = events # type: ignore[assignment] + source.children = events source.title = self._breadcrumb( data, title, @@ -653,7 +653,7 @@ class ProtectMediaSource(MediaSource): title = f"{start.strftime('%B %Y')} > {title}" events = await self._build_events(**args) # type: ignore[arg-type] - source.children = events # type: ignore[assignment] + source.children = events source.title = self._breadcrumb( data, title, diff --git a/homeassistant/components/xbox/browse_media.py b/homeassistant/components/xbox/browse_media.py index ee1eabf1e00..e4c268bd6b8 100644 --- a/homeassistant/components/xbox/browse_media.py +++ b/homeassistant/components/xbox/browse_media.py @@ -1,7 +1,7 @@ """Support for media browsing.""" from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import NamedTuple from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, SYSTEM_PFN_ID_MAP @@ -56,6 +56,7 @@ async def build_item_response( apps: InstalledPackagesList = await client.smartglass.get_installed_apps(device_id) if media_content_type in (None, "library"): + children: list[BrowseMedia] = [] library_info = BrowseMedia( media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", @@ -63,10 +64,8 @@ async def build_item_response( title="Installed Applications", can_play=False, can_expand=True, - children=[], + children=children, ) - if TYPE_CHECKING: - assert library_info.children is not None # Add Home id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID @@ -78,7 +77,7 @@ async def build_item_response( home_thumb = _find_media_image( home_catalog.products[0].localized_properties[0].images ) - library_info.children.append( + children.append( BrowseMedia( media_class=MEDIA_CLASS_APP, media_content_id="Home", @@ -101,7 +100,7 @@ async def build_item_response( tv_thumb = _find_media_image( tv_catalog.products[0].localized_properties[0].images ) - library_info.children.append( + children.append( BrowseMedia( media_class=MEDIA_CLASS_APP, media_content_id="TV", @@ -117,7 +116,7 @@ async def build_item_response( {app.content_type for app in apps.result if app.content_type in TYPE_MAP} ) for c_type in content_types: - library_info.children.append( + children.append( BrowseMedia( media_class=MEDIA_CLASS_DIRECTORY, media_content_id=c_type, From 73001e29ff22f7a5008a0b0a0e058aae22771d16 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 16 Aug 2022 16:47:21 +0200 Subject: [PATCH 398/903] Remove deprecated white_value support from MQTT light (#76848) * Remove deprecated white_value support from MQTT light * Remove deprecated white_value support from MQTT JSON light * Remove deprecated white_value support from MQTT template light --- .../components/mqtt/light/schema_basic.py | 207 +---- .../components/mqtt/light/schema_json.py | 37 +- .../components/mqtt/light/schema_template.py | 36 +- tests/components/mqtt/test_light.py | 735 +----------------- tests/components/mqtt/test_light_json.py | 136 +--- tests/components/mqtt/test_light_template.py | 145 ++-- 6 files changed, 133 insertions(+), 1163 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 1c94fa82f73..05778aa7711 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -17,13 +17,8 @@ from homeassistant.components.light import ( ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, ColorMode, LightEntity, LightEntityFeature, @@ -119,7 +114,6 @@ MQTT_LIGHT_ATTRIBUTES_BLOCKED = frozenset( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, } ) @@ -129,7 +123,6 @@ DEFAULT_NAME = "MQTT LightEntity" DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" -DEFAULT_WHITE_VALUE_SCALE = 255 DEFAULT_WHITE_SCALE = 255 DEFAULT_ON_COMMAND_TYPE = "last" @@ -153,7 +146,6 @@ VALUE_TEMPLATE_KEYS = [ CONF_RGBW_VALUE_TEMPLATE, CONF_RGBWW_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE, - CONF_WHITE_VALUE_TEMPLATE, CONF_XY_VALUE_TEMPLATE, ] @@ -207,12 +199,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( vol.Coerce(int), vol.Range(min=1) ), - vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): valid_publish_topic, - vol.Optional( - CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE - ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_XY_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_XY_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, @@ -224,22 +210,17 @@ _PLATFORM_SCHEMA_BASE = ( # The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_BASIC = vol.All( - # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_COMMAND_TOPIC), - cv.deprecated(CONF_WHITE_VALUE_SCALE), - cv.deprecated(CONF_WHITE_VALUE_STATE_TOPIC), - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) DISCOVERY_SCHEMA_BASIC = vol.All( # CONF_VALUE_TEMPLATE is no longer supported, support was removed in 2022.2 cv.removed(CONF_VALUE_TEMPLATE), - # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_COMMAND_TOPIC), - cv.deprecated(CONF_WHITE_VALUE_SCALE), - cv.deprecated(CONF_WHITE_VALUE_STATE_TOPIC), - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), + # CONF_WHITE_VALUE_* is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE_COMMAND_TOPIC), + cv.removed(CONF_WHITE_VALUE_SCALE), + cv.removed(CONF_WHITE_VALUE_STATE_TOPIC), + cv.removed(CONF_WHITE_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) @@ -266,13 +247,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._color_temp = None self._effect = None self._hs_color = None - self._legacy_mode = False self._rgb_color = None self._rgbw_color = None self._rgbww_color = None self._state = None self._supported_color_modes = None - self._white_value = None self._xy_color = None self._topic = None @@ -288,7 +267,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._optimistic_rgb_color = False self._optimistic_rgbw_color = False self._optimistic_rgbww_color = False - self._optimistic_white_value = False self._optimistic_xy_color = False MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -324,8 +302,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): CONF_RGBWW_STATE_TOPIC, CONF_STATE_TOPIC, CONF_WHITE_COMMAND_TOPIC, - CONF_WHITE_VALUE_COMMAND_TOPIC, - CONF_WHITE_VALUE_STATE_TOPIC, CONF_XY_COMMAND_TOPIC, CONF_XY_STATE_TOPIC, ) @@ -384,9 +360,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) self._optimistic_effect = optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None self._optimistic_hs_color = optimistic or topic[CONF_HS_STATE_TOPIC] is None - self._optimistic_white_value = ( - optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None - ) self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None supported_color_modes = set() if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: @@ -423,9 +396,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): # Validate the color_modes configuration self._supported_color_modes = valid_supported_color_modes(supported_color_modes) - if topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: - self._legacy_mode = True - def _is_optimistic(self, attribute): """Return True if the attribute is optimistically updated.""" return getattr(self, f"_optimistic_{attribute}") @@ -513,10 +483,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) if not rgb: return - if self._legacy_mode: - self._hs_color = color_util.color_RGB_to_hs(*rgb) - else: - self._rgb_color = rgb + self._rgb_color = rgb self.async_write_ha_state() add_topic(CONF_RGB_STATE_TOPIC, rgb_received) @@ -624,24 +591,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): add_topic(CONF_HS_STATE_TOPIC, hs_received) - @callback - @log_messages(self.hass, self.entity_id) - def white_value_received(msg): - """Handle new MQTT messages for white value.""" - payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE]( - msg.payload, None - ) - if not payload: - _LOGGER.debug("Ignoring empty white value message from '%s'", msg.topic) - return - - device_value = float(payload) - percent_white = device_value / self._config[CONF_WHITE_VALUE_SCALE] - self._white_value = percent_white * 255 - self.async_write_ha_state() - - add_topic(CONF_WHITE_VALUE_STATE_TOPIC, white_value_received) - @callback @log_messages(self.hass, self.entity_id) def xy_received(msg): @@ -654,10 +603,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): xy_color = tuple(float(val) for val in payload.split(",")) if self._optimistic_color_mode: self._color_mode = ColorMode.XY - if self._legacy_mode: - self._hs_color = color_util.color_xy_to_hs(*xy_color) - else: - self._xy_color = xy_color + self._xy_color = xy_color self.async_write_ha_state() add_topic(CONF_XY_STATE_TOPIC, xy_received) @@ -690,7 +636,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): restore_state(ATTR_COLOR_TEMP) restore_state(ATTR_EFFECT) restore_state(ATTR_HS_COLOR) - restore_state(ATTR_WHITE_VALUE) restore_state(ATTR_XY_COLOR) restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) @@ -704,19 +649,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @property def color_mode(self): """Return current color mode.""" - if self._legacy_mode: - return None return self._color_mode @property def hs_color(self): """Return the hs color value.""" - if not self._legacy_mode: - return self._hs_color - - # Legacy mode, gate color_temp with white_value == 0 - if self._white_value: - return None return self._hs_color @property @@ -742,18 +679,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @property def color_temp(self): """Return the color temperature in mired.""" - if not self._legacy_mode: - return self._color_temp - - # Legacy mode, gate color_temp with white_value > 0 - supports_color = ( - self._topic[CONF_RGB_COMMAND_TOPIC] - or self._topic[CONF_HS_COMMAND_TOPIC] - or self._topic[CONF_XY_COMMAND_TOPIC] - ) - if self._white_value or not supports_color: - return self._color_temp - return None + return self._color_temp @property def min_mireds(self): @@ -765,13 +691,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Return the warmest color_temp that this light supports.""" return self._config.get(CONF_MAX_MIREDS, super().max_mireds) - @property - def white_value(self): - """Return the white property.""" - if white_value := self._white_value: - return min(round(white_value), 255) - return None - @property def is_on(self): """Return true if device is on.""" @@ -795,8 +714,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @property def supported_color_modes(self): """Flag supported color modes.""" - if self._legacy_mode: - return None return self._supported_color_modes @property @@ -807,32 +724,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None and LightEntityFeature.EFFECT ) - if not self._legacy_mode: - return supported_features - - # Legacy mode - supported_features |= self._topic[CONF_RGB_COMMAND_TOPIC] is not None and ( - SUPPORT_COLOR | SUPPORT_BRIGHTNESS - ) - supported_features |= ( - self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None - and SUPPORT_BRIGHTNESS - ) - supported_features |= ( - self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None - and SUPPORT_COLOR_TEMP - ) - supported_features |= ( - self._topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR - ) - supported_features |= ( - self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None - and SUPPORT_WHITE_VALUE - ) - supported_features |= ( - self._topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR - ) - return supported_features async def async_turn_on(self, **kwargs): # noqa: C901 @@ -905,70 +796,38 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 hs_color = kwargs.get(ATTR_HS_COLOR) - if ( - hs_color - and self._topic[CONF_RGB_COMMAND_TOPIC] is not None - and self._legacy_mode - ): - # Legacy mode: Convert HS to RGB - rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100)) - rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, ColorMode.RGB) - await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) - should_update |= set_optimistic( - ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_RGB_COLOR - ) if hs_color and self._topic[CONF_HS_COMMAND_TOPIC] is not None: await publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ColorMode.HS) - if ( - hs_color - and self._topic[CONF_XY_COMMAND_TOPIC] is not None - and self._legacy_mode - ): - # Legacy mode: Convert HS to XY - xy_color = color_util.color_hs_to_xy(*hs_color) - await publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") - should_update |= set_optimistic( - ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_XY_COLOR - ) - - if ( - (rgb := kwargs.get(ATTR_RGB_COLOR)) - and self._topic[CONF_RGB_COMMAND_TOPIC] is not None - and not self._legacy_mode - ): + if (rgb := kwargs.get(ATTR_RGB_COLOR)) and self._topic[ + CONF_RGB_COMMAND_TOPIC + ] is not None: scaled = scale_rgbx(rgb) rgb_s = render_rgbx(scaled, CONF_RGB_COMMAND_TEMPLATE, ColorMode.RGB) await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_RGB_COLOR, rgb, ColorMode.RGB) - if ( - (rgbw := kwargs.get(ATTR_RGBW_COLOR)) - and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None - and not self._legacy_mode - ): + if (rgbw := kwargs.get(ATTR_RGBW_COLOR)) and self._topic[ + CONF_RGBW_COMMAND_TOPIC + ] is not None: scaled = scale_rgbx(rgbw) rgbw_s = render_rgbx(scaled, CONF_RGBW_COMMAND_TEMPLATE, ColorMode.RGBW) await publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) should_update |= set_optimistic(ATTR_RGBW_COLOR, rgbw, ColorMode.RGBW) - if ( - (rgbww := kwargs.get(ATTR_RGBWW_COLOR)) - and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None - and not self._legacy_mode - ): + if (rgbww := kwargs.get(ATTR_RGBWW_COLOR)) and self._topic[ + CONF_RGBWW_COMMAND_TOPIC + ] is not None: scaled = scale_rgbx(rgbww) rgbww_s = render_rgbx(scaled, CONF_RGBWW_COMMAND_TEMPLATE, ColorMode.RGBWW) await publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) should_update |= set_optimistic(ATTR_RGBWW_COLOR, rgbww, ColorMode.RGBWW) - if ( - (xy_color := kwargs.get(ATTR_XY_COLOR)) - and self._topic[CONF_XY_COMMAND_TOPIC] is not None - and not self._legacy_mode - ): + if (xy_color := kwargs.get(ATTR_XY_COLOR)) and self._topic[ + CONF_XY_COMMAND_TOPIC + ] is not None: await publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") should_update |= set_optimistic(ATTR_XY_COLOR, xy_color, ColorMode.XY) @@ -987,24 +846,10 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): device_brightness = tpl(variables={"value": device_brightness}) await publish(CONF_BRIGHTNESS_COMMAND_TOPIC, device_brightness) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) - elif ( - ATTR_BRIGHTNESS in kwargs - and ATTR_HS_COLOR not in kwargs - and self._topic[CONF_RGB_COMMAND_TOPIC] is not None - and self._legacy_mode - ): - # Legacy mode - hs_color = self._hs_color if self._hs_color is not None else (0, 0) - brightness = kwargs[ATTR_BRIGHTNESS] - rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100), brightness) - rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, ColorMode.RGB) - await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) - should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( ATTR_BRIGHTNESS in kwargs and ATTR_RGB_COLOR not in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None - and not self._legacy_mode ): rgb_color = self._rgb_color if self._rgb_color is not None else (255,) * 3 rgb = scale_rgbx(rgb_color, kwargs[ATTR_BRIGHTNESS]) @@ -1015,7 +860,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ATTR_BRIGHTNESS in kwargs and ATTR_RGBW_COLOR not in kwargs and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None - and not self._legacy_mode ): rgbw_color = ( self._rgbw_color if self._rgbw_color is not None else (255,) * 4 @@ -1028,7 +872,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ATTR_BRIGHTNESS in kwargs and ATTR_RGBWW_COLOR not in kwargs and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None - and not self._legacy_mode ): rgbww_color = ( self._rgbww_color if self._rgbww_color is not None else (255,) * 5 @@ -1069,16 +912,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ColorMode.WHITE, ) - if ( - ATTR_WHITE_VALUE in kwargs - and self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None - ): - percent_white = float(kwargs[ATTR_WHITE_VALUE]) / 255 - white_scale = self._config[CONF_WHITE_VALUE_SCALE] - device_white_value = min(round(percent_white * white_scale), white_scale) - await publish(CONF_WHITE_VALUE_COMMAND_TOPIC, device_white_value) - should_update |= set_optimistic(ATTR_WHITE_VALUE, kwargs[ATTR_WHITE_VALUE]) - if on_command_type == "last": await publish(CONF_COMMAND_TOPIC, self._payload["on"]) should_update = True diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 716366cbe22..659dd212b51 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -15,7 +15,6 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, ENTITY_ID_FORMAT, FLASH_LONG, @@ -23,7 +22,6 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, VALID_COLOR_MODES, ColorMode, LightEntity, @@ -78,7 +76,6 @@ DEFAULT_FLASH_TIME_SHORT = 2 DEFAULT_NAME = "MQTT JSON Light" DEFAULT_OPTIMISTIC = False DEFAULT_RGB = False -DEFAULT_WHITE_VALUE = False DEFAULT_XY = False DEFAULT_HS = False DEFAULT_BRIGHTNESS_SCALE = 255 @@ -97,7 +94,7 @@ CONF_MIN_MIREDS = "min_mireds" def valid_color_configuration(config): """Test color_mode is not combined with deprecated config.""" - deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_WHITE_VALUE, CONF_XY} + deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_XY} if config[CONF_COLOR_MODE] and any(config.get(key) for key in deprecated): raise vol.Invalid(f"color_mode must not be combined with any of {deprecated}") return config @@ -139,7 +136,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Unique(), valid_supported_color_modes, ), - vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, }, ) @@ -149,15 +145,13 @@ _PLATFORM_SCHEMA_BASE = ( # Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_JSON = vol.All( - # CONF_WHITE_VALUE is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE), cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), valid_color_configuration, ) DISCOVERY_SCHEMA_JSON = vol.All( - # CONF_WHITE_VALUE is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE), + # CONF_WHITE_VALUE is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), valid_color_configuration, ) @@ -197,7 +191,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._rgb = None self._rgbw = None self._rgbww = None - self._white_value = None self._xy = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -231,7 +224,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._supported_features |= config[CONF_RGB] and ( SUPPORT_COLOR | SUPPORT_BRIGHTNESS ) - self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE self._supported_features |= config[CONF_XY] and SUPPORT_COLOR def _update_color(self, values): @@ -366,14 +358,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): with suppress(KeyError): self._effect = values["effect"] - if self._supported_features and SUPPORT_WHITE_VALUE: - try: - self._white_value = int(values["white_value"]) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid white value received") - self.async_write_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: @@ -406,7 +390,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._rgb = last_attributes.get(ATTR_RGB_COLOR, self._rgb) self._rgbw = last_attributes.get(ATTR_RGBW_COLOR, self._rgbw) self._rgbww = last_attributes.get(ATTR_RGBWW_COLOR, self._rgbww) - self._white_value = last_attributes.get(ATTR_WHITE_VALUE, self._white_value) self._xy = last_attributes.get(ATTR_XY_COLOR, self._xy) @property @@ -464,11 +447,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Return the hs color value.""" return self._xy - @property - def white_value(self): - """Return the white property.""" - return self._white_value - @property def is_on(self): """Return true if device is on.""" @@ -520,7 +498,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): def _supports_color_mode(self, color_mode): return self.supported_color_modes and color_mode in self.supported_color_modes - async def async_turn_on(self, **kwargs): # noqa: C901 + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -635,13 +613,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._effect = kwargs[ATTR_EFFECT] should_update = True - if ATTR_WHITE_VALUE in kwargs: - message["white_value"] = int(kwargs[ATTR_WHITE_VALUE]) - - if self._optimistic: - self._white_value = kwargs[ATTR_WHITE_VALUE] - should_update = True - await self.async_publish( self._topic[CONF_COMMAND_TOPIC], json_dumps(message), diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 779f2f17e24..6f211e598b4 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -10,12 +10,10 @@ from homeassistant.components.light import ( ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, LightEntity, LightEntityFeature, ) @@ -84,7 +82,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_RED_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, } ) .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) @@ -93,14 +90,12 @@ _PLATFORM_SCHEMA_BASE = ( # Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_TEMPLATE = vol.All( - # CONF_WHITE_VALUE_TEMPLATE is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) DISCOVERY_SCHEMA_TEMPLATE = vol.All( - # CONF_WHITE_VALUE_TEMPLATE is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), + # CONF_WHITE_VALUE_TEMPLATE is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) @@ -131,7 +126,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): # features self._brightness = None self._color_temp = None - self._white_value = None self._hs = None self._effect = None @@ -159,7 +153,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): CONF_GREEN_TEMPLATE, CONF_RED_TEMPLATE, CONF_STATE_TEMPLATE, - CONF_WHITE_VALUE_TEMPLATE, ) } optimistic = config[CONF_OPTIMISTIC] @@ -236,16 +229,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): except ValueError: _LOGGER.warning("Invalid color value received") - if self._templates[CONF_WHITE_VALUE_TEMPLATE] is not None: - try: - self._white_value = int( - self._templates[ - CONF_WHITE_VALUE_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) - except ValueError: - _LOGGER.warning("Invalid white value received") - if self._templates[CONF_EFFECT_TEMPLATE] is not None: effect = self._templates[ CONF_EFFECT_TEMPLATE @@ -287,8 +270,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) if last_state.attributes.get(ATTR_EFFECT): self._effect = last_state.attributes.get(ATTR_EFFECT) - if last_state.attributes.get(ATTR_WHITE_VALUE): - self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) @property def brightness(self): @@ -315,11 +296,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Return the hs color value [int, int].""" return self._hs - @property - def white_value(self): - """Return the white property.""" - return self._white_value - @property def is_on(self): """Return True if entity is on.""" @@ -385,12 +361,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: self._hs = kwargs[ATTR_HS_COLOR] - if ATTR_WHITE_VALUE in kwargs: - values["white_value"] = int(kwargs[ATTR_WHITE_VALUE]) - - if self._optimistic: - self._white_value = kwargs[ATTR_WHITE_VALUE] - if ATTR_EFFECT in kwargs: values["effect"] = kwargs.get(ATTR_EFFECT) @@ -457,7 +427,5 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): features = features | LightEntityFeature.EFFECT if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: features = features | SUPPORT_COLOR_TEMP - if self._templates[CONF_WHITE_VALUE_TEMPLATE] is not None: - features = features | SUPPORT_WHITE_VALUE return features diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index bfafc99a9e2..ff529b3dda4 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -106,23 +106,6 @@ light: payload_on: "on" payload_off: "off" -config for RGB Version with white value and scale: - -light: - platform: mqtt - name: "Office Light RGB" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - white_value_state_topic: "office/rgb1/white_value/status" - white_value_command_topic: "office/rgb1/white_value/set" - white_value_scale: 99 - rgb_state_topic: "office/rgb1/rgb/status" - rgb_command_topic: "office/rgb1/rgb/set" - rgb_scale: 99 - qos: 0 - payload_on: "on" - payload_off: "off" - config for RGB Version with RGB command template: light: @@ -199,13 +182,11 @@ from homeassistant.components.mqtt.light.schema_basic import ( CONF_RGB_COMMAND_TOPIC, CONF_RGBW_COMMAND_TOPIC, CONF_RGBWW_COMMAND_TOPIC, - CONF_WHITE_VALUE_COMMAND_TOPIC, CONF_XY_COMMAND_TOPIC, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) from homeassistant.const import ( ATTR_ASSUMED_STATE, - ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, STATE_UNKNOWN, @@ -270,33 +251,6 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_conf assert hass.states.get("light.test") is None -async def test_legacy_rgb_white_light(hass, mqtt_mock_entry_with_yaml_config): - """Test legacy RGB + white light flags brightness support.""" - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test_light_rgb/set", - "rgb_command_topic": "test_light_rgb/rgb/set", - "white_value_command_topic": "test_light_rgb/white/set", - } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - expected_features = ( - light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS | light.SUPPORT_WHITE_VALUE - ) - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features - assert state.attributes.get(light.ATTR_COLOR_MODE) is None - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["hs", "rgbw"] - - async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( hass, mqtt_mock_entry_with_yaml_config ): @@ -325,7 +279,6 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) is None assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] @@ -341,7 +294,6 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) == "onoff" assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] @@ -357,138 +309,6 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( assert state.state == STATE_UNKNOWN -async def test_legacy_controlling_state_via_topic( - hass, mqtt_mock_entry_with_yaml_config -): - """Test the controlling of the state via topic for legacy light (white_value).""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_light_rgb/status", - "command_topic": "test_light_rgb/set", - "brightness_state_topic": "test_light_rgb/brightness/status", - "brightness_command_topic": "test_light_rgb/brightness/set", - "rgb_state_topic": "test_light_rgb/rgb/status", - "rgb_command_topic": "test_light_rgb/rgb/set", - "color_temp_state_topic": "test_light_rgb/color_temp/status", - "color_temp_command_topic": "test_light_rgb/color_temp/set", - "effect_state_topic": "test_light_rgb/effect/status", - "effect_command_topic": "test_light_rgb/effect/set", - "hs_state_topic": "test_light_rgb/hs/status", - "hs_command_topic": "test_light_rgb/hs/set", - "white_value_state_topic": "test_light_rgb/white_value/status", - "white_value_command_topic": "test_light_rgb/white_value/set", - "xy_state_topic": "test_light_rgb/xy/status", - "xy_command_topic": "test_light_rgb/xy/set", - "qos": "0", - "payload_on": 1, - "payload_off": 0, - } - } - color_modes = ["color_temp", "hs", "rgbw"] - - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("brightness") is None - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("rgbw_color") is None - assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get("xy_color") is None - assert state.attributes.get(light.ATTR_COLOR_MODE) is None - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "test_light_rgb/status", "1") - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("brightness") is None - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("rgbw_color") is None - assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get("xy_color") is None - assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/status", "0") - - state = hass.states.get("light.test") - assert state.state == STATE_OFF - - async_fire_mqtt_message(hass, "test_light_rgb/status", "1") - - async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") - - light_state = hass.states.get("light.test") - assert light_state.attributes["brightness"] == 100 - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") - light_state = hass.states.get("light.test") - assert light_state.attributes.get("color_temp") is None - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "100") - - light_state = hass.states.get("light.test") - assert light_state.attributes["white_value"] == 100 - assert light_state.attributes["color_temp"] == 300 - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") - light_state = hass.states.get("light.test") - assert light_state.attributes["effect"] == "rainbow" - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/status", "1") - - async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "125,125,125") - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("rgb_color") == (255, 187, 131) - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "0") - light_state = hass.states.get("light.test") - assert light_state.attributes.get("rgb_color") == (255, 255, 255) - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("hs_color") == (200, 50) - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("xy_color") == (0.672, 0.324) - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling of the state via topic.""" config = { @@ -534,7 +354,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) is None assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes @@ -551,7 +370,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes @@ -611,125 +429,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_invalid_state_via_topic( - hass, mqtt_mock_entry_with_yaml_config, caplog -): - """Test handling of empty data via topic.""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_light_rgb/status", - "command_topic": "test_light_rgb/set", - "brightness_state_topic": "test_light_rgb/brightness/status", - "brightness_command_topic": "test_light_rgb/brightness/set", - "rgb_state_topic": "test_light_rgb/rgb/status", - "rgb_command_topic": "test_light_rgb/rgb/set", - "color_temp_state_topic": "test_light_rgb/color_temp/status", - "color_temp_command_topic": "test_light_rgb/color_temp/set", - "effect_state_topic": "test_light_rgb/effect/status", - "effect_command_topic": "test_light_rgb/effect/set", - "hs_state_topic": "test_light_rgb/hs/status", - "hs_command_topic": "test_light_rgb/hs/set", - "white_value_state_topic": "test_light_rgb/white_value/status", - "white_value_command_topic": "test_light_rgb/white_value/set", - "xy_state_topic": "test_light_rgb/xy/status", - "xy_command_topic": "test_light_rgb/xy/set", - "qos": "0", - "payload_on": 1, - "payload_off": 0, - } - } - - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("brightness") is None - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get("xy_color") is None - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "test_light_rgb/status", "1") - async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "255,255,255") - async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "255") - async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "none") - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 255, 255) - assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") == "none" - assert state.attributes.get("hs_color") == (0, 0) - assert state.attributes.get("white_value") is None - assert state.attributes.get("xy_color") == (0.323, 0.329) - - async_fire_mqtt_message(hass, "test_light_rgb/status", "") - assert "Ignoring empty state message" in caplog.text - light_state = hass.states.get("light.test") - assert state.state == STATE_ON - - async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "") - assert "Ignoring empty brightness message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes["brightness"] == 255 - - async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "") - assert "Ignoring empty effect message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes["effect"] == "none" - - async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "") - assert "Ignoring empty rgb message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes.get("rgb_color") == (255, 255, 255) - - async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "") - assert "Ignoring empty hs message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes.get("hs_color") == (0, 0) - - async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "bad,bad") - assert "Failed to parse hs state update" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes.get("hs_color") == (0, 0) - - async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "") - assert "Ignoring empty xy-color message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes.get("xy_color") == (0.323, 0.329) - - async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "153") - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "255") - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 254, 250) - assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_temp") == 153 - assert state.attributes.get("effect") == "none" - assert state.attributes.get("hs_color") == (54.768, 1.6) - assert state.attributes.get("white_value") == 255 - assert state.attributes.get("xy_color") == (0.326, 0.333) - - async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "") - assert "Ignoring empty color temp message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes["color_temp"] == 153 - - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "") - assert "Ignoring empty white value message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes["white_value"] == 255 - - async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test handling of empty data via topic.""" config = { @@ -955,148 +654,6 @@ async def test_brightness_from_rgb_controlling_scale( assert state.attributes.get("brightness") == 127 -async def test_legacy_white_value_controlling_scale( - hass, mqtt_mock_entry_with_yaml_config -): - """Test the white_value controlling scale.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_scale/status", - "command_topic": "test_scale/set", - "white_value_state_topic": "test_scale/white_value/status", - "white_value_command_topic": "test_scale/white_value/set", - "white_value_scale": "99", - "qos": 0, - "payload_on": "on", - "payload_off": "off", - } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - assert state.attributes.get("white_value") is None - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "test_scale/status", "on") - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("white_value") is None - - async_fire_mqtt_message(hass, "test_scale/status", "off") - - state = hass.states.get("light.test") - assert state.state == STATE_OFF - - async_fire_mqtt_message(hass, "test_scale/status", "on") - - async_fire_mqtt_message(hass, "test_scale/white_value/status", "99") - - light_state = hass.states.get("light.test") - assert light_state.attributes["white_value"] == 255 - - -async def test_legacy_controlling_state_via_topic_with_templates( - hass, mqtt_mock_entry_with_yaml_config -): - """Test the setting of the state with a template.""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_light_rgb/status", - "command_topic": "test_light_rgb/set", - "brightness_command_topic": "test_light_rgb/brightness/set", - "rgb_command_topic": "test_light_rgb/rgb/set", - "color_temp_command_topic": "test_light_rgb/color_temp/set", - "effect_command_topic": "test_light_rgb/effect/set", - "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", - "xy_command_topic": "test_light_rgb/xy/set", - "brightness_state_topic": "test_light_rgb/brightness/status", - "color_temp_state_topic": "test_light_rgb/color_temp/status", - "effect_state_topic": "test_light_rgb/effect/status", - "hs_state_topic": "test_light_rgb/hs/status", - "rgb_state_topic": "test_light_rgb/rgb/status", - "white_value_state_topic": "test_light_rgb/white_value/status", - "xy_state_topic": "test_light_rgb/xy/status", - "state_value_template": "{{ value_json.hello }}", - "brightness_value_template": "{{ value_json.hello }}", - "color_temp_value_template": "{{ value_json.hello }}", - "effect_value_template": "{{ value_json.hello }}", - "hs_value_template": '{{ value_json.hello | join(",") }}', - "rgb_value_template": '{{ value_json.hello | join(",") }}', - "white_value_template": "{{ value_json.hello }}", - "xy_value_template": '{{ value_json.hello | join(",") }}', - } - } - - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - assert state.attributes.get("brightness") is None - assert state.attributes.get("rgb_color") is None - - async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", '{"hello": [1, 2, 3]}') - async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "ON"}') - async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", '{"hello": "50"}') - async_fire_mqtt_message( - hass, "test_light_rgb/color_temp/status", '{"hello": "300"}' - ) - async_fire_mqtt_message( - hass, "test_light_rgb/effect/status", '{"hello": "rainbow"}' - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 50 - assert state.attributes.get("rgb_color") == (84, 169, 255) - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") == "rainbow" - assert state.attributes.get("white_value") is None - - async_fire_mqtt_message( - hass, "test_light_rgb/white_value/status", '{"hello": "75"}' - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 50 - assert state.attributes.get("rgb_color") == (255, 187, 131) - assert state.attributes.get("color_temp") == 300 - assert state.attributes.get("effect") == "rainbow" - assert state.attributes.get("white_value") == 75 - - async_fire_mqtt_message(hass, "test_light_rgb/hs/status", '{"hello": [100,50]}') - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", '{"hello": "0"}') - - state = hass.states.get("light.test") - assert state.attributes.get("hs_color") == (100, 50) - - async_fire_mqtt_message( - hass, "test_light_rgb/xy/status", '{"hello": [0.123,0.123]}' - ) - - state = hass.states.get("light.test") - assert state.attributes.get("xy_color") == (0.14, 0.131) - - async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": null}') - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - - async def test_controlling_state_via_topic_with_templates( hass, mqtt_mock_entry_with_yaml_config ): @@ -1200,139 +757,6 @@ async def test_controlling_state_via_topic_with_templates( assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_sending_mqtt_commands_and_optimistic( - hass, mqtt_mock_entry_with_yaml_config -): - """Test the sending of command in optimistic mode.""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test_light_rgb/set", - "brightness_command_topic": "test_light_rgb/brightness/set", - "rgb_command_topic": "test_light_rgb/rgb/set", - "color_temp_command_topic": "test_light_rgb/color_temp/set", - "effect_command_topic": "test_light_rgb/effect/set", - "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", - "xy_command_topic": "test_light_rgb/xy/set", - "effect_list": ["colorloop", "random"], - "qos": 2, - "payload_on": "on", - "payload_off": "off", - } - } - color_modes = ["color_temp", "hs", "rgbw"] - fake_state = ha.State( - "light.test", - "on", - { - "brightness": 95, - "hs_color": [100, 100], - "effect": "random", - "color_temp": 100, - # TODO: Test restoring state with white_value - "white_value": 0, - }, - ) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ), assert_setup_component(1, light.DOMAIN): - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 95 - assert state.attributes.get("hs_color") == (100, 100) - assert state.attributes.get("effect") == "random" - assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - await common.async_turn_on(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on", 2, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - await common.async_turn_off(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "off", 2, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.state == STATE_OFF - - mqtt_mock.reset_mock() - await common.async_turn_on( - hass, "light.test", brightness=50, xy_color=[0.123, 0.123] - ) - state = hass.states.get("light.test") - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - state = hass.states.get("light.test") - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) - state = hass.states.get("light.test") - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light_rgb/set", "on", 2, False), - call("test_light_rgb/rgb/set", "255,128,0", 2, False), - call("test_light_rgb/brightness/set", "50", 2, False), - call("test_light_rgb/hs/set", "359.0,78.0", 2, False), - call("test_light_rgb/xy/set", "0.14,0.131", 2, False), - ], - any_order=True, - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes["rgb_color"] == (255, 128, 0) - assert state.attributes["brightness"] == 50 - assert state.attributes["hs_color"] == (30.118, 100) - assert state.attributes.get("white_value") is None - assert state.attributes["xy_color"] == (0.611, 0.375) - assert state.attributes.get("color_temp") is None - - await common.async_turn_on(hass, "light.test", white_value=80, color_temp=125) - state = hass.states.get("light.test") - assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light_rgb/white_value/set", "80", 2, False), - call("test_light_rgb/color_temp/set", "125", 2, False), - ], - any_order=True, - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (221, 229, 255) - assert state.attributes["brightness"] == 50 - assert state.attributes.get("hs_color") == (224.772, 13.249) - assert state.attributes["white_value"] == 80 - assert state.attributes.get("xy_color") == (0.296, 0.301) - assert state.attributes["color_temp"] == 125 - - async def test_sending_mqtt_commands_and_optimistic( hass, mqtt_mock_entry_with_yaml_config ): @@ -1884,98 +1308,6 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock_entry_with_yaml_conf ) -async def test_legacy_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): - """Test on command in RGB brightness mode.""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test_light/set", - "rgb_command_topic": "test_light/rgb", - "white_value_command_topic": "test_light/white_value", - } - } - - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - - await common.async_turn_on(hass, "light.test", brightness=127) - - # Should get the following MQTT messages. - # test_light/rgb: '127,127,127' - # test_light/set: 'ON' - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "127,127,127", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_turn_on(hass, "light.test", brightness=255) - - # Should get the following MQTT messages. - # test_light/rgb: '255,255,255' - # test_light/set: 'ON' - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "255,255,255", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_turn_on(hass, "light.test", brightness=1) - - # Should get the following MQTT messages. - # test_light/rgb: '1,1,1' - # test_light/set: 'ON' - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "1,1,1", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_turn_off(hass, "light.test") - - mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) - - # Ensure color gets scaled with brightness. - await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) - - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "1,0,0", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_turn_on(hass, "light.test", brightness=255) - - # Should get the following MQTT messages. - # test_light/rgb: '255,128,0' - # test_light/set: 'ON' - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "255,128,0", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - async def test_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { @@ -2486,7 +1818,6 @@ async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) is None assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes @@ -2503,7 +1834,6 @@ async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes @@ -2923,14 +2253,12 @@ async def test_discovery_update_light_topic_and_template( "color_temp_command_topic": "test_light_rgb/state1", "effect_command_topic": "test_light_rgb/effect/set", "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", "xy_command_topic": "test_light_rgb/xy/set", "brightness_state_topic": "test_light_rgb/state1", "color_temp_state_topic": "test_light_rgb/state1", "effect_state_topic": "test_light_rgb/state1", "hs_state_topic": "test_light_rgb/state1", "rgb_state_topic": "test_light_rgb/state1", - "white_value_state_topic": "test_light_rgb/state1", "xy_state_topic": "test_light_rgb/state1", "state_value_template": "{{ value_json.state1.state }}", "brightness_value_template": "{{ value_json.state1.brightness }}", @@ -2938,7 +2266,6 @@ async def test_discovery_update_light_topic_and_template( "effect_value_template": "{{ value_json.state1.fx }}", "hs_value_template": "{{ value_json.state1.hs }}", "rgb_value_template": "{{ value_json.state1.rgb }}", - "white_value_template": "{{ value_json.state1.white }}", "xy_value_template": "{{ value_json.state1.xy }}", } @@ -2951,14 +2278,12 @@ async def test_discovery_update_light_topic_and_template( "color_temp_command_topic": "test_light_rgb/state2", "effect_command_topic": "test_light_rgb/effect/set", "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", "xy_command_topic": "test_light_rgb/xy/set", "brightness_state_topic": "test_light_rgb/state2", "color_temp_state_topic": "test_light_rgb/state2", "effect_state_topic": "test_light_rgb/state2", "hs_state_topic": "test_light_rgb/state2", "rgb_state_topic": "test_light_rgb/state2", - "white_value_state_topic": "test_light_rgb/state2", "xy_state_topic": "test_light_rgb/state2", "state_value_template": "{{ value_json.state2.state }}", "brightness_value_template": "{{ value_json.state2.brightness }}", @@ -2966,7 +2291,6 @@ async def test_discovery_update_light_topic_and_template( "effect_value_template": "{{ value_json.state2.fx }}", "hs_value_template": "{{ value_json.state2.hs }}", "rgb_value_template": "{{ value_json.state2.rgb }}", - "white_value_template": "{{ value_json.state2.white }}", "xy_value_template": "{{ value_json.state2.xy }}", } state_data1 = [ @@ -2981,7 +2305,6 @@ async def test_discovery_update_light_topic_and_template( [ ("brightness", 100), ("color_temp", 123), - ("white_value", 100), ("effect", "cycle"), ], ), @@ -2998,7 +2321,7 @@ async def test_discovery_update_light_topic_and_template( ) ], "on", - [("hs_color", (1, 2)), ("white_value", None)], + [("hs_color", (1, 2))], ), ( [ @@ -3018,7 +2341,7 @@ async def test_discovery_update_light_topic_and_template( ) ], "on", - [("xy_color", (0.3, 0.401))], + [("xy_color", (0.3, 0.4))], ), ] state_data2 = [ @@ -3033,7 +2356,6 @@ async def test_discovery_update_light_topic_and_template( [ ("brightness", 50), ("color_temp", 200), - ("white_value", 50), ("effect", "loop"), ], ), @@ -3083,7 +2405,7 @@ async def test_discovery_update_light_topic_and_template( ) ], "on", - [("hs_color", (1.2, 2.2)), ("white_value", None)], + [("hs_color", (1.2, 2.2))], ), ( [ @@ -3186,14 +2508,12 @@ async def test_discovery_update_light_template( "color_temp_command_topic": "test_light_rgb/state1", "effect_command_topic": "test_light_rgb/effect/set", "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", "xy_command_topic": "test_light_rgb/xy/set", "brightness_state_topic": "test_light_rgb/state1", "color_temp_state_topic": "test_light_rgb/state1", "effect_state_topic": "test_light_rgb/state1", "hs_state_topic": "test_light_rgb/state1", "rgb_state_topic": "test_light_rgb/state1", - "white_value_state_topic": "test_light_rgb/state1", "xy_state_topic": "test_light_rgb/state1", "state_value_template": "{{ value_json.state1.state }}", "brightness_value_template": "{{ value_json.state1.brightness }}", @@ -3201,7 +2521,6 @@ async def test_discovery_update_light_template( "effect_value_template": "{{ value_json.state1.fx }}", "hs_value_template": "{{ value_json.state1.hs }}", "rgb_value_template": "{{ value_json.state1.rgb }}", - "white_value_template": "{{ value_json.state1.white }}", "xy_value_template": "{{ value_json.state1.xy }}", } @@ -3214,14 +2533,12 @@ async def test_discovery_update_light_template( "color_temp_command_topic": "test_light_rgb/state1", "effect_command_topic": "test_light_rgb/effect/set", "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", "xy_command_topic": "test_light_rgb/xy/set", "brightness_state_topic": "test_light_rgb/state1", "color_temp_state_topic": "test_light_rgb/state1", "effect_state_topic": "test_light_rgb/state1", "hs_state_topic": "test_light_rgb/state1", "rgb_state_topic": "test_light_rgb/state1", - "white_value_state_topic": "test_light_rgb/state1", "xy_state_topic": "test_light_rgb/state1", "state_value_template": "{{ value_json.state2.state }}", "brightness_value_template": "{{ value_json.state2.brightness }}", @@ -3229,7 +2546,6 @@ async def test_discovery_update_light_template( "effect_value_template": "{{ value_json.state2.fx }}", "hs_value_template": "{{ value_json.state2.hs }}", "rgb_value_template": "{{ value_json.state2.rgb }}", - "white_value_template": "{{ value_json.state2.white }}", "xy_value_template": "{{ value_json.state2.xy }}", } state_data1 = [ @@ -3244,7 +2560,6 @@ async def test_discovery_update_light_template( [ ("brightness", 100), ("color_temp", 123), - ("white_value", 100), ("effect", "cycle"), ], ), @@ -3281,7 +2596,7 @@ async def test_discovery_update_light_template( ) ], "on", - [("white_value", None), ("xy_color", (0.3, 0.401))], + [("xy_color", (0.3, 0.4))], ), ] state_data2 = [ @@ -3296,7 +2611,6 @@ async def test_discovery_update_light_template( [ ("brightness", 50), ("color_temp", 200), - ("white_value", 50), ("effect", "loop"), ], ), @@ -3368,7 +2682,7 @@ async def test_discovery_update_light_template( ) ], "on", - [("white_value", None), ("xy_color", (0.4, 0.3))], + [("xy_color", (0.4, 0.3))], ), ( [ @@ -3378,7 +2692,7 @@ async def test_discovery_update_light_template( ) ], "on", - [("white_value", None), ("xy_color", (0.4, 0.3))], + [("xy_color", (0.4, 0.3))], ), ] @@ -3646,7 +2960,6 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): "topic,value,attribute,attribute_value,init_payload", [ ("state_topic", "ON", None, "on", None), - ("brightness_state_topic", "60", "brightness", 60, ("state_topic", "ON")), ( "color_mode_state_topic", "200", @@ -3695,8 +3008,40 @@ async def test_encoding_subscribable_topics( config[CONF_RGBWW_COMMAND_TOPIC] = "light/CONF_RGBWW_COMMAND_TOPIC" config[CONF_XY_COMMAND_TOPIC] = "light/CONF_XY_COMMAND_TOPIC" config[CONF_EFFECT_LIST] = ["colorloop", "random"] - if attribute and attribute == "brightness": - config[CONF_WHITE_VALUE_COMMAND_TOPIC] = "light/CONF_WHITE_VALUE_COMMAND_TOPIC" + + await help_test_encoding_subscribable_topics( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + config, + topic, + value, + attribute, + attribute_value, + init_payload, + ) + + +@pytest.mark.parametrize( + "topic,value,attribute,attribute_value,init_payload", + [ + ("brightness_state_topic", "60", "brightness", 60, ("state_topic", "ON")), + ], +) +async def test_encoding_subscribable_topics_brightness( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, +): + """Test handling of incoming encoded payload for a brightness only light.""" + config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) + config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" await help_test_encoding_subscribable_topics( hass, diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index b930de9b6c3..d57fc1cceee 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -1,6 +1,6 @@ """The tests for the MQTT JSON light platform. -Configuration with RGB, brightness, color temp, effect, white value and XY: +Configuration with RGB, brightness, color temp, effect, and XY: light: platform: mqtt_json @@ -11,22 +11,8 @@ light: color_temp: true effect: true rgb: true - white_value: true xy: true -Configuration with RGB, brightness, color temp, effect, white value: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - white_value: true - Configuration with RGB, brightness, color temp and effect: light: @@ -182,7 +168,7 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_conf assert hass.states.get("light.test") is None -@pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "white_value", "xy")) +@pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "xy")) async def test_fail_setup_if_color_mode_deprecated( hass, mqtt_mock_entry_no_yaml_config, deprecated ): @@ -267,10 +253,10 @@ async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features -async def test_no_color_brightness_color_temp_white_val_if_no_topics( +async def test_no_color_brightness_color_temp_if_no_topics( hass, mqtt_mock_entry_with_yaml_config ): - """Test for no RGB, brightness, color temp, effect, white val or XY.""" + """Test for no RGB, brightness, color temp, effector XY.""" assert await async_setup_component( hass, light.DOMAIN, @@ -295,7 +281,6 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics( assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get("hs_color") is None @@ -307,7 +292,6 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics( assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get("hs_color") is None @@ -338,7 +322,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi "color_temp": True, "effect": True, "rgb": True, - "white_value": True, "xy": True, "hs": True, "qos": "0", @@ -357,19 +340,17 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi | light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION - | light.SUPPORT_WHITE_VALUE ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get("hs_color") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) - # Turn on the light, full white + # Turn on the light async_fire_mqtt_message( hass, "test_light_rgb", @@ -377,8 +358,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi '"color":{"r":255,"g":255,"b":255},' '"brightness":255,' '"color_temp":155,' - '"effect":"colorloop",' - '"white_value":150}', + '"effect":"colorloop"}', ) state = hass.states.get("light.test") @@ -387,7 +367,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert state.attributes.get("brightness") == 255 assert state.attributes.get("color_temp") == 155 assert state.attributes.get("effect") == "colorloop" - assert state.attributes.get("white_value") == 150 assert state.attributes.get("xy_color") == (0.323, 0.329) assert state.attributes.get("hs_color") == (0.0, 0.0) @@ -446,11 +425,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi light_state = hass.states.get("light.test") assert light_state.attributes.get("effect") == "colorloop" - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "white_value":155}') - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("white_value") == 155 - async def test_controlling_state_via_topic2( hass, mqtt_mock_entry_with_yaml_config, caplog @@ -499,7 +473,6 @@ async def test_controlling_state_via_topic2( assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None assert state.attributes.get("supported_color_modes") == supported_color_modes - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -512,8 +485,7 @@ async def test_controlling_state_via_topic2( '"color":{"r":255,"g":128,"b":64, "c": 32, "w": 16, "x": 1, "y": 1},' '"brightness":255,' '"color_temp":155,' - '"effect":"colorloop",' - '"white_value":150}', + '"effect":"colorloop"}', ) state = hass.states.get("light.test") @@ -526,7 +498,6 @@ async def test_controlling_state_via_topic2( assert state.attributes.get("rgb_color") == (255, 136, 74) assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") == (255, 128, 64, 32, 16) - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") == (0.571, 0.361) # Light turned off @@ -595,11 +566,6 @@ async def test_controlling_state_via_topic2( state = hass.states.get("light.test") assert state.attributes.get("effect") == "other_effect" - # White value should be ignored - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "white_value":155}') - state = hass.states.get("light.test") - assert state.attributes.get("white_value") is None - # Invalid color mode async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON", "color_mode":"col_temp"}' @@ -635,7 +601,6 @@ async def test_sending_mqtt_commands_and_optimistic( "hs_color": [100, 100], "effect": "random", "color_temp": 100, - "white_value": 50, }, ) @@ -658,7 +623,6 @@ async def test_sending_mqtt_commands_and_optimistic( "hs": True, "rgb": True, "xy": True, - "white_value": True, "qos": 2, } }, @@ -672,7 +636,6 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get("hs_color") == (100, 100) assert state.attributes.get("effect") == "random" assert state.attributes.get("color_temp") == 100 - assert state.attributes.get("white_value") == 50 expected_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR @@ -680,7 +643,6 @@ async def test_sending_mqtt_commands_and_optimistic( | light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION - | light.SUPPORT_WHITE_VALUE ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get(ATTR_ASSUMED_STATE) @@ -720,9 +682,7 @@ async def test_sending_mqtt_commands_and_optimistic( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -750,8 +710,7 @@ async def test_sending_mqtt_commands_and_optimistic( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0,' - ' "x": 0.611, "y": 0.375, "h": 30.118, "s": 100.0},' - ' "white_value": 80}' + ' "x": 0.611, "y": 0.375, "h": 30.118, "s": 100.0}}' ), 2, False, @@ -765,7 +724,6 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes["rgb_color"] == (255, 128, 0) assert state.attributes["brightness"] == 50 assert state.attributes["hs_color"] == (30.118, 100) - assert state.attributes["white_value"] == 80 assert state.attributes["xy_color"] == (0.611, 0.375) @@ -783,7 +741,6 @@ async def test_sending_mqtt_commands_and_optimistic2( "color_mode": "rgb", "effect": "random", "hs_color": [100, 100], - "white_value": 50, }, ) @@ -831,7 +788,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None assert state.attributes.get("supported_color_modes") == supported_color_modes - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(ATTR_ASSUMED_STATE) @@ -876,7 +832,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["xy_color"] == (0.654, 0.301) assert "rgbw_color" not in state.attributes assert "rgbww_color" not in state.attributes - assert "white_value" not in state.attributes mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator( @@ -887,10 +842,8 @@ async def test_sending_mqtt_commands_and_optimistic2( ) mqtt_mock.async_publish.reset_mock() - # Set rgb color, white value should be discarded - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + # Set rgb color + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes["brightness"] == 75 @@ -900,7 +853,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["xy_color"] == (0.611, 0.375) assert "rgbw_color" not in state.attributes assert "rgbww_color" not in state.attributes - assert "white_value" not in state.attributes mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator('{"state": "ON", "color": {"r": 255, "g": 128, "b": 0} }'), @@ -910,9 +862,7 @@ async def test_sending_mqtt_commands_and_optimistic2( mqtt_mock.async_publish.reset_mock() # Set rgbw color - await common.async_turn_on( - hass, "light.test", rgbw_color=[255, 128, 0, 123], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgbw_color=[255, 128, 0, 123]) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes["brightness"] == 75 @@ -921,7 +871,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["hs_color"] == (30.0, 67.451) assert state.attributes["rgb_color"] == (255, 169, 83) assert "rgbww_color" not in state.attributes - assert "white_value" not in state.attributes assert state.attributes["xy_color"] == (0.526, 0.393) mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", @@ -943,7 +892,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["hs_color"] == (29.872, 92.157) assert state.attributes["rgb_color"] == (255, 137, 20) assert "rgbw_color" not in state.attributes - assert "white_value" not in state.attributes assert state.attributes["xy_color"] == (0.596, 0.382) mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", @@ -968,7 +916,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["xy_color"] == (0.123, 0.223) assert "rgbw_color" not in state.attributes assert "rgbww_color" not in state.attributes - assert "white_value" not in state.attributes mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator( @@ -993,7 +940,6 @@ async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): "command_topic": "test_light_rgb/set", "brightness": True, "hs": True, - "white_value": True, } }, ) @@ -1008,9 +954,7 @@ async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -1034,10 +978,7 @@ async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): ), call( "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"h": 30.118, "s": 100.0},' - ' "white_value": 80}' - ), + JsonValidator('{"state": "ON", "color": {"h": 30.118, "s": 100.0}}'), 0, False, ), @@ -1193,7 +1134,6 @@ async def test_sending_rgb_color_with_brightness( "command_topic": "test_light_rgb/set", "brightness": True, "rgb": True, - "white_value": True, } }, ) @@ -1208,9 +1148,7 @@ async def test_sending_rgb_color_with_brightness( ) await common.async_turn_on(hass, "light.test", brightness=255, hs_color=[359, 78]) await common.async_turn_on(hass, "light.test", brightness=1) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -1240,10 +1178,7 @@ async def test_sending_rgb_color_with_brightness( ), call( "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0},' - ' "white_value": 80}' - ), + JsonValidator('{"state": "ON", "color": {"r": 255, "g": 128, "b": 0}}'), 0, False, ), @@ -1267,7 +1202,6 @@ async def test_sending_rgb_color_with_scaled_brightness( "brightness": True, "brightness_scale": 100, "rgb": True, - "white_value": True, } }, ) @@ -1282,9 +1216,7 @@ async def test_sending_rgb_color_with_scaled_brightness( ) await common.async_turn_on(hass, "light.test", brightness=255, hs_color=[359, 78]) await common.async_turn_on(hass, "light.test", brightness=1) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -1314,10 +1246,7 @@ async def test_sending_rgb_color_with_scaled_brightness( ), call( "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0},' - ' "white_value": 80}' - ), + JsonValidator('{"state": "ON", "color": {"r": 255, "g": 128, "b": 0}}'), 0, False, ), @@ -1338,7 +1267,6 @@ async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): "command_topic": "test_light_rgb/set", "brightness": True, "xy": True, - "white_value": True, } }, ) @@ -1352,9 +1280,7 @@ async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -1378,10 +1304,7 @@ async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): ), call( "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"x": 0.611, "y": 0.375},' - ' "white_value": 80}' - ), + JsonValidator('{"state": "ON", "color": {"x": 0.611, "y": 0.375}}'), 0, False, ), @@ -1605,7 +1528,7 @@ async def test_brightness_scale(hass, mqtt_mock_entry_with_yaml_config): async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): - """Test that invalid color/brightness/white/etc. values are ignored.""" + """Test that invalid color/brightness/etc. values are ignored.""" assert await async_setup_component( hass, light.DOMAIN, @@ -1619,7 +1542,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): "brightness": True, "color_temp": True, "rgb": True, - "white_value": True, "qos": "0", } }, @@ -1635,12 +1557,10 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): | light.SUPPORT_COLOR_TEMP | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION - | light.SUPPORT_WHITE_VALUE ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("color_temp") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -1651,7 +1571,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): '{"state":"ON",' '"color":{"r":255,"g":255,"b":255},' '"brightness": 255,' - '"white_value": 255,' '"color_temp": 100,' '"effect": "rainbow"}', ) @@ -1660,7 +1579,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 255, 255) assert state.attributes.get("brightness") == 255 - assert state.attributes.get("white_value") == 255 assert state.attributes.get("color_temp") == 100 # Empty color value @@ -1721,16 +1639,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 - # Bad white value - async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON",' '"white_value": "badValue"}' - ) - - # White value should not have changed - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("white_value") == 255 - # Bad color temperature async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON",' '"color_temp": "badValue"}' @@ -2071,8 +1979,6 @@ async def test_publishing_with_custom_encoding( config = copy.deepcopy(DEFAULT_CONFIG[domain]) if topic == "effect_command_topic": config["effect_list"] = ["random", "color_loop"] - elif topic == "white_command_topic": - config["rgb_command_topic"] = "some-cmd-topic" await help_test_publishing_with_custom_encoding( hass, diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 2c96468057f..e4f755eab4e 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -13,7 +13,6 @@ light: state_template: '{{ value.split(",")[0] }}' brightness_template: '{{ value.split(",")[1] }}' color_temp_template: '{{ value.split(",")[2] }}' - white_value_template: '{{ value.split(",")[3] }}' red_template: '{{ value.split(",")[4].split("-")[0] }}' green_template: '{{ value.split(",")[4].split("-")[1] }}' blue_template: '{{ value.split(",")[4].split("-")[2] }}' @@ -22,8 +21,6 @@ If your light doesn't support brightness feature, omit `brightness_template`. If your light doesn't support color temp feature, omit `color_temp_template`. -If your light doesn't support white value feature, omit `white_value_template`. - If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ import copy @@ -194,7 +191,6 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): "command_on_template": "on," "{{ brightness|d }}," "{{ color_temp|d }}," - "{{ white_value|d }}," "{{ red|d }}-" "{{ green|d }}-" "{{ blue|d }}", @@ -211,7 +207,6 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "test_light_rgb", "on") @@ -221,7 +216,6 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None async_fire_mqtt_message(hass, "test_light_rgb", "off") @@ -234,10 +228,10 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_UNKNOWN -async def test_state_brightness_color_effect_temp_white_change_via_topic( +async def test_state_brightness_color_effect_temp_change_via_topic( hass, mqtt_mock_entry_with_yaml_config ): - """Test state, bri, color, effect, color temp, white val change.""" + """Test state, bri, color, effect, color temp change.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( hass, @@ -253,7 +247,6 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( "command_on_template": "on," "{{ brightness|d }}," "{{ color_temp|d }}," - "{{ white_value|d }}," "{{ red|d }}-" "{{ green|d }}-" "{{ blue|d }}," @@ -262,11 +255,10 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( "state_template": '{{ value.split(",")[0] }}', "brightness_template": '{{ value.split(",")[1] }}', "color_temp_template": '{{ value.split(",")[2] }}', - "white_value_template": '{{ value.split(",")[3] }}', - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', - "effect_template": '{{ value.split(",")[5] }}', + "red_template": '{{ value.split(",")[3].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[3].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', } }, ) @@ -279,18 +271,16 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( assert state.attributes.get("brightness") is None assert state.attributes.get("effect") is None assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) - # turn on the light, full white - async_fire_mqtt_message(hass, "test_light_rgb", "on,255,145,123,255-128-64,") + # turn on the light + async_fire_mqtt_message(hass, "test_light_rgb", "on,255,145,255-128-64,") state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 128, 63) assert state.attributes.get("brightness") == 255 assert state.attributes.get("color_temp") == 145 - assert state.attributes.get("white_value") == 123 assert state.attributes.get("effect") is None # make the light state unknown @@ -318,19 +308,13 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( assert light_state.attributes["color_temp"] == 195 # change the color - async_fire_mqtt_message(hass, "test_light_rgb", "on,,,,41-42-43") + async_fire_mqtt_message(hass, "test_light_rgb", "on,,,41-42-43") light_state = hass.states.get("light.test") assert light_state.attributes.get("rgb_color") == (243, 249, 255) - # change the white value - async_fire_mqtt_message(hass, "test_light_rgb", "on,,,134") - - light_state = hass.states.get("light.test") - assert light_state.attributes["white_value"] == 134 - # change the effect - async_fire_mqtt_message(hass, "test_light_rgb", "on,,,,41-42-43,rainbow") + async_fire_mqtt_message(hass, "test_light_rgb", "on,,,41-42-43,rainbow") light_state = hass.states.get("light.test") assert light_state.attributes.get("effect") == "rainbow" @@ -348,7 +332,6 @@ async def test_sending_mqtt_commands_and_optimistic( "hs_color": [100, 100], "effect": "random", "color_temp": 100, - "white_value": 50, }, ) @@ -368,7 +351,6 @@ async def test_sending_mqtt_commands_and_optimistic( "command_on_template": "on," "{{ brightness|d }}," "{{ color_temp|d }}," - "{{ white_value|d }}," "{{ red|d }}-" "{{ green|d }}-" "{{ blue|d }}," @@ -379,11 +361,10 @@ async def test_sending_mqtt_commands_and_optimistic( "optimistic": True, "state_template": '{{ value.split(",")[0] }}', "color_temp_template": '{{ value.split(",")[2] }}', - "white_value_template": '{{ value.split(",")[3] }}', - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', - "effect_template": '{{ value.split(",")[5] }}', + "red_template": '{{ value.split(",")[3].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[3].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', "qos": 2, } }, @@ -396,7 +377,6 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get("hs_color") == (100, 100) assert state.attributes.get("effect") == "random" assert state.attributes.get("color_temp") == 100 - assert state.attributes.get("white_value") == 50 assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_off(hass, "light.test") @@ -409,7 +389,7 @@ async def test_sending_mqtt_commands_and_optimistic( await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,,--,-", 2, False + "test_light_rgb/set", "on,,,--,-", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -418,7 +398,7 @@ async def test_sending_mqtt_commands_and_optimistic( # Set color_temp await common.async_turn_on(hass, "light.test", color_temp=70) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,70,,--,-", 2, False + "test_light_rgb/set", "on,,70,--,-", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -428,29 +408,26 @@ async def test_sending_mqtt_commands_and_optimistic( # Set full brightness await common.async_turn_on(hass, "light.test", brightness=255) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,255,,,--,-", 2, False + "test_light_rgb/set", "on,255,,--,-", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON # Full brightness - no scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,80,255-128-0,30.118-100.0", 2, False + "test_light_rgb/set", "on,,,255-128-0,30.118-100.0", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 80 assert state.attributes.get("rgb_color") == (255, 128, 0) # Full brightness - normalization of RGB values sent over MQTT await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 0]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,,255-127-0,30.0-100.0", 2, False + "test_light_rgb/set", "on,,,255-127-0,30.0-100.0", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -460,36 +437,30 @@ async def test_sending_mqtt_commands_and_optimistic( # Set half brightness await common.async_turn_on(hass, "light.test", brightness=128) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,128,,,--,-", 2, False + "test_light_rgb/set", "on,128,,--,-", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON # Half brightness - scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[0, 255, 128], white_value=40 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[0, 255, 128]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,40,0-128-64,150.118-100.0", 2, False + "test_light_rgb/set", "on,,,0-128-64,150.118-100.0", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 40 assert state.attributes.get("rgb_color") == (0, 255, 128) # Half brightness - normalization+scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[0, 32, 16], white_value=40 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[0, 32, 16]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,40,0-128-64,150.0-100.0", 2, False + "test_light_rgb/set", "on,,,0-128-64,150.0-100.0", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 40 assert state.attributes.get("rgb_color") == (0, 255, 127) @@ -512,7 +483,6 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( "command_on_template": "on," "{{ brightness|d }}," "{{ color_temp|d }}," - "{{ white_value|d }}," "{{ red|d }}-" "{{ green|d }}-" "{{ blue|d }}," @@ -522,11 +492,10 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( "state_template": '{{ value.split(",")[0] }}', "brightness_template": '{{ value.split(",")[1] }}', "color_temp_template": '{{ value.split(",")[2] }}', - "white_value_template": '{{ value.split(",")[3] }}', - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', - "effect_template": '{{ value.split(",")[5] }}', + "red_template": '{{ value.split(",")[3].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[3].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', } }, ) @@ -539,7 +508,6 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( assert not state.attributes.get("hs_color") assert not state.attributes.get("effect") assert not state.attributes.get("color_temp") - assert not state.attributes.get("white_value") assert not state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_off(hass, "light.test") @@ -552,7 +520,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,,--,-", 0, False + "test_light_rgb/set", "on,,,--,-", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -561,7 +529,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( # Set color_temp await common.async_turn_on(hass, "light.test", color_temp=70) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,70,,--,-", 0, False + "test_light_rgb/set", "on,,70,--,-", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -571,7 +539,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( # Set full brightness await common.async_turn_on(hass, "light.test", brightness=255) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,255,,,--,-", 0, False + "test_light_rgb/set", "on,255,,--,-", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -579,48 +547,41 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( assert not state.attributes.get("brightness") # Full brightness - no scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,80,255-128-0,30.118-100.0", 0, False + "test_light_rgb/set", "on,,,255-128-0,30.118-100.0", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN - assert not state.attributes.get("white_value") assert not state.attributes.get("rgb_color") # Full brightness - normalization of RGB values sent over MQTT await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 0]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,,255-127-0,30.0-100.0", 0, False + "test_light_rgb/set", "on,,,255-127-0,30.0-100.0", 0, False ) mqtt_mock.async_publish.reset_mock() # Set half brightness await common.async_turn_on(hass, "light.test", brightness=128) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,128,,,--,-", 0, False + "test_light_rgb/set", "on,128,,--,-", 0, False ) mqtt_mock.async_publish.reset_mock() # Half brightness - no scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[0, 255, 128], white_value=40 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[0, 255, 128]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,40,0-255-128,150.118-100.0", 0, False + "test_light_rgb/set", "on,,,0-255-128,150.118-100.0", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") # Half brightness - normalization but no scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[0, 32, 16], white_value=40 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[0, 32, 16]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,40,0-255-127,150.0-100.0", 0, False + "test_light_rgb/set", "on,,,0-255-127,150.0-100.0", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -795,11 +756,10 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): "state_template": '{{ value.split(",")[0] }}', "brightness_template": '{{ value.split(",")[1] }}', "color_temp_template": '{{ value.split(",")[2] }}', - "white_value_template": '{{ value.split(",")[3] }}', - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', - "effect_template": '{{ value.split(",")[5] }}', + "red_template": '{{ value.split(",")[3].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[3].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', } }, ) @@ -812,20 +772,16 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None - assert state.attributes.get("white_value") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) - # turn on the light, full white - async_fire_mqtt_message( - hass, "test_light_rgb", "on,255,215,222,255-255-255,rainbow" - ) + # turn on the light + async_fire_mqtt_message(hass, "test_light_rgb", "on,255,215,255-255-255,rainbow") state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 assert state.attributes.get("color_temp") == 215 assert state.attributes.get("rgb_color") == (255, 255, 255) - assert state.attributes.get("white_value") == 222 assert state.attributes.get("effect") == "rainbow" # bad state value @@ -856,13 +812,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): state = hass.states.get("light.test") assert state.attributes.get("rgb_color") == (255, 255, 255) - # bad white value values - async_fire_mqtt_message(hass, "test_light_rgb", "on,,,off,255-255-255") - - # white value should not have changed - state = hass.states.get("light.test") - assert state.attributes.get("white_value") == 222 - # bad effect value async_fire_mqtt_message(hass, "test_light_rgb", "on,255,a-b-c,white") @@ -1191,8 +1140,6 @@ async def test_publishing_with_custom_encoding( config = copy.deepcopy(DEFAULT_CONFIG[domain]) if topic == "effect_command_topic": config["effect_list"] = ["random", "color_loop"] - elif topic == "white_command_topic": - config["rgb_command_topic"] = "some-cmd-topic" await help_test_publishing_with_custom_encoding( hass, From 2ab4817d8173825f56de536f7e34ca61e02758ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 16 Aug 2022 17:05:09 +0200 Subject: [PATCH 399/903] Use secure in Speedtest (#76852) --- homeassistant/components/speedtestdotnet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 00221c39a42..2684b24c81f 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -85,7 +85,7 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): def initialize(self) -> None: """Initialize speedtest api.""" - self.api = speedtest.Speedtest() + self.api = speedtest.Speedtest(secure=True) self.update_servers() def update_servers(self): From 1a2a20cfc5f51a3a7fb3aac69fbf5f9654c270c2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 17:09:00 +0200 Subject: [PATCH 400/903] Update google-cloud-texttospeech to 2.12.1 (#76854) --- homeassistant/components/google_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index d48571c55bd..9ce8085d5e8 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==2.12.0"], + "requirements": ["google-cloud-texttospeech==2.12.1"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 76ee358edb6..790a9572334 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -745,7 +745,7 @@ goodwe==0.2.18 google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==2.12.0 +google-cloud-texttospeech==2.12.1 # homeassistant.components.nest google-nest-sdm==2.0.0 From 93a72982ce73278c9588b88b97ae746dd394897f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 17:09:50 +0200 Subject: [PATCH 401/903] Update debugpy to 1.6.3 (#76849) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 3cc6c16e831..dc0818f54b6 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.6.2"], + "requirements": ["debugpy==1.6.3"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 790a9572334..8a19ae220d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -523,7 +523,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.2 +debugpy==1.6.3 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c6af75e928..9e04359182a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -400,7 +400,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.2 +debugpy==1.6.3 # homeassistant.components.ihc # homeassistant.components.namecheapdns From d50b5cebeec776aecb5f3236d9c4090c69003100 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 17:10:11 +0200 Subject: [PATCH 402/903] Various improvement for JustNimbus (#76858) Co-authored-by: Martin Hjelmare --- homeassistant/components/justnimbus/entity.py | 10 ++---- homeassistant/components/justnimbus/sensor.py | 8 ++--- .../components/justnimbus/test_config_flow.py | 36 ++++++++++++++++++- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/justnimbus/entity.py b/homeassistant/components/justnimbus/entity.py index f9ea5ba1151..cfd261f1bc0 100644 --- a/homeassistant/components/justnimbus/entity.py +++ b/homeassistant/components/justnimbus/entity.py @@ -1,19 +1,15 @@ """Base Entity for JustNimbus sensors.""" from __future__ import annotations -import justnimbus - -from homeassistant.components.sensor import SensorEntity -from homeassistant.helpers import update_coordinator from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import JustNimbusCoordinator from .const import DOMAIN +from .coordinator import JustNimbusCoordinator class JustNimbusEntity( - update_coordinator.CoordinatorEntity[justnimbus.JustNimbusModel], - SensorEntity, + CoordinatorEntity[JustNimbusCoordinator], ): """Defines a base JustNimbus entity.""" diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py index 6041f84e25a..73a68ac9139 100644 --- a/homeassistant/components/justnimbus/sensor.py +++ b/homeassistant/components/justnimbus/sensor.py @@ -7,6 +7,7 @@ from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, + SensorEntity, SensorEntityDescription, SensorStateClass, ) @@ -15,6 +16,7 @@ from homeassistant.const import ( CONF_CLIENT_ID, PRESSURE_BAR, TEMP_CELSIUS, + TIME_HOURS, VOLUME_LITERS, ) from homeassistant.core import HomeAssistant @@ -82,6 +84,7 @@ SENSOR_TYPES = ( name="Pump hours", icon="mdi:clock", device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.pump_hours, @@ -127,7 +130,6 @@ SENSOR_TYPES = ( name="Error code", icon="mdi:bug", entity_registry_enabled_default=False, - native_unit_of_measurement="", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.error_code, ), @@ -167,9 +169,7 @@ async def async_setup_entry( ) -class JustNimbusSensor( - JustNimbusEntity, -): +class JustNimbusSensor(JustNimbusEntity, SensorEntity): """Implementation of the JustNimbus sensor.""" def __init__( diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index d2cfb64d4c7..1d3565c21bd 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -10,6 +10,8 @@ from homeassistant.const import CONF_CLIENT_ID from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" @@ -30,9 +32,13 @@ async def test_form(hass: HomeAssistant) -> None: {"base": "invalid_auth"}, ), ( - JustNimbusError(), + JustNimbusError, {"base": "cannot_connect"}, ), + ( + RuntimeError, + {"base": "unknown"}, + ), ), ) async def test_form_errors( @@ -62,6 +68,34 @@ async def test_form_errors( await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) +async def test_abort_already_configured(hass: HomeAssistant) -> None: + """Test we abort when the device is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="JustNimbus", + data={CONF_CLIENT_ID: "test_id"}, + unique_id="test_id", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") is None + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={ + CONF_CLIENT_ID: "test_id", + }, + ) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "already_configured" + + async def _set_up_justnimbus(hass: HomeAssistant, flow_id: str) -> None: """Reusable successful setup of JustNimbus sensor.""" with patch("justnimbus.JustNimbusClient.get_data"), patch( From 65f86ce44fa663c64cbd6f16331fc6e1e9806506 Mon Sep 17 00:00:00 2001 From: Igor Pakhomov Date: Tue, 16 Aug 2022 18:30:56 +0300 Subject: [PATCH 403/903] Add additional select for dmaker.airfresh.t2017 to xiaomi_miio (#67058) --- .coveragerc | 1 - .../components/xiaomi_miio/select.py | 320 +++++++++--------- .../xiaomi_miio/strings.select.json | 10 + tests/components/xiaomi_miio/test_select.py | 158 +++++++++ 4 files changed, 329 insertions(+), 160 deletions(-) create mode 100644 tests/components/xiaomi_miio/test_select.py diff --git a/.coveragerc b/.coveragerc index 8b632a524ff..d3e1b75b928 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1478,7 +1478,6 @@ omit = homeassistant/components/xiaomi_miio/light.py homeassistant/components/xiaomi_miio/number.py homeassistant/components/xiaomi_miio/remote.py - homeassistant/components/xiaomi_miio/select.py homeassistant/components/xiaomi_miio/sensor.py homeassistant/components/xiaomi_miio/switch.py homeassistant/components/xiaomi_tv/media_player.py diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 7f6f8ce1cb1..14ceac7f2f4 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -1,9 +1,14 @@ """Support led_brightness for Mi Air Humidifier.""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import NamedTuple from miio.fan_common import LedBrightness as FanLedBrightness +from miio.integrations.airpurifier.dmaker.airfresh_t2017 import ( + DisplayOrientation as AirfreshT2017DisplayOrientation, + PtcLevel as AirfreshT2017PtcLevel, +) from miio.integrations.airpurifier.zhimi.airfresh import ( LedBrightness as AirfreshLedBrightness, ) @@ -31,53 +36,134 @@ from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, DOMAIN, - FEATURE_SET_LED_BRIGHTNESS, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, - MODEL_AIRPURIFIER_3C, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CA4, + MODEL_AIRHUMIDIFIER_CB1, + MODEL_AIRHUMIDIFIER_V1, + MODEL_AIRPURIFIER_3, + MODEL_AIRPURIFIER_3H, MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2, + MODEL_AIRPURIFIER_PROH, MODEL_FAN_SA1, MODEL_FAN_V2, MODEL_FAN_V3, MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4, - MODELS_HUMIDIFIER_MIIO, - MODELS_HUMIDIFIER_MIOT, - MODELS_PURIFIER_MIOT, ) from .device import XiaomiCoordinatedMiioEntity +ATTR_DISPLAY_ORIENTATION = "display_orientation" ATTR_LED_BRIGHTNESS = "led_brightness" - - -LED_BRIGHTNESS_MAP = {"Bright": 0, "Dim": 1, "Off": 2} -LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT = {"Bright": 2, "Dim": 1, "Off": 0} -LED_BRIGHTNESS_REVERSE_MAP = {val: key for key, val in LED_BRIGHTNESS_MAP.items()} -LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT = { - val: key for key, val in LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT.items() -} +ATTR_PTC_LEVEL = "ptc_level" @dataclass class XiaomiMiioSelectDescription(SelectEntityDescription): """A class that describes select entities.""" + attr_name: str = "" + options_map: dict = field(default_factory=dict) + set_method: str = "" + set_method_error_message: str = "" options: tuple = () -SELECTOR_TYPES = { - FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioSelectDescription( +class AttributeEnumMapping(NamedTuple): + """Class to mapping Attribute to Enum Class.""" + + attr_name: str + enum_class: type + + +MODEL_TO_ATTR_MAP: dict[str, list] = { + MODEL_AIRFRESH_T2017: [ + AttributeEnumMapping(ATTR_DISPLAY_ORIENTATION, AirfreshT2017DisplayOrientation), + AttributeEnumMapping(ATTR_PTC_LEVEL, AirfreshT2017PtcLevel), + ], + MODEL_AIRFRESH_VA2: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirfreshLedBrightness) + ], + MODEL_AIRHUMIDIFIER_CA1: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness) + ], + MODEL_AIRHUMIDIFIER_CA4: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierMiotLedBrightness) + ], + MODEL_AIRHUMIDIFIER_CB1: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness) + ], + MODEL_AIRHUMIDIFIER_V1: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness) + ], + MODEL_AIRPURIFIER_3: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], + MODEL_AIRPURIFIER_3H: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], + MODEL_AIRPURIFIER_M1: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierLedBrightness) + ], + MODEL_AIRPURIFIER_M2: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierLedBrightness) + ], + MODEL_AIRPURIFIER_PROH: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], + MODEL_FAN_SA1: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_V2: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_V3: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_ZA1: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_ZA3: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_ZA4: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], +} + +SELECTOR_TYPES = ( + XiaomiMiioSelectDescription( + key=ATTR_DISPLAY_ORIENTATION, + attr_name=ATTR_DISPLAY_ORIENTATION, + name="Display Orientation", + options_map={ + "Portrait": "Forward", + "LandscapeLeft": "Left", + "LandscapeRight": "Right", + }, + set_method="set_display_orientation", + set_method_error_message="Setting the display orientation failed.", + icon="mdi:tablet", + device_class="xiaomi_miio__display_orientation", + options=("forward", "left", "right"), + entity_category=EntityCategory.CONFIG, + ), + XiaomiMiioSelectDescription( key=ATTR_LED_BRIGHTNESS, + attr_name=ATTR_LED_BRIGHTNESS, name="Led Brightness", + set_method="set_led_brightness", + set_method_error_message="Setting the led brightness failed.", icon="mdi:brightness-6", device_class="xiaomi_miio__led_brightness", options=("bright", "dim", "off"), entity_category=EntityCategory.CONFIG, ), -} + XiaomiMiioSelectDescription( + key=ATTR_PTC_LEVEL, + attr_name=ATTR_PTC_LEVEL, + name="Auxiliary Heat Level", + set_method="set_ptc_level", + set_method_error_message="Setting the ptc level failed.", + icon="mdi:fire-circle", + device_class="xiaomi_miio__ptc_level", + options=("low", "medium", "high"), + entity_category=EntityCategory.CONFIG, + ), +) async def async_setup_entry( @@ -89,44 +175,29 @@ async def async_setup_entry( if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return - entities = [] model = config_entry.data[CONF_MODEL] + if model not in MODEL_TO_ATTR_MAP: + return + + entities = [] + unique_id = config_entry.unique_id device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + attributes = MODEL_TO_ATTR_MAP[model] - if model == MODEL_AIRPURIFIER_3C: - return - if model in MODELS_HUMIDIFIER_MIIO: - entity_class = XiaomiAirHumidifierSelector - elif model in MODELS_HUMIDIFIER_MIOT: - entity_class = XiaomiAirHumidifierMiotSelector - elif model in [MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2]: - entity_class = XiaomiAirPurifierSelector - elif model in MODELS_PURIFIER_MIOT: - entity_class = XiaomiAirPurifierMiotSelector - elif model == MODEL_AIRFRESH_VA2: - entity_class = XiaomiAirFreshSelector - elif model in ( - MODEL_FAN_ZA1, - MODEL_FAN_ZA3, - MODEL_FAN_ZA4, - MODEL_FAN_SA1, - MODEL_FAN_V2, - MODEL_FAN_V3, - ): - entity_class = XiaomiFanSelector - else: - return - - description = SELECTOR_TYPES[FEATURE_SET_LED_BRIGHTNESS] - entities.append( - entity_class( - device, - config_entry, - f"{description.key}_{config_entry.unique_id}", - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], - description, - ) - ) + for description in SELECTOR_TYPES: + for attribute in attributes: + if description.key == attribute.attr_name: + entities.append( + XiaomiGenericSelector( + device, + config_entry, + f"{description.key}_{unique_id}", + coordinator, + description, + attribute.enum_class, + ) + ) async_add_entities(entities) @@ -141,129 +212,60 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): self.entity_description = description -class XiaomiAirHumidifierSelector(XiaomiSelector): - """Representation of a Xiaomi Air Humidifier selector.""" +class XiaomiGenericSelector(XiaomiSelector): + """Representation of a Xiaomi generic selector.""" - def __init__(self, device, entry, unique_id, coordinator, description): - """Initialize the plug switch.""" + entity_description: XiaomiMiioSelectDescription + + def __init__(self, device, entry, unique_id, coordinator, description, enum_class): + """Initialize the generic Xiaomi attribute selector.""" super().__init__(device, entry, unique_id, coordinator, description) - self._current_led_brightness = self._extract_value_from_attribute( - self.coordinator.data, self.entity_description.key + self._current_attr = enum_class( + self._extract_value_from_attribute( + self.coordinator.data, self.entity_description.attr_name + ) ) + if description.options_map: + self._options_map = {} + for key, val in enum_class._member_map_.items(): + self._options_map[description.options_map[key]] = val + else: + self._options_map = enum_class._member_map_ + self._reverse_map = {val: key for key, val in self._options_map.items()} + self._enum_class = enum_class + @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - led_brightness = self._extract_value_from_attribute( - self.coordinator.data, self.entity_description.key + attr = self._enum_class( + self._extract_value_from_attribute( + self.coordinator.data, self.entity_description.attr_name + ) ) - # Sometimes (quite rarely) the device returns None as the LED brightness so we - # check that the value is not None before updating the state. - if led_brightness is not None: - self._current_led_brightness = led_brightness + if attr is not None: + self._current_attr = attr self.async_write_ha_state() @property - def current_option(self) -> str: + def current_option(self) -> str | None: """Return the current option.""" - return self.led_brightness.lower() + option = self._reverse_map.get(self._current_attr) + if option is not None: + return option.lower() + return None async def async_select_option(self, option: str) -> None: """Set an option of the miio device.""" - await self.async_set_led_brightness(option.title()) + await self.async_set_attr(option.title()) - @property - def led_brightness(self): - """Return the current led brightness.""" - return LED_BRIGHTNESS_REVERSE_MAP.get(self._current_led_brightness) - - async def async_set_led_brightness(self, brightness: str): - """Set the led brightness.""" + async def async_set_attr(self, attr_value: str): + """Set attr.""" + method = getattr(self._device, self.entity_description.set_method) if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirhumidifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]), + self.entity_description.set_method_error_message, + method, + self._enum_class(self._options_map[attr_value]), ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] - self.async_write_ha_state() - - -class XiaomiAirHumidifierMiotSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Air Humidifier (MiOT protocol) selector.""" - - @property - def led_brightness(self): - """Return the current led brightness.""" - return LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT.get( - self._current_led_brightness - ) - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirhumidifierMiotLedBrightness( - LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[brightness] - ), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[ - brightness - ] - self.async_write_ha_state() - - -class XiaomiAirPurifierSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Air Purifier (MIIO protocol) selector.""" - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirpurifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] - self.async_write_ha_state() - - -class XiaomiAirPurifierMiotSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Air Purifier (MiOT protocol) selector.""" - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirpurifierMiotLedBrightness(LED_BRIGHTNESS_MAP[brightness]), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] - self.async_write_ha_state() - - -class XiaomiFanSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Fan (MIIO protocol) selector.""" - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - FanLedBrightness(LED_BRIGHTNESS_MAP[brightness]), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] - self.async_write_ha_state() - - -class XiaomiAirFreshSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Air Fresh selector.""" - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirfreshLedBrightness(LED_BRIGHTNESS_MAP[brightness]), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] + self._current_attr = self._options_map[attr_value] self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/strings.select.json b/homeassistant/components/xiaomi_miio/strings.select.json index 265aec66531..38ee8b1aa07 100644 --- a/homeassistant/components/xiaomi_miio/strings.select.json +++ b/homeassistant/components/xiaomi_miio/strings.select.json @@ -4,6 +4,16 @@ "bright": "Bright", "dim": "Dim", "off": "Off" + }, + "xiaomi_miio__display_orientation": { + "forward": "Forward", + "left": "Left", + "right": "Right" + }, + "xiaomi_miio__ptc_level": { + "low": "Low", + "medium": "Medium", + "high": "High" } } } diff --git a/tests/components/xiaomi_miio/test_select.py b/tests/components/xiaomi_miio/test_select.py new file mode 100644 index 00000000000..3fa8a3de291 --- /dev/null +++ b/tests/components/xiaomi_miio/test_select.py @@ -0,0 +1,158 @@ +"""The tests for the xiaomi_miio select component.""" + +from unittest.mock import MagicMock, patch + +from arrow import utcnow +from miio.integrations.airpurifier.dmaker.airfresh_t2017 import ( + DisplayOrientation, + PtcLevel, +) +import pytest + +from homeassistant.components.select import DOMAIN +from homeassistant.components.select.const import ( + ATTR_OPTION, + ATTR_OPTIONS, + SERVICE_SELECT_OPTION, +) +from homeassistant.components.xiaomi_miio import UPDATE_INTERVAL +from homeassistant.components.xiaomi_miio.const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MAC, + DOMAIN as XIAOMI_DOMAIN, + MODEL_AIRFRESH_T2017, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_MODEL, + CONF_TOKEN, + Platform, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.xiaomi_miio import TEST_MAC + + +@pytest.fixture(autouse=True) +async def setup_test(hass: HomeAssistant): + """Initialize test xiaomi_miio for select entity.""" + + mock_airfresh = MagicMock() + mock_airfresh.status().display_orientation = DisplayOrientation.Portrait + mock_airfresh.status().ptc_level = PtcLevel.Low + + with patch( + "homeassistant.components.xiaomi_miio.get_platforms", + return_value=[ + Platform.SELECT, + ], + ), patch("homeassistant.components.xiaomi_miio.AirFreshT2017") as mock_airfresh_cls: + mock_airfresh_cls.return_value = mock_airfresh + yield mock_airfresh + + +async def test_select_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + + entity_name = "test_airfresh_select" + entity_id = await setup_component(hass, entity_name) + + select_entity = hass.states.get(entity_id + "_display_orientation") + assert select_entity + assert select_entity.state == "forward" + assert select_entity.attributes.get(ATTR_OPTIONS) == ["forward", "left", "right"] + + +async def test_select_bad_attr(hass: HomeAssistant) -> None: + """Test selecting a different option with invalid option value.""" + + entity_name = "test_airfresh_select" + entity_id = await setup_component(hass, entity_name) + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "forward" + + with pytest.raises(ValueError): + await hass.services.async_call( + "select", + SERVICE_SELECT_OPTION, + {ATTR_OPTION: "up", ATTR_ENTITY_ID: entity_id + "_display_orientation"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "forward" + + +async def test_select_option(hass: HomeAssistant) -> None: + """Test selecting of a option.""" + + entity_name = "test_airfresh_select" + entity_id = await setup_component(hass, entity_name) + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "forward" + + await hass.services.async_call( + "select", + SERVICE_SELECT_OPTION, + {ATTR_OPTION: "left", ATTR_ENTITY_ID: entity_id + "_display_orientation"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "left" + + +async def test_select_coordinator_update(hass: HomeAssistant, setup_test) -> None: + """Test coordinator update of a option.""" + + entity_name = "test_airfresh_select" + entity_id = await setup_component(hass, entity_name) + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "forward" + + # emulate someone change state from device maybe used app + setup_test.status().display_orientation = DisplayOrientation.LandscapeLeft + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "left" + + +async def setup_component(hass, entity_name): + """Set up component.""" + entity_id = f"{DOMAIN}.{entity_name}" + + config_entry = MockConfigEntry( + domain=XIAOMI_DOMAIN, + unique_id="123456", + title=entity_name, + data={ + CONF_FLOW_TYPE: CONF_DEVICE, + CONF_HOST: "0.0.0.0", + CONF_TOKEN: "12345678901234567890123456789012", + CONF_MODEL: MODEL_AIRFRESH_T2017, + 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 5331af214362378e600bf4d640a4983be4195768 Mon Sep 17 00:00:00 2001 From: Zach Berger Date: Tue, 16 Aug 2022 10:17:53 -0700 Subject: [PATCH 404/903] Capture local Awair firmware version to DeviceInfo (#76700) --- .strict-typing | 1 + homeassistant/components/awair/__init__.py | 19 +++++++++++++++---- homeassistant/components/awair/config_flow.py | 11 ++++++++--- homeassistant/components/awair/manifest.json | 2 +- homeassistant/components/awair/sensor.py | 12 ++++++++++-- mypy.ini | 10 ++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 47 insertions(+), 12 deletions(-) diff --git a/.strict-typing b/.strict-typing index db51acc07df..02d753b3994 100644 --- a/.strict-typing +++ b/.strict-typing @@ -61,6 +61,7 @@ homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* homeassistant.components.auth.* homeassistant.components.automation.* +homeassistant.components.awair.* homeassistant.components.backup.* homeassistant.components.baf.* homeassistant.components.binary_sensor.* diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index b0a5d39814c..359d0d6d853 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -2,7 +2,9 @@ from __future__ import annotations from asyncio import gather +from datetime import timedelta +from aiohttp import ClientSession from async_timeout import timeout from python_awair import Awair, AwairLocal from python_awair.devices import AwairBaseDevice, AwairLocalDevice @@ -63,13 +65,18 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class AwairDataUpdateCoordinator(DataUpdateCoordinator): """Define a wrapper class to update Awair data.""" - def __init__(self, hass, config_entry, update_interval) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + update_interval: timedelta | None, + ) -> None: """Set up the AwairDataUpdateCoordinator class.""" self._config_entry = config_entry super().__init__(hass, LOGGER, name=DOMAIN, update_interval=update_interval) - async def _fetch_air_data(self, device: AwairBaseDevice): + async def _fetch_air_data(self, device: AwairBaseDevice) -> AwairResult: """Fetch latest air quality data.""" LOGGER.debug("Fetching data for %s", device.uuid) air_data = await device.air_data_latest() @@ -80,7 +87,9 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): class AwairCloudDataUpdateCoordinator(AwairDataUpdateCoordinator): """Define a wrapper class to update Awair data from Cloud API.""" - def __init__(self, hass, config_entry, session) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, session: ClientSession + ) -> None: """Set up the AwairCloudDataUpdateCoordinator class.""" access_token = config_entry.data[CONF_ACCESS_TOKEN] self._awair = Awair(access_token=access_token, session=session) @@ -109,7 +118,9 @@ class AwairLocalDataUpdateCoordinator(AwairDataUpdateCoordinator): _device: AwairLocalDevice | None = None - def __init__(self, hass, config_entry, session) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, session: ClientSession + ) -> None: """Set up the AwairLocalDataUpdateCoordinator class.""" self._awair = AwairLocal( session=session, device_addrs=[config_entry.data[CONF_HOST]] diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 418413b690f..becf6ce46ff 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -7,6 +7,7 @@ from typing import Any from aiohttp.client_exceptions import ClientError from python_awair import Awair, AwairLocal, AwairLocalDevice from python_awair.exceptions import AuthError, AwairError +from python_awair.user import AwairUser import voluptuous as vol from homeassistant.components import zeroconf @@ -97,7 +98,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): title = user.email return self.async_create_entry(title=title, data=user_input) - if error != "invalid_access_token": + if error and error != "invalid_access_token": return self.async_abort(reason=error) errors = {CONF_ACCESS_TOKEN: "invalid_access_token"} @@ -215,7 +216,9 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _check_local_connection(self, device_address: str): + async def _check_local_connection( + self, device_address: str + ) -> tuple[AwairLocalDevice | None, str | None]: """Check the access token is valid.""" session = async_get_clientsession(self.hass) awair = AwairLocal(session=session, device_addrs=[device_address]) @@ -232,7 +235,9 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): LOGGER.error("Unexpected API error: %s", err) return (None, "unknown") - async def _check_cloud_connection(self, access_token: str): + async def _check_cloud_connection( + self, access_token: str + ) -> tuple[AwairUser | None, str | None]: """Check the access token is valid.""" session = async_get_clientsession(self.hass) awair = Awair(access_token=access_token, session=session) diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index 131a955a6eb..f09d9c2ee33 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -2,7 +2,7 @@ "domain": "awair", "name": "Awair", "documentation": "https://www.home-assistant.io/integrations/awair", - "requirements": ["python_awair==0.2.3"], + "requirements": ["python_awair==0.2.4"], "codeowners": ["@ahayworth", "@danielsjf"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index cda7f31095e..00d5c929409 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -2,11 +2,16 @@ from __future__ import annotations from python_awair.air_data import AirData -from python_awair.devices import AwairBaseDevice +from python_awair.devices import AwairBaseDevice, AwairLocalDevice from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_CONNECTIONS, ATTR_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_CONNECTIONS, + ATTR_NAME, + ATTR_SW_VERSION, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo @@ -209,6 +214,9 @@ class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): (dr.CONNECTION_NETWORK_MAC, self._device.mac_address) } + if isinstance(self._device, AwairLocalDevice): + info[ATTR_SW_VERSION] = self._device.fw_version + return info @property diff --git a/mypy.ini b/mypy.ini index 5d3b184880d..2645cb9d107 100644 --- a/mypy.ini +++ b/mypy.ini @@ -369,6 +369,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.awair.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.backup.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 8a19ae220d3..7a372461d3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1981,7 +1981,7 @@ python-telegram-bot==13.1 python-vlc==1.1.2 # homeassistant.components.awair -python_awair==0.2.3 +python_awair==0.2.4 # homeassistant.components.swiss_public_transport python_opendata_transport==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e04359182a..27f54b09389 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1347,7 +1347,7 @@ python-tado==0.12.0 python-telegram-bot==13.1 # homeassistant.components.awair -python_awair==0.2.3 +python_awair==0.2.4 # homeassistant.components.tile pytile==2022.02.0 From 2630ff8fce6bf4b36082b29af907c3bca4d6b642 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 16 Aug 2022 19:22:15 +0200 Subject: [PATCH 405/903] Add sensor checks to pylint plugin (#76876) --- pylint/plugins/hass_enforce_type_hints.py | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 35862742b18..e9147fcca70 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1863,6 +1863,46 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "sensor": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="SensorEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["SensorDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="state_class", + return_type=["SensorStateClass", "str", None], + ), + TypeHintMatch( + function_name="last_reset", + return_type=["datetime", None], + ), + TypeHintMatch( + function_name="native_value", + return_type=[ + "StateType", + "str", + "int", + "float", + None, + "date", + "datetime", + "Decimal", + ], + ), + TypeHintMatch( + function_name="native_unit_of_measurement", + return_type=["str", None], + ), + ], + ), + ], "siren": [ ClassTypeHintMatch( base_class="Entity", From 5736ba62309debe36781209760015dab1fbbca8b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 16 Aug 2022 19:24:00 +0200 Subject: [PATCH 406/903] Add remote checks to pylint plugin (#76875) --- pylint/plugins/hass_enforce_type_hints.py | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index e9147fcca70..b34b88c27c9 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1830,6 +1830,52 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "remote": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="RemoteEntity", + matches=[ + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="current_activity", + return_type=["str", None], + ), + TypeHintMatch( + function_name="activity_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="send_command", + arg_types={1: "Iterable[str]"}, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="learn_command", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="delete_command", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "select": [ ClassTypeHintMatch( base_class="Entity", From 73ad34244e6e1ff52e735989a7cba412494facd8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 16 Aug 2022 19:49:49 +0200 Subject: [PATCH 407/903] Bump pynetgear to 0.10.7 (#76754) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 5fd59faac83..69a21e5aace 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.6"], + "requirements": ["pynetgear==0.10.7"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 7a372461d3e..8b2e2349715 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1692,7 +1692,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.6 +pynetgear==0.10.7 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27f54b09389..b5cf5bccb54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1175,7 +1175,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.6 +pynetgear==0.10.7 # homeassistant.components.nina pynina==0.1.8 From 1e9ede25ad253bc42bfd764435a9d37bd4fd3a80 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 16 Aug 2022 14:08:35 -0400 Subject: [PATCH 408/903] Add Fully Kiosk Browser integration with initial binary sensor platform (#76737) Co-authored-by: Franck Nijhof --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/fully_kiosk/__init__.py | 31 ++++ .../components/fully_kiosk/binary_sensor.py | 73 ++++++++++ .../components/fully_kiosk/config_flow.py | 63 ++++++++ homeassistant/components/fully_kiosk/const.py | 13 ++ .../components/fully_kiosk/coordinator.py | 48 +++++++ .../components/fully_kiosk/entity.py | 26 ++++ .../components/fully_kiosk/manifest.json | 10 ++ .../components/fully_kiosk/strings.json | 20 +++ .../fully_kiosk/translations/en.json | 20 +++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 10 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/fully_kiosk/__init__.py | 1 + tests/components/fully_kiosk/conftest.py | 76 ++++++++++ .../fully_kiosk/fixtures/deviceinfo.json | 79 ++++++++++ .../fully_kiosk/test_binary_sensor.py | 93 ++++++++++++ .../fully_kiosk/test_config_flow.py | 136 ++++++++++++++++++ tests/components/fully_kiosk/test_init.py | 53 +++++++ 21 files changed, 762 insertions(+) create mode 100644 homeassistant/components/fully_kiosk/__init__.py create mode 100644 homeassistant/components/fully_kiosk/binary_sensor.py create mode 100644 homeassistant/components/fully_kiosk/config_flow.py create mode 100644 homeassistant/components/fully_kiosk/const.py create mode 100644 homeassistant/components/fully_kiosk/coordinator.py create mode 100644 homeassistant/components/fully_kiosk/entity.py create mode 100644 homeassistant/components/fully_kiosk/manifest.json create mode 100644 homeassistant/components/fully_kiosk/strings.json create mode 100644 homeassistant/components/fully_kiosk/translations/en.json create mode 100644 tests/components/fully_kiosk/__init__.py create mode 100644 tests/components/fully_kiosk/conftest.py create mode 100644 tests/components/fully_kiosk/fixtures/deviceinfo.json create mode 100644 tests/components/fully_kiosk/test_binary_sensor.py create mode 100644 tests/components/fully_kiosk/test_config_flow.py create mode 100644 tests/components/fully_kiosk/test_init.py diff --git a/.strict-typing b/.strict-typing index 02d753b3994..d9cc4ffb55a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -108,6 +108,7 @@ homeassistant.components.fritzbox_callmonitor.* homeassistant.components.fronius.* homeassistant.components.frontend.* homeassistant.components.fritz.* +homeassistant.components.fully_kiosk.* homeassistant.components.geo_location.* homeassistant.components.geocaching.* homeassistant.components.gios.* diff --git a/CODEOWNERS b/CODEOWNERS index 3ac50eeb1db..0c8aa94dfb8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -375,6 +375,8 @@ build.json @home-assistant/supervisor /homeassistant/components/frontend/ @home-assistant/frontend /tests/components/frontend/ @home-assistant/frontend /homeassistant/components/frontier_silicon/ @wlcrs +/homeassistant/components/fully_kiosk/ @cgarwood +/tests/components/fully_kiosk/ @cgarwood /homeassistant/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas /homeassistant/components/gdacs/ @exxamalte diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py new file mode 100644 index 00000000000..943f5c69cbe --- /dev/null +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -0,0 +1,31 @@ +"""The Fully Kiosk Browser integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator + +PLATFORMS = [Platform.BINARY_SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Fully Kiosk Browser from a config entry.""" + + coordinator = FullyKioskDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/fully_kiosk/binary_sensor.py b/homeassistant/components/fully_kiosk/binary_sensor.py new file mode 100644 index 00000000000..6f1fccfb9d3 --- /dev/null +++ b/homeassistant/components/fully_kiosk/binary_sensor.py @@ -0,0 +1,73 @@ +"""Fully Kiosk Browser sensor.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + +SENSORS: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="kioskMode", + name="Kiosk mode", + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="plugged", + name="Plugged in", + device_class=BinarySensorDeviceClass.PLUG, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="isDeviceAdmin", + name="Device admin", + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser sensor.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + FullyBinarySensor(coordinator, description) + for description in SENSORS + if description.key in coordinator.data + ) + + +class FullyBinarySensor(FullyKioskEntity, BinarySensorEntity): + """Representation of a Fully Kiosk Browser binary sensor.""" + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: BinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + @property + def is_on(self) -> bool | None: + """Return if the binary sensor is on.""" + if (value := self.coordinator.data.get(self.entity_description.key)) is None: + return None + return bool(value) diff --git a/homeassistant/components/fully_kiosk/config_flow.py b/homeassistant/components/fully_kiosk/config_flow.py new file mode 100644 index 00000000000..09eb94d6b07 --- /dev/null +++ b/homeassistant/components/fully_kiosk/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for Fully Kiosk Browser integration.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from fullykiosk import FullyKiosk +from fullykiosk.exceptions import FullyKioskError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_PORT, DOMAIN, LOGGER + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Fully Kiosk Browser.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + fully = FullyKiosk( + async_get_clientsession(self.hass), + user_input[CONF_HOST], + DEFAULT_PORT, + user_input[CONF_PASSWORD], + ) + + try: + async with timeout(15): + device_info = await fully.getDeviceInfo() + except (ClientConnectorError, FullyKioskError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(device_info["deviceID"]) + self._abort_if_unique_id_configured(updates=user_input) + return self.async_create_entry( + title=device_info["deviceName"], data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/fully_kiosk/const.py b/homeassistant/components/fully_kiosk/const.py new file mode 100644 index 00000000000..f21906bae73 --- /dev/null +++ b/homeassistant/components/fully_kiosk/const.py @@ -0,0 +1,13 @@ +"""Constants for the Fully Kiosk Browser integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +DOMAIN: Final = "fully_kiosk" + +LOGGER = logging.getLogger(__package__) +UPDATE_INTERVAL = timedelta(seconds=30) + +DEFAULT_PORT = 2323 diff --git a/homeassistant/components/fully_kiosk/coordinator.py b/homeassistant/components/fully_kiosk/coordinator.py new file mode 100644 index 00000000000..fbd08f8d2c5 --- /dev/null +++ b/homeassistant/components/fully_kiosk/coordinator.py @@ -0,0 +1,48 @@ +"""Provides the The Fully Kiosk Browser DataUpdateCoordinator.""" +import asyncio +from typing import Any, cast + +from async_timeout import timeout +from fullykiosk import FullyKiosk +from fullykiosk.exceptions import FullyKioskError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_PORT, LOGGER, UPDATE_INTERVAL + + +class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Fully Kiosk Browser data.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self.fully = FullyKiosk( + async_get_clientsession(hass), + entry.data[CONF_HOST], + DEFAULT_PORT, + entry.data[CONF_PASSWORD], + ) + super().__init__( + hass, + LOGGER, + name=entry.data[CONF_HOST], + update_interval=UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + try: + async with timeout(15): + # Get device info and settings in parallel + result = await asyncio.gather( + self.fully.getDeviceInfo(), self.fully.getSettings() + ) + # Store settings under settings key in data + result[0]["settings"] = result[1] + return cast(dict[str, Any], result[0]) + except FullyKioskError as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/fully_kiosk/entity.py b/homeassistant/components/fully_kiosk/entity.py new file mode 100644 index 00000000000..4e50bb6efe6 --- /dev/null +++ b/homeassistant/components/fully_kiosk/entity.py @@ -0,0 +1,26 @@ +"""Base entity for the Fully Kiosk Browser integration.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator + + +class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entity): + """Defines a Fully Kiosk Browser entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: FullyKioskDataUpdateCoordinator) -> None: + """Initialize the Fully Kiosk Browser entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.data["deviceID"])}, + name=coordinator.data["deviceName"], + manufacturer=coordinator.data["deviceManufacturer"], + model=coordinator.data["deviceModel"], + sw_version=coordinator.data["appVersionName"], + configuration_url=f"http://{coordinator.data['ip4']}:2323", + ) diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json new file mode 100644 index 00000000000..40c7e5293e7 --- /dev/null +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "fully_kiosk", + "name": "Fully Kiosk Browser", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/fullykiosk", + "requirements": ["python-fullykiosk==0.0.11"], + "dependencies": [], + "codeowners": ["@cgarwood"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/fully_kiosk/strings.json b/homeassistant/components/fully_kiosk/strings.json new file mode 100644 index 00000000000..05b9e067962 --- /dev/null +++ b/homeassistant/components/fully_kiosk/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "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_account%]" + } + } +} diff --git a/homeassistant/components/fully_kiosk/translations/en.json b/homeassistant/components/fully_kiosk/translations/en.json new file mode 100644 index 00000000000..338c50514fb --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ce95cb66cc5..8b5db1b45ba 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -125,6 +125,7 @@ FLOWS = { "fritzbox", "fritzbox_callmonitor", "fronius", + "fully_kiosk", "garages_amsterdam", "gdacs", "generic", diff --git a/mypy.ini b/mypy.ini index 2645cb9d107..570004a14dd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -839,6 +839,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fully_kiosk.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.geo_location.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 8b2e2349715..dfae2e4afd9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1914,6 +1914,9 @@ python-family-hub-local==0.0.2 # homeassistant.components.darksky python-forecastio==1.4.0 +# homeassistant.components.fully_kiosk +python-fullykiosk==0.0.11 + # homeassistant.components.sms # python-gammu==3.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5cf5bccb54..0623b6c7c79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1313,6 +1313,9 @@ python-ecobee-api==0.2.14 # homeassistant.components.darksky python-forecastio==1.4.0 +# homeassistant.components.fully_kiosk +python-fullykiosk==0.0.11 + # homeassistant.components.homewizard python-homewizard-energy==1.1.0 diff --git a/tests/components/fully_kiosk/__init__.py b/tests/components/fully_kiosk/__init__.py new file mode 100644 index 00000000000..7cdc13ace56 --- /dev/null +++ b/tests/components/fully_kiosk/__init__.py @@ -0,0 +1 @@ +"""Tests for the Fully Kiosk Browser integration.""" diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py new file mode 100644 index 00000000000..c5476ea6a9d --- /dev/null +++ b/tests/components/fully_kiosk/conftest.py @@ -0,0 +1,76 @@ +"""Fixtures for the Fully Kiosk Browser integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.fully_kiosk.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Test device", + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1", CONF_PASSWORD: "mocked-password"}, + unique_id="12345", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.fully_kiosk.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_fully_kiosk_config_flow() -> Generator[MagicMock, None, None]: + """Return a mocked Fully Kiosk client for the config flow.""" + with patch( + "homeassistant.components.fully_kiosk.config_flow.FullyKiosk", + autospec=True, + ) as client_mock: + client = client_mock.return_value + client.getDeviceInfo.return_value = { + "deviceName": "Test device", + "deviceID": "12345", + } + yield client + + +@pytest.fixture +def mock_fully_kiosk() -> Generator[MagicMock, None, None]: + """Return a mocked Fully Kiosk client.""" + with patch( + "homeassistant.components.fully_kiosk.coordinator.FullyKiosk", + autospec=True, + ) as client_mock: + client = client_mock.return_value + client.getDeviceInfo.return_value = json.loads( + load_fixture("deviceinfo.json", DOMAIN) + ) + yield client + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_fully_kiosk: MagicMock +) -> MockConfigEntry: + """Set up the Fully Kiosk Browser integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/fully_kiosk/fixtures/deviceinfo.json b/tests/components/fully_kiosk/fixtures/deviceinfo.json new file mode 100644 index 00000000000..0a530dc35c8 --- /dev/null +++ b/tests/components/fully_kiosk/fixtures/deviceinfo.json @@ -0,0 +1,79 @@ +{ + "deviceName": "Amazon Fire", + "batteryLevel": 100, + "isPlugged": true, + "SSID": "\"freewifi\"", + "Mac": "aa:bb:cc:dd:ee:ff", + "ip4": "192.168.1.234", + "ip6": "FE80::1874:2EFF:FEA2:7848", + "hostname4": "192.168.1.234", + "hostname6": "fe80::1874:2eff:fea2:7848%p2p0", + "wifiSignalLevel": 7, + "isMobileDataEnabled": true, + "screenOrientation": 90, + "screenBrightness": 9, + "screenLocked": false, + "screenOn": true, + "batteryTemperature": 27, + "plugged": true, + "keyguardLocked": false, + "locale": "en_US", + "serial": "ABCDEF1234567890", + "build": "cm_douglas-userdebug 5.1.1 LMY49M 731a881f9d test-keys", + "androidVersion": "5.1.1", + "webviewUA": "Mozilla/5.0 (Linux; Android 5.1.1; KFDOWI Build/LMY49M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/81.0.4044.117 Safari/537.36", + "motionDetectorStatus": 0, + "isDeviceAdmin": true, + "isDeviceOwner": false, + "internalStorageFreeSpace": 11675512832, + "internalStorageTotalSpace": 12938534912, + "ramUsedMemory": 1077755904, + "ramFreeMemory": 362373120, + "ramTotalMemory": 1440129024, + "appUsedMemory": 24720592, + "appFreeMemory": 59165440, + "appTotalMemory": 83886080, + "displayHeightPixels": 800, + "displayWidthPixels": 1280, + "isMenuOpen": false, + "topFragmentTag": "", + "isInDaydream": false, + "isRooted": true, + "isLicensed": true, + "isInScreensaver": false, + "kioskLocked": true, + "isInForcedSleep": false, + "maintenanceMode": false, + "kioskMode": true, + "startUrl": "https://homeassistant.local", + "currentTabIndex": 0, + "mqttConnected": true, + "deviceID": "abcdef-123456", + "appVersionCode": 875, + "appVersionName": "1.42.5", + "androidSdk": 22, + "deviceModel": "KFDOWI", + "deviceManufacturer": "amzn", + "foregroundApp": "de.ozerov.fully", + "currentPage": "https://homeassistant.local", + "lastAppStart": "8/13/2022 1:00:47 AM", + "sensorInfo": [ + { + "type": 8, + "name": "PROXIMITY", + "vendor": "MTK", + "version": 1, + "accuracy": -1 + }, + { + "type": 5, + "name": "LIGHT", + "vendor": "MTK", + "version": 1, + "accuracy": 3, + "values": [0, 0, 0], + "lastValuesTime": 1660435566561, + "lastAccuracyTime": 1660366847543 + } + ] +} diff --git a/tests/components/fully_kiosk/test_binary_sensor.py b/tests/components/fully_kiosk/test_binary_sensor.py new file mode 100644 index 00000000000..3583a66b8e7 --- /dev/null +++ b/tests/components/fully_kiosk/test_binary_sensor.py @@ -0,0 +1,93 @@ +"""Test the Fully Kiosk Browser binary sensors.""" +from unittest.mock import MagicMock + +from fullykiosk import FullyKioskError + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory +from homeassistant.util import dt + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_binary_sensors( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk binary sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("binary_sensor.amazon_fire_plugged_in") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Plugged in" + + entry = entity_registry.async_get("binary_sensor.amazon_fire_plugged_in") + assert entry + assert entry.unique_id == "abcdef-123456-plugged" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("binary_sensor.amazon_fire_kiosk_mode") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Kiosk mode" + + entry = entity_registry.async_get("binary_sensor.amazon_fire_kiosk_mode") + assert entry + assert entry.unique_id == "abcdef-123456-kioskMode" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("binary_sensor.amazon_fire_device_admin") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Device admin" + + entry = entity_registry.async_get("binary_sensor.amazon_fire_device_admin") + assert entry + assert entry.unique_id == "abcdef-123456-isDeviceAdmin" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + # Test unknown/missing data + mock_fully_kiosk.getDeviceInfo.return_value = {} + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.amazon_fire_plugged_in") + assert state + assert state.state == STATE_UNKNOWN + + # Test failed update + mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status") + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.amazon_fire_plugged_in") + assert state + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py new file mode 100644 index 00000000000..2617a3f7adb --- /dev/null +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -0,0 +1,136 @@ +"""Test the Fully Kiosk Browser config flow.""" + +import asyncio +from unittest.mock import AsyncMock, MagicMock, Mock + +from aiohttp.client_exceptions import ClientConnectorError +from fullykiosk import FullyKioskError +import pytest + +from homeassistant.components.fully_kiosk.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_fully_kiosk_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full user initiated config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("title") == "Test device" + assert result2.get("data") == { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + } + assert "result" in result2 + assert result2["result"].unique_id == "12345" + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (FullyKioskError("error", "status"), "cannot_connect"), + (ClientConnectorError(None, Mock()), "cannot_connect"), + (asyncio.TimeoutError, "cannot_connect"), + (RuntimeError, "unknown"), + ], +) +async def test_errors( + hass: HomeAssistant, + mock_fully_kiosk_config_flow: MagicMock, + mock_setup_entry: AsyncMock, + side_effect: Exception, + reason: str, +) -> None: + """Test errors raised during flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + mock_fully_kiosk_config_flow.getDeviceInfo.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "user" + assert result2.get("errors") == {"base": reason} + + assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + + mock_fully_kiosk_config_flow.getDeviceInfo.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Test device" + assert result3.get("data") == { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + } + assert "result" in result3 + assert result3["result"].unique_id == "12345" + + assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 2 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_updates_existing_entry( + hass: HomeAssistant, + mock_fully_kiosk_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test adding existing device updates existing entry.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + } + + assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1 diff --git a/tests/components/fully_kiosk/test_init.py b/tests/components/fully_kiosk/test_init.py new file mode 100644 index 00000000000..5960873e124 --- /dev/null +++ b/tests/components/fully_kiosk/test_init.py @@ -0,0 +1,53 @@ +"""Tests for the Fully Kiosk Browser integration.""" +import asyncio +from unittest.mock import MagicMock + +from fullykiosk import FullyKioskError +import pytest + +from homeassistant.components.fully_kiosk.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_fully_kiosk: MagicMock, +) -> None: + """Test the Fully Kiosk Browser configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert len(mock_fully_kiosk.getDeviceInfo.mock_calls) == 1 + assert len(mock_fully_kiosk.getSettings.mock_calls) == 1 + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "side_effect", + [FullyKioskError("error", "status"), asyncio.TimeoutError], +) +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_fully_kiosk: MagicMock, + side_effect: Exception, +) -> None: + """Test the Fully Kiosk Browser configuration entry not ready.""" + mock_fully_kiosk.getDeviceInfo.side_effect = side_effect + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY From cb2799bc37872d179f7fa8b5bc6dbf81bd75828f Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Tue, 16 Aug 2022 21:36:33 +0200 Subject: [PATCH 409/903] Fix displayed units for BMW Connected Drive (#76613) * Fix displayed units * Add tests for unit conversion * Streamline test config entry init * Refactor test to pytest fixture * Fix renamed mock Co-authored-by: rikroe --- .../components/bmw_connected_drive/sensor.py | 40 ++-- .../bmw_connected_drive/__init__.py | 86 ++++++++ .../bmw_connected_drive/conftest.py | 12 + .../I01/state_WBY00000000REXI01_0.json | 206 ++++++++++++++++++ .../vehicles/I01/vehicles_v2_bmw_0.json | 47 ++++ .../bmw_connected_drive/test_config_flow.py | 8 +- .../bmw_connected_drive/test_sensor.py | 52 +++++ 7 files changed, 420 insertions(+), 31 deletions(-) create mode 100644 tests/components/bmw_connected_drive/conftest.py create mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json create mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json create mode 100644 tests/components/bmw_connected_drive/test_sensor.py diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 26fbe19b5b1..ae3dc0bb8b9 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -15,13 +15,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - LENGTH_KILOMETERS, - LENGTH_MILES, - PERCENTAGE, - VOLUME_GALLONS, - VOLUME_LITERS, -) +from homeassistant.const import LENGTH, PERCENTAGE, VOLUME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -39,8 +33,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): """Describes BMW sensor entity.""" key_class: str | None = None - unit_metric: str | None = None - unit_imperial: str | None = None + unit_type: str | None = None value: Callable = lambda x, y: x @@ -81,56 +74,49 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { "remaining_battery_percent": BMWSensorEntityDescription( key="remaining_battery_percent", key_class="fuel_and_battery", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, ), # --- Specific --- "mileage": BMWSensorEntityDescription( key="mileage", icon="mdi:speedometer", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_total": BMWSensorEntityDescription( key="remaining_range_total", key_class="fuel_and_battery", icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_electric": BMWSensorEntityDescription( key="remaining_range_electric", key_class="fuel_and_battery", icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_fuel": BMWSensorEntityDescription( key="remaining_range_fuel", key_class="fuel_and_battery", icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_fuel": BMWSensorEntityDescription( key="remaining_fuel", key_class="fuel_and_battery", icon="mdi:gas-station", - unit_metric=VOLUME_LITERS, - unit_imperial=VOLUME_GALLONS, + unit_type=VOLUME, value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), ), "remaining_fuel_percent": BMWSensorEntityDescription( key="remaining_fuel_percent", key_class="fuel_and_battery", icon="mdi:gas-station", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_type=PERCENTAGE, ), } @@ -177,8 +163,12 @@ class BMWSensor(BMWBaseEntity, SensorEntity): self._attr_name = f"{vehicle.name} {description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}" - # Force metric system as BMW API apparently only returns metric values now - self._attr_native_unit_of_measurement = description.unit_metric + # Set the correct unit of measurement based on the unit_type + if description.unit_type: + self._attr_native_unit_of_measurement = ( + coordinator.hass.config.units.as_dict().get(description.unit_type) + or description.unit_type + ) @callback def _handle_coordinator_update(self) -> None: diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 4774032b409..c2bb65b3fa7 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -1,17 +1,29 @@ """Tests for the for the BMW Connected Drive integration.""" +import json +from pathlib import Path + +from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.utils import log_to_to_file + from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.const import ( CONF_READ_ONLY, + CONF_REFRESH_TOKEN, DOMAIN as BMW_DOMAIN, ) from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, get_fixture_path, load_fixture FIXTURE_USER_INPUT = { CONF_USERNAME: "user@domain.com", CONF_PASSWORD: "p4ssw0rd", CONF_REGION: "rest_of_world", } +FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN" FIXTURE_CONFIG_ENTRY = { "entry_id": "1", @@ -21,8 +33,82 @@ FIXTURE_CONFIG_ENTRY = { CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME], CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], + CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN, }, "options": {CONF_READ_ONLY: False}, "source": config_entries.SOURCE_USER, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } + + +async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: + """Load MyBMWVehicle from fixtures and add them to the account.""" + + fixture_path = Path(get_fixture_path("", integration=BMW_DOMAIN)) + + fixture_vehicles_bmw = list(fixture_path.rglob("vehicles_v2_bmw_*.json")) + fixture_vehicles_mini = list(fixture_path.rglob("vehicles_v2_mini_*.json")) + + # Load vehicle base lists as provided by vehicles/v2 API + vehicles = { + "bmw": [ + vehicle + for bmw_file in fixture_vehicles_bmw + for vehicle in json.loads(load_fixture(bmw_file, integration=BMW_DOMAIN)) + ], + "mini": [ + vehicle + for mini_file in fixture_vehicles_mini + for vehicle in json.loads(load_fixture(mini_file, integration=BMW_DOMAIN)) + ], + } + fetched_at = utcnow() + + # simulate storing fingerprints + if account.config.log_response_path: + for brand in ["bmw", "mini"]: + log_to_to_file( + json.dumps(vehicles[brand]), + account.config.log_response_path, + f"vehicles_v2_{brand}", + ) + + # Create a vehicle with base + specific state as provided by state/VIN API + for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]: + vehicle_state_path = ( + Path("vehicles") + / vehicle_base["attributes"]["bodyType"] + / f"state_{vehicle_base['vin']}_0.json" + ) + vehicle_state = json.loads( + load_fixture( + vehicle_state_path, + integration=BMW_DOMAIN, + ) + ) + + account.add_vehicle( + vehicle_base, + vehicle_state, + fetched_at, + ) + + # simulate storing fingerprints + if account.config.log_response_path: + log_to_to_file( + json.dumps(vehicle_state), + account.config.log_response_path, + f"state_{vehicle_base['vin']}", + ) + + +async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: + """Mock a fully setup config entry and all components based on fixtures.""" + + mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py new file mode 100644 index 00000000000..bf9d32ed9fa --- /dev/null +++ b/tests/components/bmw_connected_drive/conftest.py @@ -0,0 +1,12 @@ +"""Fixtures for BMW tests.""" + +from bimmer_connected.account import MyBMWAccount +import pytest + +from . import mock_vehicles_from_fixture + + +@pytest.fixture +async def bmw_fixture(monkeypatch): + """Patch the vehicle fixtures into a MyBMWAccount.""" + monkeypatch.setattr(MyBMWAccount, "get_vehicles", mock_vehicles_from_fixture) diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json new file mode 100644 index 00000000000..adc2bde3650 --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json @@ -0,0 +1,206 @@ +{ + "capabilities": { + "climateFunction": "AIR_CONDITIONING", + "climateNow": true, + "climateTimerTrigger": "DEPARTURE_TIMER", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isClimateTimerSupported": true, + "isCustomerEsimSupported": false, + "isDCSContractManagementSupported": true, + "isDataPrivacyEnabled": false, + "isEasyChargeEnabled": false, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteChargingCommands": {}, + "sendPoi": true, + "specialThemeSupport": [], + "unlock": true, + "vehicleFinder": false, + "vehicleStateSource": "LAST_STATE_CALL" + }, + "state": { + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "chargingMode": "DELAYED_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "chargingSettings": { + "hospitality": "NO_ACTION", + "idcc": "NO_ACTION", + "targetSoc": 100 + }, + "climatisationOn": false, + "departureTimes": [ + { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { + "hour": 7, + "minute": 35 + }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { + "hour": 18, + "minute": 0 + }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { + "hour": 7, + "minute": 0 + }, + "timerWeekDays": [] + }, + { + "action": "DEACTIVATE", + "id": 4, + "timerWeekDays": [] + } + ], + "reductionOfChargeCurrent": { + "end": { + "hour": 1, + "minute": 30 + }, + "start": { + "hour": 18, + "minute": 1 + } + } + }, + "checkControlMessages": [], + "climateTimers": [ + { + "departureTime": { + "hour": 6, + "minute": 40 + }, + "isWeeklyTimer": true, + "timerAction": "ACTIVATE", + "timerWeekDays": ["THURSDAY", "SUNDAY"] + }, + { + "departureTime": { + "hour": 12, + "minute": 50 + }, + "isWeeklyTimer": false, + "timerAction": "ACTIVATE", + "timerWeekDays": ["MONDAY"] + }, + { + "departureTime": { + "hour": 18, + "minute": 59 + }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": ["WEDNESDAY"] + } + ], + "combustionFuelLevel": { + "range": 105, + "remainingFuelLiters": 6, + "remainingFuelPercent": 65 + }, + "currentMileage": 137009, + "doorsState": { + "combinedSecurityState": "UNLOCKED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "trunk": "CLOSED" + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "electricChargingState": { + "chargingConnectionType": "CONDUCTIVE", + "chargingLevelPercent": 82, + "chargingStatus": "WAITING_FOR_CHARGING", + "chargingTarget": 100, + "isChargerConnected": true, + "range": 174 + }, + "isLeftSteering": true, + "isLscSupported": true, + "lastFetched": "2022-06-22T14:24:23.982Z", + "lastUpdatedAt": "2022-06-22T13:58:52Z", + "range": 174, + "requiredServices": [ + { + "dateTime": "2022-10-01T00:00:00.000Z", + "description": "Next service due by the specified date.", + "status": "OK", + "type": "BRAKE_FLUID" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next vehicle check due after the specified distance or date.", + "status": "OK", + "type": "VEHICLE_CHECK" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next state inspection due by the specified date.", + "status": "OK", + "type": "VEHICLE_TUV" + } + ], + "roofState": { + "roofState": "CLOSED", + "roofStateType": "SUN_ROOF" + }, + "windowsState": { + "combinedState": "CLOSED", + "leftFront": "CLOSED", + "rightFront": "CLOSED" + } + } +} diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json new file mode 100644 index 00000000000..145bc13378e --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json @@ -0,0 +1,47 @@ +[ + { + "appVehicleType": "CONNECTED", + "attributes": { + "a4aType": "USB_ONLY", + "bodyType": "I01", + "brand": "BMW_I", + "color": 4284110934, + "countryOfOrigin": "CZ", + "driveTrain": "ELECTRIC_WITH_RANGE_EXTENDER", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + }, + "headUnitType": "NBT", + "hmiVersion": "ID4", + "lastFetched": "2022-07-10T09:25:53.104Z", + "model": "i3 (+ REX)", + "softwareVersionCurrent": { + "iStep": 510, + "puStep": { + "month": 11, + "year": 21 + }, + "seriesCluster": "I001" + }, + "softwareVersionExFactory": { + "iStep": 502, + "puStep": { + "month": 3, + "year": 15 + }, + "seriesCluster": "I001" + }, + "year": 2015 + }, + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "isPrimaryUser": true, + "mappingStatus": "CONFIRMED" + }, + "vin": "WBY00000000REXI01" + } +] diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 3f22f984a54..daac0c04f7b 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -12,15 +12,11 @@ from homeassistant.components.bmw_connected_drive.const import ( ) from homeassistant.const import CONF_USERNAME -from . import FIXTURE_CONFIG_ENTRY, FIXTURE_USER_INPUT +from . import FIXTURE_CONFIG_ENTRY, FIXTURE_REFRESH_TOKEN, FIXTURE_USER_INPUT from tests.common import MockConfigEntry -FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN" -FIXTURE_COMPLETE_ENTRY = { - **FIXTURE_USER_INPUT, - CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN, -} +FIXTURE_COMPLETE_ENTRY = FIXTURE_CONFIG_ENTRY["data"] FIXTURE_IMPORT_ENTRY = {**FIXTURE_USER_INPUT, CONF_REFRESH_TOKEN: None} diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py new file mode 100644 index 00000000000..cb1299a274b --- /dev/null +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -0,0 +1,52 @@ +"""Test BMW sensors.""" +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.util.unit_system import ( + IMPERIAL_SYSTEM as IMPERIAL, + METRIC_SYSTEM as METRIC, + UnitSystem, +) + +from . import setup_mocked_integration + + +@pytest.mark.parametrize( + "entity_id,unit_system,value,unit_of_measurement", + [ + ("sensor.i3_rex_remaining_range_total", METRIC, "279", "km"), + ("sensor.i3_rex_remaining_range_total", IMPERIAL, "173.36", "mi"), + ("sensor.i3_rex_mileage", METRIC, "137009", "km"), + ("sensor.i3_rex_mileage", IMPERIAL, "85133.42", "mi"), + ("sensor.i3_rex_remaining_battery_percent", METRIC, "82", "%"), + ("sensor.i3_rex_remaining_battery_percent", IMPERIAL, "82", "%"), + ("sensor.i3_rex_remaining_range_electric", METRIC, "174", "km"), + ("sensor.i3_rex_remaining_range_electric", IMPERIAL, "108.12", "mi"), + ("sensor.i3_rex_remaining_fuel", METRIC, "6", "L"), + ("sensor.i3_rex_remaining_fuel", IMPERIAL, "1.59", "gal"), + ("sensor.i3_rex_remaining_range_fuel", METRIC, "105", "km"), + ("sensor.i3_rex_remaining_range_fuel", IMPERIAL, "65.24", "mi"), + ("sensor.i3_rex_remaining_fuel_percent", METRIC, "65", "%"), + ("sensor.i3_rex_remaining_fuel_percent", IMPERIAL, "65", "%"), + ], +) +async def test_unit_conversion( + hass: HomeAssistant, + entity_id: str, + unit_system: UnitSystem, + value: str, + unit_of_measurement: str, + bmw_fixture, +) -> None: + """Test conversion between metric and imperial units for sensors.""" + + # Set unit system + hass.config.units = unit_system + + # Setup component + assert await setup_mocked_integration(hass) + + # Test + entity = hass.states.get(entity_id) + assert entity.state == value + assert entity.attributes.get("unit_of_measurement") == unit_of_measurement From 59878ea1ef94c870368a73285b6c86a2b2c0fdc1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 16 Aug 2022 17:17:10 -0400 Subject: [PATCH 410/903] Indieauth updates (#76880) --- homeassistant/components/auth/__init__.py | 58 +++++++++------- homeassistant/components/auth/login_flow.py | 53 ++++++++++++--- tests/components/auth/test_init.py | 23 +++++-- tests/components/auth/test_init_link_user.py | 7 +- tests/components/auth/test_login_flow.py | 70 +++++++++++++++++++- 5 files changed, 169 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index a8c7019030f..ac5035de645 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -177,6 +177,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = store_result hass.http.register_view(TokenView(retrieve_result)) + hass.http.register_view(RevokeTokenView()) hass.http.register_view(LinkUserView(retrieve_result)) hass.http.register_view(OAuth2AuthorizeCallbackView()) @@ -192,8 +193,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +class RevokeTokenView(HomeAssistantView): + """View to revoke tokens.""" + + url = "/auth/revoke" + name = "api:auth:revocation" + requires_auth = False + cors_allowed = True + + async def post(self, request: web.Request) -> web.Response: + """Revoke a token.""" + hass: HomeAssistant = request.app["hass"] + data = cast(MultiDictProxy[str], await request.post()) + + # OAuth 2.0 Token Revocation [RFC7009] + # 2.2 The authorization server responds with HTTP status code 200 + # if the token has been revoked successfully or if the client + # submitted an invalid token. + if (token := data.get("token")) is None: + return web.Response(status=HTTPStatus.OK) + + refresh_token = await hass.auth.async_get_refresh_token_by_token(token) + + if refresh_token is None: + return web.Response(status=HTTPStatus.OK) + + await hass.auth.async_remove_refresh_token(refresh_token) + return web.Response(status=HTTPStatus.OK) + + class TokenView(HomeAssistantView): - """View to issue or revoke tokens.""" + """View to issue tokens.""" url = "/auth/token" name = "api:auth:token" @@ -217,7 +247,9 @@ class TokenView(HomeAssistantView): # The revocation request includes an additional parameter, # action=revoke. if data.get("action") == "revoke": - return await self._async_handle_revoke_token(hass, data) + # action=revoke is deprecated. Use /auth/revoke instead. + # Keep here for backwards compat + return await RevokeTokenView.post(self, request) # type: ignore[arg-type] if grant_type == "authorization_code": return await self._async_handle_auth_code(hass, data, request.remote) @@ -229,28 +261,6 @@ class TokenView(HomeAssistantView): {"error": "unsupported_grant_type"}, status_code=HTTPStatus.BAD_REQUEST ) - async def _async_handle_revoke_token( - self, - hass: HomeAssistant, - data: MultiDictProxy[str], - ) -> web.Response: - """Handle revoke token request.""" - - # OAuth 2.0 Token Revocation [RFC7009] - # 2.2 The authorization server responds with HTTP status code 200 - # if the token has been revoked successfully or if the client - # submitted an invalid token. - if (token := data.get("token")) is None: - return web.Response(status=HTTPStatus.OK) - - refresh_token = await hass.auth.async_get_refresh_token_by_token(token) - - if refresh_token is None: - return web.Response(status=HTTPStatus.OK) - - await hass.auth.async_remove_refresh_token(refresh_token) - return web.Response(status=HTTPStatus.OK) - async def _async_handle_auth_code( self, hass: HomeAssistant, diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index dc094cd581f..bb13431bfa7 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -101,11 +101,32 @@ async def async_setup( hass: HomeAssistant, store_result: Callable[[str, Credentials], str] ) -> None: """Component to allow users to login.""" + hass.http.register_view(WellKnownOAuthInfoView) hass.http.register_view(AuthProvidersView) hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result)) hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result)) +class WellKnownOAuthInfoView(HomeAssistantView): + """View to host the OAuth2 information.""" + + requires_auth = False + url = "/.well-known/oauth-authorization-server" + name = "well-known/oauth-authorization-server" + + async def get(self, request: web.Request) -> web.Response: + """Return the well known OAuth2 authorization info.""" + return self.json( + { + "authorization_endpoint": "/auth/authorize", + "token_endpoint": "/auth/token", + "revocation_endpoint": "/auth/revoke", + "response_types_supported": ["code"], + "service_documentation": "https://developers.home-assistant.io/docs/auth_api", + } + ) + + class AuthProvidersView(HomeAssistantView): """View to get available auth providers.""" @@ -172,6 +193,7 @@ class LoginFlowBaseView(HomeAssistantView): self, request: web.Request, client_id: str, + redirect_uri: str, result: data_entry_flow.FlowResult, ) -> web.Response: """Convert the flow result to a response.""" @@ -190,9 +212,13 @@ class LoginFlowBaseView(HomeAssistantView): await process_wrong_login(request) return self.json(_prepare_result_json(result)) + hass: HomeAssistant = request.app["hass"] + + if not await indieauth.verify_redirect_uri(hass, client_id, redirect_uri): + return self.json_message("Invalid redirect URI", HTTPStatus.FORBIDDEN) + result.pop("data") - hass: HomeAssistant = request.app["hass"] result_obj: Credentials = result.pop("result") # Result can be None if credential was never linked to a user before. @@ -234,14 +260,11 @@ class LoginFlowIndexView(LoginFlowBaseView): @log_invalid_auth async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Create a new login flow.""" - hass: HomeAssistant = request.app["hass"] client_id: str = data["client_id"] redirect_uri: str = data["redirect_uri"] - if not await indieauth.verify_redirect_uri(hass, client_id, redirect_uri): - return self.json_message( - "invalid client id or redirect uri", HTTPStatus.BAD_REQUEST - ) + if not indieauth.verify_client_id(client_id): + return self.json_message("Invalid client id", HTTPStatus.BAD_REQUEST) handler: tuple[str, ...] | str if isinstance(data["handler"], list): @@ -264,7 +287,9 @@ class LoginFlowIndexView(LoginFlowBaseView): "Handler does not support init", HTTPStatus.BAD_REQUEST ) - return await self._async_flow_result_to_response(request, client_id, result) + return await self._async_flow_result_to_response( + request, client_id, redirect_uri, result + ) class LoginFlowResourceView(LoginFlowBaseView): @@ -277,13 +302,19 @@ class LoginFlowResourceView(LoginFlowBaseView): """Do not allow getting status of a flow in progress.""" return self.json_message("Invalid flow specified", HTTPStatus.NOT_FOUND) - @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA)) + @RequestDataValidator( + vol.Schema( + {vol.Required("client_id"): str, vol.Required("redirect_uri"): str}, + extra=vol.ALLOW_EXTRA, + ) + ) @log_invalid_auth async def post( self, request: web.Request, data: dict[str, Any], flow_id: str ) -> web.Response: """Handle progressing a login flow request.""" - client_id = data.pop("client_id") + client_id: str = data.pop("client_id") + redirect_uri: str = data.pop("redirect_uri") if not indieauth.verify_client_id(client_id): return self.json_message("Invalid client id", HTTPStatus.BAD_REQUEST) @@ -299,7 +330,9 @@ class LoginFlowResourceView(LoginFlowBaseView): except vol.Invalid: return self.json_message("User input malformed", HTTPStatus.BAD_REQUEST) - return await self._async_flow_result_to_response(request, client_id, result) + return await self._async_flow_result_to_response( + request, client_id, redirect_uri, result + ) async def delete(self, request: web.Request, flow_id: str) -> web.Response: """Cancel a flow in progress.""" diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 706221d9371..09a74cf9bc9 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -62,7 +62,12 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): resp = await client.post( f"/auth/login_flow/{step['flow_id']}", - json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, + json={ + "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, + "username": "test-user", + "password": "test-pass", + }, ) assert resp.status == HTTPStatus.OK @@ -126,7 +131,12 @@ async def test_auth_code_checks_local_only_user(hass, aiohttp_client): resp = await client.post( f"/auth/login_flow/{step['flow_id']}", - json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, + json={ + "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, + "username": "test-user", + "password": "test-pass", + }, ) assert resp.status == HTTPStatus.OK @@ -358,7 +368,10 @@ async def test_refresh_token_provider_rejected( assert result["error_description"] == "Invalid access" -async def test_revoking_refresh_token(hass, aiohttp_client): +@pytest.mark.parametrize( + "url,base_data", [("/auth/token", {"action": "revoke"}), ("/auth/revoke", {})] +) +async def test_revoking_refresh_token(url, base_data, hass, aiohttp_client): """Test that we can revoke refresh tokens.""" client = await async_setup_auth(hass, aiohttp_client) refresh_token = await async_setup_user_refresh_token(hass) @@ -380,9 +393,7 @@ async def test_revoking_refresh_token(hass, aiohttp_client): ) # Revoke refresh token - resp = await client.post( - "/auth/token", data={"token": refresh_token.token, "action": "revoke"} - ) + resp = await client.post(url, data={**base_data, "token": refresh_token.token}) assert resp.status == HTTPStatus.OK # Old access token should be no longer valid diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 036dad4265f..bad6e3bfefe 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -46,7 +46,12 @@ async def async_get_code(hass, aiohttp_client): resp = await client.post( f"/auth/login_flow/{step['flow_id']}", - json={"client_id": CLIENT_ID, "username": "2nd-user", "password": "2nd-pass"}, + json={ + "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, + "username": "2nd-user", + "password": "2nd-pass", + }, ) assert resp.status == HTTPStatus.OK diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index 1fa06045de6..ce547149786 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -61,6 +61,7 @@ async def test_invalid_username_password(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, "username": "wrong-user", "password": "test-pass", }, @@ -81,6 +82,7 @@ async def test_invalid_username_password(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "wrong-pass", }, @@ -93,6 +95,49 @@ async def test_invalid_username_password(hass, aiohttp_client): assert step["step_id"] == "init" assert step["errors"]["base"] == "invalid_auth" + # Incorrect username and invalid redirect URI fails on wrong login + with patch( + "homeassistant.components.auth.login_flow.process_wrong_login" + ) as mock_process_wrong_login: + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={ + "client_id": CLIENT_ID, + "redirect_uri": "http://some-other-domain.com", + "username": "wrong-user", + "password": "test-pass", + }, + ) + + assert resp.status == HTTPStatus.OK + step = await resp.json() + assert len(mock_process_wrong_login.mock_calls) == 1 + + assert step["step_id"] == "init" + assert step["errors"]["base"] == "invalid_auth" + + # Incorrect redirect URI + with patch( + "homeassistant.components.auth.indieauth.fetch_redirect_uris", return_value=[] + ), patch( + "homeassistant.components.http.ban.process_wrong_login" + ) as mock_process_wrong_login: + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={ + "client_id": CLIENT_ID, + "redirect_uri": "http://some-other-domain.com", + "username": "test-user", + "password": "test-pass", + }, + ) + + assert resp.status == HTTPStatus.FORBIDDEN + data = await resp.json() + assert len(mock_process_wrong_login.mock_calls) == 1 + + assert data["message"] == "Invalid redirect URI" + async def test_login_exist_user(hass, aiohttp_client): """Test logging in with exist user.""" @@ -120,6 +165,7 @@ async def test_login_exist_user(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "test-pass", }, @@ -160,6 +206,7 @@ async def test_login_local_only_user(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "test-pass", }, @@ -202,9 +249,30 @@ async def test_login_exist_user_ip_changes(hass, aiohttp_client): resp = await client.post( f"/auth/login_flow/{step['flow_id']}", - json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, + json={ + "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, + "username": "test-user", + "password": "test-pass", + }, ) assert resp.status == 400 response = await resp.json() assert response == {"message": "IP address changed"} + + +async def test_well_known_auth_info(hass, aiohttp_client): + """Test logging in and the ip address changes results in an rejection.""" + client = await async_setup_auth(hass, aiohttp_client, setup_api=True) + resp = await client.get( + "/.well-known/oauth-authorization-server", + ) + assert resp.status == 200 + assert await resp.json() == { + "authorization_endpoint": "/auth/authorize", + "token_endpoint": "/auth/token", + "revocation_endpoint": "/auth/revoke", + "response_types_supported": ["code"], + "service_documentation": "https://developers.home-assistant.io/docs/auth_api", + } From 8070875ff486e05c38c1792ac0dd2b33ce1f1c76 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 16 Aug 2022 18:20:30 -0400 Subject: [PATCH 411/903] Add Fully Kiosk Browser sensor platform (#76887) --- .../components/fully_kiosk/__init__.py | 2 +- .../components/fully_kiosk/sensor.py | 138 ++++++++++++++++ tests/components/fully_kiosk/test_sensor.py | 156 ++++++++++++++++++ 3 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fully_kiosk/sensor.py create mode 100644 tests/components/fully_kiosk/test_sensor.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 943f5c69cbe..a4c71168f4e 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/sensor.py b/homeassistant/components/fully_kiosk/sensor.py new file mode 100644 index 00000000000..6b3ca3fbcb0 --- /dev/null +++ b/homeassistant/components/fully_kiosk/sensor.py @@ -0,0 +1,138 @@ +"""Fully Kiosk Browser sensor.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DATA_MEGABYTES, PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +def round_storage(value: int) -> float: + """Convert storage values from bytes to megabytes.""" + return round(value * 0.000001, 1) + + +@dataclass +class FullySensorEntityDescription(SensorEntityDescription): + """Fully Kiosk Browser sensor description.""" + + state_fn: Callable | None = None + + +SENSORS: tuple[FullySensorEntityDescription, ...] = ( + FullySensorEntityDescription( + key="batteryLevel", + name="Battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="screenOrientation", + name="Screen orientation", + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="foregroundApp", + name="Foreground app", + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="currentPage", + name="Current page", + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="internalStorageFreeSpace", + name="Internal storage free space", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), + FullySensorEntityDescription( + key="internalStorageTotalSpace", + name="Internal storage total space", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), + FullySensorEntityDescription( + key="ramFreeMemory", + name="Free memory", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), + FullySensorEntityDescription( + key="ramTotalMemory", + name="Total memory", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser sensor.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + async_add_entities( + FullySensor(coordinator, description) + for description in SENSORS + if description.key in coordinator.data + ) + + +class FullySensor(FullyKioskEntity, SensorEntity): + """Representation of a Fully Kiosk Browser sensor.""" + + entity_description: FullySensorEntityDescription + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + sensor: FullySensorEntityDescription, + ) -> None: + """Initialize the sensor entity.""" + self.entity_description = sensor + + self._attr_unique_id = f"{coordinator.data['deviceID']}-{sensor.key}" + + super().__init__(coordinator) + + @property + def native_value(self) -> Any: + """Return the state of the sensor.""" + if (value := self.coordinator.data.get(self.entity_description.key)) is None: + return None + + if self.entity_description.state_fn is not None: + return self.entity_description.state_fn(value) + + return value diff --git a/tests/components/fully_kiosk/test_sensor.py b/tests/components/fully_kiosk/test_sensor.py new file mode 100644 index 00000000000..b54627f85b2 --- /dev/null +++ b/tests/components/fully_kiosk/test_sensor.py @@ -0,0 +1,156 @@ +"""Test the Fully Kiosk Browser sensors.""" +from unittest.mock import MagicMock + +from fullykiosk import FullyKioskError + +from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory +from homeassistant.util import dt + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_sensors_sensors( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.amazon_fire_battery") + assert state + assert state.state == "100" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Battery" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_battery") + assert entry + assert entry.unique_id == "abcdef-123456-batteryLevel" + + state = hass.states.get("sensor.amazon_fire_screen_orientation") + assert state + assert state.state == "90" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Screen orientation" + + entry = entity_registry.async_get("sensor.amazon_fire_screen_orientation") + assert entry + assert entry.unique_id == "abcdef-123456-screenOrientation" + + state = hass.states.get("sensor.amazon_fire_foreground_app") + assert state + assert state.state == "de.ozerov.fully" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Foreground app" + + entry = entity_registry.async_get("sensor.amazon_fire_foreground_app") + assert entry + assert entry.unique_id == "abcdef-123456-foregroundApp" + + state = hass.states.get("sensor.amazon_fire_current_page") + assert state + assert state.state == "https://homeassistant.local" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Current page" + + entry = entity_registry.async_get("sensor.amazon_fire_current_page") + assert entry + assert entry.unique_id == "abcdef-123456-currentPage" + + state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") + assert state + assert state.state == "11675.5" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Amazon Fire Internal storage free space" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_internal_storage_free_space") + assert entry + assert entry.unique_id == "abcdef-123456-internalStorageFreeSpace" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.amazon_fire_internal_storage_total_space") + assert state + assert state.state == "12938.5" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Amazon Fire Internal storage total space" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_internal_storage_total_space") + assert entry + assert entry.unique_id == "abcdef-123456-internalStorageTotalSpace" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.amazon_fire_free_memory") + assert state + assert state.state == "362.4" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Free memory" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_free_memory") + assert entry + assert entry.unique_id == "abcdef-123456-ramFreeMemory" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.amazon_fire_total_memory") + assert state + assert state.state == "1440.1" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Total memory" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_total_memory") + assert entry + assert entry.unique_id == "abcdef-123456-ramTotalMemory" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + # Test unknown/missing data + mock_fully_kiosk.getDeviceInfo.return_value = {} + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") + assert state + assert state.state == STATE_UNKNOWN + + # Test failed update + mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status") + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") + assert state + assert state.state == STATE_UNAVAILABLE From 683132ae19cd6a01f9fe74740fd97833518f6eef Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 17 Aug 2022 00:26:42 +0000 Subject: [PATCH 412/903] [ci skip] Translation update --- .../android_ip_webcam/translations/ja.json | 1 + .../components/awair/translations/ca.json | 8 +++++++- .../components/awair/translations/de.json | 8 +++++++- .../components/awair/translations/en.json | 8 +++++++- .../components/awair/translations/es.json | 8 +++++++- .../components/awair/translations/et.json | 8 +++++++- .../components/awair/translations/id.json | 6 ++++++ .../components/awair/translations/it.json | 8 +++++++- .../components/awair/translations/no.json | 6 ++++++ .../components/awair/translations/pt-BR.json | 8 +++++++- .../components/awair/translations/ru.json | 6 ++++++ .../fully_kiosk/translations/ca.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/es.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/et.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/it.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/pt-BR.json | 20 +++++++++++++++++++ .../components/hue/translations/de.json | 17 +++++++++------- .../components/hue/translations/es.json | 17 +++++++++------- .../components/hue/translations/et.json | 17 +++++++++------- .../components/hue/translations/it.json | 17 +++++++++------- .../components/hue/translations/ja.json | 2 ++ .../components/hue/translations/no.json | 17 +++++++++------- .../components/hue/translations/ru.json | 5 ++++- .../components/hue/translations/zh-Hant.json | 17 +++++++++------- .../lutron_caseta/translations/es.json | 10 +++++----- .../openexchangerates/translations/ja.json | 1 + .../simplepush/translations/ja.json | 1 + .../tuya/translations/select.ja.json | 2 +- .../xiaomi_miio/translations/select.ca.json | 10 ++++++++++ .../xiaomi_miio/translations/select.en.json | 10 ++++++++++ .../xiaomi_miio/translations/select.es.json | 10 ++++++++++ .../xiaomi_miio/translations/select.et.json | 10 ++++++++++ .../xiaomi_miio/translations/select.it.json | 10 ++++++++++ .../translations/select.pt-BR.json | 10 ++++++++++ .../components/zha/translations/de.json | 2 +- 35 files changed, 303 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/fully_kiosk/translations/ca.json create mode 100644 homeassistant/components/fully_kiosk/translations/es.json create mode 100644 homeassistant/components/fully_kiosk/translations/et.json create mode 100644 homeassistant/components/fully_kiosk/translations/it.json create mode 100644 homeassistant/components/fully_kiosk/translations/pt-BR.json diff --git a/homeassistant/components/android_ip_webcam/translations/ja.json b/homeassistant/components/android_ip_webcam/translations/ja.json index 832d6b9c71c..519edb51609 100644 --- a/homeassistant/components/android_ip_webcam/translations/ja.json +++ b/homeassistant/components/android_ip_webcam/translations/ja.json @@ -19,6 +19,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Android IP Webcam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Android IP Webcam\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Android IP Webcam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index b5c16826078..89e461c709d 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -29,7 +29,13 @@ "data": { "host": "Adre\u00e7a IP" }, - "description": "L'API local d'Awair s'ha d'activar seguint aquests passos: {url}" + "description": "Segueix [aquestes instruccions]({url}) per com activar l'API local d'Awair.\n\nPrem 'envia' quan hagis acabat." + }, + "local_pick": { + "data": { + "device": "Dispositiu", + "host": "Adre\u00e7a IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index 43b387e8286..cba100b899c 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -29,7 +29,13 @@ "data": { "host": "IP-Adresse" }, - "description": "Awair Local API muss wie folgt aktiviert werden: {url}" + "description": "Befolge [diese Anweisungen]({url}), um die Awair Local API zu aktivieren. \n\nKlicke abschlie\u00dfend auf Senden." + }, + "local_pick": { + "data": { + "device": "Ger\u00e4t", + "host": "IP-Adresse" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/en.json b/homeassistant/components/awair/translations/en.json index ca572958c46..dfc06d2346a 100644 --- a/homeassistant/components/awair/translations/en.json +++ b/homeassistant/components/awair/translations/en.json @@ -29,7 +29,13 @@ "data": { "host": "IP Address" }, - "description": "Awair Local API must be enabled following these steps: {url}" + "description": "Follow [these instructions]({url}) on how to enable the Awair Local API.\n\nClick submit when done." + }, + "local_pick": { + "data": { + "device": "Device", + "host": "IP Address" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index df5667ff1f9..82568ce9983 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -29,7 +29,13 @@ "data": { "host": "Direcci\u00f3n IP" }, - "description": "La API local de Awair debe estar habilitada siguiendo estos pasos: {url}" + "description": "Sigue [estas instrucciones]({url}) sobre c\u00f3mo habilitar la API local de Awair. \n\nHaz clic en enviar cuando hayas terminado." + }, + "local_pick": { + "data": { + "device": "Dispositivo", + "host": "Direcci\u00f3n IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json index c40f4be1f1c..327379ace6e 100644 --- a/homeassistant/components/awair/translations/et.json +++ b/homeassistant/components/awair/translations/et.json @@ -29,7 +29,13 @@ "data": { "host": "IP aadress" }, - "description": "Awairi kohalik API tuleb lubada j\u00e4rgmiste sammude abil: {url}" + "description": "J\u00e4rgi [neid juhiseid]({url}) Awair Local API lubamise kohta.\n\nKui oled l\u00f5petanud, kl\u00f5psa nuppu Esita." + }, + "local_pick": { + "data": { + "device": "Seade", + "host": "IP aadress" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 835f0d7716a..39b63bef7e5 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -31,6 +31,12 @@ }, "description": "API Awair Local harus diaktifkan dengan mengikuti langkah-langkah berikut: {url}" }, + "local_pick": { + "data": { + "device": "Perangkat", + "host": "Alamat IP" + } + }, "reauth": { "data": { "access_token": "Token Akses", diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json index d82934fceb9..c8c1f1f6ec6 100644 --- a/homeassistant/components/awair/translations/it.json +++ b/homeassistant/components/awair/translations/it.json @@ -29,7 +29,13 @@ "data": { "host": "Indirizzo IP" }, - "description": "Awair Local API deve essere abilitato seguendo questi passaggi: {url}" + "description": "Segui [queste istruzioni]({url}) su come abilitare l'API Awair Local. \n\n Fai clic su Invia quando hai finito." + }, + "local_pick": { + "data": { + "device": "Dispositivo", + "host": "Indirizzo IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index 9ffbf544909..d1c7e89f204 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -31,6 +31,12 @@ }, "description": "Awair Local API m\u00e5 aktiveres ved \u00e5 f\u00f8lge disse trinnene: {url}" }, + "local_pick": { + "data": { + "device": "Enhet", + "host": "IP adresse" + } + }, "reauth": { "data": { "access_token": "Tilgangstoken", diff --git a/homeassistant/components/awair/translations/pt-BR.json b/homeassistant/components/awair/translations/pt-BR.json index e3c3e24f738..20d2c104b60 100644 --- a/homeassistant/components/awair/translations/pt-BR.json +++ b/homeassistant/components/awair/translations/pt-BR.json @@ -29,7 +29,13 @@ "data": { "host": "Endere\u00e7o IP" }, - "description": "Awair Local API deve ser ativada seguindo estas etapas: {url}" + "description": "Siga [estas instru\u00e7\u00f5es]({url}) sobre como ativar a API local Awair. \n\n Clique em enviar quando terminar." + }, + "local_pick": { + "data": { + "device": "Dispositivo", + "host": "Endere\u00e7o IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index c0be2df4b0a..a81ef9585bd 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -31,6 +31,12 @@ }, "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 API Awair, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0437\u0434\u0435\u0441\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f: {url}" }, + "local_pick": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "host": "IP-\u0430\u0434\u0440\u0435\u0441" + } + }, "reauth": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", diff --git a/homeassistant/components/fully_kiosk/translations/ca.json b/homeassistant/components/fully_kiosk/translations/ca.json new file mode 100644 index 00000000000..2cb3945ed01 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El compte 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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/es.json b/homeassistant/components/fully_kiosk/translations/es.json new file mode 100644 index 00000000000..1b617892060 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/et.json b/homeassistant/components/fully_kiosk/translations/et.json new file mode 100644 index 00000000000..b046deb0527 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/it.json b/homeassistant/components/fully_kiosk/translations/it.json new file mode 100644 index 00000000000..f8b414166c9 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/pt-BR.json b/homeassistant/components/fully_kiosk/translations/pt-BR.json new file mode 100644 index 00000000000..172933953e8 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index c28014031cc..06f610099c6 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -44,6 +44,8 @@ "button_2": "Zweite Taste", "button_3": "Dritte Taste", "button_4": "Vierte Taste", + "clock_wise": "Drehung im Uhrzeigersinn", + "counter_clock_wise": "Drehung gegen den Uhrzeigersinn", "dim_down": "Dimmer runter", "dim_up": "Dimmer hoch", "double_buttons_1_3": "Erste und dritte Taste", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Beide \"{subtype}\" losgelassen", - "initial_press": "Taste \"{subtype}\" anfangs gedr\u00fcckt", - "long_release": "Taste \"{subtype}\" nach langem Dr\u00fccken losgelassen", - "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", - "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", - "remote_button_short_release": "\"{subtype}\" Taste losgelassen", + "initial_press": "\"{subtype}\" anfangs gedr\u00fcckt", + "long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen", + "remote_button_long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen", + "remote_button_short_press": "\"{subtype}\" gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" losgelassen", "remote_double_button_long_press": "Beide \"{subtype}\" nach langem Dr\u00fccken losgelassen", "remote_double_button_short_press": "Beide \"{subtype}\" losgelassen", - "repeat": "Taste \"{subtype}\" gedr\u00fcckt gehalten", - "short_release": "Taste \"{subtype}\" nach kurzem Dr\u00fccken losgelassen" + "repeat": "\"{subtype}\" gedr\u00fcckt gehalten", + "short_release": "\"{subtype}\" nach kurzem Dr\u00fccken losgelassen", + "start": "\"{subtype}\" wurde anf\u00e4nglich gedr\u00fcckt" } }, "options": { diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index 96f3388f2d3..eede6e94959 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -44,6 +44,8 @@ "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", "button_4": "Cuarto bot\u00f3n", + "clock_wise": "Rotaci\u00f3n en el sentido de las agujas del reloj", + "counter_clock_wise": "Rotaci\u00f3n en sentido contrario a las agujas del reloj", "dim_down": "Bajar la intensidad", "dim_up": "Subir la intensidad", "double_buttons_1_3": "Botones Primero y Tercero", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Ambos \"{subtype}\" soltados", - "initial_press": "Bot\u00f3n \"{subtype}\" pulsado inicialmente", - "long_release": "Bot\u00f3n \"{subtype}\" soltado tras una pulsaci\u00f3n larga", - "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", - "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", - "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", + "initial_press": "\"{subtype}\" pulsado inicialmente", + "long_release": "\"{subtype}\" soltado tras una pulsaci\u00f3n larga", + "remote_button_long_release": "\"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", + "remote_button_short_press": "\"{subtype}\" pulsado", + "remote_button_short_release": "\"{subtype}\" soltado", "remote_double_button_long_press": "Ambos \"{subtype}\" soltados despu\u00e9s de pulsaci\u00f3n larga", "remote_double_button_short_press": "Ambos \"{subtype}\" soltados", - "repeat": "Bot\u00f3n \"{subtype}\" presionado", - "short_release": "Bot\u00f3n \"{subtype}\" soltado tras una breve pulsaci\u00f3n" + "repeat": "\"{subtype}\" mantenido pulsado", + "short_release": "\"{subtype}\" soltado tras una breve pulsaci\u00f3n", + "start": "\"{subtype}\" pulsado inicialmente" } }, "options": { diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json index df1288d415d..274502b1587 100644 --- a/homeassistant/components/hue/translations/et.json +++ b/homeassistant/components/hue/translations/et.json @@ -44,6 +44,8 @@ "button_2": "Teine nupp", "button_3": "Kolmas nupp", "button_4": "Neljas nupp", + "clock_wise": "P\u00f6\u00f6rlemine p\u00e4rip\u00e4eva", + "counter_clock_wise": "P\u00f6\u00f6ramine vastup\u00e4eva", "dim_down": "H\u00e4marda", "dim_up": "Tee heledamaks", "double_buttons_1_3": "Esimene ja kolmas nupp", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "\"{subtype}\" nupp vabastatati", - "initial_press": "Nuppu \"{subtype}\" on vajutatud", - "long_release": "Nupp \"{subtype}\" vabastati p\u00e4rast pikka vajutust", - "remote_button_long_release": "\"{subtype}\" nupp vabastatati p\u00e4rast pikka vajutust", - "remote_button_short_press": "\"{subtype}\" nupp on vajutatud", - "remote_button_short_release": "\"{subtype}\" nupp vabastati", + "initial_press": "\"{subtype}\" on vajutatud", + "long_release": "\"{subtype}\" vabastati p\u00e4rast pikka vajutust", + "remote_button_long_release": "\"{subtype}\" vabastatati p\u00e4rast pikka vajutust", + "remote_button_short_press": "\"{subtype}\" vajutati", + "remote_button_short_release": "\"{subtype}\" vabastati", "remote_double_button_long_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati p\u00e4rast pikka vajutust", "remote_double_button_short_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati", - "repeat": "Nuppu \" {subtype} \" hoitakse all", - "short_release": "Nupp \" {subtype} \" vabastati p\u00e4rast l\u00fchikest vajutust" + "repeat": "\" {subtype} \" hoitakse all", + "short_release": "\" {subtype} \" vabastati p\u00e4rast l\u00fchikest vajutust", + "start": "vajutati \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json index 0c1dfce4c1a..be64a3b0624 100644 --- a/homeassistant/components/hue/translations/it.json +++ b/homeassistant/components/hue/translations/it.json @@ -44,6 +44,8 @@ "button_2": "Secondo pulsante", "button_3": "Terzo pulsante", "button_4": "Quarto pulsante", + "clock_wise": "Rotazione in senso orario", + "counter_clock_wise": "Rotazione in senso antiorario", "dim_down": "Diminuisce luminosit\u00e0", "dim_up": "Aumenta luminosit\u00e0", "double_buttons_1_3": "Pulsanti Primo e Terzo", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Entrambi \"{subtype}\" rilasciati", - "initial_press": "Pulsante \"{subtype}\" premuto inizialmente", - "long_release": "Pulsante \"{subtype}\" rilasciato dopo una pressione prolungata", - "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", - "remote_button_short_press": "Pulsante \"{subtype}\" premuto", - "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", + "initial_press": "\"{subtype}\" premuto inizialmente", + "long_release": "\"{subtype}\" rilasciato dopo una pressione prolungata", + "remote_button_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_short_press": "\"{subtype}\" premuto", + "remote_button_short_release": "\"{subtype}\" rilasciato", "remote_double_button_long_press": "Entrambi i \"{subtype}\" rilasciati dopo una lunga pressione", "remote_double_button_short_press": "Entrambi i \"{subtype}\" rilasciati", - "repeat": "Pulsante \"{subtype}\" tenuto premuto", - "short_release": "Pulsante \"{subtype}\" rilasciato dopo una breve pressione" + "repeat": "\"{subtype}\" tenuto premuto", + "short_release": "\"{subtype}\" rilasciato dopo una breve pressione", + "start": "\"{subtype}\" premuto inizialmente" } }, "options": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index ec88173adca..f5eddbc0242 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -44,6 +44,8 @@ "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "clock_wise": "\u6642\u8a08\u56de\u308a\u306b\u56de\u8ee2", + "counter_clock_wise": "\u53cd\u6642\u8a08\u56de\u308a\u306b\u56de\u8ee2", "dim_down": "\u8584\u6697\u304f\u3059\u308b", "dim_up": "\u5fae\u304b\u306b\u660e\u308b\u304f\u3059\u308b", "double_buttons_1_3": "1\u756a\u76ee\u30683\u756a\u76ee\u306e\u30dc\u30bf\u30f3", diff --git a/homeassistant/components/hue/translations/no.json b/homeassistant/components/hue/translations/no.json index d34899db978..598aa77629f 100644 --- a/homeassistant/components/hue/translations/no.json +++ b/homeassistant/components/hue/translations/no.json @@ -44,6 +44,8 @@ "button_2": "Andre knapp", "button_3": "Tredje knapp", "button_4": "Fjerde knapp", + "clock_wise": "Rotasjon med klokken", + "counter_clock_wise": "Rotasjon mot klokken", "dim_down": "Dimm ned", "dim_up": "Dimm opp", "double_buttons_1_3": "F\u00f8rste og tredje knapper", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Begge \"{subtype}\" er utgitt", - "initial_press": "Knappen \"{subtype}\" ble f\u00f8rst trykket", - "long_release": "Knapp \"{subtype}\" slippes etter lang trykk", - "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", - "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", - "remote_button_short_release": "\"{subtype}\"-knappen sluppet", + "initial_press": "\" {subtype} \" trykket f\u00f8rst", + "long_release": "\" {subtype} \" utgitt etter langt trykk", + "remote_button_long_release": "\" {subtype} \" utgitt etter langt trykk", + "remote_button_short_press": "\" {subtype} \" trykket", + "remote_button_short_release": "\" {subtype} \" utgitt", "remote_double_button_long_press": "Begge \"{subtype}\" utgitt etter lang trykk", "remote_double_button_short_press": "Begge \"{subtype}\" utgitt", - "repeat": "Knappen \" {subtype} \" holdt nede", - "short_release": "Knapp \"{subtype}\" slippes etter kort trykk" + "repeat": "\" {subtype} \" holdt nede", + "short_release": "\" {subtype} \" utgitt etter kort trykk", + "start": "\" {subtype} \" trykket f\u00f8rst" } }, "options": { diff --git a/homeassistant/components/hue/translations/ru.json b/homeassistant/components/hue/translations/ru.json index 6e470b74f1f..3f34b6b99e4 100644 --- a/homeassistant/components/hue/translations/ru.json +++ b/homeassistant/components/hue/translations/ru.json @@ -44,6 +44,8 @@ "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", + "clock_wise": "\u0412\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043f\u043e \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0435", + "counter_clock_wise": "\u0412\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0442\u0438\u0432 \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0438", "dim_down": "\u0423\u043c\u0435\u043d\u044c\u0448\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", "dim_up": "\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", "double_buttons_1_3": "\u041f\u0435\u0440\u0432\u0430\u044f \u0438 \u0442\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0438", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_double_button_short_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "repeat": "{subtype} \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", - "short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f" + "short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "start": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e" } }, "options": { diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index 1816b459a85..db9ae4c5355 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -44,6 +44,8 @@ "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "clock_wise": "\u9806\u6642\u91dd\u65cb\u8f49", + "counter_clock_wise": "\u9006\u6642\u91dd\u65cb\u8f49", "dim_down": "\u8abf\u6697", "dim_up": "\u8abf\u4eae", "double_buttons_1_3": "\u7b2c\u4e00\u8207\u7b2c\u4e09\u500b\u6309\u9215", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "\"{subtype}\" \u4e00\u8d77\u91cb\u653e", - "initial_press": "\u6309\u9215 \"{subtype}\" \u6700\u521d\u6309\u4e0b", - "long_release": "\u6309\u9215 \"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", - "remote_button_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e", - "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", - "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", + "initial_press": "\"{subtype}\" \u6700\u521d\u6309\u4e0b", + "long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_short_press": "\"{subtype}\" \u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u5df2\u91cb\u653e", "remote_double_button_long_press": "\"{subtype}\" \u4e00\u8d77\u9577\u6309\u5f8c\u91cb\u653e", "remote_double_button_short_press": "\"{subtype}\" \u4e00\u8d77\u91cb\u653e", - "repeat": "\u6309\u9215 \"{subtype}\" \u6309\u4e0b", - "short_release": "\u6309\u9215 \"{subtype}\" \u77ed\u6309\u5f8c\u91cb\u653e" + "repeat": "\"{subtype}\" \u6309\u4e0b", + "short_release": "\"{subtype}\" \u77ed\u6309\u5f8c\u91cb\u653e", + "start": "\"{subtype}\" \u6700\u521d\u6309\u4e0b" } }, "options": { diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index aa33259c5a3..95559e14baf 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -43,11 +43,11 @@ "group_2_button_1": "Primer bot\u00f3n del segundo grupo", "group_2_button_2": "Segundo bot\u00f3n del segundo grupo", "lower": "Bajar", - "lower_1": "Bajar 1", - "lower_2": "Bajar 2", - "lower_3": "Bajar 3", - "lower_4": "Bajar 4", - "lower_all": "Bajar todo", + "lower_1": "Inferior 1", + "lower_2": "Inferior 2", + "lower_3": "Inferior 3", + "lower_4": "Inferior 4", + "lower_all": "Todos los inferiores", "off": "Apagado", "on": "Encendido", "open_1": "Abrir 1", diff --git a/homeassistant/components/openexchangerates/translations/ja.json b/homeassistant/components/openexchangerates/translations/ja.json index b4299af9207..0946ca7a4d9 100644 --- a/homeassistant/components/openexchangerates/translations/ja.json +++ b/homeassistant/components/openexchangerates/translations/ja.json @@ -26,6 +26,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Open Exchange Rates\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Open Exchange Rates\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Open Exchange Rates YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index c407990aa4d..11e28073318 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -24,6 +24,7 @@ "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_yaml": { + "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f" } } diff --git a/homeassistant/components/tuya/translations/select.ja.json b/homeassistant/components/tuya/translations/select.ja.json index 5712544feb3..5d8c83175b2 100644 --- a/homeassistant/components/tuya/translations/select.ja.json +++ b/homeassistant/components/tuya/translations/select.ja.json @@ -37,7 +37,7 @@ "90": "90\u00b0" }, "tuya__fingerbot_mode": { - "click": "\u62bc\u3059", + "click": "\u30d7\u30c3\u30b7\u30e5", "switch": "\u30b9\u30a4\u30c3\u30c1" }, "tuya__humidifier_level": { diff --git a/homeassistant/components/xiaomi_miio/translations/select.ca.json b/homeassistant/components/xiaomi_miio/translations/select.ca.json index bc96de04645..41d5169fda6 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ca.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ca.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Endavant", + "left": "Esquerra", + "right": "Dreta" + }, "xiaomi_miio__led_brightness": { "bright": "Brillant", "dim": "Atenua", "off": "OFF" + }, + "xiaomi_miio__ptc_level": { + "high": "Alt", + "low": "Baix", + "medium": "Mitj\u00e0" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.en.json b/homeassistant/components/xiaomi_miio/translations/select.en.json index 60a1d738b81..217b803b72f 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.en.json +++ b/homeassistant/components/xiaomi_miio/translations/select.en.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Forward", + "left": "Left", + "right": "Right" + }, "xiaomi_miio__led_brightness": { "bright": "Bright", "dim": "Dim", "off": "Off" + }, + "xiaomi_miio__ptc_level": { + "high": "High", + "low": "Low", + "medium": "Medium" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.es.json b/homeassistant/components/xiaomi_miio/translations/select.es.json index 3906ef91342..0dfd04e5ca2 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.es.json +++ b/homeassistant/components/xiaomi_miio/translations/select.es.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Adelante", + "left": "Izquierda", + "right": "Derecha" + }, "xiaomi_miio__led_brightness": { "bright": "Brillo", "dim": "Atenuar", "off": "Apagado" + }, + "xiaomi_miio__ptc_level": { + "high": "Alto", + "low": "Bajo", + "medium": "Medio" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.et.json b/homeassistant/components/xiaomi_miio/translations/select.et.json index 7195f5703b4..a13acab984f 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.et.json +++ b/homeassistant/components/xiaomi_miio/translations/select.et.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Edasi", + "left": "Vasakule", + "right": "Paremale" + }, "xiaomi_miio__led_brightness": { "bright": "Hele", "dim": "Tuhm", "off": "Kustu" + }, + "xiaomi_miio__ptc_level": { + "high": "K\u00f5rge", + "low": "Madal", + "medium": "Keskmine" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.it.json b/homeassistant/components/xiaomi_miio/translations/select.it.json index 21e79e41e99..dd755b4cf27 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.it.json +++ b/homeassistant/components/xiaomi_miio/translations/select.it.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Avanti", + "left": "Sinistra", + "right": "Destra" + }, "xiaomi_miio__led_brightness": { "bright": "Brillante", "dim": "Fioca", "off": "Spento" + }, + "xiaomi_miio__ptc_level": { + "high": "Alto", + "low": "Basso", + "medium": "Medio" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json b/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json index c4c1735bff3..1fba0d03d66 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json +++ b/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Avan\u00e7ar", + "left": "Esquerda", + "right": "Direita" + }, "xiaomi_miio__led_brightness": { "bright": "Brilhante", "dim": "Escurecido", "off": "Desligado" + }, + "xiaomi_miio__ptc_level": { + "high": "Alto", + "low": "Baixo", + "medium": "M\u00e9dio" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index b4ad58636a7..7786a3b7bf3 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -52,7 +52,7 @@ "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", - "light_transitioning_flag": "Erweiterten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", + "light_transitioning_flag": "Verbesserten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } }, From 8c62713af3efc736f5678b089ae29c9e468765e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 16 Aug 2022 20:49:27 -0400 Subject: [PATCH 413/903] Bump frontend to 20220816.0 (#76895) --- 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 ed9b381ee9d..207b57babb2 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==20220802.0"], + "requirements": ["home-assistant-frontend==20220816.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 82c37466db7..eaf463df32d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220802.0 +home-assistant-frontend==20220816.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index dfae2e4afd9..c4fdeec309b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -836,7 +836,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220802.0 +home-assistant-frontend==20220816.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0623b6c7c79..fca44241201 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -613,7 +613,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220802.0 +home-assistant-frontend==20220816.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 6f3cdb6db17f6736d9276bb15e0276ed4f417e66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Aug 2022 14:52:53 -1000 Subject: [PATCH 414/903] Reorganize bluetooth integration to prepare for remote and multi-adapter support (#76883) --- .../components/bluetooth/__init__.py | 443 ++---------------- homeassistant/components/bluetooth/const.py | 12 + homeassistant/components/bluetooth/manager.py | 393 ++++++++++++++++ homeassistant/components/bluetooth/models.py | 47 ++ tests/components/bluetooth/test_init.py | 100 ++-- .../test_passive_update_coordinator.py | 2 +- .../test_passive_update_processor.py | 2 +- tests/conftest.py | 4 +- 8 files changed, 542 insertions(+), 461 deletions(-) create mode 100644 homeassistant/components/bluetooth/manager.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 19df484c4e1..a90f367fc38 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,114 +1,51 @@ """The bluetooth integration.""" from __future__ import annotations -import asyncio from asyncio import Future from collections.abc import Callable -from dataclasses import dataclass -from datetime import datetime, timedelta -from enum import Enum -import logging -import time -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING import async_timeout -from bleak import BleakError -from dbus_next import InvalidMessageError from homeassistant import config_entries -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HomeAssistant, - callback as hass_callback, -) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import HomeAssistant, callback as hass_callback from homeassistant.helpers import discovery_flow -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.loader import async_get_bluetooth -from homeassistant.util.package import is_docker_env -from . import models -from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN -from .match import ( - ADDRESS, - BluetoothCallbackMatcher, - IntegrationMatcher, - ble_device_matches, +from .const import CONF_ADAPTER, DOMAIN, SOURCE_LOCAL +from .manager import BluetoothManager +from .match import BluetoothCallbackMatcher, IntegrationMatcher +from .models import ( + BluetoothCallback, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfo, + BluetoothServiceInfoBleak, + HaBleakScannerWrapper, + ProcessAdvertisementCallback, ) -from .models import HaBleakScanner, HaBleakScannerWrapper -from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .util import async_get_bluetooth_adapters if TYPE_CHECKING: from bleak.backends.device import BLEDevice - from bleak.backends.scanner import AdvertisementData from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) - - -UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 -START_TIMEOUT = 9 - -SOURCE_LOCAL: Final = "local" - -SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 -SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) -MONOTONIC_TIME = time.monotonic - - -@dataclass -class BluetoothServiceInfoBleak(BluetoothServiceInfo): - """BluetoothServiceInfo with bleak data. - - Integrations may need BLEDevice and AdvertisementData - to connect to the device without having bleak trigger - another scan to translate the address to the system's - internal details. - """ - - device: BLEDevice - advertisement: AdvertisementData - - @classmethod - def from_advertisement( - cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str - ) -> BluetoothServiceInfoBleak: - """Create a BluetoothServiceInfoBleak from an advertisement.""" - return cls( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=device.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - source=source, - device=device, - advertisement=advertisement_data, - ) - - -class BluetoothScanningMode(Enum): - """The mode of scanning for bluetooth devices.""" - - PASSIVE = "passive" - ACTIVE = "active" - - -SCANNING_MODE_TO_BLEAK = { - BluetoothScanningMode.ACTIVE: "active", - BluetoothScanningMode.PASSIVE: "passive", -} - - -BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") -BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] -ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] +__all__ = [ + "async_ble_device_from_address", + "async_discovered_service_info", + "async_get_scanner", + "async_process_advertisements", + "async_rediscover_address", + "async_register_callback", + "async_track_unavailable", + "BluetoothServiceInfo", + "BluetoothServiceInfoBleak", + "BluetoothScanningMode", + "BluetoothCallback", + "SOURCE_LOCAL", +] @hass_callback @@ -287,329 +224,3 @@ async def async_unload_entry( manager.async_start_reload() await manager.async_stop() return True - - -class BluetoothManager: - """Manage Bluetooth.""" - - def __init__( - self, - hass: HomeAssistant, - integration_matcher: IntegrationMatcher, - ) -> None: - """Init bluetooth discovery.""" - self.hass = hass - self._integration_matcher = integration_matcher - self.scanner: HaBleakScanner | None = None - self.start_stop_lock = asyncio.Lock() - self._cancel_device_detected: CALLBACK_TYPE | None = None - self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None - self._cancel_stop: CALLBACK_TYPE | None = None - self._cancel_watchdog: CALLBACK_TYPE | None = None - self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} - self._callbacks: list[ - tuple[BluetoothCallback, BluetoothCallbackMatcher | None] - ] = [] - self._last_detection = 0.0 - self._reloading = False - self._adapter: str | None = None - self._scanning_mode = BluetoothScanningMode.ACTIVE - - @hass_callback - def async_setup(self) -> None: - """Set up the bluetooth manager.""" - models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() - - @hass_callback - def async_get_scanner(self) -> HaBleakScannerWrapper: - """Get the scanner.""" - return HaBleakScannerWrapper() - - @hass_callback - def async_start_reload(self) -> None: - """Start reloading.""" - self._reloading = True - - async def async_start( - self, scanning_mode: BluetoothScanningMode, adapter: str | None - ) -> None: - """Set up BT Discovery.""" - assert self.scanner is not None - self._adapter = adapter - self._scanning_mode = scanning_mode - if self._reloading: - # On reload, we need to reset the scanner instance - # since the devices in its history may not be reachable - # anymore. - self.scanner.async_reset() - self._integration_matcher.async_clear_history() - self._reloading = False - scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} - if adapter and adapter not in DEFAULT_ADAPTERS: - scanner_kwargs["adapter"] = adapter - _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) - try: - self.scanner.async_setup(**scanner_kwargs) - except (FileNotFoundError, BleakError) as ex: - raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex - install_multiple_bleak_catcher() - # We have to start it right away as some integrations might - # need it straight away. - _LOGGER.debug("Starting bluetooth scanner") - self.scanner.register_detection_callback(self.scanner.async_callback_dispatcher) - self._cancel_device_detected = self.scanner.async_register_callback( - self._device_detected, {} - ) - try: - async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() # type: ignore[no-untyped-call] - except InvalidMessageError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) - raise ConfigEntryNotReady( - f"Invalid DBus message received: {ex}; try restarting `dbus`" - ) from ex - except BrokenPipeError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) - if is_docker_env(): - raise ConfigEntryNotReady( - f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" - ) from ex - raise ConfigEntryNotReady( - f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" - ) from ex - except FileNotFoundError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug( - "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True - ) - if is_docker_env(): - raise ConfigEntryNotReady( - f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" - ) from ex - raise ConfigEntryNotReady( - f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" - ) from ex - except asyncio.TimeoutError as ex: - self._async_cancel_scanner_callback() - raise ConfigEntryNotReady( - f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" - ) from ex - except BleakError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) - raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex - self.async_setup_unavailable_tracking() - self._async_setup_scanner_watchdog() - self._cancel_stop = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping - ) - - @hass_callback - def _async_setup_scanner_watchdog(self) -> None: - """If Dbus gets restarted or updated, we need to restart the scanner.""" - self._last_detection = MONOTONIC_TIME() - self._cancel_watchdog = async_track_time_interval( - self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL - ) - - async def _async_scanner_watchdog(self, now: datetime) -> None: - """Check if the scanner is running.""" - time_since_last_detection = MONOTONIC_TIME() - self._last_detection - if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: - return - _LOGGER.info( - "Bluetooth scanner has gone quiet for %s, restarting", - SCANNER_WATCHDOG_INTERVAL, - ) - async with self.start_stop_lock: - self.async_start_reload() - await self.async_stop() - await self.async_start(self._scanning_mode, self._adapter) - - @hass_callback - def async_setup_unavailable_tracking(self) -> None: - """Set up the unavailable tracking.""" - - @hass_callback - def _async_check_unavailable(now: datetime) -> None: - """Watch for unavailable devices.""" - scanner = self.scanner - assert scanner is not None - history = set(scanner.history) - active = {device.address for device in scanner.discovered_devices} - disappeared = history.difference(active) - for address in disappeared: - del scanner.history[address] - if not (callbacks := self._unavailable_callbacks.get(address)): - continue - for callback in callbacks: - try: - callback(address) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in unavailable callback") - - self._cancel_unavailable_tracking = async_track_time_interval( - self.hass, - _async_check_unavailable, - timedelta(seconds=UNAVAILABLE_TRACK_SECONDS), - ) - - @hass_callback - def _device_detected( - self, device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - self._last_detection = MONOTONIC_TIME() - matched_domains = self._integration_matcher.match_domains( - device, advertisement_data - ) - _LOGGER.debug( - "Device detected: %s with advertisement_data: %s matched domains: %s", - device.address, - advertisement_data, - matched_domains, - ) - - if not matched_domains and not self._callbacks: - return - - service_info: BluetoothServiceInfoBleak | None = None - for callback, matcher in self._callbacks: - if matcher is None or ble_device_matches( - matcher, device, advertisement_data - ): - if service_info is None: - service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, SOURCE_LOCAL - ) - try: - callback(service_info, BluetoothChange.ADVERTISEMENT) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in bluetooth callback") - - if not matched_domains: - return - if service_info is None: - service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, SOURCE_LOCAL - ) - for domain in matched_domains: - discovery_flow.async_create_flow( - self.hass, - domain, - {"source": config_entries.SOURCE_BLUETOOTH}, - service_info, - ) - - @hass_callback - def async_track_unavailable( - self, callback: Callable[[str], None], address: str - ) -> Callable[[], None]: - """Register a callback.""" - self._unavailable_callbacks.setdefault(address, []).append(callback) - - @hass_callback - def _async_remove_callback() -> None: - self._unavailable_callbacks[address].remove(callback) - if not self._unavailable_callbacks[address]: - del self._unavailable_callbacks[address] - - return _async_remove_callback - - @hass_callback - def async_register_callback( - self, - callback: BluetoothCallback, - matcher: BluetoothCallbackMatcher | None = None, - ) -> Callable[[], None]: - """Register a callback.""" - callback_entry = (callback, matcher) - self._callbacks.append(callback_entry) - - @hass_callback - def _async_remove_callback() -> None: - self._callbacks.remove(callback_entry) - - # If we have history for the subscriber, we can trigger the callback - # immediately with the last packet so the subscriber can see the - # device. - if ( - matcher - and (address := matcher.get(ADDRESS)) - and self.scanner - and (device_adv_data := self.scanner.history.get(address)) - ): - try: - callback( - BluetoothServiceInfoBleak.from_advertisement( - *device_adv_data, SOURCE_LOCAL - ), - BluetoothChange.ADVERTISEMENT, - ) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in bluetooth callback") - - return _async_remove_callback - - @hass_callback - def async_ble_device_from_address(self, address: str) -> BLEDevice | None: - """Return the BLEDevice if present.""" - if self.scanner and (ble_adv := self.scanner.history.get(address)): - return ble_adv[0] - return None - - @hass_callback - def async_address_present(self, address: str) -> bool: - """Return if the address is present.""" - return bool(self.scanner and address in self.scanner.history) - - @hass_callback - def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: - """Return if the address is present.""" - assert self.scanner is not None - return [ - BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) - for device_adv in self.scanner.history.values() - ] - - async def _async_hass_stopping(self, event: Event) -> None: - """Stop the Bluetooth integration at shutdown.""" - self._cancel_stop = None - await self.async_stop() - - @hass_callback - def _async_cancel_scanner_callback(self) -> None: - """Cancel the scanner callback.""" - if self._cancel_device_detected: - self._cancel_device_detected() - self._cancel_device_detected = None - - async def async_stop(self) -> None: - """Stop bluetooth discovery.""" - _LOGGER.debug("Stopping bluetooth discovery") - if self._cancel_watchdog: - self._cancel_watchdog() - self._cancel_watchdog = None - self._async_cancel_scanner_callback() - if self._cancel_unavailable_tracking: - self._cancel_unavailable_tracking() - self._cancel_unavailable_tracking = None - if self._cancel_stop: - self._cancel_stop() - self._cancel_stop = None - if self.scanner: - try: - await self.scanner.stop() # type: ignore[no-untyped-call] - except BleakError as ex: - # This is not fatal, and they may want to reload - # the config entry to restart the scanner if they - # change the bluetooth dongle. - _LOGGER.error("Error stopping scanner: %s", ex) - uninstall_multiple_bleak_catcher() - - @hass_callback - def async_rediscover_address(self, address: str) -> None: - """Trigger discovery of devices which have already been seen.""" - self._integration_matcher.async_clear_address(address) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index f3f00f581ee..fac191202b0 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -1,4 +1,8 @@ """Constants for the Bluetooth integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Final DOMAIN = "bluetooth" DEFAULT_NAME = "Bluetooth" @@ -9,3 +13,11 @@ MACOS_DEFAULT_BLUETOOTH_ADAPTER = "CoreBluetooth" UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} + +SOURCE_LOCAL: Final = "local" + + +UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 +START_TIMEOUT = 12 +SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 +SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py new file mode 100644 index 00000000000..e4f75350575 --- /dev/null +++ b/homeassistant/components/bluetooth/manager.py @@ -0,0 +1,393 @@ +"""The bluetooth integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from datetime import datetime, timedelta +import logging +import time +from typing import TYPE_CHECKING + +import async_timeout +from bleak import BleakError +from dbus_next import InvalidMessageError + +from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + callback as hass_callback, +) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery_flow +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.package import is_docker_env + +from . import models +from .const import ( + DEFAULT_ADAPTERS, + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, + SOURCE_LOCAL, + START_TIMEOUT, + UNAVAILABLE_TRACK_SECONDS, +) +from .match import ( + ADDRESS, + BluetoothCallbackMatcher, + IntegrationMatcher, + ble_device_matches, +) +from .models import ( + BluetoothCallback, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + HaBleakScanner, + HaBleakScannerWrapper, +) +from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + +_LOGGER = logging.getLogger(__name__) + + +MONOTONIC_TIME = time.monotonic + + +SCANNING_MODE_TO_BLEAK = { + BluetoothScanningMode.ACTIVE: "active", + BluetoothScanningMode.PASSIVE: "passive", +} + + +class BluetoothManager: + """Manage Bluetooth.""" + + def __init__( + self, + hass: HomeAssistant, + integration_matcher: IntegrationMatcher, + ) -> None: + """Init bluetooth discovery.""" + self.hass = hass + self._integration_matcher = integration_matcher + self.scanner: HaBleakScanner | None = None + self.start_stop_lock = asyncio.Lock() + self._cancel_device_detected: CALLBACK_TYPE | None = None + self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None + self._cancel_stop: CALLBACK_TYPE | None = None + self._cancel_watchdog: CALLBACK_TYPE | None = None + self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} + self._callbacks: list[ + tuple[BluetoothCallback, BluetoothCallbackMatcher | None] + ] = [] + self._last_detection = 0.0 + self._reloading = False + self._adapter: str | None = None + self._scanning_mode = BluetoothScanningMode.ACTIVE + + @hass_callback + def async_setup(self) -> None: + """Set up the bluetooth manager.""" + models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() + + @hass_callback + def async_get_scanner(self) -> HaBleakScannerWrapper: + """Get the scanner.""" + return HaBleakScannerWrapper() + + @hass_callback + def async_start_reload(self) -> None: + """Start reloading.""" + self._reloading = True + + async def async_start( + self, scanning_mode: BluetoothScanningMode, adapter: str | None + ) -> None: + """Set up BT Discovery.""" + assert self.scanner is not None + self._adapter = adapter + self._scanning_mode = scanning_mode + if self._reloading: + # On reload, we need to reset the scanner instance + # since the devices in its history may not be reachable + # anymore. + self.scanner.async_reset() + self._integration_matcher.async_clear_history() + self._reloading = False + scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} + if adapter and adapter not in DEFAULT_ADAPTERS: + scanner_kwargs["adapter"] = adapter + _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) + try: + self.scanner.async_setup(**scanner_kwargs) + except (FileNotFoundError, BleakError) as ex: + raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex + install_multiple_bleak_catcher() + # We have to start it right away as some integrations might + # need it straight away. + _LOGGER.debug("Starting bluetooth scanner") + self.scanner.register_detection_callback(self.scanner.async_callback_dispatcher) + self._cancel_device_detected = self.scanner.async_register_callback( + self._device_detected, {} + ) + try: + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + self._async_cancel_scanner_callback() + _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) + raise ConfigEntryNotReady( + f"Invalid DBus message received: {ex}; try restarting `dbus`" + ) from ex + except BrokenPipeError as ex: + self._async_cancel_scanner_callback() + _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + self._async_cancel_scanner_callback() + _LOGGER.debug( + "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ConfigEntryNotReady( + f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" + ) from ex + except asyncio.TimeoutError as ex: + self._async_cancel_scanner_callback() + raise ConfigEntryNotReady( + f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex + except BleakError as ex: + self._async_cancel_scanner_callback() + _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) + raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex + self.async_setup_unavailable_tracking() + self._async_setup_scanner_watchdog() + self._cancel_stop = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping + ) + + @hass_callback + def _async_setup_scanner_watchdog(self) -> None: + """If Dbus gets restarted or updated, we need to restart the scanner.""" + self._last_detection = MONOTONIC_TIME() + self._cancel_watchdog = async_track_time_interval( + self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL + ) + + async def _async_scanner_watchdog(self, now: datetime) -> None: + """Check if the scanner is running.""" + time_since_last_detection = MONOTONIC_TIME() - self._last_detection + if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: + return + _LOGGER.info( + "Bluetooth scanner has gone quiet for %s, restarting", + SCANNER_WATCHDOG_INTERVAL, + ) + async with self.start_stop_lock: + self.async_start_reload() + await self.async_stop() + await self.async_start(self._scanning_mode, self._adapter) + + @hass_callback + def async_setup_unavailable_tracking(self) -> None: + """Set up the unavailable tracking.""" + + @hass_callback + def _async_check_unavailable(now: datetime) -> None: + """Watch for unavailable devices.""" + scanner = self.scanner + assert scanner is not None + history = set(scanner.history) + active = {device.address for device in scanner.discovered_devices} + disappeared = history.difference(active) + for address in disappeared: + del scanner.history[address] + if not (callbacks := self._unavailable_callbacks.get(address)): + continue + for callback in callbacks: + try: + callback(address) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in unavailable callback") + + self._cancel_unavailable_tracking = async_track_time_interval( + self.hass, + _async_check_unavailable, + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS), + ) + + @hass_callback + def _device_detected( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + self._last_detection = MONOTONIC_TIME() + matched_domains = self._integration_matcher.match_domains( + device, advertisement_data + ) + _LOGGER.debug( + "Device detected: %s with advertisement_data: %s matched domains: %s", + device.address, + advertisement_data, + matched_domains, + ) + + if not matched_domains and not self._callbacks: + return + + service_info: BluetoothServiceInfoBleak | None = None + for callback, matcher in self._callbacks: + if matcher is None or ble_device_matches( + matcher, device, advertisement_data + ): + if service_info is None: + service_info = BluetoothServiceInfoBleak.from_advertisement( + device, advertisement_data, SOURCE_LOCAL + ) + try: + callback(service_info, BluetoothChange.ADVERTISEMENT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + + if not matched_domains: + return + if service_info is None: + service_info = BluetoothServiceInfoBleak.from_advertisement( + device, advertisement_data, SOURCE_LOCAL + ) + for domain in matched_domains: + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) + + @hass_callback + def async_track_unavailable( + self, callback: Callable[[str], None], address: str + ) -> Callable[[], None]: + """Register a callback.""" + self._unavailable_callbacks.setdefault(address, []).append(callback) + + @hass_callback + def _async_remove_callback() -> None: + self._unavailable_callbacks[address].remove(callback) + if not self._unavailable_callbacks[address]: + del self._unavailable_callbacks[address] + + return _async_remove_callback + + @hass_callback + def async_register_callback( + self, + callback: BluetoothCallback, + matcher: BluetoothCallbackMatcher | None = None, + ) -> Callable[[], None]: + """Register a callback.""" + callback_entry = (callback, matcher) + self._callbacks.append(callback_entry) + + @hass_callback + def _async_remove_callback() -> None: + self._callbacks.remove(callback_entry) + + # If we have history for the subscriber, we can trigger the callback + # immediately with the last packet so the subscriber can see the + # device. + if ( + matcher + and (address := matcher.get(ADDRESS)) + and self.scanner + and (device_adv_data := self.scanner.history.get(address)) + ): + try: + callback( + BluetoothServiceInfoBleak.from_advertisement( + *device_adv_data, SOURCE_LOCAL + ), + BluetoothChange.ADVERTISEMENT, + ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + + return _async_remove_callback + + @hass_callback + def async_ble_device_from_address(self, address: str) -> BLEDevice | None: + """Return the BLEDevice if present.""" + if self.scanner and (ble_adv := self.scanner.history.get(address)): + return ble_adv[0] + return None + + @hass_callback + def async_address_present(self, address: str) -> bool: + """Return if the address is present.""" + return bool(self.scanner and address in self.scanner.history) + + @hass_callback + def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: + """Return if the address is present.""" + assert self.scanner is not None + return [ + BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) + for device_adv in self.scanner.history.values() + ] + + async def _async_hass_stopping(self, event: Event) -> None: + """Stop the Bluetooth integration at shutdown.""" + self._cancel_stop = None + await self.async_stop() + + @hass_callback + def _async_cancel_scanner_callback(self) -> None: + """Cancel the scanner callback.""" + if self._cancel_device_detected: + self._cancel_device_detected() + self._cancel_device_detected = None + + async def async_stop(self) -> None: + """Stop bluetooth discovery.""" + _LOGGER.debug("Stopping bluetooth discovery") + if self._cancel_watchdog: + self._cancel_watchdog() + self._cancel_watchdog = None + self._async_cancel_scanner_callback() + if self._cancel_unavailable_tracking: + self._cancel_unavailable_tracking() + self._cancel_unavailable_tracking = None + if self._cancel_stop: + self._cancel_stop() + self._cancel_stop = None + if self.scanner: + try: + await self.scanner.stop() # type: ignore[no-untyped-call] + except BleakError as ex: + # This is not fatal, and they may want to reload + # the config entry to restart the scanner if they + # change the bluetooth dongle. + _LOGGER.error("Error stopping scanner: %s", ex) + uninstall_multiple_bleak_catcher() + + @hass_callback + def async_rediscover_address(self, address: str) -> None: + """Trigger discovery of devices which have already been seen.""" + self._integration_matcher.async_clear_address(address) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 51704a2f530..d5cb1429a2a 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -2,7 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import contextlib +from dataclasses import dataclass +from enum import Enum import logging from typing import TYPE_CHECKING, Any, Final @@ -14,6 +17,7 @@ from bleak.backends.scanner import ( ) from homeassistant.core import CALLBACK_TYPE, callback as hass_callback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -26,6 +30,49 @@ FILTER_UUIDS: Final = "UUIDs" HA_BLEAK_SCANNER: HaBleakScanner | None = None +@dataclass +class BluetoothServiceInfoBleak(BluetoothServiceInfo): + """BluetoothServiceInfo with bleak data. + + Integrations may need BLEDevice and AdvertisementData + to connect to the device without having bleak trigger + another scan to translate the address to the system's + internal details. + """ + + device: BLEDevice + advertisement: AdvertisementData + + @classmethod + def from_advertisement( + cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str + ) -> BluetoothServiceInfoBleak: + """Create a BluetoothServiceInfoBleak from an advertisement.""" + return cls( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=source, + device=device, + advertisement=advertisement_data, + ) + + +class BluetoothScanningMode(Enum): + """The mode of scanning for bluetooth devices.""" + + PASSIVE = "passive" + ACTIVE = "active" + + +BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") +BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] +ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] + + def _dispatch_callback( callback: AdvertisementDataCallback, filters: dict[str, set[str]], diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 796b3ffb469..2387d35fc23 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -10,20 +10,21 @@ import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( - SCANNER_WATCHDOG_INTERVAL, - SCANNER_WATCHDOG_TIMEOUT, - SOURCE_LOCAL, - UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothScanningMode, BluetoothServiceInfo, async_process_advertisements, async_rediscover_address, async_track_unavailable, + manager, models, ) from homeassistant.components.bluetooth.const import ( CONF_ADAPTER, + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, + SOURCE_LOCAL, + UNAVAILABLE_TRACK_SECONDS, UNIX_DEFAULT_BLUETOOTH_ADAPTER, ) from homeassistant.config_entries import ConfigEntryState @@ -62,7 +63,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} ] with patch( - "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup", side_effect=BleakError, ) as mock_ha_bleak_scanner, patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -83,8 +84,10 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): async def test_setup_and_stop_broken_bluetooth(hass, caplog): """Test we fail gracefully when bluetooth/dbus is broken.""" mock_bt = [] - with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -109,10 +112,10 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): async def _mock_hang(): await asyncio.sleep(1) - with patch.object(bluetooth, "START_TIMEOUT", 0), patch( - "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + with patch.object(manager, "START_TIMEOUT", 0), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" ), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=_mock_hang, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -132,8 +135,10 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): """Test we retry if the adapter is not yet available.""" mock_bt = [] - with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -152,14 +157,14 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): assert entry.state == ConfigEntryState.SETUP_RETRY with patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.models.HaBleakScanner.start", ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.HaBleakScanner.stop", + "homeassistant.components.bluetooth.models.HaBleakScanner.stop", ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -168,8 +173,10 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): """Test we can successfully reload when the entry is in a retry state.""" mock_bt = [] - with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -188,7 +195,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): assert entry.state == ConfigEntryState.SETUP_RETRY with patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.models.HaBleakScanner.start", ): await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() @@ -196,7 +203,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.HaBleakScanner.stop", + "homeassistant.components.bluetooth.models.HaBleakScanner.stop", ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -206,7 +213,7 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup", side_effect=FileNotFoundError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -1514,7 +1521,8 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.HaBleakScanner.stop", side_effect=BleakError + "homeassistant.components.bluetooth.models.HaBleakScanner.stop", + side_effect=BleakError, ): await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() @@ -1533,11 +1541,11 @@ async def test_changing_the_adapter_at_runtime(hass): entry.add_to_hass(hass) with patch( - "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" ) as mock_setup, patch( - "homeassistant.components.bluetooth.HaBleakScanner.start" + "homeassistant.components.bluetooth.models.HaBleakScanner.start" ), patch( - "homeassistant.components.bluetooth.HaBleakScanner.stop" + "homeassistant.components.bluetooth.models.HaBleakScanner.stop" ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1558,9 +1566,11 @@ async def test_dbus_socket_missing_in_container(hass, caplog): """Test we handle dbus being missing in the container.""" with patch( - "homeassistant.components.bluetooth.is_docker_env", return_value=True - ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.manager.is_docker_env", return_value=True + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=FileNotFoundError, ): assert await async_setup_component( @@ -1580,9 +1590,11 @@ async def test_dbus_socket_missing(hass, caplog): """Test we handle dbus being missing.""" with patch( - "homeassistant.components.bluetooth.is_docker_env", return_value=False - ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.manager.is_docker_env", return_value=False + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=FileNotFoundError, ): assert await async_setup_component( @@ -1602,9 +1614,11 @@ async def test_dbus_broken_pipe_in_container(hass, caplog): """Test we handle dbus broken pipe in the container.""" with patch( - "homeassistant.components.bluetooth.is_docker_env", return_value=True - ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.manager.is_docker_env", return_value=True + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BrokenPipeError, ): assert await async_setup_component( @@ -1625,9 +1639,11 @@ async def test_dbus_broken_pipe(hass, caplog): """Test we handle dbus broken pipe.""" with patch( - "homeassistant.components.bluetooth.is_docker_env", return_value=False - ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.manager.is_docker_env", return_value=False + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BrokenPipeError, ): assert await async_setup_component( @@ -1647,8 +1663,10 @@ async def test_dbus_broken_pipe(hass, caplog): async def test_invalid_dbus_message(hass, caplog): """Test we handle invalid dbus message.""" - with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=InvalidMessageError, ): assert await async_setup_component( @@ -1678,7 +1696,7 @@ async def test_recovery_from_dbus_restart( # Ensure we don't restart the scanner if we don't need to with patch( - "homeassistant.components.bluetooth.MONOTONIC_TIME", + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", return_value=start_time_monotonic + 10, ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) @@ -1688,7 +1706,7 @@ async def test_recovery_from_dbus_restart( # Fire a callback to reset the timer with patch( - "homeassistant.components.bluetooth.MONOTONIC_TIME", + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", return_value=start_time_monotonic, ): scanner._callback( @@ -1698,7 +1716,7 @@ async def test_recovery_from_dbus_restart( # Ensure we don't restart the scanner if we don't need to with patch( - "homeassistant.components.bluetooth.MONOTONIC_TIME", + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", return_value=start_time_monotonic + 20, ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) @@ -1708,7 +1726,7 @@ async def test_recovery_from_dbus_restart( # We hit the timer, so we restart the scanner with patch( - "homeassistant.components.bluetooth.MONOTONIC_TIME", + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 31530cd6995..12531c52e40 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -8,10 +8,10 @@ from unittest.mock import MagicMock, patch from homeassistant.components.bluetooth import ( DOMAIN, - UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothScanningMode, ) +from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, PassiveBluetoothDataUpdateCoordinator, diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 5653b938ada..6b21d1aa32c 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -14,10 +14,10 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.bluetooth import ( DOMAIN, - UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothScanningMode, ) +from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, diff --git a/tests/conftest.py b/tests/conftest.py index 50c24df8d44..3b43fcd14ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -904,8 +904,8 @@ def mock_bleak_scanner_start(): scanner = bleak.BleakScanner bluetooth_models.HA_BLEAK_SCANNER = None - with patch("homeassistant.components.bluetooth.HaBleakScanner.stop"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch("homeassistant.components.bluetooth.models.HaBleakScanner.stop"), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", ) as mock_bleak_scanner_start: yield mock_bleak_scanner_start From ec1b133201a4301f0179d1c10bdac9e1eafab16c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 17 Aug 2022 03:19:23 +0200 Subject: [PATCH 415/903] Add DHCP updates to Fully Kiosk (#76896) --- .../components/fully_kiosk/config_flow.py | 24 +++++++- .../components/fully_kiosk/entity.py | 2 + .../components/fully_kiosk/manifest.json | 3 +- homeassistant/generated/dhcp.py | 1 + tests/components/fully_kiosk/conftest.py | 9 ++- .../fully_kiosk/test_config_flow.py | 55 ++++++++++++++++++- 6 files changed, 87 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fully_kiosk/config_flow.py b/homeassistant/components/fully_kiosk/config_flow.py index 09eb94d6b07..5257030ecf0 100644 --- a/homeassistant/components/fully_kiosk/config_flow.py +++ b/homeassistant/components/fully_kiosk/config_flow.py @@ -11,9 +11,11 @@ from fullykiosk.exceptions import FullyKioskError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.components.dhcp import DhcpServiceInfo +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac from .const import DEFAULT_PORT, DOMAIN, LOGGER @@ -48,7 +50,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(device_info["deviceID"]) self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry( - title=device_info["deviceName"], data=user_input + title=device_info["deviceName"], + data=user_input | {CONF_MAC: format_mac(device_info["Mac"])}, ) return self.async_show_form( @@ -61,3 +64,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle dhcp discovery.""" + mac = format_mac(discovery_info.macaddress) + + for entry in self._async_current_entries(): + if entry.data[CONF_MAC] == mac: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | {CONF_HOST: discovery_info.ip}, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + + return self.async_abort(reason="unknown") diff --git a/homeassistant/components/fully_kiosk/entity.py b/homeassistant/components/fully_kiosk/entity.py index 4e50bb6efe6..7be06c79573 100644 --- a/homeassistant/components/fully_kiosk/entity.py +++ b/homeassistant/components/fully_kiosk/entity.py @@ -1,6 +1,7 @@ """Base entity for the Fully Kiosk Browser integration.""" from __future__ import annotations +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -23,4 +24,5 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit model=coordinator.data["deviceModel"], sw_version=coordinator.data["appVersionName"], configuration_url=f"http://{coordinator.data['ip4']}:2323", + connections={(CONNECTION_NETWORK_MAC, coordinator.data["Mac"])}, ) diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json index 40c7e5293e7..8918ce28062 100644 --- a/homeassistant/components/fully_kiosk/manifest.json +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -6,5 +6,6 @@ "requirements": ["python-fullykiosk==0.0.11"], "dependencies": [], "codeowners": ["@cgarwood"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "dhcp": [{ "registered_devices": true }] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 9179a314215..841db03c3a6 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -43,6 +43,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'flux_led', 'hostname': 'zengge_[0-9a-f][0-9a-f]_*'}, {'domain': 'flux_led', 'hostname': 'sta*', 'macaddress': 'C82E47*'}, {'domain': 'fronius', 'macaddress': '0003AC*'}, + {'domain': 'fully_kiosk', 'registered_devices': True}, {'domain': 'goalzero', 'registered_devices': True}, {'domain': 'goalzero', 'hostname': 'yeti*'}, {'domain': 'gogogate2', 'hostname': 'ismartgate*'}, diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py index c5476ea6a9d..35d5c12e694 100644 --- a/tests/components/fully_kiosk/conftest.py +++ b/tests/components/fully_kiosk/conftest.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.fully_kiosk.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -20,7 +20,11 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( title="Test device", domain=DOMAIN, - data={CONF_HOST: "127.0.0.1", CONF_PASSWORD: "mocked-password"}, + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "mocked-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, unique_id="12345", ) @@ -45,6 +49,7 @@ def mock_fully_kiosk_config_flow() -> Generator[MagicMock, None, None]: client.getDeviceInfo.return_value = { "deviceName": "Test device", "deviceID": "12345", + "Mac": "AA:BB:CC:DD:EE:FF", } yield client diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py index 2617a3f7adb..19a8715b4cd 100644 --- a/tests/components/fully_kiosk/test_config_flow.py +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -7,9 +7,10 @@ from aiohttp.client_exceptions import ClientConnectorError from fullykiosk import FullyKioskError import pytest +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fully_kiosk.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -42,6 +43,7 @@ async def test_full_flow( assert result2.get("data") == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", } assert "result" in result2 assert result2["result"].unique_id == "12345" @@ -95,6 +97,7 @@ async def test_errors( assert result3.get("data") == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", } assert "result" in result3 assert result3["result"].unique_id == "12345" @@ -131,6 +134,54 @@ async def test_duplicate_updates_existing_entry( assert mock_config_entry.data == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", } assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1 + + +async def test_dhcp_discovery_updates_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery updates config entries.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="tablet", + ip="127.0.0.2", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.2", + CONF_PASSWORD: "mocked-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + } + + +async def test_dhcp_unknown_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unknown DHCP discovery aborts flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="tablet", + ip="127.0.0.2", + macaddress="aa:bb:cc:dd:ee:00", + ), + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "unknown" From d73754d292136d7daf952fec731583a6e83833eb Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 17 Aug 2022 03:19:55 +0200 Subject: [PATCH 416/903] Fix TypeAlias + TypeVar names (#76897) --- homeassistant/components/samsungtv/bridge.py | 14 +++++++------- homeassistant/components/zamg/sensor.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index fe0b102647a..f1618bfba14 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -74,8 +74,8 @@ ENCRYPTED_MODEL_USES_POWER = {"JU6400", "JU641D"} REST_EXCEPTIONS = (HttpApiError, AsyncioTimeoutError, ResponseError) -_TRemote = TypeVar("_TRemote", SamsungTVWSAsyncRemote, SamsungTVEncryptedWSAsyncRemote) -_TCommand = TypeVar("_TCommand", SamsungTVCommand, SamsungTVEncryptedCommand) +_RemoteT = TypeVar("_RemoteT", SamsungTVWSAsyncRemote, SamsungTVEncryptedWSAsyncRemote) +_CommandT = TypeVar("_CommandT", SamsungTVCommand, SamsungTVEncryptedCommand) def mac_from_device_info(info: dict[str, Any]) -> str | None: @@ -367,7 +367,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): LOGGER.debug("Could not establish connection") -class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): +class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_RemoteT, _CommandT]): """The Bridge for WebSocket TVs (v1/v2).""" def __init__( @@ -379,7 +379,7 @@ class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): ) -> None: """Initialize Bridge.""" super().__init__(hass, method, host, port) - self._remote: _TRemote | None = None + self._remote: _RemoteT | None = None self._remote_lock = asyncio.Lock() async def async_is_on(self) -> bool: @@ -389,7 +389,7 @@ class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): return remote.is_alive() # type: ignore[no-any-return] return False - async def _async_send_commands(self, commands: list[_TCommand]) -> None: + async def _async_send_commands(self, commands: list[_CommandT]) -> None: """Send the commands using websocket protocol.""" try: # recreate connection if connection was dead @@ -410,7 +410,7 @@ class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): # Different reasons, e.g. hostname not resolveable pass - async def _async_get_remote(self) -> _TRemote | None: + async def _async_get_remote(self) -> _RemoteT | None: """Create or return a remote control instance.""" if (remote := self._remote) and remote.is_alive(): # If we have one then try to use it @@ -422,7 +422,7 @@ class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): return await self._async_get_remote_under_lock() @abstractmethod - async def _async_get_remote_under_lock(self) -> _TRemote | None: + async def _async_get_remote_under_lock(self) -> _RemoteT | None: """Create or return a remote control instance.""" async def async_close_remote(self) -> None: diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 8452841520b..e8d5f745cce 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -51,7 +51,7 @@ DEFAULT_NAME = "zamg" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) VIENNA_TIME_ZONE = dt_util.get_time_zone("Europe/Vienna") -DTypeT = Union[type[int], type[float], type[str]] +_DType = Union[type[int], type[float], type[str]] @dataclass @@ -59,7 +59,7 @@ class ZamgRequiredKeysMixin: """Mixin for required keys.""" col_heading: str - dtype: DTypeT + dtype: _DType @dataclass @@ -178,7 +178,7 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -API_FIELDS: dict[str, tuple[str, DTypeT]] = { +API_FIELDS: dict[str, tuple[str, _DType]] = { desc.col_heading: (desc.key, desc.dtype) for desc in SENSOR_TYPES } From 7a82279af80fea5216f7678726a53a24b873f756 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 17 Aug 2022 03:20:47 +0200 Subject: [PATCH 417/903] Update hass-nabucasa to 0.55.0 (#76892) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 4987169d280..02ffa0a4775 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.54.1"], + "requirements": ["hass-nabucasa==0.55.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eaf463df32d..d29a186f445 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 -hass-nabucasa==0.54.1 +hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 home-assistant-frontend==20220816.0 httpx==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index c4fdeec309b..c28a923b82d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -803,7 +803,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.1 +hass-nabucasa==0.55.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fca44241201..bdd90857d01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -592,7 +592,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.1 +hass-nabucasa==0.55.0 # homeassistant.components.tasmota hatasmota==0.5.1 From ee1b08bbd6af8a4267c97376bf2119b992fd1e82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Aug 2022 15:21:47 -1000 Subject: [PATCH 418/903] Bump govee-ble to 0.16.0 (#76882) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/govee_ble/test_sensor.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 7c65fe35b5d..d31abe48cae 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -36,7 +36,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.14.1"], + "requirements": ["govee-ble==0.16.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index c28a923b82d..5aeb4d57d1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -757,7 +757,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.1 +govee-ble==0.16.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdd90857d01..c6659b8c5b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.1 +govee-ble==0.16.0 # homeassistant.components.gree greeclimate==1.3.0 diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index 75d269ea0ba..da67d32e681 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensors(hass): temp_sensor = hass.states.get("sensor.h5075_2762_temperature") temp_sensor_attribtes = temp_sensor.attributes - assert temp_sensor.state == "21.3442" + assert temp_sensor.state == "21.34" assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075_2762 Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From 7e366a78e62725e415d371209d0e790503837289 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 17 Aug 2022 02:36:56 -0400 Subject: [PATCH 419/903] Add Fully Kiosk Browser button platform (#76894) Co-authored-by: Franck Nijhof --- .../components/fully_kiosk/__init__.py | 2 +- .../components/fully_kiosk/button.py | 105 ++++++++++++++++++ tests/components/fully_kiosk/test_button.py | 69 ++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fully_kiosk/button.py create mode 100644 tests/components/fully_kiosk/test_button.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index a4c71168f4e..311dae20082 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/button.py b/homeassistant/components/fully_kiosk/button.py new file mode 100644 index 00000000000..387ee638547 --- /dev/null +++ b/homeassistant/components/fully_kiosk/button.py @@ -0,0 +1,105 @@ +"""Fully Kiosk Browser button.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from fullykiosk import FullyKiosk + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +@dataclass +class FullyButtonEntityDescriptionMixin: + """Mixin to describe a Fully Kiosk Browser button entity.""" + + press_action: Callable[[FullyKiosk], Any] + + +@dataclass +class FullyButtonEntityDescription( + ButtonEntityDescription, FullyButtonEntityDescriptionMixin +): + """Fully Kiosk Browser button description.""" + + +BUTTONS: tuple[FullyButtonEntityDescription, ...] = ( + FullyButtonEntityDescription( + key="restartApp", + name="Restart browser", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + press_action=lambda fully: fully.restartApp(), + ), + FullyButtonEntityDescription( + key="rebootDevice", + name="Reboot device", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + press_action=lambda fully: fully.rebootDevice(), + ), + FullyButtonEntityDescription( + key="toForeground", + name="Bring to foreground", + press_action=lambda fully: fully.toForeground(), + ), + FullyButtonEntityDescription( + key="toBackground", + name="Send to background", + press_action=lambda fully: fully.toBackground(), + ), + FullyButtonEntityDescription( + key="loadStartUrl", + name="Load start URL", + press_action=lambda fully: fully.loadStartUrl(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser button entities.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + FullyButtonEntity(coordinator, description) for description in BUTTONS + ) + + +class FullyButtonEntity(FullyKioskEntity, ButtonEntity): + """Representation of a Fully Kiosk Browser button.""" + + entity_description: FullyButtonEntityDescription + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: FullyButtonEntityDescription, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + async def async_press(self) -> None: + """Set the value of the entity.""" + await self.entity_description.press_action(self.coordinator.fully) + await self.coordinator.async_refresh() diff --git a/tests/components/fully_kiosk/test_button.py b/tests/components/fully_kiosk/test_button.py new file mode 100644 index 00000000000..7183fc3db92 --- /dev/null +++ b/tests/components/fully_kiosk/test_button.py @@ -0,0 +1,69 @@ +"""Test the Fully Kiosk Browser buttons.""" +from unittest.mock import MagicMock + +import homeassistant.components.button as button +from homeassistant.components.fully_kiosk.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_binary_sensors( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk binary sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + entry = entity_registry.async_get("button.amazon_fire_restart_browser") + assert entry + assert entry.unique_id == "abcdef-123456-restartApp" + await call_service(hass, "press", "button.amazon_fire_restart_browser") + assert len(mock_fully_kiosk.restartApp.mock_calls) == 1 + + entry = entity_registry.async_get("button.amazon_fire_reboot_device") + assert entry + assert entry.unique_id == "abcdef-123456-rebootDevice" + await call_service(hass, "press", "button.amazon_fire_reboot_device") + assert len(mock_fully_kiosk.rebootDevice.mock_calls) == 1 + + entry = entity_registry.async_get("button.amazon_fire_bring_to_foreground") + assert entry + assert entry.unique_id == "abcdef-123456-toForeground" + await call_service(hass, "press", "button.amazon_fire_bring_to_foreground") + assert len(mock_fully_kiosk.toForeground.mock_calls) == 1 + + entry = entity_registry.async_get("button.amazon_fire_send_to_background") + assert entry + assert entry.unique_id == "abcdef-123456-toBackground" + await call_service(hass, "press", "button.amazon_fire_send_to_background") + assert len(mock_fully_kiosk.toBackground.mock_calls) == 1 + + entry = entity_registry.async_get("button.amazon_fire_load_start_url") + assert entry + assert entry.unique_id == "abcdef-123456-loadStartUrl" + await call_service(hass, "press", "button.amazon_fire_load_start_url") + assert len(mock_fully_kiosk.loadStartUrl.mock_calls) == 1 + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + +def call_service(hass, service, entity_id): + """Call any service on entity.""" + return hass.services.async_call( + button.DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) From 5ef6b5a3003b8fd5d4821f2e2ca1192fe5b21f9f Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 17 Aug 2022 03:09:19 -0400 Subject: [PATCH 420/903] Add BLE sensor to Aladdin_connect (#76221) * Add BLE sensor Default Enable BLE & Battery for Model 02 * recommended changes * Recommended changes Model 02 -> 01 (oops) 2x async_block_till_done() not needed. --- .../components/aladdin_connect/model.py | 1 + .../components/aladdin_connect/sensor.py | 15 +++- tests/components/aladdin_connect/conftest.py | 3 + .../components/aladdin_connect/test_sensor.py | 81 ++++++++++++++++++- 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py index 63624b223a9..9b250459d3b 100644 --- a/homeassistant/components/aladdin_connect/model.py +++ b/homeassistant/components/aladdin_connect/model.py @@ -12,3 +12,4 @@ class DoorDevice(TypedDict): name: str status: str serial: str + model: str diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 68631c57fc8..3d319a724c1 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -56,6 +56,15 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=AladdinConnectClient.get_rssi_status, ), + AccSensorEntityDescription( + key="ble_strength", + name="BLE Strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=AladdinConnectClient.get_ble_strength, + ), ) @@ -89,14 +98,17 @@ class AladdinConnectSensor(SensorEntity): device: DoorDevice, description: AccSensorEntityDescription, ) -> None: - """Initialize a sensor for an Abode device.""" + """Initialize a sensor for an Aladdin Connect device.""" self._device_id = device["device_id"] self._number = device["door_number"] self._name = device["name"] + self._model = device["model"] self._acc = acc self.entity_description = description self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}" self._attr_has_entity_name = True + if self._model == "01" and description.key in ("battery_level", "ble_strength"): + self._attr_entity_registry_enabled_default = True @property def device_info(self) -> DeviceInfo | None: @@ -105,6 +117,7 @@ class AladdinConnectSensor(SensorEntity): identifiers={(DOMAIN, self._device_id)}, name=self._name, manufacturer="Overhead Door", + model=self._model, ) @property diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index c8f7d240ba5..ee9afed9823 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -11,6 +11,7 @@ DEVICE_CONFIG_OPEN = { "status": "open", "link_status": "Connected", "serial": "12345", + "model": "02", } @@ -31,6 +32,8 @@ def fixture_mock_aladdinconnect_api(): mock_opener.get_battery_status.return_value = "99" mock_opener.async_get_rssi_status = AsyncMock(return_value="-55") mock_opener.get_rssi_status.return_value = "-55" + mock_opener.async_get_ble_strength = AsyncMock(return_value="-45") + mock_opener.get_ble_strength.return_value = "-45" mock_opener.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) mock_opener.register_callback = mock.Mock(return_value=True) diff --git a/tests/components/aladdin_connect/test_sensor.py b/tests/components/aladdin_connect/test_sensor.py index 3702bcd9efa..282f6d3e04c 100644 --- a/tests/components/aladdin_connect/test_sensor.py +++ b/tests/components/aladdin_connect/test_sensor.py @@ -1,6 +1,6 @@ """Test the Aladdin Connect Sensors.""" from datetime import timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL @@ -10,6 +10,17 @@ from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed +DEVICE_CONFIG_MODEL_01 = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "closed", + "link_status": "Connected", + "serial": "12345", + "model": "01", +} + + CONFIG = {"username": "test-user", "password": "test-password"} RELOAD_AFTER_UPDATE_DELAY = timedelta(seconds=31) @@ -83,3 +94,71 @@ async def test_sensors( state = hass.states.get("sensor.home_wi_fi_rssi") assert state + + +async def test_sensors_model_01( + hass: HomeAssistant, + mock_aladdinconnect_api: MagicMock, +) -> None: + """Test Sensors for AladdinConnect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + + await hass.async_block_till_done() + + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + mock_aladdinconnect_api.get_doors = AsyncMock( + return_value=[DEVICE_CONFIG_MODEL_01] + ) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entry = registry.async_get("sensor.home_battery_level") + assert entry + assert entry.disabled is False + assert entry.disabled_by is None + state = hass.states.get("sensor.home_battery_level") + assert state + + entry = registry.async_get("sensor.home_wi_fi_rssi") + await hass.async_block_till_done() + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state is None + + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state + + entry = registry.async_get("sensor.home_ble_strength") + await hass.async_block_till_done() + assert entry + assert entry.disabled is False + assert entry.disabled_by is None + state = hass.states.get("sensor.home_ble_strength") + assert state From b4323108b139680e44e7b5c5ff3d02d92443fee4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 17 Aug 2022 09:41:50 +0200 Subject: [PATCH 421/903] Update cryptography to 37.0.4 (#76853) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d29a186f445..8381e204a77 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ bleak==0.15.1 bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 -cryptography==36.0.2 +cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/pyproject.toml b/pyproject.toml index c0d96aa5ee0..3297cb39db2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "lru-dict==1.1.8", "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==36.0.2", + "cryptography==37.0.4", "orjson==3.7.11", "pip>=21.0,<22.3", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index fce57620224..f190aa50233 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.4.0 -cryptography==36.0.2 +cryptography==37.0.4 orjson==3.7.11 pip>=21.0,<22.3 python-slugify==4.0.1 From 0ed265e2be5436b56dffda5a43b070ca4f0f4207 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 10:53:05 +0200 Subject: [PATCH 422/903] Correct restoring of mobile_app sensors (#76886) --- .../components/mobile_app/binary_sensor.py | 5 +- homeassistant/components/mobile_app/entity.py | 5 +- homeassistant/components/mobile_app/sensor.py | 28 ++- tests/components/mobile_app/test_sensor.py | 160 ++++++++++++++++-- 4 files changed, 172 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index fd8545b1f98..69ecb913c98 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -75,9 +75,8 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity): """Return the state of the binary sensor.""" return self._config[ATTR_SENSOR_STATE] - @callback - def async_restore_last_state(self, last_state): + async def async_restore_last_state(self, last_state): """Restore previous state.""" - super().async_restore_last_state(last_state) + await super().async_restore_last_state(last_state) self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index d4c4374b8d9..3a2f038a0af 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -43,10 +43,9 @@ class MobileAppEntity(RestoreEntity): if (state := await self.async_get_last_state()) is None: return - self.async_restore_last_state(state) + await self.async_restore_last_state(state) - @callback - def async_restore_last_state(self, last_state): + async def async_restore_last_state(self, last_state): """Restore previous state.""" self._config[ATTR_SENSOR_STATE] = last_state.state self._config[ATTR_SENSOR_ATTRIBUTES] = { diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index d7cfc9545f6..ef7dd122496 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -3,9 +3,9 @@ from __future__ import annotations from typing import Any -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN +from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,6 +27,7 @@ from .const import ( DOMAIN, ) from .entity import MobileAppEntity +from .webhook import _extract_sensor_unique_id async def async_setup_entry( @@ -73,9 +74,30 @@ async def async_setup_entry( ) -class MobileAppSensor(MobileAppEntity, SensorEntity): +class MobileAppSensor(MobileAppEntity, RestoreSensor): """Representation of an mobile app sensor.""" + async def async_restore_last_state(self, last_state): + """Restore previous state.""" + + await super().async_restore_last_state(last_state) + + if not (last_sensor_data := await self.async_get_last_sensor_data()): + # Workaround to handle migration to RestoreSensor, can be removed + # in HA Core 2023.4 + self._config[ATTR_SENSOR_STATE] = None + webhook_id = self._entry.data[CONF_WEBHOOK_ID] + sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id) + if ( + self.device_class == SensorDeviceClass.TEMPERATURE + and sensor_unique_id == "battery_temperature" + ): + self._config[ATTR_SENSOR_UOM] = TEMP_CELSIUS + return + + self._config[ATTR_SENSOR_STATE] = last_sensor_data.native_value + self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement + @property def native_value(self): """Return the state of the sensor.""" diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index c0f7f126a49..930fb522c4c 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -1,15 +1,34 @@ """Entity tests for mobile_app.""" from http import HTTPStatus +from unittest.mock import patch import pytest from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + PERCENTAGE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -async def test_sensor(hass, create_registrations, webhook_client): +@pytest.mark.parametrize( + "unit_system, state_unit, state1, state2", + ( + (METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), + (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + ), +) +async def test_sensor( + hass, create_registrations, webhook_client, unit_system, state_unit, state1, state2 +): """Test that sensors can be registered and updated.""" + hass.config.units = unit_system + webhook_id = create_registrations[1]["webhook_id"] webhook_url = f"/api/webhook/{webhook_id}" @@ -19,15 +38,15 @@ async def test_sensor(hass, create_registrations, webhook_client): "type": "register_sensor", "data": { "attributes": {"foo": "bar"}, - "device_class": "battery", + "device_class": "temperature", "icon": "mdi:battery", - "name": "Battery State", + "name": "Battery Temperature", "state": 100, "type": "sensor", "entity_category": "diagnostic", - "unique_id": "battery_state", + "unique_id": "battery_temp", "state_class": "total", - "unit_of_measurement": PERCENTAGE, + "unit_of_measurement": TEMP_CELSIUS, }, }, ) @@ -38,20 +57,23 @@ async def test_sensor(hass, create_registrations, webhook_client): assert json == {"success": True} await hass.async_block_till_done() - entity = hass.states.get("sensor.test_1_battery_state") + entity = hass.states.get("sensor.test_1_battery_temperature") assert entity is not None - assert entity.attributes["device_class"] == "battery" + assert entity.attributes["device_class"] == "temperature" assert entity.attributes["icon"] == "mdi:battery" - assert entity.attributes["unit_of_measurement"] == PERCENTAGE + # unit of temperature sensor is automatically converted to the system UoM + assert entity.attributes["unit_of_measurement"] == state_unit assert entity.attributes["foo"] == "bar" assert entity.attributes["state_class"] == "total" assert entity.domain == "sensor" - assert entity.name == "Test 1 Battery State" - assert entity.state == "100" + assert entity.name == "Test 1 Battery Temperature" + assert entity.state == state1 assert ( - er.async_get(hass).async_get("sensor.test_1_battery_state").entity_category + er.async_get(hass) + .async_get("sensor.test_1_battery_temperature") + .entity_category == "diagnostic" ) @@ -64,7 +86,7 @@ async def test_sensor(hass, create_registrations, webhook_client): "icon": "mdi:battery-unknown", "state": 123, "type": "sensor", - "unique_id": "battery_state", + "unique_id": "battery_temp", }, # This invalid data should not invalidate whole request {"type": "sensor", "unique_id": "invalid_state", "invalid": "data"}, @@ -77,8 +99,8 @@ async def test_sensor(hass, create_registrations, webhook_client): json = await update_resp.json() assert json["invalid_state"]["success"] is False - updated_entity = hass.states.get("sensor.test_1_battery_state") - assert updated_entity.state == "123" + updated_entity = hass.states.get("sensor.test_1_battery_temperature") + assert updated_entity.state == state2 assert "foo" not in updated_entity.attributes dev_reg = dr.async_get(hass) @@ -88,16 +110,120 @@ async def test_sensor(hass, create_registrations, webhook_client): 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") + unloaded_entity = hass.states.get("sensor.test_1_battery_temperature") assert unloaded_entity.state == 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") + restored_entity = hass.states.get("sensor.test_1_battery_temperature") assert restored_entity.state == updated_entity.state assert restored_entity.attributes == updated_entity.attributes +@pytest.mark.parametrize( + "unique_id, unit_system, state_unit, state1, state2", + ( + ("battery_temperature", METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), + ("battery_temperature", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + # The unique_id doesn't match that of the mobile app's battery temperature sensor + ("battery_temp", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "123"), + ), +) +async def test_sensor_migration( + hass, + create_registrations, + webhook_client, + unique_id, + unit_system, + state_unit, + state1, + state2, +): + """Test migration to RestoreSensor.""" + hass.config.units = unit_system + + 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": "temperature", + "icon": "mdi:battery", + "name": "Battery Temperature", + "state": 100, + "type": "sensor", + "entity_category": "diagnostic", + "unique_id": unique_id, + "state_class": "total", + "unit_of_measurement": TEMP_CELSIUS, + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("sensor.test_1_battery_temperature") + assert entity is not None + + assert entity.attributes["device_class"] == "temperature" + assert entity.attributes["icon"] == "mdi:battery" + # unit of temperature sensor is automatically converted to the system UoM + assert entity.attributes["unit_of_measurement"] == state_unit + assert entity.attributes["foo"] == "bar" + assert entity.attributes["state_class"] == "total" + assert entity.domain == "sensor" + assert entity.name == "Test 1 Battery Temperature" + assert entity.state == state1 + + # 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_temperature") + assert unloaded_entity.state == STATE_UNAVAILABLE + + # Simulate migration to RestoreSensor + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_extra_data", + return_value=None, + ): + 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_temperature") + assert restored_entity.state == "unknown" + assert restored_entity.attributes == entity.attributes + + # Test unit conversion is working + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + { + "icon": "mdi:battery-unknown", + "state": 123, + "type": "sensor", + "unique_id": unique_id, + }, + ], + }, + ) + + assert update_resp.status == HTTPStatus.OK + + updated_entity = hass.states.get("sensor.test_1_battery_temperature") + assert updated_entity.state == state2 + assert "foo" not in 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"] From 4cc1428eea98efc961a124be88e4ab4604ca0ee2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 13:07:50 +0200 Subject: [PATCH 423/903] Add support for color_mode white to MQTT JSON light (#76918) --- .../components/mqtt/light/schema_json.py | 28 ++- tests/components/mqtt/test_light_json.py | 174 ++++++++++++++++-- 2 files changed, 188 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 659dd212b51..910d48f750d 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -15,6 +15,7 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_TRANSITION, + ATTR_WHITE, ATTR_XY_COLOR, ENTITY_ID_FORMAT, FLASH_LONG, @@ -61,7 +62,11 @@ from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..util import valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA -from .schema_basic import CONF_BRIGHTNESS_SCALE, MQTT_LIGHT_ATTRIBUTES_BLOCKED +from .schema_basic import ( + CONF_BRIGHTNESS_SCALE, + CONF_WHITE_SCALE, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, +) _LOGGER = logging.getLogger(__name__) @@ -79,6 +84,7 @@ DEFAULT_RGB = False DEFAULT_XY = False DEFAULT_HS = False DEFAULT_BRIGHTNESS_SCALE = 255 +DEFAULT_WHITE_SCALE = 255 CONF_COLOR_MODE = "color_mode" CONF_SUPPORTED_COLOR_MODES = "supported_color_modes" @@ -136,6 +142,9 @@ _PLATFORM_SCHEMA_BASE = ( vol.Unique(), valid_supported_color_modes, ), + vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, }, ) @@ -294,6 +303,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): w = int(values["color"]["w"]) # pylint: disable=invalid-name self._color_mode = ColorMode.RGBWW self._rgbww = (r, g, b, c, w) + elif color_mode == ColorMode.WHITE: + self._color_mode = ColorMode.WHITE elif color_mode == ColorMode.XY: x = float(values["color"]["x"]) # pylint: disable=invalid-name y = float(values["color"]["y"]) # pylint: disable=invalid-name @@ -498,7 +509,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): def _supports_color_mode(self, color_mode): return self.supported_color_modes and color_mode in self.supported_color_modes - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. @@ -613,6 +624,19 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._effect = kwargs[ATTR_EFFECT] should_update = True + if ATTR_WHITE in kwargs and self._supports_color_mode(ColorMode.WHITE): + white_normalized = kwargs[ATTR_WHITE] / DEFAULT_WHITE_SCALE + white_scale = self._config[CONF_WHITE_SCALE] + device_white_level = min(round(white_normalized * white_scale), white_scale) + # Make sure the brightness is not rounded down to 0 + device_white_level = max(device_white_level, 1) + message["white"] = device_white_level + + if self._optimistic: + self._color_mode = ColorMode.WHITE + self._brightness = kwargs[ATTR_WHITE] + should_update = True + await self.async_publish( self._topic[CONF_COMMAND_TOPIC], json_dumps(message), diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index d57fc1cceee..89e966112c1 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -383,7 +383,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert light_state.attributes["brightness"] == 100 async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON", ' '"color":{"r":125,"g":125,"b":125}}' + hass, "test_light_rgb", '{"state":"ON", "color":{"r":125,"g":125,"b":125}}' ) light_state = hass.states.get("light.test") @@ -430,7 +430,7 @@ async def test_controlling_state_via_topic2( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling of the state via topic for a light supporting color mode.""" - supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "white", "xy"] assert await async_setup_component( hass, @@ -560,6 +560,17 @@ async def test_controlling_state_via_topic2( assert state.attributes.get("color_mode") == "color_temp" assert state.attributes.get("color_temp") == 155 + # White + async_fire_mqtt_message( + hass, + "test_light_rgb", + '{"state":"ON", "color_mode":"white", "brightness":123}', + ) + state = hass.states.get("light.test") + assert state.attributes.get("color_mode") == "white" + assert state.attributes.get("brightness") == 123 + + # Effect async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON", "effect":"other_effect"}' ) @@ -731,7 +742,7 @@ async def test_sending_mqtt_commands_and_optimistic2( hass, mqtt_mock_entry_with_yaml_config ): """Test the sending of command in optimistic mode for a light supporting color mode.""" - supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "white", "xy"] fake_state = ha.State( "light.test", "on", @@ -788,6 +799,7 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None assert state.attributes.get("supported_color_modes") == supported_color_modes + assert state.attributes.get("white") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(ATTR_ASSUMED_STATE) @@ -835,7 +847,7 @@ async def test_sending_mqtt_commands_and_optimistic2( mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator( - '{"state": "ON", "color": {"h": 359.0, "s": 78.0},' ' "brightness": 75}' + '{"state": "ON", "color": {"h": 359.0, "s": 78.0}, "brightness": 75}' ), 2, False, @@ -919,13 +931,51 @@ async def test_sending_mqtt_commands_and_optimistic2( mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator( - '{"state": "ON", "color": {"x": 0.123, "y": 0.223},' ' "brightness": 50}' + '{"state": "ON", "color": {"x": 0.123, "y": 0.223}, "brightness": 50}' ), 2, False, ) mqtt_mock.async_publish.reset_mock() + # Set to white + await common.async_turn_on(hass, "light.test", white=75) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["brightness"] == 75 + assert state.attributes["color_mode"] == "white" + assert "hs_color" not in state.attributes + assert "rgb_color" not in state.attributes + assert "xy_color" not in state.attributes + assert "rgbw_color" not in state.attributes + assert "rgbww_color" not in state.attributes + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator('{"state": "ON", "white": 75}'), + 2, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set to white, brightness also present in turn_on + await common.async_turn_on(hass, "light.test", brightness=60, white=80) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["brightness"] == 60 + assert state.attributes["color_mode"] == "white" + assert "hs_color" not in state.attributes + assert "rgb_color" not in state.attributes + assert "xy_color" not in state.attributes + assert "rgbw_color" not in state.attributes + assert "rgbww_color" not in state.attributes + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator('{"state": "ON", "white": 60}'), + 2, + False, + ) + mqtt_mock.async_publish.reset_mock() + async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends hs color parameters.""" @@ -1254,6 +1304,50 @@ async def test_sending_rgb_color_with_scaled_brightness( ) +async def test_sending_scaled_white(hass, mqtt_mock_entry_with_yaml_config): + """Test light.turn_on with scaled white.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "command_topic": "test_light_rgb/set", + "brightness": True, + "brightness_scale": 100, + "color_mode": True, + "supported_color_modes": ["hs", "white"], + "white_scale": 50, + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + + await common.async_turn_on(hass, "light.test", brightness=128) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", JsonValidator('{"state":"ON", "brightness":50}'), 0, False + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255, white=25) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", JsonValidator('{"state":"ON", "white":50}'), 0, False + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", white=25) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", JsonValidator('{"state":"ON", "white":5}'), 0, False + ) + mqtt_mock.async_publish.reset_mock() + + async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends xy color parameters.""" assert await async_setup_component( @@ -1527,6 +1621,62 @@ async def test_brightness_scale(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("brightness") == 255 +async def test_white_scale(hass, mqtt_mock_entry_with_yaml_config): + """Test for white scaling.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "state_topic": "test_light_bright_scale", + "command_topic": "test_light_bright_scale/set", + "brightness": True, + "brightness_scale": 99, + "color_mode": True, + "supported_color_modes": ["hs", "white"], + "white_scale": 50, + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get("brightness") is None + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Turn on the light + async_fire_mqtt_message(hass, "test_light_bright_scale", '{"state":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") is None + + # Turn on the light with brightness + async_fire_mqtt_message( + hass, "test_light_bright_scale", '{"state":"ON", "brightness": 99}' + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 255 + + # Turn on the light with white - white_scale is NOT used + async_fire_mqtt_message( + hass, + "test_light_bright_scale", + '{"state":"ON", "color_mode":"white", "brightness": 50}', + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 128 + + async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid color/brightness/etc. values are ignored.""" assert await async_setup_component( @@ -1585,7 +1735,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON",' '"color":{}}', + '{"state":"ON", "color":{}}', ) # Color should not have changed @@ -1597,7 +1747,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON",' '"color":{"h":"bad","s":"val"}}', + '{"state":"ON", "color":{"h":"bad","s":"val"}}', ) # Color should not have changed @@ -1609,7 +1759,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON",' '"color":{"r":"bad","g":"val","b":"test"}}', + '{"state":"ON", "color":{"r":"bad","g":"val","b":"test"}}', ) # Color should not have changed @@ -1621,7 +1771,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON",' '"color":{"x":"bad","y":"val"}}', + '{"state":"ON", "color":{"x":"bad","y":"val"}}', ) # Color should not have changed @@ -1631,7 +1781,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): # Bad brightness values async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON",' '"brightness": "badValue"}' + hass, "test_light_rgb", '{"state":"ON", "brightness": "badValue"}' ) # Brightness should not have changed @@ -1641,7 +1791,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): # Bad color temperature async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON",' '"color_temp": "badValue"}' + hass, "test_light_rgb", '{"state":"ON", "color_temp": "badValue"}' ) # Color temperature should not have changed @@ -1767,7 +1917,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered mqtt_json lights.""" - data = '{ "name": "test",' ' "schema": "json",' ' "command_topic": "test_topic" }' + data = '{ "name": "test", "schema": "json", "command_topic": "test_topic" }' await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, From 426a620084c75e98804ea03266b86041819f62ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 13:53:56 +0200 Subject: [PATCH 424/903] Remove deprecated white_value support from template light (#76923) --- homeassistant/components/template/light.py | 68 +--------------- tests/components/template/test_light.py | 93 ---------------------- 2 files changed, 2 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index f4ad971c1d6..f10e6c2ea09 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -12,12 +12,10 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, LightEntity, LightEntityFeature, ) @@ -88,16 +86,14 @@ LIGHT_SCHEMA = vol.All( vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_WHITE_VALUE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, } ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY.schema), ) PLATFORM_SCHEMA = vol.All( # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_ACTION), - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), + cv.removed(CONF_WHITE_VALUE_ACTION), + cv.removed(CONF_WHITE_VALUE_TEMPLATE), PLATFORM_SCHEMA.extend( {vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_SCHEMA)} ), @@ -171,12 +167,6 @@ class LightTemplate(TemplateEntity, LightEntity): if (color_action := config.get(CONF_COLOR_ACTION)) is not None: self._color_script = Script(hass, color_action, friendly_name, DOMAIN) self._color_template = config.get(CONF_COLOR_TEMPLATE) - self._white_value_script = None - if (white_value_action := config.get(CONF_WHITE_VALUE_ACTION)) is not None: - self._white_value_script = Script( - hass, white_value_action, friendly_name, DOMAIN - ) - self._white_value_template = config.get(CONF_WHITE_VALUE_TEMPLATE) self._effect_script = None if (effect_action := config.get(CONF_EFFECT_ACTION)) is not None: self._effect_script = Script(hass, effect_action, friendly_name, DOMAIN) @@ -190,7 +180,6 @@ class LightTemplate(TemplateEntity, LightEntity): self._brightness = None self._temperature = None self._color = None - self._white_value = None self._effect = None self._effect_list = None self._max_mireds = None @@ -223,11 +212,6 @@ class LightTemplate(TemplateEntity, LightEntity): return super().min_mireds - @property - def white_value(self) -> int | None: - """Return the white value.""" - return self._white_value - @property def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" @@ -253,8 +237,6 @@ class LightTemplate(TemplateEntity, LightEntity): supported_features |= SUPPORT_COLOR_TEMP if self._color_script is not None: supported_features |= SUPPORT_COLOR - if self._white_value_script is not None: - supported_features |= SUPPORT_WHITE_VALUE if self._effect_script is not None: supported_features |= LightEntityFeature.EFFECT if self._supports_transition is True: @@ -312,14 +294,6 @@ class LightTemplate(TemplateEntity, LightEntity): self._update_color, none_on_template_error=True, ) - if self._white_value_template: - self.add_template_attribute( - "_white_value", - self._white_value_template, - None, - self._update_white_value, - none_on_template_error=True, - ) if self._effect_list_template: self.add_template_attribute( "_effect_list", @@ -361,13 +335,6 @@ class LightTemplate(TemplateEntity, LightEntity): self._brightness = kwargs[ATTR_BRIGHTNESS] optimistic_set = True - if self._white_value_template is None and ATTR_WHITE_VALUE in kwargs: - _LOGGER.debug( - "Optimistically setting white value to %s", kwargs[ATTR_WHITE_VALUE] - ) - self._white_value = kwargs[ATTR_WHITE_VALUE] - optimistic_set = True - if self._temperature_template is None and ATTR_COLOR_TEMP in kwargs: _LOGGER.debug( "Optimistically setting color temperature to %s", @@ -404,14 +371,6 @@ class LightTemplate(TemplateEntity, LightEntity): run_variables=common_params, context=self._context, ) - elif ATTR_WHITE_VALUE in kwargs and self._white_value_script: - common_params["white_value"] = kwargs[ATTR_WHITE_VALUE] - - await self.async_run_script( - self._white_value_script, - run_variables=common_params, - context=self._context, - ) elif ATTR_EFFECT in kwargs and self._effect_script: effect = kwargs[ATTR_EFFECT] if effect not in self._effect_list: @@ -486,29 +445,6 @@ class LightTemplate(TemplateEntity, LightEntity): ) self._brightness = None - @callback - def _update_white_value(self, white_value): - """Update the white value from the template.""" - try: - if white_value in (None, "None", ""): - self._white_value = None - return - if 0 <= int(white_value) <= 255: - self._white_value = int(white_value) - else: - _LOGGER.error( - "Received invalid white value: %s for entity %s. Expected: 0-255", - white_value, - self.entity_id, - ) - self._white_value = None - except ValueError: - _LOGGER.error( - "Template must supply an integer white_value from 0-255, or 'None'", - exc_info=True, - ) - self._white_value = None - @callback def _update_effect_list(self, effect_list): """Update the effect list from the template.""" diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index ca03c6f5d82..9b0ad613120 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -9,12 +9,10 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, ColorMode, LightEntityFeature, ) @@ -616,97 +614,6 @@ async def test_off_action_optimistic( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes,expected_color_mode", - [(SUPPORT_WHITE_VALUE, [ColorMode.RGBW], ColorMode.UNKNOWN)], -) -@pytest.mark.parametrize( - "light_config", - [ - { - "test_template_light": { - **OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG, - "value_template": "{{1 == 1}}", - } - }, - ], -) -async def test_white_value_action_no_template( - hass, - setup_light, - calls, - supported_color_modes, - supported_features, - expected_color_mode, -): - """Test setting white value with optimistic template.""" - state = hass.states.get("light.test_template_light") - assert state.attributes.get("white_value") is None - - await hass.services.async_call( - light.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_template_light", ATTR_WHITE_VALUE: 124}, - blocking=True, - ) - - assert len(calls) == 1 - assert calls[-1].data["action"] == "set_white_value" - assert calls[-1].data["caller"] == "light.test_template_light" - assert calls[-1].data["white_value"] == 124 - - state = hass.states.get("light.test_template_light") - assert state.attributes.get("white_value") == 124 - assert state.state == STATE_ON - assert state.attributes["color_mode"] == expected_color_mode # hs_color is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features - - -@pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes,expected_color_mode", - [(SUPPORT_WHITE_VALUE, [ColorMode.RGBW], ColorMode.UNKNOWN)], -) -@pytest.mark.parametrize( - "expected_white_value,white_value_template", - [ - (255, "{{255}}"), - (None, "{{256}}"), - (None, "{{x-12}}"), - (None, "{{ none }}"), - (None, ""), - ], -) -async def test_white_value_template( - hass, - expected_white_value, - supported_features, - supported_color_modes, - expected_color_mode, - count, - white_value_template, -): - """Test the template for the white value.""" - light_config = { - "test_template_light": { - **OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG, - "value_template": "{{ 1 == 1 }}", - "white_value_template": white_value_template, - } - } - await async_setup_light(hass, count, light_config) - - state = hass.states.get("light.test_template_light") - assert state is not None - assert state.attributes.get("white_value") == expected_white_value - assert state.state == STATE_ON - assert state.attributes["color_mode"] == expected_color_mode # hs_color is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features - - @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes,expected_color_mode", From d034ed1fc209868d3e96dfb4c623ad5c68e79093 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 13:54:54 +0200 Subject: [PATCH 425/903] Remove some error prone code from Alexa tests (#76917) --- tests/components/alexa/test_smart_home.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index df45d90358b..d04b3719bf9 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -26,10 +26,11 @@ 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 STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import STATE_UNKNOWN, TEMP_FAHRENHEIT from homeassistant.core import Context from homeassistant.helpers import entityfilter from homeassistant.setup import async_setup_component +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from .test_common import ( MockConfig, @@ -2019,7 +2020,7 @@ async def test_unknown_sensor(hass): async def test_thermostat(hass): """Test thermostat discovery.""" - hass.config.units.temperature_unit = TEMP_FAHRENHEIT + hass.config.units = IMPERIAL_SYSTEM device = ( "climate.test_thermostat", "cool", @@ -2287,9 +2288,6 @@ async def test_thermostat(hass): ) assert call.data["preset_mode"] == "eco" - # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. - hass.config.units.temperature_unit = TEMP_CELSIUS - async def test_exclude_filters(hass): """Test exclusion filters.""" From 05e33821fdc15fa6df589452dc8a2baf036f9275 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 14:01:50 +0200 Subject: [PATCH 426/903] Remove white_value support from group light (#76924) --- homeassistant/components/group/light.py | 16 +-------- tests/components/group/test_light.py | 43 ------------------------- 2 files changed, 1 insertion(+), 58 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index e0645da6141..1563e811fe9 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -25,10 +25,8 @@ from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, PLATFORM_SCHEMA, - SUPPORT_WHITE_VALUE, ColorMode, LightEntity, LightEntityFeature, @@ -71,10 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) SUPPORT_GROUP_LIGHT = ( - LightEntityFeature.EFFECT - | LightEntityFeature.FLASH - | LightEntityFeature.TRANSITION - | SUPPORT_WHITE_VALUE + LightEntityFeature.EFFECT | LightEntityFeature.FLASH | LightEntityFeature.TRANSITION ) _LOGGER = logging.getLogger(__name__) @@ -128,7 +123,6 @@ FORWARDED_ATTRIBUTES = frozenset( ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, } ) @@ -148,7 +142,6 @@ class LightGroup(GroupEntity, LightEntity): ) -> None: """Initialize a light group.""" self._entity_ids = entity_ids - self._white_value: int | None = None self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} @@ -174,11 +167,6 @@ class LightGroup(GroupEntity, LightEntity): await super().async_added_to_hass() - @property - def white_value(self) -> int | None: - """Return the white value of this light group between 0..255.""" - return self._white_value - async def async_turn_on(self, **kwargs: Any) -> None: """Forward the turn_on command to all lights in the light group.""" data = { @@ -251,8 +239,6 @@ class LightGroup(GroupEntity, LightEntity): on_states, ATTR_XY_COLOR, reduce=mean_tuple ) - self._white_value = reduce_attribute(on_states, ATTR_WHITE_VALUE) - self._attr_color_temp = reduce_attribute(on_states, ATTR_COLOR_TEMP) self._attr_min_mireds = reduce_attribute( states, ATTR_MIN_MIREDS, default=154, reduce=min diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index f3083812553..5329d3074b4 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -618,48 +618,6 @@ async def test_color_rgbww(hass, enable_custom_integrations): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_white_value(hass): - """Test white value reporting.""" - await async_setup_component( - hass, - LIGHT_DOMAIN, - { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - "all": "false", - } - }, - ) - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - hass.states.async_set( - "light.test1", STATE_ON, {ATTR_WHITE_VALUE: 255, ATTR_SUPPORTED_FEATURES: 128} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 - assert state.attributes[ATTR_WHITE_VALUE] == 255 - - hass.states.async_set( - "light.test2", STATE_ON, {ATTR_WHITE_VALUE: 100, ATTR_SUPPORTED_FEATURES: 128} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 - assert state.attributes[ATTR_WHITE_VALUE] == 177 - - hass.states.async_set( - "light.test1", STATE_OFF, {ATTR_WHITE_VALUE: 255, ATTR_SUPPORTED_FEATURES: 128} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 - assert state.attributes[ATTR_WHITE_VALUE] == 100 - - async def test_white(hass, enable_custom_integrations): """Test white reporting.""" platform = getattr(hass.components, "test.light") @@ -1493,7 +1451,6 @@ async def test_invalid_service_calls(hass): ATTR_XY_COLOR: (0.5, 0.42), ATTR_RGB_COLOR: (80, 120, 50), ATTR_COLOR_TEMP: 1234, - ATTR_WHITE_VALUE: 1, ATTR_EFFECT: "Sunshine", ATTR_TRANSITION: 4, ATTR_FLASH: "long", From 924704e0d128bb15d84ace156adaf90d9ba9ed9e Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 17 Aug 2022 10:22:12 -0400 Subject: [PATCH 427/903] Fix fully_kiosk button test docstring and function name (#76935) Fix button test docstring and function name --- tests/components/fully_kiosk/test_button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/fully_kiosk/test_button.py b/tests/components/fully_kiosk/test_button.py index 7183fc3db92..8616d7107f7 100644 --- a/tests/components/fully_kiosk/test_button.py +++ b/tests/components/fully_kiosk/test_button.py @@ -10,12 +10,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry -async def test_binary_sensors( +async def test_buttons( hass: HomeAssistant, mock_fully_kiosk: MagicMock, init_integration: MockConfigEntry, ) -> None: - """Test standard Fully Kiosk binary sensors.""" + """Test standard Fully Kiosk buttons.""" entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) From ef6b6e78504d9f4d071217101a817d2e1f7567e2 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 17 Aug 2022 15:25:34 +0100 Subject: [PATCH 428/903] Remove deprecated utility_meter entity (#76480) * remove deprecated utility_meter domain * remove select_tariff --- .../components/utility_meter/__init__.py | 4 - .../components/utility_meter/const.py | 3 - .../components/utility_meter/select.py | 131 +----------------- .../components/utility_meter/services.yaml | 22 --- tests/components/utility_meter/test_init.py | 107 +------------- 5 files changed, 10 insertions(+), 257 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index b17592cdf0b..ea5fb4933be 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -13,7 +13,6 @@ from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers import discovery, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from .const import ( @@ -27,7 +26,6 @@ from .const import ( CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_TARIFFS, - DATA_LEGACY_COMPONENT, DATA_TARIFF_SENSORS, DATA_UTILITY, DOMAIN, @@ -101,8 +99,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an Utility Meter.""" - hass.data[DATA_LEGACY_COMPONENT] = EntityComponent(_LOGGER, DOMAIN, hass) - hass.data[DATA_UTILITY] = {} async def async_reset_meters(service_call): diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 2bac649aace..9b85e9e3ae9 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -25,7 +25,6 @@ METER_TYPES = [ DATA_UTILITY = "utility_meter_data" DATA_TARIFF_SENSORS = "utility_meter_sensors" -DATA_LEGACY_COMPONENT = "utility_meter_legacy_component" CONF_METER = "meter" CONF_SOURCE_SENSOR = "source" @@ -48,6 +47,4 @@ SIGNAL_START_PAUSE_METER = "utility_meter_start_pause" SIGNAL_RESET_METER = "utility_meter_reset" SERVICE_RESET = "reset" -SERVICE_SELECT_TARIFF = "select_tariff" -SERVICE_SELECT_NEXT_TARIFF = "next_tariff" SERVICE_CALIBRATE_METER = "calibrate" diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index 008a0ff6120..efddfba94e5 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -3,41 +3,15 @@ from __future__ import annotations import logging -import voluptuous as vol - from homeassistant.components.select import SelectEntity -from homeassistant.components.select.const import ( - ATTR_OPTION, - ATTR_OPTIONS, - DOMAIN as SELECT_DOMAIN, - SERVICE_SELECT_OPTION, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, - CONF_UNIQUE_ID, - STATE_UNAVAILABLE, -) -from homeassistant.core import Event, HomeAssistant, callback, split_entity_id -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ( - ATTR_TARIFF, - ATTR_TARIFFS, - CONF_METER, - CONF_TARIFFS, - DATA_LEGACY_COMPONENT, - DATA_UTILITY, - SERVICE_SELECT_NEXT_TARIFF, - SERVICE_SELECT_TARIFF, - TARIFF_ICON, -) +from .const import CONF_METER, CONF_TARIFFS, DATA_UTILITY, TARIFF_ICON _LOGGER = logging.getLogger(__name__) @@ -51,9 +25,8 @@ async def async_setup_entry( name = config_entry.title tariffs = config_entry.options[CONF_TARIFFS] - legacy_add_entities = None unique_id = config_entry.entry_id - tariff_select = TariffSelect(name, tariffs, legacy_add_entities, unique_id) + tariff_select = TariffSelect(name, tariffs, unique_id) async_add_entities([tariff_select]) @@ -71,7 +44,6 @@ async def async_setup_platform( ) return - legacy_component = hass.data[DATA_LEGACY_COMPONENT] meter: str = discovery_info[CONF_METER] conf_meter_unique_id: str | None = hass.data[DATA_UTILITY][meter].get( CONF_UNIQUE_ID @@ -82,27 +54,16 @@ async def async_setup_platform( TariffSelect( discovery_info[CONF_METER], discovery_info[CONF_TARIFFS], - legacy_component.async_add_entities, conf_meter_unique_id, ) ] ) - legacy_component.async_register_entity_service( - SERVICE_SELECT_TARIFF, - {vol.Required(ATTR_TARIFF): cv.string}, - "async_select_tariff", - ) - - legacy_component.async_register_entity_service( - SERVICE_SELECT_NEXT_TARIFF, {}, "async_next_tariff" - ) - class TariffSelect(SelectEntity, RestoreEntity): """Representation of a Tariff selector.""" - def __init__(self, name, tariffs, add_legacy_entities, unique_id): + def __init__(self, name, tariffs, unique_id): """Initialize a tariff selector.""" self._attr_name = name self._attr_unique_id = unique_id @@ -110,7 +71,6 @@ class TariffSelect(SelectEntity, RestoreEntity): self._tariffs = tariffs self._attr_icon = TARIFF_ICON self._attr_should_poll = False - self._add_legacy_entities = add_legacy_entities @property def options(self): @@ -126,9 +86,6 @@ class TariffSelect(SelectEntity, RestoreEntity): """Run when entity about to be added.""" await super().async_added_to_hass() - if self._add_legacy_entities: - await self._add_legacy_entities([LegacyTariffSelect(self.entity_id)]) - state = await self.async_get_last_state() if not state or state.state not in self._tariffs: self._current_tariff = self._tariffs[0] @@ -139,81 +96,3 @@ class TariffSelect(SelectEntity, RestoreEntity): """Select new tariff (option).""" self._current_tariff = option self.async_write_ha_state() - - -class LegacyTariffSelect(Entity): - """Backwards compatibility for deprecated utility_meter select entity.""" - - def __init__(self, tracked_entity_id): - """Initialize the entity.""" - self._attr_icon = TARIFF_ICON - # Set name to influence entity_id - self._attr_name = split_entity_id(tracked_entity_id)[1] - self.tracked_entity_id = tracked_entity_id - - @callback - def async_state_changed_listener(self, event: Event | None = None) -> None: - """Handle child updates.""" - if ( - state := self.hass.states.get(self.tracked_entity_id) - ) is None or state.state == STATE_UNAVAILABLE: - self._attr_available = False - return - - self._attr_available = True - - self._attr_name = state.attributes.get(ATTR_FRIENDLY_NAME) - self._attr_state = state.state - self._attr_extra_state_attributes = { - ATTR_TARIFFS: state.attributes.get(ATTR_OPTIONS) - } - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def _async_state_changed_listener(event: Event | None = None) -> None: - """Handle child updates.""" - self.async_state_changed_listener(event) - self.async_write_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, [self.tracked_entity_id], _async_state_changed_listener - ) - ) - - # Call once on adding - _async_state_changed_listener() - - async def async_select_tariff(self, tariff): - """Select new option.""" - _LOGGER.warning( - "The 'utility_meter.select_tariff' service has been deprecated and will " - "be removed in HA Core 2022.7. Please use 'select.select_option' instead", - ) - await self.hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: self.tracked_entity_id, ATTR_OPTION: tariff}, - blocking=True, - context=self._context, - ) - - async def async_next_tariff(self): - """Offset current index.""" - _LOGGER.warning( - "The 'utility_meter.next_tariff' service has been deprecated and will " - "be removed in HA Core 2022.7. Please use 'select.select_option' instead", - ) - if ( - not self.available - or (state := self.hass.states.get(self.tracked_entity_id)) is None - ): - return - tariffs = state.attributes.get(ATTR_OPTIONS) - current_tariff = state.state - current_index = tariffs.index(current_tariff) - new_index = (current_index + 1) % len(tariffs) - - await self.async_select_tariff(tariffs[new_index]) diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index fc1e08b153a..194eff5d7d0 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -7,28 +7,6 @@ reset: entity: domain: select -next_tariff: - name: Next Tariff - description: Changes the tariff to the next one. - target: - entity: - domain: utility_meter - -select_tariff: - name: Select Tariff - description: Selects the current tariff of a utility meter. - target: - entity: - domain: utility_meter - fields: - tariff: - name: Tariff - description: Name of the tariff to switch to - example: "offpeak" - required: true - selector: - text: - calibrate: name: Calibrate description: Calibrates a utility meter sensor. diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index d358d7e4b9e..faafd559852 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -7,16 +7,11 @@ from unittest.mock import patch import pytest from homeassistant.components.select.const import ( + ATTR_OPTION, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.components.utility_meter.const import ( - DOMAIN, - SERVICE_RESET, - SERVICE_SELECT_NEXT_TARIFF, - SERVICE_SELECT_TARIFF, - SIGNAL_RESET_METER, -) +from homeassistant.components.utility_meter.const import DOMAIN, SERVICE_RESET import homeassistant.components.utility_meter.select as um_select import homeassistant.components.utility_meter.sensor as um_sensor from homeassistant.const import ( @@ -28,9 +23,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, State -from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -71,8 +64,6 @@ async def test_restore_state(hass): ( ["select.energy_bill"], "select.energy_bill", - ["utility_meter.energy_bill"], - "utility_meter.energy_bill", ), ) async def test_services(hass, meter): @@ -119,9 +110,9 @@ async def test_services(hass, meter): state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "0" - # Next tariff - only supported on legacy entity - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) + # Change tariff + data = {ATTR_ENTITY_ID: "select.energy_bill", ATTR_OPTION: "offpeak"} + await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data) await hass.async_block_till_done() now += timedelta(seconds=10) @@ -243,12 +234,6 @@ async def test_services_config_entry(hass): state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "0" - # Next tariff - only supported on legacy entity - with pytest.raises(ServiceNotFound): - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) - await hass.async_block_till_done() - # Change tariff data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "offpeak"} await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data) @@ -394,88 +379,6 @@ async def test_setup_missing_discovery(hass): assert not await um_sensor.async_setup_platform(hass, {CONF_PLATFORM: DOMAIN}, None) -async def test_legacy_support(hass): - """Test legacy entity support.""" - config = { - "utility_meter": { - "energy_bill": { - "source": "sensor.energy", - "cycle": "hourly", - "tariffs": ["peak", "offpeak"], - }, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, Platform.SENSOR, config) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - - assert select_state.state == legacy_state.state == "peak" - select_attributes = select_state.attributes - legacy_attributes = legacy_state.attributes - assert select_attributes.keys() == { - "friendly_name", - "icon", - "options", - } - assert legacy_attributes.keys() == {"friendly_name", "icon", "tariffs"} - assert select_attributes["friendly_name"] == legacy_attributes["friendly_name"] - assert select_attributes["icon"] == legacy_attributes["icon"] - assert select_attributes["options"] == legacy_attributes["tariffs"] - - # Change tariff on the select - data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "offpeak"} - await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - assert select_state.state == legacy_state.state == "offpeak" - - # Change tariff on the legacy entity - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill", "tariff": "offpeak"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_TARIFF, data) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - assert select_state.state == legacy_state.state == "offpeak" - - # Cycle tariffs on the select - not supported - data = {ATTR_ENTITY_ID: "select.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - assert select_state.state == legacy_state.state == "offpeak" - - # Cycle tariffs on the legacy entity - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - assert select_state.state == legacy_state.state == "peak" - - # Reset the legacy entity - reset_calls = [] - - def async_reset_meter(entity_id): - reset_calls.append(entity_id) - - async_dispatcher_connect(hass, SIGNAL_RESET_METER, async_reset_meter) - - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_RESET, data) - await hass.async_block_till_done() - assert reset_calls == ["select.energy_bill"] - - @pytest.mark.parametrize( "tariffs,expected_entities", ( From eec0f3351a5be10a0e0b1a1822339c7582b74a76 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:27:47 +0200 Subject: [PATCH 429/903] Add weather checks to pylint plugin (#76915) --- pylint/plugins/hass_enforce_type_hints.py | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index b34b88c27c9..3d86767df43 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1968,6 +1968,77 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "weather": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="WeatherEntity", + matches=[ + TypeHintMatch( + function_name="native_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_temperature_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="native_pressure", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_pressure_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="humidity", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_wind_speed", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_wind_speed_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="wind_bearing", + return_type=["float", "str", None], + ), + TypeHintMatch( + function_name="ozone", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_visibility", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_visibility_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="forecast", + return_type=["list[Forecast]", None], + ), + TypeHintMatch( + function_name="native_precipitation_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="precision", + return_type="float", + ), + TypeHintMatch( + function_name="condition", + return_type=["str", None], + ), + ], + ), + ], } From 79ab13d118fbcca74720f4817579e5ac5ec26004 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 17 Aug 2022 10:30:20 -0400 Subject: [PATCH 430/903] Add Fully Kiosk Browser switch platform (#76931) --- .../components/fully_kiosk/__init__.py | 2 +- .../components/fully_kiosk/switch.py | 117 ++++++ tests/components/fully_kiosk/conftest.py | 3 + .../fully_kiosk/fixtures/listsettings.json | 332 ++++++++++++++++++ tests/components/fully_kiosk/test_switch.py | 81 +++++ 5 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fully_kiosk/switch.py create mode 100644 tests/components/fully_kiosk/fixtures/listsettings.json create mode 100644 tests/components/fully_kiosk/test_switch.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 311dae20082..5a3d6078004 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/switch.py b/homeassistant/components/fully_kiosk/switch.py new file mode 100644 index 00000000000..7dfcc1e71ac --- /dev/null +++ b/homeassistant/components/fully_kiosk/switch.py @@ -0,0 +1,117 @@ +"""Fully Kiosk Browser switch.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from fullykiosk import FullyKiosk + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +@dataclass +class FullySwitchEntityDescriptionMixin: + """Fully Kiosk Browser switch entity description mixin.""" + + on_action: Callable[[FullyKiosk], Any] + off_action: Callable[[FullyKiosk], Any] + is_on_fn: Callable[[dict[str, Any]], Any] + + +@dataclass +class FullySwitchEntityDescription( + SwitchEntityDescription, FullySwitchEntityDescriptionMixin +): + """Fully Kiosk Browser switch entity description.""" + + +SWITCHES: tuple[FullySwitchEntityDescription, ...] = ( + FullySwitchEntityDescription( + key="screensaver", + name="Screensaver", + on_action=lambda fully: fully.startScreensaver(), + off_action=lambda fully: fully.stopScreensaver(), + is_on_fn=lambda data: data.get("isInScreensaver"), + ), + FullySwitchEntityDescription( + key="maintenance", + name="Maintenance mode", + entity_category=EntityCategory.CONFIG, + on_action=lambda fully: fully.enableLockedMode(), + off_action=lambda fully: fully.disableLockedMode(), + is_on_fn=lambda data: data.get("maintenanceMode"), + ), + FullySwitchEntityDescription( + key="kiosk", + name="Kiosk lock", + entity_category=EntityCategory.CONFIG, + on_action=lambda fully: fully.lockKiosk(), + off_action=lambda fully: fully.unlockKiosk(), + is_on_fn=lambda data: data.get("kioskLocked"), + ), + FullySwitchEntityDescription( + key="motion-detection", + name="Motion detection", + entity_category=EntityCategory.CONFIG, + on_action=lambda fully: fully.enableMotionDetection(), + off_action=lambda fully: fully.disableMotionDetection(), + is_on_fn=lambda data: data["settings"].get("motionDetection"), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser switch.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + FullySwitchEntity(coordinator, description) for description in SWITCHES + ) + + +class FullySwitchEntity(FullyKioskEntity, SwitchEntity): + """Fully Kiosk Browser switch entity.""" + + entity_description: FullySwitchEntityDescription + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: FullySwitchEntityDescription, + ) -> None: + """Initialize the Fully Kiosk Browser switch entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + @property + def is_on(self) -> bool | None: + """Return true if the entity is on.""" + if self.entity_description.is_on_fn(self.coordinator.data) is not None: + return bool(self.entity_description.is_on_fn(self.coordinator.data)) + return None + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.on_action(self.coordinator.fully) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.entity_description.off_action(self.coordinator.fully) + await self.coordinator.async_refresh() diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py index 35d5c12e694..bed08b532fd 100644 --- a/tests/components/fully_kiosk/conftest.py +++ b/tests/components/fully_kiosk/conftest.py @@ -65,6 +65,9 @@ def mock_fully_kiosk() -> Generator[MagicMock, None, None]: client.getDeviceInfo.return_value = json.loads( load_fixture("deviceinfo.json", DOMAIN) ) + client.getSettings.return_value = json.loads( + load_fixture("listsettings.json", DOMAIN) + ) yield client diff --git a/tests/components/fully_kiosk/fixtures/listsettings.json b/tests/components/fully_kiosk/fixtures/listsettings.json new file mode 100644 index 00000000000..d11291032a9 --- /dev/null +++ b/tests/components/fully_kiosk/fixtures/listsettings.json @@ -0,0 +1,332 @@ +{ + "tapsToPinDialogInSingleAppMode": "7", + "mdmSystemUpdatePolicy": "0", + "showMenuHint": true, + "movementBeaconList": "", + "authUsername": "", + "motionSensitivity": "90", + "remoteAdminPassword": "admin_password", + "launchOnBoot": true, + "kioskAppBlacklist": "", + "folderCleanupTime": "", + "preventSleepWhileScreenOff": false, + "touchInteraction": true, + "showBackButton": true, + "volumeLimits": "", + "addressBarBgColor": -2236963, + "mdmSystemAppsToEnable": "", + "rebootTime": "", + "kioskWifiPin": "", + "appLauncherTextColor": -16777216, + "wifiSSID": "", + "keepSleepingIfUnplugged": false, + "microphoneAccess": false, + "barcodeScanBroadcastExtra": "", + "cacheMode": "-1", + "disableOutgoingCalls": false, + "knoxDisableUsbDebugging": false, + "screensaverBrightness": "0", + "thirdPartyCookies": true, + "desktopMode": false, + "protectedContent": false, + "knoxDisableBluetoothTethering": false, + "safeBrowsing": false, + "mqttBrokerPassword": "mqtt_password", + "screensaverOtherAppIntent": "", + "disablePowerButton": true, + "inUseWhileKeyboardVisible": false, + "screensaverFullscreen": true, + "actionBarTitle": "Fully Kiosk Browser", + "pageTransitions": false, + "urlBlacklist": "", + "appBlockReturnIntent": "", + "playAlarmSoundUntilPin": false, + "enableQrScan": false, + "knoxDisableBackup": false, + "appLauncherScaling": "100", + "showAppLauncherOnStart": false, + "appToRunInForegroundOnStart": "", + "usageStatistics": true, + "restartOnCrash": true, + "tabsFgColor": -16777216, + "knoxDisableGoogleAccountsAutoSync": false, + "appToRunOnStart": "", + "knoxDisableDeveloperMode": false, + "sleepOnPowerDisconnect": false, + "remotePdfFileMode": "0", + "keepOnWhileFullscreen": true, + "mainWebAutomation": "", + "mdmMinimumPasswordLength": "5", + "deleteHistoryOnReload": false, + "showQrScanButton": false, + "mdmApkToInstall": "", + "autoplayVideos": false, + "loadOverview": false, + "startURL": "https://homeassistant.local/", + "screensaverOtherApp": false, + "showNewTabButton": false, + "advancedKioskProtection": false, + "mdmDisableKeyguard": false, + "searchProviderUrl": "https://www.google.com/search?q=", + "defaultWebviewBackgroundColor": -1, + "motionDetectionAcoustic": false, + "reloadOnScreenOn": false, + "knoxDisableMultiUser": false, + "enableBackButton": true, + "mdmDisableUsbStorage": false, + "actionBarBgUrl": "", + "lockSafeMode": false, + "setWifiWakelock": false, + "knoxHideStatusBar": false, + "ignoreMotionWhenScreensaverOnOff": false, + "kioskWifiPinCustomIntent": "", + "showTabCloseButtons": true, + "knoxDisablePowerSavingMode": false, + "killOtherApps": false, + "knoxDisableMtp": false, + "deleteWebstorageOnReload": false, + "knoxDisablePowerOff": false, + "knoxDisableGoogleCrashReport": false, + "barcodeScanIntent": "", + "mdmAppsToDisable": "", + "barcodeScanTargetUrl": "", + "knoxDisablePowerButton": false, + "actionBarFgColor": -1, + "fadeInOutDuration": "200", + "audioRecordUploads": false, + "autoplayAudio": false, + "remoteAdminCamshot": true, + "showPrintButton": false, + "knoxDisableTaskManager": false, + "kioskExitGesture": "2", + "remoteAdminScreenshot": true, + "kioskAppWhitelist": "de.badaix.snapcast", + "enableVersionInfo": true, + "removeStatusBar": false, + "knoxDisableCellularData": false, + "showShareButton": false, + "recreateTabsOnReload": false, + "injectJsCode": "", + "screensaverWallpaperURL": "fully://color#000000", + "removeNavigationBar": false, + "forceScreenOrientation": "0", + "showStatusBar": false, + "knoxDisableWiFi": false, + "inUseWhileAnotherAppInForeground": false, + "appLauncherBackgroundColor": -1, + "mdmAppLockTaskWhitelist": "", + "webcamAccess": false, + "knoxDisableClipboard": false, + "playAlarmSoundOnMovement": false, + "loadContentZipFileUrl": "", + "forceImmersive": false, + "forceShowKeyboard": false, + "keepScreenOn": true, + "pauseMotionInBackground": false, + "showCamPreview": false, + "timeToScreenOffV2": "0", + "softKeyboard": true, + "statusBarColor": 0, + "mqttBrokerUsername": "mqtt", + "fileUploads": false, + "rootEnable": true, + "knoxDisableUsbTethering": false, + "screensaverPlaylist": "", + "showNavigationBar": false, + "launcherApps": "[\n {\n \"label\": \"Plex\",\n \"component\": \"com.plexapp.android\\/com.plexapp.plex.activities.SplashActivity\"\n },\n {\n \"label\": \"Spotify\",\n \"component\": \"com.spotify.music\\/com.spotify.music.MainActivity\"\n }\n]", + "autoImportSettings": true, + "motionDetection": false, + "cloudService": false, + "webviewDarkMode": "1", + "killAppsBeforeStartingList": "", + "websiteIntegration": true, + "clientCaPassword": "", + "deleteCookiesOnReload": false, + "errorURL": "", + "knoxDisableFactoryReset": false, + "knoxDisableCamera": false, + "knoxEnabled": false, + "playMedia": true, + "stopScreensaverOnMotion": true, + "redirectBlocked": false, + "mdmDisableADB": true, + "showTabs": false, + "restartAfterUpdate": true, + "mqttEventTopic": "$appId/event/$event/$deviceId", + "fontSize": "100", + "clearCacheEach": false, + "mdmDisableVolumeButtons": false, + "knoxDisableVpn": false, + "confirmExit": true, + "ignoreSSLerrors": true, + "mqttClientId": "", + "setRemoveSystemUI": false, + "wifiKey": "", + "screenOffInDarkness": false, + "knoxDisableWifiDirect": false, + "inactiveTabsBgColor": -4144960, + "useWideViewport": true, + "forceSwipeUnlock": false, + "geoLocationAccess": false, + "graphicsAccelerationMode": "2", + "barcodeScanBroadcastAction": "", + "authPassword": "", + "tabsBgColor": -2236963, + "loadCurrentPageOnReload": false, + "skipReloadIfStartUrlShowing": false, + "enablePullToRefresh": false, + "motionSensitivityAcoustic": "90", + "wifiMode": "0", + "ignoreMotionWhenMoving": false, + "knoxDisableAirViewMode": false, + "actionBarCustomButtonUrl": "", + "kioskHomeStartURL": true, + "environmentSensorsEnabled": true, + "mdmRuntimePermissionPolicy": "0", + "disableCamera": false, + "knoxDisableRecentTaskButton": false, + "knoxDisableAirCommandMode": false, + "remoteAdminSingleAppExit": false, + "mqttDeviceInfoTopic": "$appId/deviceInfo/$deviceId", + "darknessLevel": "10", + "actionBarInSettings": false, + "remoteAdminLan": true, + "knoxDisableSDCardWrite": false, + "movementBeaconDistance": "5", + "forceSleepIfUnplugged": false, + "swipeTabs": false, + "alarmSoundFileUrl": "", + "knoxDisableStatusBar": false, + "reloadOnInternet": false, + "knoxDisableNonMarketApps": false, + "knoxDisableSettingsChanges": false, + "barcodeScanListenKeys": false, + "mdmLockTask": false, + "timeToScreensaverV2": "900", + "movementDetection": true, + "clientCaUrl": "", + "sleepOnPowerConnect": false, + "resumeVideoAudio": true, + "addXffHeader": false, + "readNfcTag": false, + "swipeNavigation": false, + "knoxDisableVolumeButtons": false, + "screenOnOnMovement": true, + "knoxHideNavigationBar": false, + "timeToClearSingleAppData": "0", + "timeToGoBackground": "0", + "timeToShutdownOnPowerDisconnect": "0", + "webviewDragging": true, + "timeToRegainFocus": "600", + "pauseWebviewOnPause": false, + "knoxDisableUsbHostStorage": false, + "folderCleanupList": "", + "movementWhenUnplugged": false, + "actionBarBgColor": -15906911, + "enableZoom": false, + "resetZoomEach": false, + "knoxDisableAndroidBeam": false, + "reloadOnWifiOn": true, + "compassSensitivity": "50", + "touchesOtherAppsBreakIdle": true, + "reloadPageFailure": "300", + "knoxDisableBackButton": false, + "actionBarIconUrl": "", + "initialScale": "0", + "knoxDisableMicrophoneState": false, + "remoteAdminFileManagement": true, + "knoxDisableFirmwareRecovery": false, + "errorUrlOnDisconnection": "0", + "addRefererHeader": true, + "showHomeButton": true, + "reloadEachSeconds": "0", + "accelerometerSensitivity": "80", + "knoxDisableScreenCapture": false, + "knoxDisableWifiTethering": false, + "knoxDisableEdgeScreen": false, + "forceScreenOrientationGlobal": false, + "mqttBrokerUrl": "tcp://192.168.1.2:1883", + "wakeupOnPowerConnect": false, + "knoxDisableClipboardShare": false, + "forceScreenUnlock": true, + "knoxDisableAirplaneMode": false, + "barcodeScanInsertInputField": false, + "waitInternetOnReload": true, + "forceOpenByAppUrl": "", + "disableVolumeButtons": true, + "disableStatusBar": true, + "kioskPin": "1234", + "screensaverDaydream": false, + "detectIBeacons": false, + "kioskWifiPinAction": "0", + "stopScreensaverOnMovement": true, + "videoCaptureUploads": false, + "disableIncomingCalls": false, + "motionCameraId": "", + "knoxDisableSafeMode": false, + "webHostFilter": false, + "urlWhitelist": "", + "disableOtherApps": true, + "progressBarColor": -13611010, + "enableTapSound": true, + "volumeLicenseKey": "", + "showActionBar": false, + "ignoreJustOnceLauncher": false, + "remoteAdmin": true, + "launcherBgUrl": "", + "mdmDisableSafeModeBoot": true, + "launcherInjectCode": "", + "cameraCaptureUploads": false, + "disableNotifications": false, + "sleepSchedule": "", + "knoxDisableAudioRecord": false, + "setCpuWakelock": false, + "enablePopups": false, + "mqttEnabled": true, + "knoxDisableOtaUpgrades": false, + "mdmDisableStatusBar": false, + "lastVersionInfo": "1.39", + "textSelection": false, + "jsAlerts": true, + "knoxActiveByKiosk": false, + "disableHomeButton": true, + "webviewDebugging": true, + "mdmDisableAppsFromUnknownSources": true, + "knoxDisableBluetooth": false, + "showAddressBar": false, + "batteryWarning": "20", + "playerCacheImages": true, + "localPdfFileMode": "0", + "resendFormData": false, + "mdmDisableScreenCapture": false, + "kioskMode": true, + "singleAppMode": false, + "mdmPasswordQuality": "0", + "enableUrlOtherApps": true, + "navigationBarColor": 0, + "isRunning": true, + "knoxDisableHomeButton": false, + "webviewScrolling": true, + "motionFps": "5", + "enableLocalhost": false, + "reloadOnScreensaverStop": false, + "customUserAgent": "", + "screenBrightness": "9", + "knoxDisableVideoRecord": false, + "showProgressBar": true, + "formAutoComplete": true, + "showRefreshButton": false, + "knoxDisableHeadphoneState": false, + "showForwardButton": true, + "timeToClearLauncherAppData": "0", + "bluetoothMode": "0", + "enableFullscreenVideos": true, + "remoteFileMode": "0", + "barcodeScanSubmitInputField": false, + "knoxDisableMultiWindowMode": false, + "singleAppIntent": "", + "userAgent": "0", + "runInForeground": true, + "deleteCacheOnReload": false, + "screenOnOnMotion": true +} diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py new file mode 100644 index 00000000000..6b6d2829790 --- /dev/null +++ b/tests/components/fully_kiosk/test_switch.py @@ -0,0 +1,81 @@ +"""Test the Fully Kiosk Browser switches.""" +from unittest.mock import MagicMock + +from homeassistant.components.fully_kiosk.const import DOMAIN +import homeassistant.components.switch as switch +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_switches( + hass: HomeAssistant, mock_fully_kiosk: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test Fully Kiosk switches.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + entity = hass.states.get("switch.amazon_fire_screensaver") + assert entity + assert entity.state == "off" + entry = entity_registry.async_get("switch.amazon_fire_screensaver") + assert entry + assert entry.unique_id == "abcdef-123456-screensaver" + await call_service(hass, "turn_on", "switch.amazon_fire_screensaver") + assert len(mock_fully_kiosk.startScreensaver.mock_calls) == 1 + await call_service(hass, "turn_off", "switch.amazon_fire_screensaver") + assert len(mock_fully_kiosk.stopScreensaver.mock_calls) == 1 + + entity = hass.states.get("switch.amazon_fire_maintenance_mode") + assert entity + assert entity.state == "off" + entry = entity_registry.async_get("switch.amazon_fire_maintenance_mode") + assert entry + assert entry.unique_id == "abcdef-123456-maintenance" + await call_service(hass, "turn_on", "switch.amazon_fire_maintenance_mode") + assert len(mock_fully_kiosk.enableLockedMode.mock_calls) == 1 + await call_service(hass, "turn_off", "switch.amazon_fire_maintenance_mode") + assert len(mock_fully_kiosk.disableLockedMode.mock_calls) == 1 + + entity = hass.states.get("switch.amazon_fire_kiosk_lock") + assert entity + assert entity.state == "on" + entry = entity_registry.async_get("switch.amazon_fire_kiosk_lock") + assert entry + assert entry.unique_id == "abcdef-123456-kiosk" + await call_service(hass, "turn_off", "switch.amazon_fire_kiosk_lock") + assert len(mock_fully_kiosk.unlockKiosk.mock_calls) == 1 + await call_service(hass, "turn_on", "switch.amazon_fire_kiosk_lock") + assert len(mock_fully_kiosk.lockKiosk.mock_calls) == 1 + + entity = hass.states.get("switch.amazon_fire_motion_detection") + assert entity + assert entity.state == "off" + entry = entity_registry.async_get("switch.amazon_fire_motion_detection") + assert entry + assert entry.unique_id == "abcdef-123456-motion-detection" + await call_service(hass, "turn_on", "switch.amazon_fire_motion_detection") + assert len(mock_fully_kiosk.enableMotionDetection.mock_calls) == 1 + await call_service(hass, "turn_off", "switch.amazon_fire_motion_detection") + assert len(mock_fully_kiosk.disableMotionDetection.mock_calls) == 1 + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + +def call_service(hass, service, entity_id): + """Call any service on entity.""" + return hass.services.async_call( + switch.DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) From 8619df52940dd6fd2402f32bf8971fce2bf19aeb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:38:41 +0200 Subject: [PATCH 431/903] Improve type hints in utility_meter select entity (#76447) --- homeassistant/components/utility_meter/select.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index efddfba94e5..55845569af0 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -23,7 +23,7 @@ async def async_setup_entry( ) -> None: """Initialize Utility Meter config entry.""" name = config_entry.title - tariffs = config_entry.options[CONF_TARIFFS] + tariffs: list[str] = config_entry.options[CONF_TARIFFS] unique_id = config_entry.entry_id tariff_select = TariffSelect(name, tariffs, unique_id) @@ -52,7 +52,7 @@ async def async_setup_platform( async_add_entities( [ TariffSelect( - discovery_info[CONF_METER], + meter, discovery_info[CONF_TARIFFS], conf_meter_unique_id, ) @@ -67,22 +67,22 @@ class TariffSelect(SelectEntity, RestoreEntity): """Initialize a tariff selector.""" self._attr_name = name self._attr_unique_id = unique_id - self._current_tariff = None + self._current_tariff: str | None = None self._tariffs = tariffs self._attr_icon = TARIFF_ICON self._attr_should_poll = False @property - def options(self): + def options(self) -> list[str]: """Return the available tariffs.""" return self._tariffs @property - def current_option(self): + def current_option(self) -> str | None: """Return current tariff.""" return self._current_tariff - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" await super().async_added_to_hass() From 673a72503d14497b85e06c630bb6e8c5f36361cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:58:17 +0200 Subject: [PATCH 432/903] Improve type hints in water_heater (#76910) * Improve type hints in water_heater * Adjust melcloud --- .../components/melcloud/water_heater.py | 10 +++-- .../components/water_heater/__init__.py | 43 +++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index c9075abb008..58da39b1a61 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -9,6 +9,8 @@ from pymelcloud.atw_device import ( from pymelcloud.device import PROPERTY_POWER from homeassistant.components.water_heater import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, WaterHeaterEntity, WaterHeaterEntityFeature, ) @@ -122,11 +124,11 @@ class AtwWaterHeater(WaterHeaterEntity): await self._device.set({PROPERTY_OPERATION_MODE: operation_mode}) @property - def min_temp(self) -> float | None: + def min_temp(self) -> float: """Return the minimum temperature.""" - return self._device.target_tank_temperature_min + return self._device.target_tank_temperature_min or DEFAULT_MIN_TEMP @property - def max_temp(self) -> float | None: + def max_temp(self) -> float: """Return the maximum temperature.""" - return self._device.target_tank_temperature_max + return self._device.target_tank_temperature_max or DEFAULT_MAX_TEMP diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index e24d117f678..50ba1d15f2e 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -1,12 +1,13 @@ """Support for water heater devices.""" from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta from enum import IntEnum import functools as ft import logging -from typing import final +from typing import Any, final import voluptuous as vol @@ -23,7 +24,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -188,11 +189,11 @@ class WaterHeaterEntity(Entity): return PRECISION_WHOLE @property - def capability_attributes(self): + def capability_attributes(self) -> Mapping[str, Any]: """Return capability attributes.""" supported_features = self.supported_features or 0 - data = { + data: dict[str, Any] = { ATTR_MIN_TEMP: show_temp( self.hass, self.min_temp, self.temperature_unit, self.precision ), @@ -208,9 +209,9 @@ class WaterHeaterEntity(Entity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" - data = { + data: dict[str, Any] = { ATTR_CURRENT_TEMPERATURE: show_temp( self.hass, self.current_temperature, @@ -237,7 +238,7 @@ class WaterHeaterEntity(Entity): ), } - supported_features = self.supported_features + supported_features = self.supported_features or 0 if supported_features & WaterHeaterEntityFeature.OPERATION_MODE: data[ATTR_OPERATION_MODE] = self.current_operation @@ -288,42 +289,42 @@ class WaterHeaterEntity(Entity): """Return true if away mode is on.""" return self._attr_is_away_mode_on - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" raise NotImplementedError() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self.hass.async_add_executor_job( ft.partial(self.set_temperature, **kwargs) ) - def set_operation_mode(self, operation_mode): + def set_operation_mode(self, operation_mode: str) -> None: """Set new target operation mode.""" raise NotImplementedError() - async def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode: str) -> None: """Set new target operation mode.""" await self.hass.async_add_executor_job(self.set_operation_mode, operation_mode) - def turn_away_mode_on(self): + def turn_away_mode_on(self) -> None: """Turn away mode on.""" raise NotImplementedError() - async def async_turn_away_mode_on(self): + async def async_turn_away_mode_on(self) -> None: """Turn away mode on.""" await self.hass.async_add_executor_job(self.turn_away_mode_on) - def turn_away_mode_off(self): + def turn_away_mode_off(self) -> None: """Turn away mode off.""" raise NotImplementedError() - async def async_turn_away_mode_off(self): + async def async_turn_away_mode_off(self) -> None: """Turn away mode off.""" await self.hass.async_add_executor_job(self.turn_away_mode_off) @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" if hasattr(self, "_attr_min_temp"): return self._attr_min_temp @@ -332,7 +333,7 @@ class WaterHeaterEntity(Entity): ) @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" if hasattr(self, "_attr_max_temp"): return self._attr_max_temp @@ -341,7 +342,9 @@ class WaterHeaterEntity(Entity): ) -async def async_service_away_mode(entity, service): +async def async_service_away_mode( + entity: WaterHeaterEntity, service: ServiceCall +) -> None: """Handle away mode service.""" if service.data[ATTR_AWAY_MODE]: await entity.async_turn_away_mode_on() @@ -349,7 +352,9 @@ async def async_service_away_mode(entity, service): await entity.async_turn_away_mode_off() -async def async_service_temperature_set(entity, service): +async def async_service_temperature_set( + entity: WaterHeaterEntity, service: ServiceCall +) -> None: """Handle set temperature service.""" hass = entity.hass kwargs = {} From 35d01d79392eb4121019f1df0631d68ac46ecf54 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:58:42 +0200 Subject: [PATCH 433/903] Add RestoreNumber to number checks in pylint (#76933) --- pylint/plugins/hass_enforce_type_hints.py | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 3d86767df43..a6c4a968aa7 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -577,6 +577,20 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ has_async_counterpart=True, ), ] +_RESTORE_ENTITY_MATCH: list[TypeHintMatch] = [ + TypeHintMatch( + function_name="async_get_last_state", + return_type=["State", None], + ), + TypeHintMatch( + function_name="async_get_last_extra_data", + return_type=["ExtraStoredData", None], + ), + TypeHintMatch( + function_name="extra_restore_state_data", + return_type=["ExtraStoredData", None], + ), +] _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="is_on", @@ -1786,6 +1800,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { base_class="Entity", matches=_ENTITY_MATCH, ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), ClassTypeHintMatch( base_class="NumberEntity", matches=[ @@ -1829,6 +1847,19 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), ], ), + ClassTypeHintMatch( + base_class="RestoreNumber", + matches=[ + TypeHintMatch( + function_name="extra_restore_state_data", + return_type="NumberExtraStoredData", + ), + TypeHintMatch( + function_name="async_get_last_number_data", + return_type=["NumberExtraStoredData", None], + ), + ], + ), ], "remote": [ ClassTypeHintMatch( From 171893b4847bd73f85fee7f0a8f613b6301da8aa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 17:00:30 +0200 Subject: [PATCH 434/903] Fix acmeda set cover tilt position (#76927) --- homeassistant/components/acmeda/cover.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py index 887e26cd7fc..3c3a5ba825a 100644 --- a/homeassistant/components/acmeda/cover.py +++ b/homeassistant/components/acmeda/cover.py @@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .base import AcmedaBase from .const import ACMEDA_HUB_UPDATE, DOMAIN from .helpers import async_add_acmeda_entities +from .hub import PulseHub async def async_setup_entry( @@ -24,7 +25,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Acmeda Rollers from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id] current: set[int] = set() @@ -122,6 +123,6 @@ class AcmedaCover(AcmedaBase, CoverEntity): """Stop the roller.""" await self.roller.move_stop() - async def async_set_cover_tilt(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Tilt the roller shutter to a specific position.""" await self.roller.move_to(100 - kwargs[ATTR_POSITION]) From ce077489792ea0ec2545514987b35a0912618fa2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 17:50:00 +0200 Subject: [PATCH 435/903] Add water_heater checks to pylint plugin (#76911) --- pylint/plugins/hass_enforce_type_hints.py | 77 +++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index a6c4a968aa7..004f01aab42 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1999,6 +1999,83 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "water_heater": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="WaterHeaterEntity", + matches=[ + TypeHintMatch( + function_name="precision", + return_type="float", + ), + TypeHintMatch( + function_name="temperature_unit", + return_type="str", + ), + TypeHintMatch( + function_name="current_operation", + return_type=["str", None], + ), + TypeHintMatch( + function_name="operation_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="current_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_high", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_low", + return_type=["float", None], + ), + TypeHintMatch( + function_name="is_away_mode_on", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="set_temperature", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_operation_mode", + arg_types={1: "str"}, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_away_mode_on", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_away_mode_off", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="min_temp", + return_type="float", + ), + TypeHintMatch( + function_name="max_temp", + return_type="float", + ), + ], + ), + ], "weather": [ ClassTypeHintMatch( base_class="Entity", From a63a3b96b73fc802b3048288b20ae5ff1903d354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Wed, 17 Aug 2022 17:53:21 +0200 Subject: [PATCH 436/903] Bump pysma to 0.6.12 (#76937) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 6438c3a5777..8183e7a97c0 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.11"], + "requirements": ["pysma==0.6.12"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling", "loggers": ["pysma"] diff --git a/requirements_all.txt b/requirements_all.txt index 5aeb4d57d1b..000e34e6218 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1840,7 +1840,7 @@ pysignalclirestapi==0.3.18 pyskyqhub==0.1.4 # homeassistant.components.sma -pysma==0.6.11 +pysma==0.6.12 # homeassistant.components.smappee pysmappee==0.2.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6659b8c5b5..07a11f0aae6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1275,7 +1275,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.18 # homeassistant.components.sma -pysma==0.6.11 +pysma==0.6.12 # homeassistant.components.smappee pysmappee==0.2.29 From fc6c66fe6c5b48c669b50f45869a2670b5c5087e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 18:42:35 +0200 Subject: [PATCH 437/903] Add RestoreEntity to button checks in pylint (#76932) --- pylint/plugins/hass_enforce_type_hints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 004f01aab42..4e81b7b77af 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -724,6 +724,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { base_class="Entity", matches=_ENTITY_MATCH, ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), ClassTypeHintMatch( base_class="ButtonEntity", matches=[ From 27b5ebb9d3a1912f55ec365b805f6e15f8bfe1a8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 18:43:28 +0200 Subject: [PATCH 438/903] Add RestoreSensor to sensor checks in pylint (#76916) --- pylint/plugins/hass_enforce_type_hints.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 4e81b7b77af..7eb31eba523 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1949,6 +1949,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { base_class="Entity", matches=_ENTITY_MATCH, ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), ClassTypeHintMatch( base_class="SensorEntity", matches=[ @@ -1983,6 +1987,19 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), ], ), + ClassTypeHintMatch( + base_class="RestoreSensor", + matches=[ + TypeHintMatch( + function_name="extra_restore_state_data", + return_type="SensorExtraStoredData", + ), + TypeHintMatch( + function_name="async_get_last_sensor_data", + return_type=["SensorExtraStoredData", None], + ), + ], + ), ], "siren": [ ClassTypeHintMatch( From 49c793b1a2a5e110b210d81729c18c1f5a419352 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 18:44:08 +0200 Subject: [PATCH 439/903] Add scene checks to pylint plugin (#76908) --- pylint/plugins/hass_enforce_type_hints.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 7eb31eba523..99d06c380fd 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1911,6 +1911,27 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "scene": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="Scene", + matches=[ + TypeHintMatch( + function_name="activate", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "select": [ ClassTypeHintMatch( base_class="Entity", From ff7ef7e5265c565921ee33a4b9f4ab35290d1a1c Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 17 Aug 2022 12:45:34 -0400 Subject: [PATCH 440/903] Bump version of pyunifiprotect to 4.1.2 (#76936) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 246a4643412..218516dc81b 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.13", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.1.2", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 000e34e6218..ecebd456696 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2016,7 +2016,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.13 +pyunifiprotect==4.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07a11f0aae6..321398720cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1373,7 +1373,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.13 +pyunifiprotect==4.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 3bcc274dfa90d7d3c01ace83137c46a0898c107f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 10:51:56 -1000 Subject: [PATCH 441/903] Rework bluetooth to support scans from multiple sources (#76900) --- .../components/bluetooth/__init__.py | 56 +-- homeassistant/components/bluetooth/const.py | 1 + homeassistant/components/bluetooth/manager.py | 329 ++++++-------- homeassistant/components/bluetooth/match.py | 4 - homeassistant/components/bluetooth/models.py | 98 +---- homeassistant/components/bluetooth/scanner.py | 241 ++++++++++ tests/components/bluetooth/__init__.py | 36 +- tests/components/bluetooth/test_init.py | 412 ++++-------------- .../test_passive_update_coordinator.py | 14 +- .../test_passive_update_processor.py | 15 +- tests/components/bluetooth/test_scanner.py | 264 +++++++++++ tests/conftest.py | 20 +- 12 files changed, 805 insertions(+), 685 deletions(-) create mode 100644 homeassistant/components/bluetooth/scanner.py create mode 100644 tests/components/bluetooth/test_scanner.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index a90f367fc38..8bb275c94fd 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,11 +8,14 @@ from typing import TYPE_CHECKING import async_timeout from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback as hass_callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.loader import async_get_bluetooth -from .const import CONF_ADAPTER, DOMAIN, SOURCE_LOCAL +from . import models +from .const import CONF_ADAPTER, DATA_MANAGER, DOMAIN, SOURCE_LOCAL from .manager import BluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher from .models import ( @@ -24,6 +27,7 @@ from .models import ( HaBleakScannerWrapper, ProcessAdvertisementCallback, ) +from .scanner import HaScanner, create_bleak_scanner from .util import async_get_bluetooth_adapters if TYPE_CHECKING: @@ -55,10 +59,7 @@ def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: This is a wrapper around our BleakScanner singleton that allows multiple integrations to share the same BleakScanner. """ - if DOMAIN not in hass.data: - raise RuntimeError("Bluetooth integration not loaded") - manager: BluetoothManager = hass.data[DOMAIN] - return manager.async_get_scanner() + return HaBleakScannerWrapper() @hass_callback @@ -66,9 +67,9 @@ def async_discovered_service_info( hass: HomeAssistant, ) -> list[BluetoothServiceInfoBleak]: """Return the discovered devices list.""" - if DOMAIN not in hass.data: + if DATA_MANAGER not in hass.data: return [] - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_discovered_service_info() @@ -78,9 +79,9 @@ def async_ble_device_from_address( address: str, ) -> BLEDevice | None: """Return BLEDevice for an address if its present.""" - if DOMAIN not in hass.data: + if DATA_MANAGER not in hass.data: return None - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_ble_device_from_address(address) @@ -90,9 +91,9 @@ def async_address_present( address: str, ) -> bool: """Check if an address is present in the bluetooth device list.""" - if DOMAIN not in hass.data: + if DATA_MANAGER not in hass.data: return False - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_address_present(address) @@ -112,7 +113,7 @@ def async_register_callback( Returns a callback that can be used to cancel the registration. """ - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_register_callback(callback, match_dict) @@ -152,14 +153,14 @@ def async_track_unavailable( Returns a callback that can be used to cancel the registration. """ - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_track_unavailable(callback, address) @hass_callback def async_rediscover_address(hass: HomeAssistant, address: str) -> None: """Trigger discovery of devices which have already been seen.""" - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] manager.async_rediscover_address(address) @@ -173,7 +174,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) manager = BluetoothManager(hass, integration_matcher) manager.async_setup() - hass.data[DOMAIN] = manager + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) + hass.data[DATA_MANAGER] = models.MANAGER = manager # The config entry is responsible for starting the manager # if its enabled @@ -198,13 +200,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: - """Set up the bluetooth integration from a config entry.""" - manager: BluetoothManager = hass.data[DOMAIN] - async with manager.start_stop_lock: - await manager.async_start( - BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) - ) + """Set up a config entry for a bluetooth scanner.""" + manager: BluetoothManager = hass.data[DATA_MANAGER] + adapter: str | None = entry.options.get(CONF_ADAPTER) + try: + bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter) + except RuntimeError as err: + raise ConfigEntryNotReady from err + scanner = HaScanner(hass, bleak_scanner, adapter) + entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) + await scanner.async_start() + entry.async_on_unload(manager.async_register_scanner(scanner)) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner return True @@ -219,8 +227,6 @@ async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Unload a config entry.""" - manager: BluetoothManager = hass.data[DOMAIN] - async with manager.start_stop_lock: - manager.async_start_reload() - await manager.async_stop() + scanner: HaScanner = hass.data[DOMAIN].pop(entry.entry_id) + await scanner.async_stop() return True diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index fac191202b0..04581b841b9 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -16,6 +16,7 @@ DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAP SOURCE_LOCAL: Final = "local" +DATA_MANAGER: Final = "bluetooth_manager" UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 START_TIMEOUT = 12 diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index e4f75350575..15b05271bd4 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -1,70 +1,66 @@ """The bluetooth integration.""" from __future__ import annotations -import asyncio -from collections.abc import Callable +from collections.abc import Callable, Iterable from datetime import datetime, timedelta +import itertools import logging -import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final -import async_timeout -from bleak import BleakError -from dbus_next import InvalidMessageError +from bleak.backends.scanner import AdvertisementDataCallback from homeassistant import config_entries -from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( CALLBACK_TYPE, Event, HomeAssistant, callback as hass_callback, ) -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util.package import is_docker_env -from . import models -from .const import ( - DEFAULT_ADAPTERS, - SCANNER_WATCHDOG_INTERVAL, - SCANNER_WATCHDOG_TIMEOUT, - SOURCE_LOCAL, - START_TIMEOUT, - UNAVAILABLE_TRACK_SECONDS, -) +from .const import SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS from .match import ( ADDRESS, BluetoothCallbackMatcher, IntegrationMatcher, ble_device_matches, ) -from .models import ( - BluetoothCallback, - BluetoothChange, - BluetoothScanningMode, - BluetoothServiceInfoBleak, - HaBleakScanner, - HaBleakScannerWrapper, -) +from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher if TYPE_CHECKING: from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData + from .scanner import HaScanner + +FILTER_UUIDS: Final = "UUIDs" + _LOGGER = logging.getLogger(__name__) -MONOTONIC_TIME = time.monotonic +def _dispatch_bleak_callback( + callback: AdvertisementDataCallback, + filters: dict[str, set[str]], + device: BLEDevice, + advertisement_data: AdvertisementData, +) -> None: + """Dispatch the callback.""" + if not callback: + # Callback destroyed right before being called, ignore + return # type: ignore[unreachable] # pragma: no cover + if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( + advertisement_data.service_uuids + ): + return -SCANNING_MODE_TO_BLEAK = { - BluetoothScanningMode.ACTIVE: "active", - BluetoothScanningMode.PASSIVE: "passive", -} + try: + callback(device, advertisement_data) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in callback: %s", callback) class BluetoothManager: @@ -75,136 +71,46 @@ class BluetoothManager: hass: HomeAssistant, integration_matcher: IntegrationMatcher, ) -> None: - """Init bluetooth discovery.""" + """Init bluetooth manager.""" self.hass = hass self._integration_matcher = integration_matcher - self.scanner: HaBleakScanner | None = None - self.start_stop_lock = asyncio.Lock() - self._cancel_device_detected: CALLBACK_TYPE | None = None self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None - self._cancel_stop: CALLBACK_TYPE | None = None - self._cancel_watchdog: CALLBACK_TYPE | None = None self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] - self._last_detection = 0.0 - self._reloading = False - self._adapter: str | None = None - self._scanning_mode = BluetoothScanningMode.ACTIVE + self._bleak_callbacks: list[ + tuple[AdvertisementDataCallback, dict[str, set[str]]] + ] = [] + self.history: dict[str, tuple[BLEDevice, AdvertisementData, float, str]] = {} + self._scanners: list[HaScanner] = [] @hass_callback def async_setup(self) -> None: """Set up the bluetooth manager.""" - models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() - - @hass_callback - def async_get_scanner(self) -> HaBleakScannerWrapper: - """Get the scanner.""" - return HaBleakScannerWrapper() - - @hass_callback - def async_start_reload(self) -> None: - """Start reloading.""" - self._reloading = True - - async def async_start( - self, scanning_mode: BluetoothScanningMode, adapter: str | None - ) -> None: - """Set up BT Discovery.""" - assert self.scanner is not None - self._adapter = adapter - self._scanning_mode = scanning_mode - if self._reloading: - # On reload, we need to reset the scanner instance - # since the devices in its history may not be reachable - # anymore. - self.scanner.async_reset() - self._integration_matcher.async_clear_history() - self._reloading = False - scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} - if adapter and adapter not in DEFAULT_ADAPTERS: - scanner_kwargs["adapter"] = adapter - _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) - try: - self.scanner.async_setup(**scanner_kwargs) - except (FileNotFoundError, BleakError) as ex: - raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex install_multiple_bleak_catcher() - # We have to start it right away as some integrations might - # need it straight away. - _LOGGER.debug("Starting bluetooth scanner") - self.scanner.register_detection_callback(self.scanner.async_callback_dispatcher) - self._cancel_device_detected = self.scanner.async_register_callback( - self._device_detected, {} - ) - try: - async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() # type: ignore[no-untyped-call] - except InvalidMessageError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) - raise ConfigEntryNotReady( - f"Invalid DBus message received: {ex}; try restarting `dbus`" - ) from ex - except BrokenPipeError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) - if is_docker_env(): - raise ConfigEntryNotReady( - f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" - ) from ex - raise ConfigEntryNotReady( - f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" - ) from ex - except FileNotFoundError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug( - "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True - ) - if is_docker_env(): - raise ConfigEntryNotReady( - f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" - ) from ex - raise ConfigEntryNotReady( - f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" - ) from ex - except asyncio.TimeoutError as ex: - self._async_cancel_scanner_callback() - raise ConfigEntryNotReady( - f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" - ) from ex - except BleakError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) - raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() - self._async_setup_scanner_watchdog() - self._cancel_stop = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping + + @hass_callback + def async_stop(self, event: Event) -> None: + """Stop the Bluetooth integration at shutdown.""" + _LOGGER.debug("Stopping bluetooth manager") + if self._cancel_unavailable_tracking: + self._cancel_unavailable_tracking() + self._cancel_unavailable_tracking = None + uninstall_multiple_bleak_catcher() + + @hass_callback + def async_all_discovered_devices(self) -> Iterable[BLEDevice]: + """Return all of discovered devices from all the scanners including duplicates.""" + return itertools.chain.from_iterable( + scanner.discovered_devices for scanner in self._scanners ) @hass_callback - def _async_setup_scanner_watchdog(self) -> None: - """If Dbus gets restarted or updated, we need to restart the scanner.""" - self._last_detection = MONOTONIC_TIME() - self._cancel_watchdog = async_track_time_interval( - self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL - ) - - async def _async_scanner_watchdog(self, now: datetime) -> None: - """Check if the scanner is running.""" - time_since_last_detection = MONOTONIC_TIME() - self._last_detection - if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: - return - _LOGGER.info( - "Bluetooth scanner has gone quiet for %s, restarting", - SCANNER_WATCHDOG_INTERVAL, - ) - async with self.start_stop_lock: - self.async_start_reload() - await self.async_stop() - await self.async_start(self._scanning_mode, self._adapter) + def async_discovered_devices(self) -> list[BLEDevice]: + """Return all of combined best path to discovered from all the scanners.""" + return [history[0] for history in self.history.values()] @hass_callback def async_setup_unavailable_tracking(self) -> None: @@ -213,13 +119,13 @@ class BluetoothManager: @hass_callback def _async_check_unavailable(now: datetime) -> None: """Watch for unavailable devices.""" - scanner = self.scanner - assert scanner is not None - history = set(scanner.history) - active = {device.address for device in scanner.discovered_devices} - disappeared = history.difference(active) + history_set = set(self.history) + active_addresses = { + device.address for device in self.async_all_discovered_devices() + } + disappeared = history_set.difference(active_addresses) for address in disappeared: - del scanner.history[address] + del self.history[address] if not (callbacks := self._unavailable_callbacks.get(address)): continue for callback in callbacks: @@ -235,16 +141,40 @@ class BluetoothManager: ) @hass_callback - def _device_detected( - self, device: BLEDevice, advertisement_data: AdvertisementData + def scanner_adv_received( + self, + device: BLEDevice, + advertisement_data: AdvertisementData, + monotonic_time: float, + source: str, ) -> None: - """Handle a detected device.""" - self._last_detection = MONOTONIC_TIME() + """Handle a new advertisement from any scanner. + + Callbacks from all the scanners arrive here. + + In the future we will only process callbacks if + + - The device is not in the history + - The RSSI is above a certain threshold better than + than the source from the history or the timestamp + in the history is older than 180s + """ + self.history[device.address] = ( + device, + advertisement_data, + monotonic_time, + source, + ) + + for callback_filters in self._bleak_callbacks: + _dispatch_bleak_callback(*callback_filters, device, advertisement_data) + matched_domains = self._integration_matcher.match_domains( device, advertisement_data ) _LOGGER.debug( - "Device detected: %s with advertisement_data: %s matched domains: %s", + "%s: %s %s match: %s", + source, device.address, advertisement_data, matched_domains, @@ -260,7 +190,7 @@ class BluetoothManager: ): if service_info is None: service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, SOURCE_LOCAL + device, advertisement_data, source ) try: callback(service_info, BluetoothChange.ADVERTISEMENT) @@ -271,7 +201,7 @@ class BluetoothManager: return if service_info is None: service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, SOURCE_LOCAL + device, advertisement_data, source ) for domain in matched_domains: discovery_flow.async_create_flow( @@ -316,13 +246,13 @@ class BluetoothManager: if ( matcher and (address := matcher.get(ADDRESS)) - and self.scanner - and (device_adv_data := self.scanner.history.get(address)) + and (device_adv_data := self.history.get(address)) ): + ble_device, adv_data, _, _ = device_adv_data try: callback( BluetoothServiceInfoBleak.from_advertisement( - *device_adv_data, SOURCE_LOCAL + ble_device, adv_data, SOURCE_LOCAL ), BluetoothChange.ADVERTISEMENT, ) @@ -334,60 +264,55 @@ class BluetoothManager: @hass_callback def async_ble_device_from_address(self, address: str) -> BLEDevice | None: """Return the BLEDevice if present.""" - if self.scanner and (ble_adv := self.scanner.history.get(address)): + if ble_adv := self.history.get(address): return ble_adv[0] return None @hass_callback def async_address_present(self, address: str) -> bool: """Return if the address is present.""" - return bool(self.scanner and address in self.scanner.history) + return address in self.history @hass_callback def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: """Return if the address is present.""" - assert self.scanner is not None return [ - BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) - for device_adv in self.scanner.history.values() + BluetoothServiceInfoBleak.from_advertisement( + device_adv[0], device_adv[1], SOURCE_LOCAL + ) + for device_adv in self.history.values() ] - async def _async_hass_stopping(self, event: Event) -> None: - """Stop the Bluetooth integration at shutdown.""" - self._cancel_stop = None - await self.async_stop() - - @hass_callback - def _async_cancel_scanner_callback(self) -> None: - """Cancel the scanner callback.""" - if self._cancel_device_detected: - self._cancel_device_detected() - self._cancel_device_detected = None - - async def async_stop(self) -> None: - """Stop bluetooth discovery.""" - _LOGGER.debug("Stopping bluetooth discovery") - if self._cancel_watchdog: - self._cancel_watchdog() - self._cancel_watchdog = None - self._async_cancel_scanner_callback() - if self._cancel_unavailable_tracking: - self._cancel_unavailable_tracking() - self._cancel_unavailable_tracking = None - if self._cancel_stop: - self._cancel_stop() - self._cancel_stop = None - if self.scanner: - try: - await self.scanner.stop() # type: ignore[no-untyped-call] - except BleakError as ex: - # This is not fatal, and they may want to reload - # the config entry to restart the scanner if they - # change the bluetooth dongle. - _LOGGER.error("Error stopping scanner: %s", ex) - uninstall_multiple_bleak_catcher() - @hass_callback def async_rediscover_address(self, address: str) -> None: """Trigger discovery of devices which have already been seen.""" self._integration_matcher.async_clear_address(address) + + def async_register_scanner(self, scanner: HaScanner) -> CALLBACK_TYPE: + """Register a new scanner.""" + + def _unregister_scanner() -> None: + self._scanners.remove(scanner) + + self._scanners.append(scanner) + return _unregister_scanner + + @hass_callback + def async_register_bleak_callback( + self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] + ) -> CALLBACK_TYPE: + """Register a callback.""" + callback_entry = (callback, filters) + self._bleak_callbacks.append(callback_entry) + + @hass_callback + def _remove_callback() -> None: + self._bleak_callbacks.remove(callback_entry) + + # Replay the history since otherwise we miss devices + # that were already discovered before the callback was registered + # or we are in passive mode + for device, advertisement_data, _, _ in self.history.values(): + _dispatch_bleak_callback(callback, filters, device, advertisement_data) + + return _remove_callback diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 9c942d9f411..49f9e49db54 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -74,10 +74,6 @@ class IntegrationMatcher: MAX_REMEMBER_ADDRESSES ) - def async_clear_history(self) -> None: - """Clear the history.""" - self._matched = {} - def async_clear_address(self, address: str) -> None: """Clear the history matches for a set of domains.""" self._matched.pop(address, None) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index d5cb1429a2a..1006ed912dd 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -9,25 +9,26 @@ from enum import Enum import logging from typing import TYPE_CHECKING, Any, Final -from bleak import BleakScanner from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, BaseBleakScanner, ) -from homeassistant.core import CALLBACK_TYPE, callback as hass_callback +from homeassistant.core import CALLBACK_TYPE from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo if TYPE_CHECKING: from bleak.backends.device import BLEDevice + from .manager import BluetoothManager + _LOGGER = logging.getLogger(__name__) FILTER_UUIDS: Final = "UUIDs" -HA_BLEAK_SCANNER: HaBleakScanner | None = None +MANAGER: BluetoothManager | None = None @dataclass @@ -73,89 +74,6 @@ BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] -def _dispatch_callback( - callback: AdvertisementDataCallback, - filters: dict[str, set[str]], - device: BLEDevice, - advertisement_data: AdvertisementData, -) -> None: - """Dispatch the callback.""" - if not callback: - # Callback destroyed right before being called, ignore - return # type: ignore[unreachable] - - if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( - advertisement_data.service_uuids - ): - return - - try: - callback(device, advertisement_data) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in callback: %s", callback) - - -class HaBleakScanner(BleakScanner): - """BleakScanner that cannot be stopped.""" - - def __init__( # pylint: disable=super-init-not-called - self, *args: Any, **kwargs: Any - ) -> None: - """Initialize the BleakScanner.""" - self._callbacks: list[ - tuple[AdvertisementDataCallback, dict[str, set[str]]] - ] = [] - self.history: dict[str, tuple[BLEDevice, AdvertisementData]] = {} - # Init called later in async_setup if we are enabling the scanner - # since init has side effects that can throw exceptions - self._setup = False - - @hass_callback - def async_setup(self, *args: Any, **kwargs: Any) -> None: - """Deferred setup of the BleakScanner since __init__ has side effects.""" - if not self._setup: - super().__init__(*args, **kwargs) - self._setup = True - - @hass_callback - def async_reset(self) -> None: - """Reset the scanner so it can be setup again.""" - self.history = {} - self._setup = False - - @hass_callback - def async_register_callback( - self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] - ) -> CALLBACK_TYPE: - """Register a callback.""" - callback_entry = (callback, filters) - self._callbacks.append(callback_entry) - - @hass_callback - def _remove_callback() -> None: - self._callbacks.remove(callback_entry) - - # Replay the history since otherwise we miss devices - # that were already discovered before the callback was registered - # or we are in passive mode - for device, advertisement_data in self.history.values(): - _dispatch_callback(callback, filters, device, advertisement_data) - - return _remove_callback - - def async_callback_dispatcher( - self, device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Dispatch the callback. - - Here we get the actual callback from bleak and dispatch - it to all the wrapped HaBleakScannerWrapper classes - """ - self.history[device.address] = (device, advertisement_data) - for callback_filters in self._callbacks: - _dispatch_callback(*callback_filters, device, advertisement_data) - - class HaBleakScannerWrapper(BaseBleakScanner): """A wrapper that uses the single instance.""" @@ -215,8 +133,8 @@ class HaBleakScannerWrapper(BaseBleakScanner): @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" - assert HA_BLEAK_SCANNER is not None - return HA_BLEAK_SCANNER.discovered_devices + assert MANAGER is not None + return list(MANAGER.async_discovered_devices()) def register_detection_callback( self, callback: AdvertisementDataCallback | None @@ -235,9 +153,9 @@ class HaBleakScannerWrapper(BaseBleakScanner): return self._cancel_callback() super().register_detection_callback(self._adv_data_callback) - assert HA_BLEAK_SCANNER is not None + assert MANAGER is not None assert self._callback is not None - self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( + self._detection_cancel = MANAGER.async_register_bleak_callback( self._callback, self._mapped_filters ) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py new file mode 100644 index 00000000000..c3d45dbde95 --- /dev/null +++ b/homeassistant/components/bluetooth/scanner.py @@ -0,0 +1,241 @@ +"""The bluetooth integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from datetime import datetime +import logging +import time + +import async_timeout +import bleak +from bleak import BleakError +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData +from dbus_next import InvalidMessageError + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + callback as hass_callback, +) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.package import is_docker_env + +from .const import ( + DEFAULT_ADAPTERS, + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, + SOURCE_LOCAL, + START_TIMEOUT, +) +from .models import BluetoothScanningMode + +OriginalBleakScanner = bleak.BleakScanner +MONOTONIC_TIME = time.monotonic + + +_LOGGER = logging.getLogger(__name__) + + +MONOTONIC_TIME = time.monotonic + + +SCANNING_MODE_TO_BLEAK = { + BluetoothScanningMode.ACTIVE: "active", + BluetoothScanningMode.PASSIVE: "passive", +} + + +def create_bleak_scanner( + scanning_mode: BluetoothScanningMode, adapter: str | None +) -> bleak.BleakScanner: + """Create a Bleak scanner.""" + scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} + if adapter and adapter not in DEFAULT_ADAPTERS: + scanner_kwargs["adapter"] = adapter + _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) + try: + return OriginalBleakScanner(**scanner_kwargs) # type: ignore[arg-type] + except (FileNotFoundError, BleakError) as ex: + raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex + + +class HaScanner: + """Operate a BleakScanner. + + Multiple BleakScanner can be used at the same time + if there are multiple adapters. This is only useful + if the adapters are not located physically next to each other. + + Example use cases are usbip, a long extension cable, usb to bluetooth + over ethernet, usb over ethernet, etc. + """ + + def __init__( + self, hass: HomeAssistant, scanner: bleak.BleakScanner, adapter: str | None + ) -> None: + """Init bluetooth discovery.""" + self.hass = hass + self.scanner = scanner + self.adapter = adapter + self._start_stop_lock = asyncio.Lock() + self._cancel_stop: CALLBACK_TYPE | None = None + self._cancel_watchdog: CALLBACK_TYPE | None = None + self._last_detection = 0.0 + self._callbacks: list[ + Callable[[BLEDevice, AdvertisementData, float, str], None] + ] = [] + self.name = self.adapter or "default" + self.source = self.adapter or SOURCE_LOCAL + + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + return self.scanner.discovered_devices + + @hass_callback + def async_register_callback( + self, callback: Callable[[BLEDevice, AdvertisementData, float, str], None] + ) -> CALLBACK_TYPE: + """Register a callback. + + Currently this is used to feed the callbacks into the + central manager. + """ + + def _remove() -> None: + self._callbacks.remove(callback) + + self._callbacks.append(callback) + return _remove + + @hass_callback + def _async_detection_callback( + self, + ble_device: BLEDevice, + advertisement_data: AdvertisementData, + ) -> None: + """Call the callback when an advertisement is received. + + Currently this is used to feed the callbacks into the + central manager. + """ + self._last_detection = MONOTONIC_TIME() + for callback in self._callbacks: + callback(ble_device, advertisement_data, self._last_detection, self.source) + + async def async_start(self) -> None: + """Start bluetooth scanner.""" + self.scanner.register_detection_callback(self._async_detection_callback) + + async with self._start_stop_lock: + await self._async_start() + + async def _async_start(self) -> None: + """Start bluetooth scanner under the lock.""" + try: + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + _LOGGER.debug( + "%s: Invalid DBus message received: %s", self.name, ex, exc_info=True + ) + raise ConfigEntryNotReady( + f"{self.name}: Invalid DBus message received: {ex}; try restarting `dbus`" + ) from ex + except BrokenPipeError as ex: + _LOGGER.debug( + "%s: DBus connection broken: %s", self.name, ex, exc_info=True + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"{self.name}: DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ConfigEntryNotReady( + f"{self.name}: DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + _LOGGER.debug( + "%s: FileNotFoundError while starting bluetooth: %s", + self.name, + ex, + exc_info=True, + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"{self.name}: DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ConfigEntryNotReady( + f"{self.name}: DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" + ) from ex + except asyncio.TimeoutError as ex: + raise ConfigEntryNotReady( + f"{self.name}: Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex + except BleakError as ex: + _LOGGER.debug( + "%s: BleakError while starting bluetooth: %s", + self.name, + ex, + exc_info=True, + ) + raise ConfigEntryNotReady( + f"{self.name}: Failed to start Bluetooth: {ex}" + ) from ex + self._async_setup_scanner_watchdog() + self._cancel_stop = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping + ) + + @hass_callback + def _async_setup_scanner_watchdog(self) -> None: + """If Dbus gets restarted or updated, we need to restart the scanner.""" + self._last_detection = MONOTONIC_TIME() + self._cancel_watchdog = async_track_time_interval( + self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL + ) + + async def _async_scanner_watchdog(self, now: datetime) -> None: + """Check if the scanner is running.""" + time_since_last_detection = MONOTONIC_TIME() - self._last_detection + if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: + return + _LOGGER.info( + "%s: Bluetooth scanner has gone quiet for %s, restarting", + self.name, + SCANNER_WATCHDOG_INTERVAL, + ) + async with self._start_stop_lock: + await self._async_stop() + await self._async_start() + + async def _async_hass_stopping(self, event: Event) -> None: + """Stop the Bluetooth integration at shutdown.""" + self._cancel_stop = None + await self.async_stop() + + async def async_stop(self) -> None: + """Stop bluetooth scanner.""" + async with self._start_stop_lock: + await self._async_stop() + + async def _async_stop(self) -> None: + """Stop bluetooth discovery under the lock.""" + _LOGGER.debug("Stopping bluetooth discovery") + if self._cancel_watchdog: + self._cancel_watchdog() + self._cancel_watchdog = None + if self._cancel_stop: + self._cancel_stop() + self._cancel_stop = None + try: + await self.scanner.stop() # type: ignore[no-untyped-call] + except BleakError as ex: + # This is not fatal, and they may want to reload + # the config entry to restart the scanner if they + # change the bluetooth dongle. + _LOGGER.error("Error stopping scanner: %s", ex) diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 3dc80d55590..2b6ad75d2b9 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -1,8 +1,38 @@ """Tests for the Bluetooth integration.""" -from homeassistant.components.bluetooth import models + +import time +from unittest.mock import patch + +from bleak.backends.scanner import AdvertisementData, BLEDevice + +from homeassistant.components.bluetooth import SOURCE_LOCAL, models +from homeassistant.components.bluetooth.manager import BluetoothManager -def _get_underlying_scanner(): +def _get_manager() -> BluetoothManager: + """Return the bluetooth manager.""" + return models.MANAGER + + +def inject_advertisement(device: BLEDevice, adv: AdvertisementData) -> None: """Return the underlying scanner that has been wrapped.""" - return models.HA_BLEAK_SCANNER + return _get_manager().scanner_adv_received( + device, adv, time.monotonic(), SOURCE_LOCAL + ) + + +def patch_all_discovered_devices(mock_discovered: list[BLEDevice]) -> None: + """Mock all the discovered devices from all the scanners.""" + manager = _get_manager() + return patch.object( + manager, "async_all_discovered_devices", return_value=mock_discovered + ) + + +def patch_discovered_devices(mock_discovered: list[BLEDevice]) -> None: + """Mock the combined best path to discovered devices from all the scanners.""" + manager = _get_manager() + return patch.object( + manager, "async_discovered_devices", return_value=mock_discovered + ) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 2387d35fc23..84c37300dc4 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice -from dbus_next import InvalidMessageError import pytest from homeassistant.components import bluetooth @@ -16,16 +15,12 @@ from homeassistant.components.bluetooth import ( async_process_advertisements, async_rediscover_address, async_track_unavailable, - manager, models, + scanner, ) from homeassistant.components.bluetooth.const import ( - CONF_ADAPTER, - SCANNER_WATCHDOG_INTERVAL, - SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, - UNIX_DEFAULT_BLUETOOTH_ADAPTER, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP @@ -33,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_underlying_scanner +from . import _get_manager, inject_advertisement, patch_discovered_devices from tests.common import MockConfigEntry, async_fire_time_changed @@ -63,7 +58,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} ] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", side_effect=BleakError, ) as mock_ha_bleak_scanner, patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -85,9 +80,7 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): """Test we fail gracefully when bluetooth/dbus is broken.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -112,10 +105,8 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): async def _mock_hang(): await asyncio.sleep(1) - with patch.object(manager, "START_TIMEOUT", 0), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + with patch.object(scanner, "START_TIMEOUT", 0), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=_mock_hang, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -136,9 +127,7 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): """Test we retry if the adapter is not yet available.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -157,14 +146,14 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): assert entry.state == ConfigEntryState.SETUP_RETRY with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.stop", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop", ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -174,9 +163,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): """Test we can successfully reload when the entry is in a retry state.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -195,7 +182,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): assert entry.state == ConfigEntryState.SETUP_RETRY with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", ): await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() @@ -203,7 +190,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.stop", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop", ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -213,7 +200,7 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", side_effect=FileNotFoundError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -252,7 +239,7 @@ async def test_discovery_match_by_service_uuid( wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - _get_underlying_scanner()._callback(wrong_device, wrong_adv) + inject_advertisement(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -262,7 +249,7 @@ async def test_discovery_match_by_service_uuid( local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -289,7 +276,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - _get_underlying_scanner()._callback(wrong_device, wrong_adv) + inject_advertisement(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -297,7 +284,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -343,21 +330,21 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( # 1st discovery with no manufacturer data # should not trigger config flow - _get_underlying_scanner()._callback(hkc_device, hkc_adv_no_mfr_data) + inject_advertisement(hkc_device, hkc_adv_no_mfr_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() # 2nd discovery with manufacturer data # should trigger a config flow - _get_underlying_scanner()._callback(hkc_device, hkc_adv) + inject_advertisement(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller" mock_config_flow.reset_mock() # 3rd discovery should not generate another flow - _get_underlying_scanner()._callback(hkc_device, hkc_adv) + inject_advertisement(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -368,7 +355,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} ) - _get_underlying_scanner()._callback(not_hkc_device, not_hkc_adv) + inject_advertisement(not_hkc_device, not_hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -377,7 +364,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} ) - _get_underlying_scanner()._callback(not_apple_device, not_apple_adv) + inject_advertisement(not_apple_device, not_apple_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -453,21 +440,21 @@ async def test_discovery_match_by_service_data_uuid_then_others( ) # 1st discovery should not generate a flow because the # service_data_uuid is not in the advertisement - _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + inject_advertisement(device, adv_without_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() # 2nd discovery should not generate a flow because the # service_data_uuid is not in the advertisement - _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + inject_advertisement(device, adv_without_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() # 3rd discovery should generate a flow because the # manufacturer_data is in the advertisement - _get_underlying_scanner()._callback(device, adv_with_mfr_data) + inject_advertisement(device, adv_with_mfr_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "other_domain" @@ -476,7 +463,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 4th discovery should generate a flow because the # service_data_uuid is in the advertisement and # we never saw a service_data_uuid before - _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + inject_advertisement(device, adv_with_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "my_domain" @@ -484,16 +471,14 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 5th discovery should not generate a flow because the # we already saw an advertisement with the service_data_uuid - _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + inject_advertisement(device, adv_with_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 6th discovery should not generate a flow because the # manufacturer_data is in the advertisement # and we saw manufacturer_data before - _get_underlying_scanner()._callback( - device, adv_with_service_data_uuid_and_mfr_data - ) + inject_advertisement(device, adv_with_service_data_uuid_and_mfr_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() @@ -501,7 +486,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 7th discovery should generate a flow because the # service_uuids is in the advertisement # and we never saw service_uuids before - _get_underlying_scanner()._callback( + inject_advertisement( device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid ) await hass.async_block_till_done() @@ -514,7 +499,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 8th discovery should not generate a flow # since all fields have been seen at this point - _get_underlying_scanner()._callback( + inject_advertisement( device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid ) await hass.async_block_till_done() @@ -523,19 +508,19 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 9th discovery should not generate a flow # since all fields have been seen at this point - _get_underlying_scanner()._callback(device, adv_with_service_uuid) + inject_advertisement(device, adv_with_service_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 10th discovery should not generate a flow # since all fields have been seen at this point - _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + inject_advertisement(device, adv_with_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 11th discovery should not generate a flow # since all fields have been seen at this point - _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + inject_advertisement(device, adv_without_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -582,7 +567,7 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( # 1st discovery with matches service_uuid # should trigger config flow - _get_underlying_scanner()._callback(device, adv_service_uuids) + inject_advertisement(device, adv_service_uuids) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "my_domain" @@ -590,19 +575,19 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( # 2nd discovery with manufacturer data # should trigger a config flow - _get_underlying_scanner()._callback(device, adv_manufacturer_data) + inject_advertisement(device, adv_manufacturer_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "my_domain" mock_config_flow.reset_mock() # 3rd discovery should not generate another flow - _get_underlying_scanner()._callback(device, adv_service_uuids) + inject_advertisement(device, adv_service_uuids) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 4th discovery should not generate another flow - _get_underlying_scanner()._callback(device, adv_manufacturer_data) + inject_advertisement(device, adv_manufacturer_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -628,10 +613,10 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -639,7 +624,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): async_rediscover_address(hass, "44:44:33:11:23:45") - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 2 @@ -672,10 +657,10 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - _get_underlying_scanner()._callback(wrong_device, wrong_adv) + inject_advertisement(wrong_device, wrong_adv) switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) wrong_device_went_unavailable = False switchbot_device_went_unavailable = False @@ -709,8 +694,8 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): assert wrong_device_went_unavailable is True # See the devices again - _get_underlying_scanner()._callback(wrong_device, wrong_adv) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(wrong_device, wrong_adv) + inject_advertisement(switchbot_device, switchbot_adv) # Cancel the callbacks wrong_device_unavailable_cancel() switchbot_device_unavailable_cancel() @@ -776,25 +761,25 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() assert len(callbacks) == 3 @@ -862,25 +847,25 @@ async def test_register_callback_by_address( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() # Now register again with a callback that fails to @@ -953,7 +938,7 @@ async def test_register_callback_survives_reload( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] assert service_info.name == "wohand" @@ -964,7 +949,7 @@ async def test_register_callback_survives_reload( await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) assert len(callbacks) == 2 service_info: BluetoothServiceInfo = callbacks[1][0] assert service_info.name == "wohand" @@ -1001,9 +986,9 @@ async def test_process_advertisements_bail_on_good_advertisement( service_data={"00000d00-0000-1000-8000-00805f9b34fa": b"H\x10c"}, ) - _get_underlying_scanner()._callback(device, adv) - _get_underlying_scanner()._callback(device, adv) - _get_underlying_scanner()._callback(device, adv) + inject_advertisement(device, adv) + inject_advertisement(device, adv) + inject_advertisement(device, adv) await asyncio.sleep(0) @@ -1043,14 +1028,14 @@ async def test_process_advertisements_ignore_bad_advertisement( # The goal of this loop is to make sure that async_process_advertisements sees at least one # callback that returns False while not done.is_set(): - _get_underlying_scanner()._callback(device, adv) + inject_advertisement(device, adv) await asyncio.sleep(0) # Set the return value and mutate the advertisement # Check that scan ends and correct advertisement data is returned return_value.set() adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c" - _get_underlying_scanner()._callback(device, adv) + inject_advertisement(device, adv) await asyncio.sleep(0) result = await handle @@ -1105,20 +1090,18 @@ async def test_wrapped_instance_with_filter( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) - mock_discovered = [MagicMock()] - type(_get_underlying_scanner()).discovered_devices = mock_discovered - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() discovered = await scanner.discover(timeout=0) assert len(discovered) == 1 - assert discovered == mock_discovered + assert discovered == [switchbot_device] assert len(detected) == 1 scanner.register_detection_callback(_device_detected) @@ -1128,17 +1111,17 @@ async def test_wrapped_instance_with_filter( # We should get a reply from the history when we register again assert len(detected) == 3 - type(_get_underlying_scanner()).discovered_devices = [] - discovered = await scanner.discover(timeout=0) - assert len(discovered) == 0 - assert discovered == [] + with patch_discovered_devices([]): + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 0 + assert discovered == [] - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) assert len(detected) == 4 # The filter we created in the wrapped scanner with should be respected # and we should not get another callback - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) assert len(detected) == 4 @@ -1176,22 +1159,21 @@ async def test_wrapped_instance_with_service_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) assert len(detected) == 2 @@ -1229,15 +1211,15 @@ async def test_wrapped_instance_with_broken_callbacks( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 1 @@ -1275,23 +1257,22 @@ async def test_wrapped_instance_changes_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) assert len(detected) == 2 @@ -1328,23 +1309,22 @@ async def test_wrapped_instance_changes_filters( empty_device = BLEDevice("11:22:33:44:55:62", "empty") empty_adv = AdvertisementData(local_name="empty") - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) - type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) assert len(detected) == 2 @@ -1363,7 +1343,7 @@ async def test_wrapped_instance_unsupported_filter( with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={ @@ -1401,7 +1381,7 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert ( @@ -1501,235 +1481,7 @@ async def test_no_auto_detect_bluetooth_adapters_windows(hass): assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 -async def test_raising_runtime_error_when_no_bluetooth(hass): - """Test we raise an exception if we try to get the scanner when its not there.""" - with pytest.raises(RuntimeError): - bluetooth.async_get_scanner(hass) - - async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_bluetooth): """Test getting the scanner returns the wrapped instance.""" scanner = bluetooth.async_get_scanner(hass) assert isinstance(scanner, models.HaBleakScannerWrapper) - - -async def test_config_entry_can_be_reloaded_when_stop_raises( - hass, caplog, enable_bluetooth -): - """Test we can reload if stopping the scanner raises.""" - entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED - - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.stop", - side_effect=BleakError, - ): - await hass.config_entries.async_reload(entry.entry_id) - await hass.async_block_till_done() - - assert entry.state == ConfigEntryState.LOADED - assert "Error stopping scanner" in caplog.text - - -async def test_changing_the_adapter_at_runtime(hass): - """Test we can change the adapter at runtime.""" - entry = MockConfigEntry( - domain=bluetooth.DOMAIN, - data={}, - options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ) as mock_setup, patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.stop" - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert "adapter" not in mock_setup.mock_calls[0][2] - - entry.options = {CONF_ADAPTER: "hci1"} - - await hass.config_entries.async_reload(entry.entry_id) - await hass.async_block_till_done() - assert mock_setup.mock_calls[1][2]["adapter"] == "hci1" - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - - -async def test_dbus_socket_missing_in_container(hass, caplog): - """Test we handle dbus being missing in the container.""" - - with patch( - "homeassistant.components.bluetooth.manager.is_docker_env", return_value=True - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=FileNotFoundError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - 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 "/run/dbus" in caplog.text - assert "docker" in caplog.text - - -async def test_dbus_socket_missing(hass, caplog): - """Test we handle dbus being missing.""" - - with patch( - "homeassistant.components.bluetooth.manager.is_docker_env", return_value=False - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=FileNotFoundError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - 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 "DBus" in caplog.text - assert "docker" not in caplog.text - - -async def test_dbus_broken_pipe_in_container(hass, caplog): - """Test we handle dbus broken pipe in the container.""" - - with patch( - "homeassistant.components.bluetooth.manager.is_docker_env", return_value=True - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=BrokenPipeError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - 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 "dbus" in caplog.text - assert "restarting" in caplog.text - assert "container" in caplog.text - - -async def test_dbus_broken_pipe(hass, caplog): - """Test we handle dbus broken pipe.""" - - with patch( - "homeassistant.components.bluetooth.manager.is_docker_env", return_value=False - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=BrokenPipeError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - 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 "DBus" in caplog.text - assert "restarting" in caplog.text - assert "container" not in caplog.text - - -async def test_invalid_dbus_message(hass, caplog): - """Test we handle invalid dbus message.""" - - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=InvalidMessageError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - 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 "dbus" in caplog.text - - -async def test_recovery_from_dbus_restart( - hass, mock_bleak_scanner_start, enable_bluetooth -): - """Test we can recover when DBus gets restarted out from under us.""" - assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) - await hass.async_block_till_done() - assert len(mock_bleak_scanner_start.mock_calls) == 1 - - start_time_monotonic = 1000 - scanner = _get_underlying_scanner() - mock_discovered = [MagicMock()] - type(scanner).discovered_devices = mock_discovered - - # Ensure we don't restart the scanner if we don't need to - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=start_time_monotonic + 10, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() - - assert len(mock_bleak_scanner_start.mock_calls) == 1 - - # Fire a callback to reset the timer - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=start_time_monotonic, - ): - scanner._callback( - BLEDevice("44:44:33:11:23:42", "any_name"), - AdvertisementData(local_name="any_name"), - ) - - # Ensure we don't restart the scanner if we don't need to - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=start_time_monotonic + 20, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() - - assert len(mock_bleak_scanner_start.mock_calls) == 1 - - # We hit the timer, so we restart the scanner - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() - - assert len(mock_bleak_scanner_start.mock_calls) == 2 diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 12531c52e40..9a90f99d11b 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -20,7 +20,7 @@ from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_underlying_scanner +from . import _get_manager, patch_all_discovered_devices from tests.common import async_fire_time_changed @@ -178,11 +178,10 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True - scanner = _get_underlying_scanner() + scanner = _get_manager() - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", - [MagicMock(address="44:44:33:11:23:45")], + with patch_all_discovered_devices( + [MagicMock(address="44:44:33:11:23:45")] ), patch.object( scanner, "history", @@ -197,9 +196,8 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", - [MagicMock(address="44:44:33:11:23:45")], + with patch_all_discovered_devices( + [MagicMock(address="44:44:33:11:23:45")] ), patch.object( scanner, "history", diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 6b21d1aa32c..ac35e9f2bee 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -32,7 +32,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_underlying_scanner +from . import _get_manager, patch_all_discovered_devices from tests.common import MockEntityPlatform, async_fire_time_changed @@ -246,11 +246,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True assert processor.available is True - scanner = _get_underlying_scanner() - - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", - [MagicMock(address="44:44:33:11:23:45")], + scanner = _get_manager() + with patch_all_discovered_devices( + [MagicMock(address="44:44:33:11:23:45")] ), patch.object( scanner, "history", @@ -268,9 +266,8 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): assert coordinator.available is True assert processor.available is True - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", - [MagicMock(address="44:44:33:11:23:45")], + with patch_all_discovered_devices( + [MagicMock(address="44:44:33:11:23:45")] ), patch.object( scanner, "history", diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py new file mode 100644 index 00000000000..032b67662df --- /dev/null +++ b/tests/components/bluetooth/test_scanner.py @@ -0,0 +1,264 @@ +"""Tests for the Bluetooth integration scanners.""" +from unittest.mock import MagicMock, patch + +from bleak import BleakError +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BLEDevice, +) +from dbus_next import InvalidMessageError + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.const import ( + CONF_ADAPTER, + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, + UNIX_DEFAULT_BLUETOOTH_ADAPTER, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import _get_manager + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_config_entry_can_be_reloaded_when_stop_raises( + hass, caplog, enable_bluetooth +): + """Test we can reload if stopping the scanner raises.""" + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop", + side_effect=BleakError, + ): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert "Error stopping scanner" in caplog.text + + +async def test_changing_the_adapter_at_runtime(hass): + """Test we can change the adapter at runtime.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, + data={}, + options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start" + ), patch("homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop"): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entry.options = {CONF_ADAPTER: "hci1"} + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_dbus_socket_missing_in_container(hass, caplog): + """Test we handle dbus being missing in the container.""" + + with patch( + "homeassistant.components.bluetooth.scanner.is_docker_env", return_value=True + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "/run/dbus" in caplog.text + assert "docker" in caplog.text + + +async def test_dbus_socket_missing(hass, caplog): + """Test we handle dbus being missing.""" + + with patch( + "homeassistant.components.bluetooth.scanner.is_docker_env", return_value=False + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "DBus" in caplog.text + assert "docker" not in caplog.text + + +async def test_dbus_broken_pipe_in_container(hass, caplog): + """Test we handle dbus broken pipe in the container.""" + + with patch( + "homeassistant.components.bluetooth.scanner.is_docker_env", return_value=True + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "dbus" in caplog.text + assert "restarting" in caplog.text + assert "container" in caplog.text + + +async def test_dbus_broken_pipe(hass, caplog): + """Test we handle dbus broken pipe.""" + + with patch( + "homeassistant.components.bluetooth.scanner.is_docker_env", return_value=False + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "DBus" in caplog.text + assert "restarting" in caplog.text + assert "container" not in caplog.text + + +async def test_invalid_dbus_message(hass, caplog): + """Test we handle invalid dbus message.""" + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=InvalidMessageError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + 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 "dbus" in caplog.text + + +async def test_recovery_from_dbus_restart(hass): + """Test we can recover when DBus gets restarted out from under us.""" + + called_start = 0 + called_stop = 0 + _callback = None + mock_discovered = [] + + class MockBleakScanner: + async def start(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_start + called_start += 1 + + async def stop(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_stop + called_stop += 1 + + @property + def discovered_devices(self): + """Mock discovered_devices.""" + nonlocal mock_discovered + return mock_discovered + + def register_detection_callback(self, callback: AdvertisementDataCallback): + """Mock Register Detection Callback.""" + nonlocal _callback + _callback = callback + + scanner = MockBleakScanner() + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + assert called_start == 1 + + start_time_monotonic = 1000 + scanner = _get_manager() + mock_discovered = [MagicMock()] + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # Fire a callback to reset the timer + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic, + ): + _callback( + BLEDevice("44:44:33:11:23:42", "any_name"), + AdvertisementData(local_name="any_name"), + ) + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # We hit the timer, so we restart the scanner + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 2 diff --git a/tests/conftest.py b/tests/conftest.py index 3b43fcd14ba..4c268206805 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -895,26 +895,18 @@ def mock_bleak_scanner_start(): # Late imports to avoid loading bleak unless we need it - import bleak # pylint: disable=import-outside-toplevel - from homeassistant.components.bluetooth import ( # pylint: disable=import-outside-toplevel - models as bluetooth_models, + scanner as bluetooth_scanner, ) - scanner = bleak.BleakScanner - bluetooth_models.HA_BLEAK_SCANNER = None - - with patch("homeassistant.components.bluetooth.models.HaBleakScanner.stop"), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - ) as mock_bleak_scanner_start: - yield mock_bleak_scanner_start - # We need to drop the stop method from the object since we patched # out start and this fixture will expire before the stop method is called # when EVENT_HOMEASSISTANT_STOP is fired. - if bluetooth_models.HA_BLEAK_SCANNER: - bluetooth_models.HA_BLEAK_SCANNER.stop = AsyncMock() - bleak.BleakScanner = scanner + bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + ) as mock_bleak_scanner_start: + yield mock_bleak_scanner_start @pytest.fixture(name="mock_bluetooth") From 7bf13167d8170898c693b592e98e708c6fa28e6d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 11:42:12 -1000 Subject: [PATCH 442/903] Prevent bluetooth scanner from being shutdown by BleakClient not using BLEDevice (#76945) --- homeassistant/components/bluetooth/models.py | 34 ++++++++++- homeassistant/components/bluetooth/usage.py | 5 +- tests/components/bluetooth/test_usage.py | 60 +++++++++++++++++++- 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 1006ed912dd..7857b02f121 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -9,6 +9,8 @@ from enum import Enum import logging from typing import TYPE_CHECKING, Any, Final +from bleak import BleakClient, BleakError +from bleak.backends.device import BLEDevice from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, @@ -16,10 +18,10 @@ from bleak.backends.scanner import ( ) from homeassistant.core import CALLBACK_TYPE +from homeassistant.helpers.frame import report from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo if TYPE_CHECKING: - from bleak.backends.device import BLEDevice from .manager import BluetoothManager @@ -165,3 +167,33 @@ class HaBleakScannerWrapper(BaseBleakScanner): # Nothing to do if event loop is already closed with contextlib.suppress(RuntimeError): asyncio.get_running_loop().call_soon_threadsafe(self._detection_cancel) + + +class HaBleakClientWrapper(BleakClient): + """Wrap the BleakClient to ensure it does not shutdown our scanner. + + If an address is passed into BleakClient instead of a BLEDevice, + bleak will quietly start a new scanner under the hood to resolve + the address. This can cause a conflict with our scanner. We need + to handle translating the address to the BLEDevice in this case + to avoid the whole stack from getting stuck in an in progress state + when an integration does this. + """ + + def __init__( + self, address_or_ble_device: str | BLEDevice, *args: Any, **kwargs: Any + ) -> None: + """Initialize the BleakClient.""" + if isinstance(address_or_ble_device, BLEDevice): + super().__init__(address_or_ble_device, *args, **kwargs) + return + report( + "attempted to call BleakClient with an address instead of a BLEDevice", + exclude_integrations={"bluetooth"}, + error_if_core=False, + ) + assert MANAGER is not None + ble_device = MANAGER.async_ble_device_from_address(address_or_ble_device) + if ble_device is None: + raise BleakError(f"No device found for address {address_or_ble_device}") + super().__init__(ble_device, *args, **kwargs) diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index b3a6783cf30..d282ca7415b 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -4,16 +4,19 @@ from __future__ import annotations import bleak -from .models import HaBleakScannerWrapper +from .models import HaBleakClientWrapper, HaBleakScannerWrapper ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner +ORIGINAL_BLEAK_CLIENT = bleak.BleakClient def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] + bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc] def uninstall_multiple_bleak_catcher() -> None: """Unwrap the bleak classes.""" bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] + bleak.BleakClient = ORIGINAL_BLEAK_CLIENT # type: ignore[misc] diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 8e566a7ce5a..3e35547d2f2 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -1,14 +1,27 @@ """Tests for the Bluetooth integration.""" -import bleak +from unittest.mock import patch -from homeassistant.components.bluetooth.models import HaBleakScannerWrapper +import bleak +from bleak.backends.device import BLEDevice +import pytest + +from homeassistant.components.bluetooth.models import ( + HaBleakClientWrapper, + HaBleakScannerWrapper, +) from homeassistant.components.bluetooth.usage import ( install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher, ) +from . import _get_manager + +MOCK_BLE_DEVICE = BLEDevice( + "00:00:00:00:00:00", "any", delegate="", details={"path": "/dev/hci0/device"} +) + async def test_multiple_bleak_scanner_instances(hass): """Test creating multiple BleakScanners without an integration.""" @@ -23,3 +36,46 @@ async def test_multiple_bleak_scanner_instances(hass): instance = bleak.BleakScanner() assert not isinstance(instance, HaBleakScannerWrapper) + + +async def test_wrapping_bleak_client(hass, enable_bluetooth): + """Test we wrap BleakClient.""" + install_multiple_bleak_catcher() + + instance = bleak.BleakClient(MOCK_BLE_DEVICE) + + assert isinstance(instance, HaBleakClientWrapper) + + uninstall_multiple_bleak_catcher() + + instance = bleak.BleakClient(MOCK_BLE_DEVICE) + + assert not isinstance(instance, HaBleakClientWrapper) + + +async def test_bleak_client_reports_with_address(hass, enable_bluetooth, caplog): + """Test we report when we pass an address to BleakClient.""" + install_multiple_bleak_catcher() + + with pytest.raises(bleak.BleakError): + instance = bleak.BleakClient("00:00:00:00:00:00") + + with patch.object( + _get_manager(), + "async_ble_device_from_address", + return_value=MOCK_BLE_DEVICE, + ): + instance = bleak.BleakClient("00:00:00:00:00:00") + + assert "BleakClient with an address instead of a BLEDevice" in caplog.text + + assert isinstance(instance, HaBleakClientWrapper) + + uninstall_multiple_bleak_catcher() + + caplog.clear() + + instance = bleak.BleakClient("00:00:00:00:00:00") + + assert not isinstance(instance, HaBleakClientWrapper) + assert "BleakClient with an address instead of a BLEDevice" not in caplog.text From 071cae2c0b01032e2eac0cffc61b2cd9d904272b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 12:38:04 -1000 Subject: [PATCH 443/903] Implement auto switching when there are multiple bluetooth scanners (#76947) --- homeassistant/components/bluetooth/manager.py | 84 +++++++-- tests/components/bluetooth/__init__.py | 20 +- tests/components/bluetooth/test_manager.py | 175 ++++++++++++++++++ 3 files changed, 259 insertions(+), 20 deletions(-) create mode 100644 tests/components/bluetooth/test_manager.py diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 15b05271bd4..0fba2d2aae1 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable +from dataclasses import dataclass from datetime import datetime, timedelta import itertools import logging @@ -38,9 +39,56 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" +RSSI_SWITCH_THRESHOLD = 10 +STALE_ADVERTISEMENT_SECONDS = 180 + _LOGGER = logging.getLogger(__name__) +@dataclass +class AdvertisementHistory: + """Bluetooth advertisement history.""" + + ble_device: BLEDevice + advertisement_data: AdvertisementData + time: float + source: str + + +def _prefer_previous_adv(old: AdvertisementHistory, new: AdvertisementHistory) -> bool: + """Prefer previous advertisement if it is better.""" + if new.time - old.time > STALE_ADVERTISEMENT_SECONDS: + # If the old advertisement is stale, any new advertisement is preferred + if new.source != old.source: + _LOGGER.debug( + "%s (%s): Switching from %s to %s (time_elapsed:%s > stale_seconds:%s)", + new.advertisement_data.local_name, + new.ble_device.address, + old.source, + new.source, + new.time - old.time, + STALE_ADVERTISEMENT_SECONDS, + ) + return False + if new.ble_device.rssi - RSSI_SWITCH_THRESHOLD > old.ble_device.rssi: + # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred + if new.source != old.source: + _LOGGER.debug( + "%s (%s): Switching from %s to %s (new_rssi:%s - threadshold:%s > old_rssi:%s)", + new.advertisement_data.local_name, + new.ble_device.address, + old.source, + new.source, + new.ble_device.rssi, + RSSI_SWITCH_THRESHOLD, + old.ble_device.rssi, + ) + return False + # If the source is the different, the old one is preferred because its + # not stale and its RSSI_SWITCH_THRESHOLD less than the new one + return old.source != new.source + + def _dispatch_bleak_callback( callback: AdvertisementDataCallback, filters: dict[str, set[str]], @@ -82,7 +130,7 @@ class BluetoothManager: self._bleak_callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] - self.history: dict[str, tuple[BLEDevice, AdvertisementData, float, str]] = {} + self.history: dict[str, AdvertisementHistory] = {} self._scanners: list[HaScanner] = [] @hass_callback @@ -110,7 +158,7 @@ class BluetoothManager: @hass_callback def async_discovered_devices(self) -> list[BLEDevice]: """Return all of combined best path to discovered from all the scanners.""" - return [history[0] for history in self.history.values()] + return [history.ble_device for history in self.history.values()] @hass_callback def async_setup_unavailable_tracking(self) -> None: @@ -159,12 +207,15 @@ class BluetoothManager: than the source from the history or the timestamp in the history is older than 180s """ - self.history[device.address] = ( - device, - advertisement_data, - monotonic_time, - source, + new_history = AdvertisementHistory( + device, advertisement_data, monotonic_time, source ) + if (old_history := self.history.get(device.address)) and _prefer_previous_adv( + old_history, new_history + ): + return + + self.history[device.address] = new_history for callback_filters in self._bleak_callbacks: _dispatch_bleak_callback(*callback_filters, device, advertisement_data) @@ -246,13 +297,12 @@ class BluetoothManager: if ( matcher and (address := matcher.get(ADDRESS)) - and (device_adv_data := self.history.get(address)) + and (history := self.history.get(address)) ): - ble_device, adv_data, _, _ = device_adv_data try: callback( BluetoothServiceInfoBleak.from_advertisement( - ble_device, adv_data, SOURCE_LOCAL + history.ble_device, history.advertisement_data, SOURCE_LOCAL ), BluetoothChange.ADVERTISEMENT, ) @@ -264,8 +314,8 @@ class BluetoothManager: @hass_callback def async_ble_device_from_address(self, address: str) -> BLEDevice | None: """Return the BLEDevice if present.""" - if ble_adv := self.history.get(address): - return ble_adv[0] + if history := self.history.get(address): + return history.ble_device return None @hass_callback @@ -278,9 +328,9 @@ class BluetoothManager: """Return if the address is present.""" return [ BluetoothServiceInfoBleak.from_advertisement( - device_adv[0], device_adv[1], SOURCE_LOCAL + history.ble_device, history.advertisement_data, SOURCE_LOCAL ) - for device_adv in self.history.values() + for history in self.history.values() ] @hass_callback @@ -312,7 +362,9 @@ class BluetoothManager: # Replay the history since otherwise we miss devices # that were already discovered before the callback was registered # or we are in passive mode - for device, advertisement_data, _, _ in self.history.values(): - _dispatch_bleak_callback(callback, filters, device, advertisement_data) + for history in self.history.values(): + _dispatch_bleak_callback( + callback, filters, history.ble_device, history.advertisement_data + ) return _remove_callback diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 2b6ad75d2b9..44da1a60f03 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -16,10 +16,22 @@ def _get_manager() -> BluetoothManager: def inject_advertisement(device: BLEDevice, adv: AdvertisementData) -> None: - """Return the underlying scanner that has been wrapped.""" - return _get_manager().scanner_adv_received( - device, adv, time.monotonic(), SOURCE_LOCAL - ) + """Inject an advertisement into the manager.""" + return inject_advertisement_with_source(device, adv, SOURCE_LOCAL) + + +def inject_advertisement_with_source( + device: BLEDevice, adv: AdvertisementData, source: str +) -> None: + """Inject an advertisement into the manager from a specific source.""" + inject_advertisement_with_time_and_source(device, adv, time.monotonic(), source) + + +def inject_advertisement_with_time_and_source( + device: BLEDevice, adv: AdvertisementData, time: float, source: str +) -> None: + """Inject an advertisement into the manager from a specific source at a time.""" + return _get_manager().scanner_adv_received(device, adv, time, source) def patch_all_discovered_devices(mock_discovered: list[BLEDevice]) -> None: diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py new file mode 100644 index 00000000000..eb6363521f8 --- /dev/null +++ b/tests/components/bluetooth/test_manager.py @@ -0,0 +1,175 @@ +"""Tests for the Bluetooth integration manager.""" + + +from bleak.backends.scanner import AdvertisementData, BLEDevice + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.manager import STALE_ADVERTISEMENT_SECONDS + +from . import ( + inject_advertisement_with_source, + inject_advertisement_with_time_and_source, +) + + +async def test_advertisements_do_not_switch_adapters_for_no_reason( + hass, enable_bluetooth +): + """Test we only switch adapters when needed.""" + + address = "44:44:33:11:23:12" + + switchbot_device_signal_100 = BLEDevice(address, "wohand_signal_100", rssi=-100) + switchbot_adv_signal_100 = AdvertisementData( + local_name="wohand_signal_100", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_signal_100, switchbot_adv_signal_100, "hci0" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_signal_100 + ) + + switchbot_device_signal_99 = BLEDevice(address, "wohand_signal_99", rssi=-99) + switchbot_adv_signal_99 = AdvertisementData( + local_name="wohand_signal_99", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_signal_99, switchbot_adv_signal_99, "hci0" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_signal_99 + ) + + switchbot_device_signal_98 = BLEDevice(address, "wohand_good_signal", rssi=-98) + switchbot_adv_signal_98 = AdvertisementData( + local_name="wohand_good_signal", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_signal_98, switchbot_adv_signal_98, "hci1" + ) + + # should not switch to hci1 + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_signal_99 + ) + + +async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): + """Test switching adapters based on rssi.""" + + address = "44:44:33:11:23:45" + + switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal", rssi=-100) + switchbot_adv_poor_signal = AdvertisementData( + local_name="wohand_poor_signal", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal + ) + + switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal", rssi=-60) + switchbot_adv_good_signal = AdvertisementData( + local_name="wohand_good_signal", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_good_signal, switchbot_adv_good_signal, "hci1" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + inject_advertisement_with_source( + switchbot_device_good_signal, switchbot_adv_poor_signal, "hci0" + ) + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + # We should not switch adapters unless the signal hits the threshold + switchbot_device_similar_signal = BLEDevice( + address, "wohand_similar_signal", rssi=-62 + ) + switchbot_adv_similar_signal = AdvertisementData( + local_name="wohand_similar_signal", service_uuids=[] + ) + + inject_advertisement_with_source( + switchbot_device_similar_signal, switchbot_adv_similar_signal, "hci0" + ) + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + +async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): + """Test switching adapters based on the previous advertisement being stale.""" + + address = "44:44:33:11:23:41" + start_time_monotonic = 50.0 + + switchbot_device_poor_signal_hci0 = BLEDevice( + address, "wohand_poor_signal_hci0", rssi=-100 + ) + switchbot_adv_poor_signal_hci0 = AdvertisementData( + local_name="wohand_poor_signal_hci0", service_uuids=[] + ) + inject_advertisement_with_time_and_source( + switchbot_device_poor_signal_hci0, + switchbot_adv_poor_signal_hci0, + start_time_monotonic, + "hci0", + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci0 + ) + + switchbot_device_poor_signal_hci1 = BLEDevice( + address, "wohand_poor_signal_hci1", rssi=-99 + ) + switchbot_adv_poor_signal_hci1 = AdvertisementData( + local_name="wohand_poor_signal_hci1", service_uuids=[] + ) + inject_advertisement_with_time_and_source( + switchbot_device_poor_signal_hci1, + switchbot_adv_poor_signal_hci1, + start_time_monotonic, + "hci1", + ) + + # Should not switch adapters until the advertisement is stale + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci0 + ) + + # Should switch to hci1 since the previous advertisement is stale + # even though the signal is poor because the device is now + # likely unreachable via hci0 + inject_advertisement_with_time_and_source( + switchbot_device_poor_signal_hci1, + switchbot_adv_poor_signal_hci1, + start_time_monotonic + STALE_ADVERTISEMENT_SECONDS + 1, + "hci1", + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci1 + ) From 71cdc1645b343fd1a053bdcd5a5a4821afdde7cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Aug 2022 00:49:11 +0200 Subject: [PATCH 444/903] Refactor LaMetric integration (#76759) * Refactor LaMetric integration * Use async_setup Co-authored-by: Martin Hjelmare * use async_get_service Co-authored-by: Martin Hjelmare * Update tests/components/lametric/conftest.py Co-authored-by: Martin Hjelmare * Update tests/components/lametric/conftest.py Co-authored-by: Martin Hjelmare * Pass hassconfig * Remove try/catch * Fix passing hassconfig * Use menu Co-authored-by: Martin Hjelmare --- .coveragerc | 3 +- CODEOWNERS | 1 + homeassistant/components/lametric/__init__.py | 97 ++- .../lametric/application_credentials.py | 11 + .../components/lametric/config_flow.py | 251 +++++++ homeassistant/components/lametric/const.py | 6 +- .../components/lametric/manifest.json | 13 +- homeassistant/components/lametric/notify.py | 171 ++--- .../components/lametric/strings.json | 50 ++ .../components/lametric/translations/en.json | 50 ++ .../generated/application_credentials.py | 1 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 5 + requirements_all.txt | 6 +- requirements_test_all.txt | 3 + tests/components/lametric/__init__.py | 1 + tests/components/lametric/conftest.py | 81 ++ .../lametric/fixtures/cloud_devices.json | 38 + .../components/lametric/fixtures/device.json | 72 ++ tests/components/lametric/test_config_flow.py | 697 ++++++++++++++++++ 20 files changed, 1385 insertions(+), 173 deletions(-) create mode 100644 homeassistant/components/lametric/application_credentials.py create mode 100644 homeassistant/components/lametric/config_flow.py create mode 100644 homeassistant/components/lametric/strings.json create mode 100644 homeassistant/components/lametric/translations/en.json create mode 100644 tests/components/lametric/__init__.py create mode 100644 tests/components/lametric/conftest.py create mode 100644 tests/components/lametric/fixtures/cloud_devices.json create mode 100644 tests/components/lametric/fixtures/device.json create mode 100644 tests/components/lametric/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index d3e1b75b928..49cfbb3acc7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -639,7 +639,8 @@ omit = homeassistant/components/kostal_plenticore/switch.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py - homeassistant/components/lametric/* + homeassistant/components/lametric/__init__.py + homeassistant/components/lametric/notify.py homeassistant/components/lannouncer/notify.py homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 0c8aa94dfb8..1f3ce12e63a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -586,6 +586,7 @@ build.json @home-assistant/supervisor /homeassistant/components/lacrosse_view/ @IceBotYT /tests/components/lacrosse_view/ @IceBotYT /homeassistant/components/lametric/ @robbiet480 @frenck +/tests/components/lametric/ @robbiet480 @frenck /homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol /tests/components/launch_library/ @ludeeus @DurgNomis-drol /homeassistant/components/laundrify/ @xLarry diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index 970bdd4b3b6..2f89d88d79d 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,52 +1,83 @@ """Support for LaMetric time.""" -from lmnotify import LaMetricManager +from demetriek import LaMetricConnectionError, LaMetricDevice import voluptuous as vol -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_HOST, + CONF_NAME, + Platform, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, LOGGER +from .const import DOMAIN CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the LaMetricManager.""" - LOGGER.debug("Setting up LaMetric platform") - conf = config[DOMAIN] - hlmn = HassLaMetricManager( - client_id=conf[CONF_CLIENT_ID], client_secret=conf[CONF_CLIENT_SECRET] - ) - if not (devices := hlmn.manager.get_devices()): - LOGGER.error("No LaMetric devices found") - return False - - hass.data[DOMAIN] = hlmn - for dev in devices: - LOGGER.debug("Discovered LaMetric device: %s", dev) +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the LaMetric integration.""" + hass.data[DOMAIN] = {"hass_config": config} + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "manual_migration", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="manual_migration", + ) return True -class HassLaMetricManager: - """A class that encapsulated requests to the LaMetric manager.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up LaMetric from a config entry.""" + lametric = LaMetricDevice( + host=entry.data[CONF_HOST], + api_key=entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), + ) - def __init__(self, client_id: str, client_secret: str) -> None: - """Initialize HassLaMetricManager and connect to LaMetric.""" + try: + device = await lametric.device() + except LaMetricConnectionError as ex: + raise ConfigEntryNotReady("Cannot connect to LaMetric device") from ex - LOGGER.debug("Connecting to LaMetric") - self.manager = LaMetricManager(client_id, client_secret) - self._client_id = client_id - self._client_secret = client_secret + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = lametric + + # Set up notify platform, no entry support for notify component yet, + # have to use discovery to load platform. + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_NAME: device.name, "entry_id": entry.entry_id}, + hass.data[DOMAIN]["hass_config"], + ) + ) + return True diff --git a/homeassistant/components/lametric/application_credentials.py b/homeassistant/components/lametric/application_credentials.py new file mode 100644 index 00000000000..ab763c8f6fb --- /dev/null +++ b/homeassistant/components/lametric/application_credentials.py @@ -0,0 +1,11 @@ +"""Application credentials platform for LaMetric.""" +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url="https://developer.lametric.com/api/v2/oauth2/authorize", + token_url="https://developer.lametric.com/api/v2/oauth2/token", + ) diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py new file mode 100644 index 00000000000..4bb293b0a4d --- /dev/null +++ b/homeassistant/components/lametric/config_flow.py @@ -0,0 +1,251 @@ +"""Config flow to configure the LaMetric integration.""" +from __future__ import annotations + +from ipaddress import ip_address +import logging +from typing import Any + +from demetriek import ( + CloudDevice, + LaMetricCloud, + LaMetricConnectionError, + LaMetricDevice, + Model, + Notification, + NotificationIconType, + NotificationSound, + Simple, + Sound, +) +import voluptuous as vol +from yarl import URL + +from homeassistant.components.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) +from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TextSelector, + TextSelectorConfig, + TextSelectorType, +) +from homeassistant.util.network import is_link_local + +from .const import DOMAIN, LOGGER + + +class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): + """Handle a LaMetric config flow.""" + + DOMAIN = DOMAIN + VERSION = 1 + + devices: dict[str, CloudDevice] + discovered_host: str + discovered_serial: str + discovered: bool = False + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return LOGGER + + @property + def extra_authorize_data(self) -> dict[str, Any]: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "basic devices_read"} + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + return await self.async_step_choice_enter_manual_or_fetch_cloud() + + async def async_step_ssdp(self, discovery_info: SsdpServiceInfo) -> FlowResult: + """Handle a flow initiated by SSDP discovery.""" + url = URL(discovery_info.ssdp_location or "") + if url.host is None or not ( + serial := discovery_info.upnp.get(ATTR_UPNP_SERIAL) + ): + return self.async_abort(reason="invalid_discovery_info") + + if is_link_local(ip_address(url.host)): + return self.async_abort(reason="link_local_address") + + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={CONF_HOST: url.host}) + + self.context.update( + { + "title_placeholders": { + "name": discovery_info.upnp.get( + ATTR_UPNP_FRIENDLY_NAME, "LaMetric TIME" + ), + }, + "configuration_url": "https://developer.lametric.com", + } + ) + + self.discovered = True + self.discovered_host = str(url.host) + self.discovered_serial = serial + return await self.async_step_choice_enter_manual_or_fetch_cloud() + + async def async_step_choice_enter_manual_or_fetch_cloud( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user's choice of entering the manual credentials or fetching the cloud credentials.""" + return self.async_show_menu( + step_id="choice_enter_manual_or_fetch_cloud", + menu_options=["pick_implementation", "manual_entry"], + ) + + async def async_step_manual_entry( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user's choice of entering the device manually.""" + errors: dict[str, str] = {} + if user_input is not None: + if self.discovered: + host = self.discovered_host + else: + host = user_input[CONF_HOST] + + try: + return await self._async_step_create_entry( + host, user_input[CONF_API_KEY] + ) + except AbortFlow as ex: + raise ex + except LaMetricConnectionError as ex: + LOGGER.error("Error connecting to LaMetric: %s", ex) + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected error occurred") + errors["base"] = "unknown" + + # Don't ask for a host if it was discovered + schema = { + vol.Required(CONF_API_KEY): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ) + } + if not self.discovered: + schema = {vol.Required(CONF_HOST): TextSelector()} | schema + + return self.async_show_form( + step_id="manual_entry", + data_schema=vol.Schema(schema), + errors=errors, + ) + + async def async_step_cloud_fetch_devices(self, data: dict[str, Any]) -> FlowResult: + """Fetch information about devices from the cloud.""" + lametric = LaMetricCloud( + token=data["token"]["access_token"], + session=async_get_clientsession(self.hass), + ) + self.devices = { + device.serial_number: device + for device in sorted(await lametric.devices(), key=lambda d: d.name) + } + + if not self.devices: + return self.async_abort(reason="no_devices") + + return await self.async_step_cloud_select_device() + + async def async_step_cloud_select_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle device selection from devices offered by the cloud.""" + if self.discovered: + user_input = {CONF_DEVICE: self.discovered_serial} + elif len(self.devices) == 1: + user_input = {CONF_DEVICE: list(self.devices.values())[0].serial_number} + + errors: dict[str, str] = {} + if user_input is not None: + device = self.devices[user_input[CONF_DEVICE]] + try: + return await self._async_step_create_entry( + str(device.ip), device.api_key + ) + except AbortFlow as ex: + raise ex + except LaMetricConnectionError as ex: + LOGGER.error("Error connecting to LaMetric: %s", ex) + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected error occurred") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="cloud_select_device", + data_schema=vol.Schema( + { + vol.Required(CONF_DEVICE): SelectSelector( + SelectSelectorConfig( + mode=SelectSelectorMode.DROPDOWN, + options=[ + SelectOptionDict( + value=device.serial_number, + label=device.name, + ) + for device in self.devices.values() + ], + ) + ), + } + ), + errors=errors, + ) + + async def _async_step_create_entry(self, host: str, api_key: str) -> FlowResult: + """Create entry.""" + lametric = LaMetricDevice( + host=host, + api_key=api_key, + session=async_get_clientsession(self.hass), + ) + + device = await lametric.device() + + await self.async_set_unique_id(device.serial_number) + self._abort_if_unique_id_configured( + updates={CONF_HOST: lametric.host, CONF_API_KEY: lametric.api_key} + ) + + await lametric.notify( + notification=Notification( + icon_type=NotificationIconType.INFO, + model=Model( + cycles=2, + frames=[Simple(text="Connected to Home Assistant!", icon=7956)], + sound=Sound(id=NotificationSound.WIN), + ), + ) + ) + + return self.async_create_entry( + title=device.name, + data={ + CONF_API_KEY: lametric.api_key, + CONF_HOST: lametric.host, + CONF_MAC: device.wifi.mac, + }, + ) + + # Replace OAuth create entry with a fetch devices step + # LaMetric only use OAuth to get device information, but doesn't + # use it later on. + async_oauth_create_entry = async_step_cloud_fetch_devices diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py index 85e61cd8d9a..d357f678d9d 100644 --- a/homeassistant/components/lametric/const.py +++ b/homeassistant/components/lametric/const.py @@ -7,10 +7,8 @@ DOMAIN: Final = "lametric" LOGGER = logging.getLogger(__package__) -AVAILABLE_PRIORITIES: Final = ["info", "warning", "critical"] -AVAILABLE_ICON_TYPES: Final = ["none", "info", "alert"] - CONF_CYCLES: Final = "cycles" +CONF_ICON_TYPE: Final = "icon_type" CONF_LIFETIME: Final = "lifetime" CONF_PRIORITY: Final = "priority" -CONF_ICON_TYPE: Final = "icon_type" +CONF_SOUND: Final = "sound" diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index a2c0aecb58d..1a40f962156 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -2,8 +2,15 @@ "domain": "lametric", "name": "LaMetric", "documentation": "https://www.home-assistant.io/integrations/lametric", - "requirements": ["lmnotify==0.0.4"], + "requirements": ["demetriek==0.2.2"], "codeowners": ["@robbiet480", "@frenck"], - "iot_class": "cloud_push", - "loggers": ["lmnotify"] + "iot_class": "local_push", + "dependencies": ["application_credentials", "repairs"], + "loggers": ["demetriek"], + "config_flow": true, + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:LaMetric:1" + } + ] } diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index f3c098a841e..4b404840388 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -3,157 +3,70 @@ from __future__ import annotations from typing import Any -from lmnotify import Model, SimpleFrame, Sound -from oauthlib.oauth2 import TokenExpiredError -from requests.exceptions import ConnectionError as RequestsConnectionError -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, +from demetriek import ( + LaMetricDevice, + LaMetricError, + Model, + Notification, + NotificationIconType, + NotificationPriority, + Simple, + Sound, ) + +from homeassistant.components.notify import ATTR_DATA, BaseNotificationService from homeassistant.const import CONF_ICON from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import HassLaMetricManager -from .const import ( - AVAILABLE_ICON_TYPES, - AVAILABLE_PRIORITIES, - CONF_CYCLES, - CONF_ICON_TYPE, - CONF_LIFETIME, - CONF_PRIORITY, - DOMAIN, - LOGGER, -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_ICON, default="a7956"): cv.string, - vol.Optional(CONF_LIFETIME, default=10): cv.positive_int, - vol.Optional(CONF_CYCLES, default=1): cv.positive_int, - vol.Optional(CONF_PRIORITY, default="warning"): vol.In(AVAILABLE_PRIORITIES), - vol.Optional(CONF_ICON_TYPE, default="info"): vol.In(AVAILABLE_ICON_TYPES), - } -) +from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN -def get_service( +async def async_get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, -) -> LaMetricNotificationService: +) -> LaMetricNotificationService | None: """Get the LaMetric notification service.""" - return LaMetricNotificationService( - hass.data[DOMAIN], - config[CONF_ICON], - config[CONF_LIFETIME] * 1000, - config[CONF_CYCLES], - config[CONF_PRIORITY], - config[CONF_ICON_TYPE], - ) + if discovery_info is None: + return None + lametric: LaMetricDevice = hass.data[DOMAIN][discovery_info["entry_id"]] + return LaMetricNotificationService(lametric) class LaMetricNotificationService(BaseNotificationService): """Implement the notification service for LaMetric.""" - def __init__( - self, - hasslametricmanager: HassLaMetricManager, - icon: str, - lifetime: int, - cycles: int, - priority: str, - icon_type: str, - ) -> None: + def __init__(self, lametric: LaMetricDevice) -> None: """Initialize the service.""" - self.hasslametricmanager = hasslametricmanager - self._icon = icon - self._lifetime = lifetime - self._cycles = cycles - self._priority = priority - self._icon_type = icon_type - self._devices: list[dict[str, Any]] = [] + self.lametric = lametric - def send_message(self, message: str = "", **kwargs: Any) -> None: - """Send a message to some LaMetric device.""" + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: + """Send a message to a LaMetric device.""" + if not (data := kwargs.get(ATTR_DATA)): + data = {} - targets = kwargs.get(ATTR_TARGET) - data = kwargs.get(ATTR_DATA) - LOGGER.debug("Targets/Data: %s/%s", targets, data) - icon = self._icon - cycles = self._cycles sound = None - priority = self._priority - icon_type = self._icon_type + if CONF_SOUND in data: + sound = Sound(id=data[CONF_SOUND], category=None) - # Additional data? - if data is not None: - if "icon" in data: - icon = data["icon"] - if "sound" in data: - try: - sound = Sound(category="notifications", sound_id=data["sound"]) - LOGGER.debug("Adding notification sound %s", data["sound"]) - except AssertionError: - LOGGER.error("Sound ID %s unknown, ignoring", data["sound"]) - if "cycles" in data: - cycles = int(data["cycles"]) - if "icon_type" in data: - if data["icon_type"] in AVAILABLE_ICON_TYPES: - icon_type = data["icon_type"] - else: - LOGGER.warning( - "Priority %s invalid, using default %s", - data["priority"], - priority, + notification = Notification( + icon_type=NotificationIconType(data.get(CONF_ICON_TYPE, "none")), + priority=NotificationPriority(data.get(CONF_PRIORITY, "info")), + model=Model( + frames=[ + Simple( + icon=data.get(CONF_ICON, "a7956"), + text=message, ) - if "priority" in data: - if data["priority"] in AVAILABLE_PRIORITIES: - priority = data["priority"] - else: - LOGGER.warning( - "Priority %s invalid, using default %s", - data["priority"], - priority, - ) - text_frame = SimpleFrame(icon, message) - LOGGER.debug( - "Icon/Message/Cycles/Lifetime: %s, %s, %d, %d", - icon, - message, - self._cycles, - self._lifetime, + ], + cycles=int(data.get(CONF_CYCLES, 1)), + sound=sound, + ), ) - frames = [text_frame] - - model = Model(frames=frames, cycles=cycles, sound=sound) - lmn = self.hasslametricmanager.manager try: - self._devices = lmn.get_devices() - except TokenExpiredError: - LOGGER.debug("Token expired, fetching new token") - lmn.get_token() - self._devices = lmn.get_devices() - except RequestsConnectionError: - LOGGER.warning( - "Problem connecting to LaMetric, using cached devices instead" - ) - for dev in self._devices: - if targets is None or dev["name"] in targets: - try: - lmn.set_device(dev) - lmn.send_notification( - model, - lifetime=self._lifetime, - priority=priority, - icon_type=icon_type, - ) - LOGGER.debug("Sent notification to LaMetric %s", dev["name"]) - except OSError: - LOGGER.warning("Cannot connect to LaMetric %s", dev["name"]) + await self.lametric.notify(notification=notification) + except LaMetricError as ex: + raise HomeAssistantError("Could not send LaMetric notification") from ex diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json new file mode 100644 index 00000000000..53271a8d0d8 --- /dev/null +++ b/homeassistant/components/lametric/strings.json @@ -0,0 +1,50 @@ +{ + "config": { + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "A LaMetric device can be set up in Home Assistant in two different ways.\n\nYou can enter all device information and API tokens yourself, or Home Asssistant can import them from your LaMetric.com account.", + "menu_options": { + "pick_implementation": "Import from LaMetric.com (recommended)", + "manual_entry": "Enter manually" + } + }, + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "manual_entry": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "host": "The IP address or hostname of your LaMetric TIME on your network.", + "api_key": "You can find this API key in [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices)." + } + }, + "user_cloud_select_device": { + "data": { + "device": "Select the LaMetric device to add" + } + } + }, + "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%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "invalid_discovery_info": "Invalid discovery information received", + "link_local_address": "Link local addresses are not supported", + "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.", + "no_devices": "The authorized user has no LaMetric devices", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + } + }, + "issues": { + "manual_migration": { + "title": "Manual migration required for LaMetric", + "description": "The LaMetric integration has been modernized: It is now configured and set up via the user interface and the communcations are now local.\n\nUnfortunately, there is no automatic migration path possible and thus requires you to re-setup your LaMetric with Home Assistant. Please consult the Home Assistant LaMetric integration documentation on how to set it up.\n\nRemove the old LaMetric YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/lametric/translations/en.json b/homeassistant/components/lametric/translations/en.json new file mode 100644 index 00000000000..c02b7d6d05f --- /dev/null +++ b/homeassistant/components/lametric/translations/en.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "authorize_url_timeout": "Timeout generating authorize URL.", + "invalid_discovery_info": "Invalid discovery information received", + "link_local_address": "Link local addresses are not supported", + "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.", + "no_devices": "The authorized user has no LaMetric devices", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "A LaMetric device can be set up in Home Assistant in two different ways.\n\nYou can enter all device information and API tokens yourself, or Home Asssistant can import them from your LaMetric.com account.", + "menu_options": { + "manual_entry": "Enter manually", + "pick_implementation": "Import from LaMetric.com (recommended)" + } + }, + "manual_entry": { + "data": { + "api_key": "API Key", + "host": "Host" + }, + "data_description": { + "api_key": "You can find this API key in [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices).", + "host": "The IP address or hostname of your LaMetric TIME on your network." + } + }, + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "user_cloud_select_device": { + "data": { + "device": "Select the LaMetric device to add" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "The LaMetric integration has been modernized: It is now configured and set up via the user interface and the communcations are now local.\n\nUnfortunately, there is no automatic migration path possible and thus requires you to re-setup your LaMetric with Home Assistant. Please consult the Home Assistant LaMetric integration documentation on how to set it up.\n\nRemove the old LaMetric YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "Manual migration required for LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index ba9762f58c0..4673cf2378d 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -9,6 +9,7 @@ APPLICATION_CREDENTIALS = [ "geocaching", "google", "home_connect", + "lametric", "lyric", "neato", "nest", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8b5db1b45ba..42b426b8864 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -196,6 +196,7 @@ FLOWS = { "kraken", "kulersky", "lacrosse_view", + "lametric", "launch_library", "laundrify", "lg_soundbar", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 851d9b0fd10..b55240d1dc6 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -190,6 +190,11 @@ SSDP = { "manufacturer": "konnected.io" } ], + "lametric": [ + { + "deviceType": "urn:schemas-upnp-org:device:LaMetric:1" + } + ], "nanoleaf": [ { "st": "Nanoleaf_aurora:light" diff --git a/requirements_all.txt b/requirements_all.txt index ecebd456696..81e62987592 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -539,6 +539,9 @@ defusedxml==0.7.1 # homeassistant.components.deluge deluge-client==1.7.1 +# homeassistant.components.lametric +demetriek==0.2.2 + # homeassistant.components.denonavr denonavr==0.10.11 @@ -979,9 +982,6 @@ limitlessled==1.1.3 # homeassistant.components.linode linode-api==4.1.9b1 -# homeassistant.components.lametric -lmnotify==0.0.4 - # homeassistant.components.google_maps locationsharinglib==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 321398720cf..69d56e4ed40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -410,6 +410,9 @@ defusedxml==0.7.1 # homeassistant.components.deluge deluge-client==1.7.1 +# homeassistant.components.lametric +demetriek==0.2.2 + # homeassistant.components.denonavr denonavr==0.10.11 diff --git a/tests/components/lametric/__init__.py b/tests/components/lametric/__init__.py new file mode 100644 index 00000000000..330b8c40cc9 --- /dev/null +++ b/tests/components/lametric/__init__.py @@ -0,0 +1 @@ +"""Tests for the LaMetric integration.""" diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py new file mode 100644 index 00000000000..3640742c8ff --- /dev/null +++ b/tests/components/lametric/conftest.py @@ -0,0 +1,81 @@ +"""Fixtures for LaMetric integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from demetriek import CloudDevice, Device +from pydantic import parse_raw_as +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.lametric.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(autouse=True) +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, DOMAIN, ClientCredential("client", "secret"), "credentials" + ) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="My LaMetric", + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.2", + CONF_API_KEY: "mock-from-fixture", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + }, + unique_id="SA110405124500W00BS9", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.lametric.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_lametric_config_flow() -> Generator[MagicMock, None, None]: + """Return a mocked LaMetric client.""" + with patch( + "homeassistant.components.lametric.config_flow.LaMetricDevice", autospec=True + ) as lametric_mock: + lametric = lametric_mock.return_value + lametric.api_key = "mock-api-key" + lametric.host = "127.0.0.1" + lametric.device.return_value = Device.parse_raw( + load_fixture("device.json", DOMAIN) + ) + yield lametric + + +@pytest.fixture +def mock_lametric_cloud_config_flow() -> Generator[MagicMock, None, None]: + """Return a mocked LaMetric Cloud client.""" + with patch( + "homeassistant.components.lametric.config_flow.LaMetricCloud", autospec=True + ) as lametric_mock: + lametric = lametric_mock.return_value + lametric.devices.return_value = parse_raw_as( + list[CloudDevice], load_fixture("cloud_devices.json", DOMAIN) + ) + yield lametric diff --git a/tests/components/lametric/fixtures/cloud_devices.json b/tests/components/lametric/fixtures/cloud_devices.json new file mode 100644 index 00000000000..a375f968baa --- /dev/null +++ b/tests/components/lametric/fixtures/cloud_devices.json @@ -0,0 +1,38 @@ +[ + { + "id": 1, + "name": "Frenck's LaMetric", + "state": "configured", + "serial_number": "SA110405124500W00BS9", + "api_key": "mock-api-key", + "ipv4_internal": "127.0.0.1", + "mac": "AA:BB:CC:DD:EE:FF", + "wifi_ssid": "IoT", + "created_at": "2015-08-12T15:15:55+00:00", + "updated_at": "2016-08-13T18:16:17+00:00" + }, + { + "id": 21, + "name": "Blackjack", + "state": "configured", + "serial_number": "SA140100002200W00B21", + "api_key": "8adaa0c98278dbb1ecb218d1c3e11f9312317ba474ab3361f80c0bd4f13a6721", + "ipv4_internal": "192.168.1.21", + "mac": "AA:BB:CC:DD:EE:21", + "wifi_ssid": "AllYourBaseAreBelongToUs", + "created_at": "2015-03-06T15:15:55+00:00", + "updated_at": "2016-06-14T18:27:13+00:00" + }, + { + "id": 42, + "name": "The Answer", + "state": "configured", + "serial_number": "SA140100002200W00B42", + "api_key": "8adaa0c98278dbb1ecb218d1c3e11f9312317ba474ab3361f80c0bd4f13a6742", + "ipv4_internal": "192.168.1.42", + "mac": "AA:BB:CC:DD:EE:42", + "wifi_ssid": "AllYourBaseAreBelongToUs", + "created_at": "2015-03-06T15:15:55+00:00", + "updated_at": "2016-06-14T18:27:13+00:00" + } +] diff --git a/tests/components/lametric/fixtures/device.json b/tests/components/lametric/fixtures/device.json new file mode 100644 index 00000000000..a184d9f0aa1 --- /dev/null +++ b/tests/components/lametric/fixtures/device.json @@ -0,0 +1,72 @@ +{ + "audio": { + "volume": 100, + "volume_limit": { + "max": 100, + "min": 0 + }, + "volume_range": { + "max": 100, + "min": 0 + } + }, + "bluetooth": { + "active": false, + "address": "AA:BB:CC:DD:EE:FF", + "available": true, + "discoverable": true, + "low_energy": { + "active": true, + "advertising": true, + "connectable": true + }, + "name": "LM1234", + "pairable": true + }, + "display": { + "brightness": 100, + "brightness_limit": { + "max": 100, + "min": 2 + }, + "brightness_mode": "auto", + "brightness_range": { + "max": 100, + "min": 0 + }, + "height": 8, + "screensaver": { + "enabled": false, + "modes": { + "time_based": { + "enabled": true, + "local_start_time": "01:00:39", + "start_time": "00:00:39" + }, + "when_dark": { + "enabled": false + } + }, + "widget": "08b8eac21074f8f7e5a29f2855ba8060" + }, + "type": "mixed", + "width": 37 + }, + "id": "12345", + "mode": "auto", + "model": "LM 37X8", + "name": "Frenck's LaMetric", + "os_version": "2.2.2", + "serial_number": "SA110405124500W00BS9", + "wifi": { + "active": true, + "mac": "AA:BB:CC:DD:EE:FF", + "available": true, + "encryption": "WPA", + "ssid": "IoT", + "ip": "127.0.0.1", + "mode": "dhcp", + "netmask": "255.255.255.0", + "rssi": 21 + } +} diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py new file mode 100644 index 00000000000..2134ee135f6 --- /dev/null +++ b/tests/components/lametric/test_config_flow.py @@ -0,0 +1,697 @@ +"""Tests for the LaMetric config flow.""" +from collections.abc import Awaitable, Callable +from http import HTTPStatus +from unittest.mock import MagicMock + +from aiohttp.test_utils import TestClient +from demetriek import ( + LaMetricConnectionError, + LaMetricConnectionTimeoutError, + LaMetricError, +) +import pytest + +from homeassistant.components.lametric.const import DOMAIN +from homeassistant.components.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) +from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +SSDP_DISCOVERY_INFO = SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://127.0.0.1:44057/465d585b-1c05-444a-b14e-6ffb875b46a6/device_description.xml", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "LaMetric Time (LM1245)", + ATTR_UPNP_SERIAL: "SA110405124500W00BS9", + }, +) + + +async def test_full_cloud_import_flow_multiple_devices( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_setup_entry: MagicMock, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow importing from cloud, with multiple devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("url") == ( + "https://developer.lametric.com/api/v2/oauth2/authorize" + "?response_type=code&client_id=client" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=basic+devices_read" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result3 = await hass.config_entries.flow.async_configure(flow_id) + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "cloud_select_device" + + result4 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} + ) + + assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("title") == "Frenck's LaMetric" + assert result4.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result4 + assert result4["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_cloud_import_flow_single_device( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_setup_entry: MagicMock, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow importing from cloud, with a single device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("url") == ( + "https://developer.lametric.com/api/v2/oauth2/authorize" + "?response_type=code&client_id=client" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=basic+devices_read" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + # Stage a single device + # Should skip step that ask for device selection + mock_lametric_cloud_config_flow.devices.return_value = [ + mock_lametric_cloud_config_flow.devices.return_value[0] + ] + result3 = await hass.config_entries.flow.async_configure(flow_id) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_manual( + hass: HomeAssistant, + mock_setup_entry: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow manual entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "manual_entry"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "manual_entry" + + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_ssdp_with_cloud_import( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_setup_entry: MagicMock, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow triggered by SSDP, importing from cloud.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("url") == ( + "https://developer.lametric.com/api/v2/oauth2/authorize" + "?response_type=code&client_id=client" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=basic+devices_read" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result3 = await hass.config_entries.flow.async_configure(flow_id) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_ssdp_manual_entry( + hass: HomeAssistant, + mock_setup_entry: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow triggered by SSDP, with manual API key entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "manual_entry"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "manual_entry" + + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_API_KEY: "mock-api-key"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "data,reason", + [ + ( + SsdpServiceInfo(ssdp_usn="mock_usn", ssdp_st="mock_st", upnp={}), + "invalid_discovery_info", + ), + ( + SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://169.254.0.1:44057/465d585b-1c05-444a-b14e-6ffb875b46a6/device_description.xml", + upnp={ + ATTR_UPNP_SERIAL: "SA110405124500W00BS9", + }, + ), + "link_local_address", + ), + ], +) +async def test_ssdp_abort_invalid_discovery( + hass: HomeAssistant, data: SsdpServiceInfo, reason: str +) -> None: + """Check a full flow triggered by SSDP, with manual API key entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=data + ) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == reason + + +async def test_cloud_import_updates_existing_entry( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test cloud importing existing device updates existing entry.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + await client.get(f"/auth/external/callback?code=abcd&state={state}") + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + await hass.config_entries.flow.async_configure(flow_id) + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} + ) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 0 + + +async def test_manual_updates_existing_entry( + hass: HomeAssistant, + mock_lametric_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test adding existing device updates existing entry.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "manual_entry"} + ) + + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} + ) + + assert result3.get("type") == FlowResultType.ABORT + assert result3.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 0 + + +async def test_discovery_updates_existing_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test discovery of existing device updates entry.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-from-fixture", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + +async def test_cloud_abort_no_devices( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_lametric_cloud_config_flow: MagicMock, +) -> None: + """Test cloud importing aborts when account has no devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + await client.get(f"/auth/external/callback?code=abcd&state={state}") + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + # Stage there are no devices + mock_lametric_cloud_config_flow.devices.return_value = [] + result2 = await hass.config_entries.flow.async_configure(flow_id) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "no_devices" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (LaMetricConnectionTimeoutError, "cannot_connect"), + (LaMetricConnectionError, "cannot_connect"), + (LaMetricError, "unknown"), + (RuntimeError, "unknown"), + ], +) +async def test_manual_errors( + hass: HomeAssistant, + mock_lametric_config_flow: MagicMock, + mock_setup_entry: MagicMock, + side_effect: Exception, + reason: str, +) -> None: + """Test adding existing device updates existing entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "manual_entry"} + ) + + mock_lametric_config_flow.device.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "manual_entry" + assert result2.get("errors") == {"base": reason} + + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + mock_lametric_config_flow.device.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_config_flow.device.mock_calls) == 2 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (LaMetricConnectionTimeoutError, "cannot_connect"), + (LaMetricConnectionError, "cannot_connect"), + (LaMetricError, "unknown"), + (RuntimeError, "unknown"), + ], +) +async def test_cloud_errors( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_setup_entry: MagicMock, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, + side_effect: Exception, + reason: str, +) -> None: + """Test adding existing device updates existing entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + await client.get(f"/auth/external/callback?code=abcd&state={state}") + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + await hass.config_entries.flow.async_configure(flow_id) + + mock_lametric_config_flow.device.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "cloud_select_device" + assert result2.get("errors") == {"base": reason} + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + mock_lametric_config_flow.device.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 2 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From d2e5d91eba103989f9c3ceee40babbb67da26f1f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 18 Aug 2022 00:25:40 +0000 Subject: [PATCH 445/903] [ci skip] Translation update --- .../components/ambee/translations/ja.json | 1 + .../components/awair/translations/el.json | 6 ++++++ .../components/awair/translations/fr.json | 8 +++++++- .../components/awair/translations/ja.json | 6 ++++++ .../components/awair/translations/no.json | 2 +- .../components/awair/translations/ru.json | 2 +- .../awair/translations/zh-Hant.json | 8 +++++++- .../deutsche_bahn/translations/ja.json | 1 + .../components/foscam/translations/bg.json | 1 + .../components/fritz/translations/bg.json | 3 ++- .../fritzbox_callmonitor/translations/bg.json | 1 + .../fully_kiosk/translations/de.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/el.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/fr.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/ja.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/no.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/ru.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/zh-Hant.json | 20 +++++++++++++++++++ .../components/google/translations/ja.json | 3 ++- .../google_travel_time/translations/bg.json | 3 +++ .../components/hue/translations/el.json | 5 ++++- .../components/hue/translations/fr.json | 17 +++++++++------- .../components/hue/translations/ja.json | 3 ++- .../components/hue/translations/ru.json | 16 +++++++-------- .../components/lametric/translations/de.json | 12 +++++++++++ .../components/lyric/translations/ja.json | 2 +- .../components/miflora/translations/ja.json | 1 + .../components/mitemp_bt/translations/ja.json | 1 + .../components/nest/translations/ja.json | 2 +- .../components/nuki/translations/bg.json | 3 +++ .../radiotherm/translations/ja.json | 2 +- .../components/senz/translations/bg.json | 3 +++ .../components/spotify/translations/ja.json | 2 +- .../steam_online/translations/ja.json | 2 +- .../components/subaru/translations/bg.json | 1 + .../components/uscis/translations/ja.json | 1 + .../waze_travel_time/translations/bg.json | 3 +++ .../components/xbox/translations/ja.json | 2 +- .../xiaomi_miio/translations/select.de.json | 10 ++++++++++ .../xiaomi_miio/translations/select.el.json | 10 ++++++++++ .../xiaomi_miio/translations/select.fr.json | 10 ++++++++++ .../xiaomi_miio/translations/select.ja.json | 10 ++++++++++ .../xiaomi_miio/translations/select.no.json | 10 ++++++++++ .../xiaomi_miio/translations/select.ru.json | 10 ++++++++++ .../translations/select.zh-Hant.json | 10 ++++++++++ 45 files changed, 304 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/fully_kiosk/translations/de.json create mode 100644 homeassistant/components/fully_kiosk/translations/el.json create mode 100644 homeassistant/components/fully_kiosk/translations/fr.json create mode 100644 homeassistant/components/fully_kiosk/translations/ja.json create mode 100644 homeassistant/components/fully_kiosk/translations/no.json create mode 100644 homeassistant/components/fully_kiosk/translations/ru.json create mode 100644 homeassistant/components/fully_kiosk/translations/zh-Hant.json create mode 100644 homeassistant/components/lametric/translations/de.json diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json index e4502068d69..2d6bf3b2466 100644 --- a/homeassistant/components/ambee/translations/ja.json +++ b/homeassistant/components/ambee/translations/ja.json @@ -27,6 +27,7 @@ }, "issues": { "pending_removal": { + "description": "Ambee\u306e\u7d71\u5408\u306fHome Assistant\u304b\u3089\u306e\u524a\u9664\u306f\u4fdd\u7559\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\nAmbee\u304c\u7121\u6599(\u9650\u5b9a)\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u524a\u9664\u3057\u3001\u4e00\u822c\u30e6\u30fc\u30b6\u30fc\u304c\u6709\u6599\u30d7\u30e9\u30f3\u306b\u30b5\u30a4\u30f3\u30a2\u30c3\u30d7\u3059\u308b\u65b9\u6cd5\u3092\u63d0\u4f9b\u3057\u306a\u304f\u306a\u3063\u305f\u305f\u3081\u3001\u7d71\u5408\u304c\u524a\u9664\u3055\u308c\u308b\u3053\u3068\u306b\u306a\u308a\u307e\u3057\u305f\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u304b\u3089Ambee\u306e\u7d71\u5408\u306e\u30a8\u30f3\u30c8\u30ea\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Ambee\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 75447f29c15..187551f40a3 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -31,6 +31,12 @@ }, "description": "\u03a4\u03bf Awair Local API \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1: {url}" }, + "local_pick": { + "data": { + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + } + }, "reauth": { "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index 2b4d572609d..79101fd9128 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -29,7 +29,13 @@ "data": { "host": "Adresse IP" }, - "description": "L'API locale Awair doit \u00eatre activ\u00e9e en suivant ces \u00e9tapes\u00a0: {url}" + "description": "Suivez [ces instructions]({url}) pour activer l\u2019API locale Awair.\n\nCliquez sur Envoyer apr\u00e8s avoir termin\u00e9." + }, + "local_pick": { + "data": { + "device": "Appareil", + "host": "Adresse IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 6599af1bd14..a6bbe56c838 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -31,6 +31,12 @@ }, "description": "\u6b21\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001Awair Local API\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {url}" }, + "local_pick": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9", + "host": "IP\u30a2\u30c9\u30ec\u30b9" + } + }, "reauth": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index d1c7e89f204..b71736d7a6d 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -29,7 +29,7 @@ "data": { "host": "IP adresse" }, - "description": "Awair Local API m\u00e5 aktiveres ved \u00e5 f\u00f8lge disse trinnene: {url}" + "description": "F\u00f8lg [disse instruksjonene]( {url} ) om hvordan du aktiverer Awair Local API. \n\n Klikk p\u00e5 send n\u00e5r du er ferdig." }, "local_pick": { "data": { diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index a81ef9585bd..d59c4ed50f1 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -29,7 +29,7 @@ "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 API Awair, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0437\u0434\u0435\u0441\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f: {url}" + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u044d\u0442\u0438\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({url}), \u0447\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 API Awair.\n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f." }, "local_pick": { "data": { diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index e7953517823..fb6e6acf9cb 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -29,7 +29,13 @@ "data": { "host": "IP \u4f4d\u5740" }, - "description": "\u5fc5\u9808\u900f\u904e\u4ee5\u4e0b\u6b65\u9a5f\u4ee5\u555f\u7528 Awair \u672c\u5730\u7aef API\uff1a{url}" + "description": "\u8ddf\u96a8 [\u4ee5\u4e0b\u6b65\u9a5f]({url}) \u4ee5\u555f\u7528 Awair \u672c\u5730\u7aef API\u3002\n\n\u5b8c\u6210\u5f8c\u9ede\u9078\u50b3\u9001\u3002" + }, + "local_pick": { + "data": { + "device": "\u88dd\u7f6e", + "host": "IP \u4f4d\u5740" + } }, "reauth": { "data": { diff --git a/homeassistant/components/deutsche_bahn/translations/ja.json b/homeassistant/components/deutsche_bahn/translations/ja.json index 191ec874abf..b76158e0b22 100644 --- a/homeassistant/components/deutsche_bahn/translations/ja.json +++ b/homeassistant/components/deutsche_bahn/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "Deutsche Bahn(\u30c9\u30a4\u30c4\u9244\u9053)\u306e\u7d71\u5408\u306f\u3001Home Assistant\u304b\u3089\u306e\u524a\u9664\u306f\u4fdd\u7559\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant 2022.11\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u7d71\u5408\u306f\u3001\u8a31\u53ef\u3055\u308c\u3066\u3044\u306a\u3044Web\u30b9\u30af\u30ec\u30a4\u30d4\u30f3\u30b0\u306b\u4f9d\u5b58\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u524a\u9664\u3055\u308c\u308b\u4e88\u5b9a\u3067\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089Deutsche Bahn YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "\u30c9\u30a4\u30c4\u9244\u9053(Deutsche Bahn)\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/foscam/translations/bg.json b/homeassistant/components/foscam/translations/bg.json index 5c41e03c838..8e0b1bac052 100644 --- a/homeassistant/components/foscam/translations/bg.json +++ b/homeassistant/components/foscam/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "rtsp_port": "RTSP \u043f\u043e\u0440\u0442", diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index fa080a7662e..e9162b8b35b 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -4,7 +4,8 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/bg.json b/homeassistant/components/fritzbox_callmonitor/translations/bg.json index ed2dd868df7..ee2dbc7bb3a 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/bg.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/bg.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/fully_kiosk/translations/de.json b/homeassistant/components/fully_kiosk/translations/de.json new file mode 100644 index 00000000000..cd099ee8ef9 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/el.json b/homeassistant/components/fully_kiosk/translations/el.json new file mode 100644 index 00000000000..9af93e0c36a --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/el.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/fr.json b/homeassistant/components/fully_kiosk/translations/fr.json new file mode 100644 index 00000000000..a4a0a822a88 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/ja.json b/homeassistant/components/fully_kiosk/translations/ja.json new file mode 100644 index 00000000000..b5ef5895312 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/no.json b/homeassistant/components/fully_kiosk/translations/no.json new file mode 100644 index 00000000000..3234879412b --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/ru.json b/homeassistant/components/fully_kiosk/translations/ru.json new file mode 100644 index 00000000000..00a9a3616ff --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/ru.json @@ -0,0 +1,20 @@ +{ + "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": "\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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/zh-Hant.json b/homeassistant/components/fully_kiosk/translations/zh-Hant.json new file mode 100644 index 00000000000..5b13923ac70 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "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", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 7ab3209ac1c..eaa61a0fd91 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -35,10 +35,11 @@ }, "issues": { "deprecated_yaml": { - "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fcyaml\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_track_new_yaml": { + "description": "configuration.yaml\u3067\u3001Google \u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u8ffd\u8de1\u3092\u7121\u52b9\u306b\u3067\u304d\u307e\u3057\u305f\u304c\u3001\u3053\u308c\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002UI\u306e\u7d71\u5408\u30b7\u30b9\u30c6\u30e0\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u5909\u66f4\u3057\u3066\u3001\u65b0\u3057\u304f\u691c\u51fa\u3055\u308c\u305f\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4eca\u5f8c\u7121\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059(disable newly discovered entities going forward)\u3002configuration.yaml\u304b\u3089track_new\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u3066\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3057\u307e\u3059\u3002", "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f" } }, diff --git a/homeassistant/components/google_travel_time/translations/bg.json b/homeassistant/components/google_travel_time/translations/bg.json index d49807e49af..215f8c00629 100644 --- a/homeassistant/components/google_travel_time/translations/bg.json +++ b/homeassistant/components/google_travel_time/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hue/translations/el.json b/homeassistant/components/hue/translations/el.json index 7d1c2cabd44..5641ef04add 100644 --- a/homeassistant/components/hue/translations/el.json +++ b/homeassistant/components/hue/translations/el.json @@ -44,6 +44,8 @@ "button_2": "\u0394\u03b5\u03cd\u03c4\u03b5\u03c1\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", "button_3": "\u03a4\u03c1\u03af\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", "button_4": "\u03a4\u03ad\u03c4\u03b1\u03c1\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", + "clock_wise": "\u03a0\u03b5\u03c1\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03b4\u03b5\u03be\u03b9\u03cc\u03c3\u03c4\u03c1\u03bf\u03c6\u03b1", + "counter_clock_wise": "\u03a0\u03b5\u03c1\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03cc\u03c3\u03c4\u03c1\u03bf\u03c6\u03b1", "dim_down": "\u039c\u03b5\u03af\u03c9\u03c3\u03b7 \u03ad\u03bd\u03c4\u03b1\u03c3\u03b7\u03c2", "dim_up": "\u0391\u03cd\u03be\u03b7\u03c3\u03b7 \u03ad\u03bd\u03c4\u03b1\u03c3\u03b7\u03c2", "double_buttons_1_3": "\u03a0\u03c1\u03ce\u03c4\u03bf \u03ba\u03b1\u03b9 \u03c4\u03c1\u03af\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", "remote_double_button_short_press": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd", "repeat": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{subtype}\" \u03ba\u03c1\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c0\u03b1\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf", - "short_release": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \" {subtype} \" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c3\u03cd\u03bd\u03c4\u03bf\u03bc\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1" + "short_release": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \" {subtype} \" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c3\u03cd\u03bd\u03c4\u03bf\u03bc\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", + "start": "\"{subtype}\" \u03c0\u03b9\u03ad\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ac" } }, "options": { diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index cd07cff9fea..276e12c6c30 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -44,6 +44,8 @@ "button_2": "Deuxi\u00e8me bouton", "button_3": "Troisi\u00e8me bouton", "button_4": "Quatri\u00e8me bouton", + "clock_wise": "Rotation horaire", + "counter_clock_wise": "Rotation anti-horaire", "dim_down": "Assombrir", "dim_up": "\u00c9claircir", "double_buttons_1_3": "Premier et troisi\u00e8me boutons", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Les deux \u00ab\u00a0{subtype}\u00a0\u00bb sont rel\u00e2ch\u00e9s", - "initial_press": "D\u00e9but de l'appui du bouton \u00ab\u00a0{subtype}\u00a0\u00bb", - "long_release": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui long", - "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", - "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", - "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", + "initial_press": "D\u00e9but de l'appui sur \u00ab\u00a0{subtype}\u00a0\u00bb", + "long_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui long", + "remote_button_long_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui long", + "remote_button_short_press": "\u00ab\u00a0{subtype}\u00a0\u00bb appuy\u00e9", + "remote_button_short_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9", "remote_double_button_long_press": "Les deux \"{sous-type}\" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s apr\u00e8s un appui long", "remote_double_button_short_press": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s", - "repeat": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb maintenu enfonc\u00e9", - "short_release": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui court" + "repeat": "\u00ab\u00a0{subtype}\u00a0\u00bb maintenu appuy\u00e9", + "short_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui court", + "start": "D\u00e9but de l'appui sur \u00ab\u00a0{subtype}\u00a0\u00bb" } }, "options": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index f5eddbc0242..dcf38b0e48c 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -63,7 +63,8 @@ "remote_double_button_long_press": "\u4e21\u65b9\u306e \"{subtype}\" \u306f\u9577\u62bc\u3057\u5f8c\u306b\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_double_button_short_press": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", "repeat": "\u30dc\u30bf\u30f3 \"{subtype}\" \u3092\u62bc\u3057\u305f\u307e\u307e", - "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059" + "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059", + "start": "\"{subtype}\" \u304c\u6700\u521d\u306b\u62bc\u3055\u308c\u307e\u3057\u305f" } }, "options": { diff --git a/homeassistant/components/hue/translations/ru.json b/homeassistant/components/hue/translations/ru.json index 3f34b6b99e4..0614d75e176 100644 --- a/homeassistant/components/hue/translations/ru.json +++ b/homeassistant/components/hue/translations/ru.json @@ -55,16 +55,16 @@ }, "trigger_type": { "double_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "initial_press": "{subtype} \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_short_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "initial_press": "\"{subtype}\" \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_double_button_long_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_double_button_short_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "repeat": "{subtype} \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", - "short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "start": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e" + "repeat": "\"{subtype}\" \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", + "short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "start": "\"{subtype}\" \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430" } }, "options": { diff --git a/homeassistant/components/lametric/translations/de.json b/homeassistant/components/lametric/translations/de.json new file mode 100644 index 00000000000..1ea4d15dcd8 --- /dev/null +++ b/homeassistant/components/lametric/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle eine Authentifizierungsmethode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index bd8b4b93442..c9bf880233d 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -20,7 +20,7 @@ }, "issues": { "removed_yaml": { - "description": "Honeywell Lyric\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Honeywell Lyric\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Honeywell Lyric YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/miflora/translations/ja.json b/homeassistant/components/miflora/translations/ja.json index 30b2730980b..a9c2df5bf2e 100644 --- a/homeassistant/components/miflora/translations/ja.json +++ b/homeassistant/components/miflora/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "replaced": { + "description": "Mi Flora\u306e\u7d71\u5408\u306fHome Assistant 2022.7\u3067\u52d5\u4f5c\u3057\u306a\u304f\u306a\u308a\u30012022.8\u306e\u30ea\u30ea\u30fc\u30b9\u3067Xiaomi BLE\u7d71\u5408\u306b\u7f6e\u304d\u63db\u308f\u308a\u307e\u3057\u305f\u3002\n\n\u79fb\u884c\u30d1\u30b9\u306f\u3042\u308a\u307e\u305b\u3093\u306e\u3067\u3001\u65b0\u3057\u3044\u7d71\u5408\u3092\u4f7f\u7528\u3057\u3066Mi Flora\u30c7\u30d0\u30a4\u30b9\u3092\u624b\u52d5\u3067\u8ffd\u52a0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u65e2\u5b58\u306eMi Flora\u306eYAML\u69cb\u6210\u306fHome Assistant\u3067\u4f7f\u7528\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001config.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "MiFlora\u306e\u7d71\u5408\u306f\u7f6e\u304d\u63db\u3048\u3089\u308c\u307e\u3057\u305f" } } diff --git a/homeassistant/components/mitemp_bt/translations/ja.json b/homeassistant/components/mitemp_bt/translations/ja.json index 11212382f1f..da129842bc8 100644 --- a/homeassistant/components/mitemp_bt/translations/ja.json +++ b/homeassistant/components/mitemp_bt/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "replaced": { + "description": "Xiaomi Mijia BLE\u6e29\u5ea6\u30fb\u6e7f\u5ea6\u30bb\u30f3\u30b5\u30fc\u306e\u7d71\u5408\u306f\u3001Home Assistant 2022.7\u3067\u52d5\u4f5c\u3057\u306a\u304f\u306a\u308a\u30012022.8\u306e\u30ea\u30ea\u30fc\u30b9\u3067Xiaomi BLE\u7d71\u5408\u306b\u7f6e\u304d\u63db\u308f\u308a\u307e\u3057\u305f\u3002\n\n\u79fb\u884c\u30d1\u30b9\u306f\u3042\u308a\u307e\u305b\u3093\u306e\u3067\u3001\u65b0\u3057\u3044\u7d71\u5408\u3092\u4f7f\u7528\u3057\u3066Xiaomi Mijia BLE\u30c7\u30d0\u30a4\u30b9\u3092\u624b\u52d5\u3067\u8ffd\u52a0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u65e2\u5b58\u306eXiaomi Mijia BLE\u6e29\u5ea6\u30fb\u6e7f\u5ea6\u30bb\u30f3\u30b5\u30fc\u306eYAML\u69cb\u6210\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Xiaomi Mijia BLE\u6e29\u5ea6\u304a\u3088\u3073\u6e7f\u5ea6\u30bb\u30f3\u30b5\u30fc\u306e\u7d71\u5408\u306f\u3001\u30ea\u30d7\u30ec\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f\u3002" } } diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 5509cd497eb..06c5e54ef88 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -92,7 +92,7 @@ }, "issues": { "deprecated_yaml": { - "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Nest\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.10\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Nest\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.10\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Nest YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_app_auth": { diff --git a/homeassistant/components/nuki/translations/bg.json b/homeassistant/components/nuki/translations/bg.json index 4983c9a14b2..37e8e854866 100644 --- a/homeassistant/components/nuki/translations/bg.json +++ b/homeassistant/components/nuki/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/radiotherm/translations/ja.json b/homeassistant/components/radiotherm/translations/ja.json index e3cf6571357..64d3cca9112 100644 --- a/homeassistant/components/radiotherm/translations/ja.json +++ b/homeassistant/components/radiotherm/translations/ja.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "YAML\u3092\u4f7f\u7528\u3057\u305f\u3001Radio Thermostat climate(\u6c17\u5019)\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "YAML\u3092\u4f7f\u7528\u3057\u305f\u3001Radio Thermostat climate(\u6c17\u5019)\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Radio Thermostat YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/senz/translations/bg.json b/homeassistant/components/senz/translations/bg.json index a99746433a0..9493398f365 100644 --- a/homeassistant/components/senz/translations/bg.json +++ b/homeassistant/components/senz/translations/bg.json @@ -4,6 +4,9 @@ "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, "step": { "pick_implementation": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index 6f1c48c8572..efcfa959e5a 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -21,7 +21,7 @@ }, "issues": { "removed_yaml": { - "description": "Spotify\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Spotify\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Spotify YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index 1524e2afc5a..75fe7d9cd99 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "Steam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Steam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Steam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index 212303991b0..c43cb84d5f6 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -6,6 +6,7 @@ }, "error": { "bad_pin_format": "\u041f\u0418\u041d \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 4 \u0446\u0438\u0444\u0440\u0438", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "incorrect_validation_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043a\u043e\u0434 \u0437\u0430 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/uscis/translations/ja.json b/homeassistant/components/uscis/translations/ja.json index b5abb7e0825..46f021952d8 100644 --- a/homeassistant/components/uscis/translations/ja.json +++ b/homeassistant/components/uscis/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "\u7c73\u56fd\u5e02\u6c11\u6a29\u79fb\u6c11\u5c40(US Citizenship and Immigration Services (USCIS))\u306e\u7d71\u5408\u306f\u3001Home Assistant\u304b\u3089\u306e\u524a\u9664\u306f\u4fdd\u7559\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u7d71\u5408\u306f\u3001\u8a31\u53ef\u3055\u308c\u3066\u3044\u306a\u3044Web\u30b9\u30af\u30ec\u30a4\u30d4\u30f3\u30b0\u306b\u4f9d\u5b58\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u524a\u9664\u3055\u308c\u308b\u4e88\u5b9a\u3067\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "USCIS\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/waze_travel_time/translations/bg.json b/homeassistant/components/waze_travel_time/translations/bg.json index f7d35259c93..fb5df032671 100644 --- a/homeassistant/components/waze_travel_time/translations/bg.json +++ b/homeassistant/components/waze_travel_time/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index 530299b0b24..2534412e55b 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -16,7 +16,7 @@ }, "issues": { "deprecated_yaml": { - "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Xbox\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Xbox\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Xbox YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.de.json b/homeassistant/components/xiaomi_miio/translations/select.de.json index 804eb7a7629..a4ac15de93b 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.de.json +++ b/homeassistant/components/xiaomi_miio/translations/select.de.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Vorw\u00e4rts", + "left": "Links", + "right": "Rechts" + }, "xiaomi_miio__led_brightness": { "bright": "Helligkeit", "dim": "Dimmer", "off": "Aus" + }, + "xiaomi_miio__ptc_level": { + "high": "Hoch", + "low": "Niedrig", + "medium": "Mittel" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.el.json b/homeassistant/components/xiaomi_miio/translations/select.el.json index 24c8a037676..911360a6ee5 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.el.json +++ b/homeassistant/components/xiaomi_miio/translations/select.el.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u0395\u03bc\u03c0\u03c1\u03cc\u03c2", + "left": "\u0391\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", + "right": "\u0394\u03b5\u03be\u03b9\u03ac" + }, "xiaomi_miio__led_brightness": { "bright": "\u03a6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc", "dim": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" + }, + "xiaomi_miio__ptc_level": { + "high": "\u03a5\u03c8\u03b7\u03bb\u03ae", + "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03ae", + "medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03b1" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.fr.json b/homeassistant/components/xiaomi_miio/translations/select.fr.json index 29c9afe1e95..8ffc46043bc 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.fr.json +++ b/homeassistant/components/xiaomi_miio/translations/select.fr.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Vers l'avant", + "left": "Gauche", + "right": "Droite" + }, "xiaomi_miio__led_brightness": { "bright": "Brillant", "dim": "Faible", "off": "\u00c9teint" + }, + "xiaomi_miio__ptc_level": { + "high": "\u00c9lev\u00e9", + "low": "Faible", + "medium": "Moyen" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.ja.json b/homeassistant/components/xiaomi_miio/translations/select.ja.json index 22a7a4ea058..245225a5cae 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ja.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ja.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u9032\u3080", + "left": "\u5de6", + "right": "\u53f3" + }, "xiaomi_miio__led_brightness": { "bright": "\u660e\u308b\u3044", "dim": "\u8584\u6697\u3044", "off": "\u30aa\u30d5" + }, + "xiaomi_miio__ptc_level": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.no.json b/homeassistant/components/xiaomi_miio/translations/select.no.json index 8205447ac2c..611095b4712 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.no.json +++ b/homeassistant/components/xiaomi_miio/translations/select.no.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Framover", + "left": "Venstre", + "right": "H\u00f8yre" + }, "xiaomi_miio__led_brightness": { "bright": "Lys", "dim": "Dim", "off": "Av" + }, + "xiaomi_miio__ptc_level": { + "high": "H\u00f8y", + "low": "Lav", + "medium": "Medium" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.ru.json b/homeassistant/components/xiaomi_miio/translations/select.ru.json index 138d2b4fdce..322dc84e01c 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ru.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ru.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u0412\u043f\u0435\u0440\u0435\u0434", + "left": "\u041d\u0430\u043b\u0435\u0432\u043e", + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e" + }, "xiaomi_miio__led_brightness": { "bright": "\u042f\u0440\u043a\u043e", "dim": "\u0422\u0443\u0441\u043a\u043b\u043e", "off": "\u041e\u0442\u043a\u043b." + }, + "xiaomi_miio__ptc_level": { + "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", + "low": "\u041d\u0438\u0437\u043a\u0438\u0439", + "medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/select.zh-Hant.json index 3c3152db0da..527bad0c120 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/select.zh-Hant.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u524d", + "left": "\u5de6", + "right": "\u53f3" + }, "xiaomi_miio__led_brightness": { "bright": "\u4eae\u5149", "dim": "\u5fae\u5149", "off": "\u95dc\u9589" + }, + "xiaomi_miio__ptc_level": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d" } } } \ No newline at end of file From 3eaa1c30af36320ec54e4cb96b23b5a654919575 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 18 Aug 2022 04:15:48 +0200 Subject: [PATCH 446/903] Restore fixed step fan speeds for google assistant (#76871) --- .../components/google_assistant/const.py | 18 +++ .../components/google_assistant/trait.py | 58 ++++++++- .../components/google_assistant/test_trait.py | 117 +++++++++++++++++- 3 files changed, 191 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index dbcf60ac098..20c4ab60e88 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -186,3 +186,21 @@ SOURCE_CLOUD = "cloud" SOURCE_LOCAL = "local" NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK} + +FAN_SPEEDS = { + "5/5": ["High", "Max", "Fast", "5"], + "4/5": ["Medium High", "4"], + "3/5": ["Medium", "3"], + "2/5": ["Medium Low", "2"], + "1/5": ["Low", "Min", "Slow", "1"], + "4/4": ["High", "Max", "Fast", "4"], + "3/4": ["Medium High", "3"], + "2/4": ["Medium Low", "2"], + "1/4": ["Low", "Min", "Slow", "1"], + "3/3": ["High", "Max", "Fast", "3"], + "2/3": ["Medium", "2"], + "1/3": ["Low", "Min", "Slow", "1"], + "2/2": ["High", "Max", "Fast", "2"], + "1/2": ["Low", "Min", "Slow", "1"], + "1/1": ["Normal", "1"], +} diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index edc8ed124b3..defc5b0cc89 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components import ( alarm_control_panel, @@ -68,6 +69,10 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.helpers.network import get_url from homeassistant.util import color as color_util, dt, temperature as temp_util +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) from .const import ( CHALLENGE_ACK_NEEDED, @@ -82,6 +87,7 @@ from .const import ( ERR_NOT_SUPPORTED, ERR_UNSUPPORTED_INPUT, ERR_VALUE_OUT_OF_RANGE, + FAN_SPEEDS, ) from .error import ChallengeNeeded, SmartHomeError @@ -157,6 +163,8 @@ COMMAND_CHARGE = f"{PREFIX_COMMANDS}Charge" TRAITS = [] +FAN_SPEED_MAX_SPEED_COUNT = 5 + def register_trait(trait): """Decorate a function to register a trait.""" @@ -1359,6 +1367,20 @@ class ArmDisArmTrait(_Trait): ) +def _get_fan_speed(speed_name: str) -> dict[str, Any]: + """Return a fan speed synonyms for a speed name.""" + speed_synonyms = FAN_SPEEDS.get(speed_name, [f"{speed_name}"]) + return { + "speed_name": speed_name, + "speed_values": [ + { + "speed_synonym": speed_synonyms, + "lang": "en", + } + ], + } + + @register_trait class FanSpeedTrait(_Trait): """Trait to control speed of Fan. @@ -1369,6 +1391,18 @@ class FanSpeedTrait(_Trait): name = TRAIT_FANSPEED commands = [COMMAND_FANSPEED, COMMAND_REVERSE] + def __init__(self, hass, state, config): + """Initialize a trait for a state.""" + super().__init__(hass, state, config) + if state.domain == fan.DOMAIN: + speed_count = min( + FAN_SPEED_MAX_SPEED_COUNT, + round(100 / self.state.attributes.get(fan.ATTR_PERCENTAGE_STEP) or 1.0), + ) + self._ordered_speed = [ + f"{speed}/{speed_count}" for speed in range(1, speed_count + 1) + ] + @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" @@ -1397,6 +1431,18 @@ class FanSpeedTrait(_Trait): } ) + if self._ordered_speed: + result.update( + { + "availableFanSpeeds": { + "speeds": [ + _get_fan_speed(speed) for speed in self._ordered_speed + ], + "ordered": True, + }, + } + ) + elif domain == climate.DOMAIN: modes = self.state.attributes.get(climate.ATTR_FAN_MODES) or [] for mode in modes: @@ -1428,6 +1474,9 @@ class FanSpeedTrait(_Trait): if domain == fan.DOMAIN: percent = attrs.get(fan.ATTR_PERCENTAGE) or 0 response["currentFanSpeedPercent"] = percent + response["currentFanSpeedSetting"] = percentage_to_ordered_list_item( + self._ordered_speed, percent + ) return response @@ -1447,12 +1496,19 @@ class FanSpeedTrait(_Trait): ) if domain == fan.DOMAIN: + if fan_speed := params.get("fanSpeed"): + fan_speed_percent = ordered_list_item_to_percentage( + self._ordered_speed, fan_speed + ) + else: + fan_speed_percent = params.get("fanSpeedPercent") + await self.hass.services.async_call( fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE, { ATTR_ENTITY_ID: self.state.entity_id, - fan.ATTR_PERCENTAGE: params["fanSpeedPercent"], + fan.ATTR_PERCENTAGE: fan_speed_percent, }, blocking=not self.config.should_report_state, context=data.context, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 0012826074b..a3024c184d6 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,6 +1,6 @@ """Tests for the Google Assistant traits.""" from datetime import datetime, timedelta -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest @@ -1601,10 +1601,12 @@ async def test_fan_speed(hass): assert trt.sync_attributes() == { "reversible": False, "supportsFanSpeedPercent": True, + "availableFanSpeeds": ANY, } assert trt.query_attributes() == { "currentFanSpeedPercent": 33, + "currentFanSpeedSetting": ANY, } assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeedPercent": 10}) @@ -1616,6 +1618,117 @@ async def test_fan_speed(hass): assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10} +@pytest.mark.parametrize( + "percentage,percentage_step, speed, speeds, percentage_result", + [ + ( + 33, + 1.0, + "2/5", + [ + ["Low", "Min", "Slow", "1"], + ["Medium Low", "2"], + ["Medium", "3"], + ["Medium High", "4"], + ["High", "Max", "Fast", "5"], + ], + 40, + ), + ( + 40, + 1.0, + "2/5", + [ + ["Low", "Min", "Slow", "1"], + ["Medium Low", "2"], + ["Medium", "3"], + ["Medium High", "4"], + ["High", "Max", "Fast", "5"], + ], + 40, + ), + ( + 33, + 100 / 3, + "1/3", + [ + ["Low", "Min", "Slow", "1"], + ["Medium", "2"], + ["High", "Max", "Fast", "3"], + ], + 33, + ), + ( + 20, + 100 / 4, + "1/4", + [ + ["Low", "Min", "Slow", "1"], + ["Medium Low", "2"], + ["Medium High", "3"], + ["High", "Max", "Fast", "4"], + ], + 25, + ), + ], +) +async def test_fan_speed_ordered( + hass, + percentage: int, + percentage_step: float, + speed: str, + speeds: list[list[str]], + percentage_result: int, +): + """Test FanSpeed trait speed control support for fan domain.""" + assert helpers.get_google_type(fan.DOMAIN, None) is not None + assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None, None) + + trt = trait.FanSpeedTrait( + hass, + State( + "fan.living_room_fan", + STATE_ON, + attributes={ + "percentage": percentage, + "percentage_step": percentage_step, + }, + ), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == { + "reversible": False, + "supportsFanSpeedPercent": True, + "availableFanSpeeds": { + "ordered": True, + "speeds": [ + { + "speed_name": f"{idx+1}/{len(speeds)}", + "speed_values": [{"lang": "en", "speed_synonym": x}], + } + for idx, x in enumerate(speeds) + ], + }, + } + + assert trt.query_attributes() == { + "currentFanSpeedPercent": percentage, + "currentFanSpeedSetting": speed, + } + + assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeed": speed}) + + calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE) + await trt.execute(trait.COMMAND_FANSPEED, BASIC_DATA, {"fanSpeed": speed}, {}) + + assert len(calls) == 1 + assert calls[0].data == { + "entity_id": "fan.living_room_fan", + "percentage": percentage_result, + } + + @pytest.mark.parametrize( "direction_state,direction_call", [ @@ -1647,10 +1760,12 @@ async def test_fan_reverse(hass, direction_state, direction_call): assert trt.sync_attributes() == { "reversible": True, "supportsFanSpeedPercent": True, + "availableFanSpeeds": ANY, } assert trt.query_attributes() == { "currentFanSpeedPercent": 33, + "currentFanSpeedSetting": ANY, } assert trt.can_execute(trait.COMMAND_REVERSE, params={}) From 03fac0c529fea8ffaa5290a194c863423f923d5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 16:37:47 -1000 Subject: [PATCH 447/903] Fix race in notify setup (#76954) --- homeassistant/components/notify/__init__.py | 12 ++- homeassistant/components/notify/legacy.py | 21 ++--- tests/components/notify/test_init.py | 99 ++++++++++++++++++++- 3 files changed, 119 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 788c698c0ca..60d24578593 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -1,6 +1,8 @@ """Provides functionality to notify people.""" from __future__ import annotations +import asyncio + import voluptuous as vol import homeassistant.components.persistent_notification as pn @@ -40,13 +42,19 @@ PLATFORM_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" + platform_setups = async_setup_legacy(hass, config) + # We need to add the component here break the deadlock # when setting up integrations from config entries as # they would otherwise wait for notify to be # setup and thus the config entries would not be able to - # setup their platforms. + # setup their platforms, but we need to do it after + # the dispatcher is connected so we don't miss integrations + # that are registered before the dispatcher is connected hass.config.components.add(DOMAIN) - await async_setup_legacy(hass, config) + + if platform_setups: + await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups]) async def persistent_notification(service: ServiceCall) -> None: """Send notification via the built-in persistsent_notify integration.""" diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index 50b02324827..f9066b7dff9 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine from functools import partial from typing import Any, cast @@ -32,7 +33,10 @@ NOTIFY_SERVICES = "notify_services" NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher" -async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: +@callback +def async_setup_legacy( + hass: HomeAssistant, config: ConfigType +) -> list[Coroutine[Any, Any, None]]: """Set up legacy notify services.""" hass.data.setdefault(NOTIFY_SERVICES, {}) hass.data.setdefault(NOTIFY_DISCOVERY_DISPATCHER, None) @@ -101,15 +105,6 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: ) hass.config.components.add(f"{DOMAIN}.{integration_name}") - setup_tasks = [ - asyncio.create_task(async_setup_platform(integration_name, p_config)) - for integration_name, p_config in config_per_platform(config, DOMAIN) - if integration_name is not None - ] - - if setup_tasks: - await asyncio.wait(setup_tasks) - async def async_platform_discovered( platform: str, info: DiscoveryInfoType | None ) -> None: @@ -120,6 +115,12 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: hass, DOMAIN, async_platform_discovered ) + return [ + async_setup_platform(integration_name, p_config) + for integration_name, p_config in config_per_platform(config, DOMAIN) + if integration_name is not None + ] + @callback def check_templates_warn(hass: HomeAssistant, tpl: template.Template) -> None: diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index ae32884add7..b691ed7a051 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -1,12 +1,13 @@ """The tests for notify services that change targets.""" +import asyncio from unittest.mock import Mock, patch import yaml from homeassistant import config as hass_config from homeassistant.components import notify -from homeassistant.const import SERVICE_RELOAD +from homeassistant.const import SERVICE_RELOAD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.reload import async_setup_reload_service @@ -330,3 +331,99 @@ async def test_setup_platform_and_reload(hass, caplog, tmp_path): # Check if the dynamically notify services from setup were removed assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_before_notify_setup(hass, caplog, tmp_path): + """Test trying to setup a platform before notify is setup.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + load_task = asyncio.create_task(load_coro) + setup_task = asyncio.create_task(setup_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_after_notify_setup(hass, caplog, tmp_path): + """Test trying to setup a platform after notify is setup.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + setup_task = asyncio.create_task(setup_coro) + load_task = asyncio.create_task(load_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") From 280ae91ba17aa7ed98eec2f41607969a03685b2f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 17 Aug 2022 22:41:28 -0400 Subject: [PATCH 448/903] Pass the real config for Slack (#76960) --- homeassistant/components/slack/__init__.py | 6 ++++-- homeassistant/components/slack/const.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/slack/__init__.py b/homeassistant/components/slack/__init__.py index ae52013621f..a89f645e9b6 100644 --- a/homeassistant/components/slack/__init__.py +++ b/homeassistant/components/slack/__init__.py @@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, discovery from homeassistant.helpers.typing import ConfigType -from .const import DATA_CLIENT, DOMAIN +from .const import DATA_CLIENT, DATA_HASS_CONFIG, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,6 +21,8 @@ PLATFORMS = [Platform.NOTIFY] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Slack component.""" + hass.data[DATA_HASS_CONFIG] = config + # Iterate all entries for notify to only get Slack if Platform.NOTIFY in config: for entry in config[Platform.NOTIFY]: @@ -55,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Platform.NOTIFY, DOMAIN, hass.data[DOMAIN][entry.entry_id], - hass.data[DOMAIN], + hass.data[DATA_HASS_CONFIG], ) ) diff --git a/homeassistant/components/slack/const.py b/homeassistant/components/slack/const.py index b7b5707aeeb..83937f4a43e 100644 --- a/homeassistant/components/slack/const.py +++ b/homeassistant/components/slack/const.py @@ -14,3 +14,5 @@ CONF_DEFAULT_CHANNEL = "default_channel" DATA_CLIENT = "client" DEFAULT_TIMEOUT_SECONDS = 15 DOMAIN: Final = "slack" + +DATA_HASS_CONFIG = "slack_hass_config" From 6ab9652b600de93edec05e4ea40d768c6e04c9f6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 17 Aug 2022 22:41:59 -0400 Subject: [PATCH 449/903] Pass the real config for Discord (#76959) --- homeassistant/components/discord/__init__.py | 16 ++++++++++------ homeassistant/components/discord/const.py | 2 ++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py index ae06447f741..a52c079ac8e 100644 --- a/homeassistant/components/discord/__init__.py +++ b/homeassistant/components/discord/__init__.py @@ -7,12 +7,20 @@ from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DATA_HASS_CONFIG, DOMAIN PLATFORMS = [Platform.NOTIFY] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Discord component.""" + + hass.data[DATA_HASS_CONFIG] = config + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Discord from a config entry.""" nextcord.VoiceClient.warn_nacl = False @@ -30,11 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.async_create_task( discovery.async_load_platform( - hass, - Platform.NOTIFY, - DOMAIN, - hass.data[DOMAIN][entry.entry_id], - hass.data[DOMAIN], + hass, Platform.NOTIFY, DOMAIN, dict(entry.data), hass.data[DATA_HASS_CONFIG] ) ) diff --git a/homeassistant/components/discord/const.py b/homeassistant/components/discord/const.py index 9f11c3e2d7a..82ddb890685 100644 --- a/homeassistant/components/discord/const.py +++ b/homeassistant/components/discord/const.py @@ -8,3 +8,5 @@ DEFAULT_NAME = "Discord" DOMAIN: Final = "discord" URL_PLACEHOLDER = {CONF_URL: "https://www.home-assistant.io/integrations/discord"} + +DATA_HASS_CONFIG = "discord_hass_config" From 82b6deeb799221e3a1b1043fd12edb5b07b26e93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 16:43:22 -1000 Subject: [PATCH 450/903] Bump qingping-ble to 0.2.4 (#76958) --- homeassistant/components/qingping/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 221087de8c4..20adbaf15f1 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qingping", "bluetooth": [{ "local_name": "Qingping*" }], - "requirements": ["qingping-ble==0.2.3"], + "requirements": ["qingping-ble==0.2.4"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 81e62987592..785428e4c9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2067,7 +2067,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.2.3 +qingping-ble==0.2.4 # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69d56e4ed40..b499659dc44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1412,7 +1412,7 @@ pyws66i==1.1 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.2.3 +qingping-ble==0.2.4 # homeassistant.components.rachio rachiopy==1.0.3 From 4a84a8caa95b6b13e8744790d31ed81e311f3f63 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 10:22:49 +0200 Subject: [PATCH 451/903] Use Platform enum (#76967) --- homeassistant/components/bayesian/__init__.py | 4 +++- homeassistant/components/google/__init__.py | 3 ++- homeassistant/components/ping/const.py | 4 +++- homeassistant/components/sabnzbd/__init__.py | 3 ++- .../components/slimproto/__init__.py | 4 ++-- homeassistant/components/sonos/const.py | 20 ++++++++----------- homeassistant/components/vulcan/__init__.py | 3 ++- homeassistant/components/webostv/const.py | 4 +++- homeassistant/components/ws66i/__init__.py | 4 ++-- 9 files changed, 27 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/bayesian/__init__.py b/homeassistant/components/bayesian/__init__.py index 485592dc5e4..e6f865b5656 100644 --- a/homeassistant/components/bayesian/__init__.py +++ b/homeassistant/components/bayesian/__init__.py @@ -1,4 +1,6 @@ """The bayesian component.""" +from homeassistant.const import Platform + DOMAIN = "bayesian" -PLATFORMS = ["binary_sensor"] +PLATFORMS = [Platform.BINARY_SENSOR] diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 4b72aaa77ad..c983868b167 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_ENTITIES, CONF_NAME, CONF_OFFSET, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ( @@ -88,7 +89,7 @@ YAML_DEVICES = f"{DOMAIN}_calendars.yaml" TOKEN_FILE = f".{DOMAIN}.token" -PLATFORMS = ["calendar"] +PLATFORMS = [Platform.CALENDAR] CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ping/const.py b/homeassistant/components/ping/const.py index 9ca99db2419..1a77c62fa5c 100644 --- a/homeassistant/components/ping/const.py +++ b/homeassistant/components/ping/const.py @@ -1,5 +1,7 @@ """Tracks devices by sending a ICMP echo request (ping).""" +from homeassistant.const import Platform + # The ping binary and icmplib timeouts are not the same # timeout. ping is an overall timeout, icmplib is the # time since the data was sent. @@ -13,6 +15,6 @@ ICMP_TIMEOUT = 1 PING_ATTEMPTS_COUNT = 3 DOMAIN = "ping" -PLATFORMS = ["binary_sensor"] +PLATFORMS = [Platform.BINARY_SENSOR] PING_PRIVS = "ping_privs" diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index aca8d1cd9f4..fe9a64f3d6b 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SSL, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError @@ -47,7 +48,7 @@ from .const import ( from .sab import get_client from .sensor import OLD_SENSOR_KEYS -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) SERVICES = ( diff --git a/homeassistant/components/slimproto/__init__.py b/homeassistant/components/slimproto/__init__.py index a96ff7ae925..c22349bb2f2 100644 --- a/homeassistant/components/slimproto/__init__.py +++ b/homeassistant/components/slimproto/__init__.py @@ -4,13 +4,13 @@ from __future__ import annotations from aioslimproto import SlimServer from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr from .const import DOMAIN -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index e6a5225e30e..4c10bb113c7 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -1,8 +1,6 @@ """Const for Sonos.""" import datetime -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, @@ -19,22 +17,20 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TRACK, ) -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import Platform UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1" DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" DATA_SONOS_DISCOVERY_MANAGER = "sonos_discovery_manager" -PLATFORMS = { - BINARY_SENSOR_DOMAIN, - MP_DOMAIN, - NUMBER_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, -} +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.MEDIA_PLAYER, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] SONOS_ARTIST = "artists" SONOS_ALBUM = "albums" diff --git a/homeassistant/components/vulcan/__init__.py b/homeassistant/components/vulcan/__init__.py index 8e66075935b..0bfd09d590d 100644 --- a/homeassistant/components/vulcan/__init__.py +++ b/homeassistant/components/vulcan/__init__.py @@ -4,13 +4,14 @@ from aiohttp import ClientConnectorError from vulcan import Account, Keystore, UnauthorizedCertificateException, Vulcan from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -PLATFORMS = ["calendar"] +PLATFORMS = [Platform.CALENDAR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py index f471ca7340d..830c0a4134a 100644 --- a/homeassistant/components/webostv/const.py +++ b/homeassistant/components/webostv/const.py @@ -4,8 +4,10 @@ import asyncio from aiowebostv import WebOsTvCommandError from websockets.exceptions import ConnectionClosed, ConnectionClosedOK +from homeassistant.const import Platform + DOMAIN = "webostv" -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] DATA_CONFIG_ENTRY = "config_entry" DATA_HASS_CONFIG = "hass_config" DEFAULT_NAME = "LG webOS Smart TV" diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index 0b40ce84816..cffedc2f684 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -6,7 +6,7 @@ import logging from pyws66i import WS66i, get_ws66i from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -16,7 +16,7 @@ from .models import SourceRep, Ws66iData _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] @callback From 6fbdc8e33939b12053e58047db8862b3f99fa0d1 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 18 Aug 2022 04:51:28 -0400 Subject: [PATCH 452/903] Add Fully Kiosk Browser number platform (#76952) Co-authored-by: Franck Nijhof --- .../components/fully_kiosk/__init__.py | 8 +- .../components/fully_kiosk/number.py | 99 +++++++++++++++++++ tests/components/fully_kiosk/test_number.py | 91 +++++++++++++++++ 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fully_kiosk/number.py create mode 100644 tests/components/fully_kiosk/test_number.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 5a3d6078004..ebd9af1134a 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,13 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/number.py b/homeassistant/components/fully_kiosk/number.py new file mode 100644 index 00000000000..d39f3f6391d --- /dev/null +++ b/homeassistant/components/fully_kiosk/number.py @@ -0,0 +1,99 @@ +"""Fully Kiosk Browser number entity.""" +from __future__ import annotations + +from contextlib import suppress + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TIME_SECONDS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + +ENTITY_TYPES: tuple[NumberEntityDescription, ...] = ( + NumberEntityDescription( + key="timeToScreensaverV2", + name="Screensaver timer", + native_max_value=9999, + native_step=1, + native_min_value=0, + native_unit_of_measurement=TIME_SECONDS, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key="screensaverBrightness", + name="Screensaver brightness", + native_max_value=255, + native_step=1, + native_min_value=0, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key="timeToScreenOffV2", + name="Screen off timer", + native_max_value=9999, + native_step=1, + native_min_value=0, + native_unit_of_measurement=TIME_SECONDS, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key="screenBrightness", + name="Screen brightness", + native_max_value=255, + native_step=1, + native_min_value=0, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser number entities.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + FullyNumberEntity(coordinator, entity) + for entity in ENTITY_TYPES + if entity.key in coordinator.data["settings"] + ) + + +class FullyNumberEntity(FullyKioskEntity, NumberEntity): + """Representation of a Fully Kiosk Browser entity.""" + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: NumberEntityDescription, + ) -> None: + """Initialize the number entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + @property + def native_value(self) -> int | None: + """Return the state of the number entity.""" + if ( + value := self.coordinator.data["settings"].get(self.entity_description.key) + ) is None: + return None + + with suppress(ValueError): + return int(value) + + return None + + async def async_set_native_value(self, value: float) -> None: + """Set the value of the entity.""" + await self.coordinator.fully.setConfigurationString( + self.entity_description.key, int(value) + ) diff --git a/tests/components/fully_kiosk/test_number.py b/tests/components/fully_kiosk/test_number.py new file mode 100644 index 00000000000..968faa3f0b4 --- /dev/null +++ b/tests/components/fully_kiosk/test_number.py @@ -0,0 +1,91 @@ +"""Test the Fully Kiosk Browser number entities.""" +from unittest.mock import MagicMock + +from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL +import homeassistant.components.number as number +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_numbers( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk numbers.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("number.amazon_fire_screensaver_timer") + assert state + assert state.state == "900" + entry = entity_registry.async_get("number.amazon_fire_screensaver_timer") + assert entry + assert entry.unique_id == "abcdef-123456-timeToScreensaverV2" + await set_value(hass, "number.amazon_fire_screensaver_timer", 600) + assert len(mock_fully_kiosk.setConfigurationString.mock_calls) == 1 + + state = hass.states.get("number.amazon_fire_screensaver_brightness") + assert state + assert state.state == "0" + entry = entity_registry.async_get("number.amazon_fire_screensaver_brightness") + assert entry + assert entry.unique_id == "abcdef-123456-screensaverBrightness" + + state = hass.states.get("number.amazon_fire_screen_off_timer") + assert state + assert state.state == "0" + entry = entity_registry.async_get("number.amazon_fire_screen_off_timer") + assert entry + assert entry.unique_id == "abcdef-123456-timeToScreenOffV2" + + state = hass.states.get("number.amazon_fire_screen_brightness") + assert state + assert state.state == "9" + entry = entity_registry.async_get("number.amazon_fire_screen_brightness") + assert entry + assert entry.unique_id == "abcdef-123456-screenBrightness" + + # Test invalid numeric data + mock_fully_kiosk.getSettings.return_value = {"screenBrightness": "invalid"} + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("number.amazon_fire_screen_brightness") + assert state + assert state.state == STATE_UNKNOWN + + # Test unknown/missing data + mock_fully_kiosk.getSettings.return_value = {} + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("number.amazon_fire_screensaver_timer") + assert state + assert state.state == STATE_UNKNOWN + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + +def set_value(hass, entity_id, value): + """Set the value of a number entity.""" + return hass.services.async_call( + number.DOMAIN, + "set_value", + {ATTR_ENTITY_ID: entity_id, number.ATTR_VALUE: value}, + blocking=True, + ) From 6e9c67c203aec123cd111e26c1588776cecb4e39 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Aug 2022 10:52:55 +0200 Subject: [PATCH 453/903] Update coverage to 6.4.4 (#76907) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 63634bc4022..94a1e0c3120 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.4.3 +coverage==6.4.4 freezegun==1.2.1 mock-open==1.4.0 mypy==0.971 From 681b726128efb223179a8aec44f09226d0e60bc2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 18 Aug 2022 11:40:24 +0200 Subject: [PATCH 454/903] Add parental control switches to NextDNS integration (#76559) * Add new switches * Make new switches disabled by default * Update tests --- homeassistant/components/nextdns/switch.py | 336 ++++++++++ tests/components/nextdns/test_switch.py | 673 +++++++++++++++++++++ 2 files changed, 1009 insertions(+) diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index 4bd3c14c20f..a353106723b 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -184,6 +184,342 @@ SWITCHES = ( icon="mdi:youtube", state=lambda data: data.youtube_restricted_mode, ), + NextDnsSwitchEntityDescription[Settings]( + key="block_9gag", + name="Block 9GAG", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:file-gif-box", + state=lambda data: data.block_9gag, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_amazon", + name="Block Amazon", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:cart-outline", + state=lambda data: data.block_amazon, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_blizzard", + name="Block Blizzard", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:sword-cross", + state=lambda data: data.block_blizzard, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_dailymotion", + name="Block Dailymotion", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:movie-search-outline", + state=lambda data: data.block_dailymotion, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_discord", + name="Block Discord", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:message-text", + state=lambda data: data.block_discord, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_disneyplus", + name="Block Disney Plus", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:movie-search-outline", + state=lambda data: data.block_disneyplus, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_ebay", + name="Block eBay", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:basket-outline", + state=lambda data: data.block_ebay, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_facebook", + name="Block Facebook", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:facebook", + state=lambda data: data.block_facebook, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_fortnite", + name="Block Fortnite", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:tank", + state=lambda data: data.block_fortnite, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_hulu", + name="Block Hulu", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:hulu", + state=lambda data: data.block_hulu, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_imgur", + name="Block Imgur", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:camera-image", + state=lambda data: data.block_imgur, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_instagram", + name="Block Instagram", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:instagram", + state=lambda data: data.block_instagram, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_leagueoflegends", + name="Block League of Legends", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:sword", + state=lambda data: data.block_leagueoflegends, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_messenger", + name="Block Messenger", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:message-text", + state=lambda data: data.block_messenger, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_minecraft", + name="Block Minecraft", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:minecraft", + state=lambda data: data.block_minecraft, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_netflix", + name="Block Netflix", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:netflix", + state=lambda data: data.block_netflix, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_pinterest", + name="Block Pinterest", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:pinterest", + state=lambda data: data.block_pinterest, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_primevideo", + name="Block Prime Video", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:filmstrip", + state=lambda data: data.block_primevideo, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_reddit", + name="Block Reddit", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:reddit", + state=lambda data: data.block_reddit, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_roblox", + name="Block Roblox", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:robot", + state=lambda data: data.block_roblox, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_signal", + name="Block Signal", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:chat-outline", + state=lambda data: data.block_signal, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_skype", + name="Block Skype", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:skype", + state=lambda data: data.block_skype, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_snapchat", + name="Block Snapchat", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:snapchat", + state=lambda data: data.block_snapchat, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_spotify", + name="Block Spotify", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:spotify", + state=lambda data: data.block_spotify, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_steam", + name="Block Steam", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:steam", + state=lambda data: data.block_steam, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_telegram", + name="Block Telegram", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:send-outline", + state=lambda data: data.block_telegram, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_tiktok", + name="Block TikTok", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:music-note", + state=lambda data: data.block_tiktok, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_tinder", + name="Block Tinder", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:fire", + state=lambda data: data.block_tinder, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_tumblr", + name="Block Tumblr", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:image-outline", + state=lambda data: data.block_tumblr, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_twitch", + name="Block Twitch", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:twitch", + state=lambda data: data.block_twitch, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_twitter", + name="Block Twitter", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:twitter", + state=lambda data: data.block_twitter, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_vimeo", + name="Block Vimeo", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:vimeo", + state=lambda data: data.block_vimeo, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_vk", + name="Block VK", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:power-socket-eu", + state=lambda data: data.block_vk, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_whatsapp", + name="Block WhatsApp", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:whatsapp", + state=lambda data: data.block_whatsapp, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_xboxlive", + name="Block Xbox Live", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:microsoft-xbox", + state=lambda data: data.block_xboxlive, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_youtube", + name="Block YouTube", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:youtube", + state=lambda data: data.block_youtube, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_zoom", + name="Block Zoom", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:video", + state=lambda data: data.block_zoom, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_dating", + name="Block dating", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:candelabra", + state=lambda data: data.block_dating, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_gambling", + name="Block gambling", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:slot-machine", + state=lambda data: data.block_gambling, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_piracy", + name="Block piracy", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:pirate", + state=lambda data: data.block_piracy, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_porn", + name="Block porn", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:movie-off", + state=lambda data: data.block_porn, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_social_networks", + name="Block social networks", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:facebook", + state=lambda data: data.block_social_networks, + ), ) diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index 3e07a2633d1..cc873fcc4b7 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -4,6 +4,7 @@ from unittest.mock import patch from nextdns import ApiError +from homeassistant.components.nextdns.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, @@ -26,6 +27,342 @@ async def test_switch(hass: HomeAssistant) -> None: """Test states of the switches.""" registry = er.async_get(hass) + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_9gag", + suggested_object_id="fake_profile_block_9gag", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_amazon", + suggested_object_id="fake_profile_block_amazon", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_blizzard", + suggested_object_id="fake_profile_block_blizzard", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_dailymotion", + suggested_object_id="fake_profile_block_dailymotion", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_discord", + suggested_object_id="fake_profile_block_discord", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_disneyplus", + suggested_object_id="fake_profile_block_disneyplus", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_ebay", + suggested_object_id="fake_profile_block_ebay", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_facebook", + suggested_object_id="fake_profile_block_facebook", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_fortnite", + suggested_object_id="fake_profile_block_fortnite", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_hulu", + suggested_object_id="fake_profile_block_hulu", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_imgur", + suggested_object_id="fake_profile_block_imgur", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_instagram", + suggested_object_id="fake_profile_block_instagram", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_leagueoflegends", + suggested_object_id="fake_profile_block_league_of_legends", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_messenger", + suggested_object_id="fake_profile_block_messenger", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_minecraft", + suggested_object_id="fake_profile_block_minecraft", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_netflix", + suggested_object_id="fake_profile_block_netflix", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_pinterest", + suggested_object_id="fake_profile_block_pinterest", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_primevideo", + suggested_object_id="fake_profile_block_primevideo", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_reddit", + suggested_object_id="fake_profile_block_reddit", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_roblox", + suggested_object_id="fake_profile_block_roblox", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_signal", + suggested_object_id="fake_profile_block_signal", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_skype", + suggested_object_id="fake_profile_block_skype", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_snapchat", + suggested_object_id="fake_profile_block_snapchat", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_spotify", + suggested_object_id="fake_profile_block_spotify", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_steam", + suggested_object_id="fake_profile_block_steam", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_telegram", + suggested_object_id="fake_profile_block_telegram", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_tiktok", + suggested_object_id="fake_profile_block_tiktok", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_tinder", + suggested_object_id="fake_profile_block_tinder", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_tumblr", + suggested_object_id="fake_profile_block_tumblr", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_twitch", + suggested_object_id="fake_profile_block_twitch", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_twitter", + suggested_object_id="fake_profile_block_twitter", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_vimeo", + suggested_object_id="fake_profile_block_vimeo", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_vk", + suggested_object_id="fake_profile_block_vk", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_whatsapp", + suggested_object_id="fake_profile_block_whatsapp", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_xboxlive", + suggested_object_id="fake_profile_block_xboxlive", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_youtube", + suggested_object_id="fake_profile_block_youtube", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_zoom", + suggested_object_id="fake_profile_block_zoom", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_dating", + suggested_object_id="fake_profile_block_dating", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_gambling", + suggested_object_id="fake_profile_block_gambling", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_piracy", + suggested_object_id="fake_profile_block_piracy", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_porn", + suggested_object_id="fake_profile_block_porn", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_social_networks", + suggested_object_id="fake_profile_block_social_networks", + disabled_by=None, + ) + await init_integration(hass) state = hass.states.get("switch.fake_profile_ai_driven_threat_detection") @@ -218,6 +555,342 @@ async def test_switch(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "xyz12_web3" + state = hass.states.get("switch.fake_profile_block_9gag") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_9gag") + assert entry + assert entry.unique_id == "xyz12_block_9gag" + + state = hass.states.get("switch.fake_profile_block_amazon") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_amazon") + assert entry + assert entry.unique_id == "xyz12_block_amazon" + + state = hass.states.get("switch.fake_profile_block_blizzard") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_blizzard") + assert entry + assert entry.unique_id == "xyz12_block_blizzard" + + state = hass.states.get("switch.fake_profile_block_dailymotion") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_dailymotion") + assert entry + assert entry.unique_id == "xyz12_block_dailymotion" + + state = hass.states.get("switch.fake_profile_block_discord") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_discord") + assert entry + assert entry.unique_id == "xyz12_block_discord" + + state = hass.states.get("switch.fake_profile_block_disneyplus") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_disneyplus") + assert entry + assert entry.unique_id == "xyz12_block_disneyplus" + + state = hass.states.get("switch.fake_profile_block_ebay") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_ebay") + assert entry + assert entry.unique_id == "xyz12_block_ebay" + + state = hass.states.get("switch.fake_profile_block_facebook") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_facebook") + assert entry + assert entry.unique_id == "xyz12_block_facebook" + + state = hass.states.get("switch.fake_profile_block_fortnite") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_fortnite") + assert entry + assert entry.unique_id == "xyz12_block_fortnite" + + state = hass.states.get("switch.fake_profile_block_hulu") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_hulu") + assert entry + assert entry.unique_id == "xyz12_block_hulu" + + state = hass.states.get("switch.fake_profile_block_imgur") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_imgur") + assert entry + assert entry.unique_id == "xyz12_block_imgur" + + state = hass.states.get("switch.fake_profile_block_instagram") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_instagram") + assert entry + assert entry.unique_id == "xyz12_block_instagram" + + state = hass.states.get("switch.fake_profile_block_league_of_legends") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_league_of_legends") + assert entry + assert entry.unique_id == "xyz12_block_leagueoflegends" + + state = hass.states.get("switch.fake_profile_block_messenger") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_messenger") + assert entry + assert entry.unique_id == "xyz12_block_messenger" + + state = hass.states.get("switch.fake_profile_block_minecraft") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_minecraft") + assert entry + assert entry.unique_id == "xyz12_block_minecraft" + + state = hass.states.get("switch.fake_profile_block_netflix") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_netflix") + assert entry + assert entry.unique_id == "xyz12_block_netflix" + + state = hass.states.get("switch.fake_profile_block_pinterest") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_pinterest") + assert entry + assert entry.unique_id == "xyz12_block_pinterest" + + state = hass.states.get("switch.fake_profile_block_primevideo") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_primevideo") + assert entry + assert entry.unique_id == "xyz12_block_primevideo" + + state = hass.states.get("switch.fake_profile_block_reddit") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_reddit") + assert entry + assert entry.unique_id == "xyz12_block_reddit" + + state = hass.states.get("switch.fake_profile_block_roblox") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_roblox") + assert entry + assert entry.unique_id == "xyz12_block_roblox" + + state = hass.states.get("switch.fake_profile_block_signal") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_signal") + assert entry + assert entry.unique_id == "xyz12_block_signal" + + state = hass.states.get("switch.fake_profile_block_skype") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_skype") + assert entry + assert entry.unique_id == "xyz12_block_skype" + + state = hass.states.get("switch.fake_profile_block_snapchat") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_snapchat") + assert entry + assert entry.unique_id == "xyz12_block_snapchat" + + state = hass.states.get("switch.fake_profile_block_spotify") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_spotify") + assert entry + assert entry.unique_id == "xyz12_block_spotify" + + state = hass.states.get("switch.fake_profile_block_steam") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_steam") + assert entry + assert entry.unique_id == "xyz12_block_steam" + + state = hass.states.get("switch.fake_profile_block_telegram") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_telegram") + assert entry + assert entry.unique_id == "xyz12_block_telegram" + + state = hass.states.get("switch.fake_profile_block_tiktok") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_tiktok") + assert entry + assert entry.unique_id == "xyz12_block_tiktok" + + state = hass.states.get("switch.fake_profile_block_tinder") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_tinder") + assert entry + assert entry.unique_id == "xyz12_block_tinder" + + state = hass.states.get("switch.fake_profile_block_tumblr") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_tumblr") + assert entry + assert entry.unique_id == "xyz12_block_tumblr" + + state = hass.states.get("switch.fake_profile_block_twitch") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_twitch") + assert entry + assert entry.unique_id == "xyz12_block_twitch" + + state = hass.states.get("switch.fake_profile_block_twitter") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_twitter") + assert entry + assert entry.unique_id == "xyz12_block_twitter" + + state = hass.states.get("switch.fake_profile_block_vimeo") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_vimeo") + assert entry + assert entry.unique_id == "xyz12_block_vimeo" + + state = hass.states.get("switch.fake_profile_block_vk") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_vk") + assert entry + assert entry.unique_id == "xyz12_block_vk" + + state = hass.states.get("switch.fake_profile_block_whatsapp") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_whatsapp") + assert entry + assert entry.unique_id == "xyz12_block_whatsapp" + + state = hass.states.get("switch.fake_profile_block_xboxlive") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_xboxlive") + assert entry + assert entry.unique_id == "xyz12_block_xboxlive" + + state = hass.states.get("switch.fake_profile_block_youtube") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_youtube") + assert entry + assert entry.unique_id == "xyz12_block_youtube" + + state = hass.states.get("switch.fake_profile_block_zoom") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_zoom") + assert entry + assert entry.unique_id == "xyz12_block_zoom" + + state = hass.states.get("switch.fake_profile_block_dating") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_dating") + assert entry + assert entry.unique_id == "xyz12_block_dating" + + state = hass.states.get("switch.fake_profile_block_gambling") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_gambling") + assert entry + assert entry.unique_id == "xyz12_block_gambling" + + state = hass.states.get("switch.fake_profile_block_piracy") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_piracy") + assert entry + assert entry.unique_id == "xyz12_block_piracy" + + state = hass.states.get("switch.fake_profile_block_porn") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_porn") + assert entry + assert entry.unique_id == "xyz12_block_porn" + + state = hass.states.get("switch.fake_profile_block_social_networks") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_social_networks") + assert entry + assert entry.unique_id == "xyz12_block_social_networks" + async def test_switch_on(hass: HomeAssistant) -> None: """Test the switch can be turned on.""" From a79f578b1da5f499c3dca81d352f836176665ab5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 12:45:24 +0200 Subject: [PATCH 455/903] Add issue_domain parameter to repairs.create_issue (#76972) --- homeassistant/components/repairs/issue_handler.py | 6 ++++-- homeassistant/components/repairs/issue_registry.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index b37d4c10e06..eecbfe59bde 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -119,11 +119,11 @@ def async_create_issue( domain: str, issue_id: str, *, - issue_domain: str | None = None, breaks_in_ha_version: str | None = None, data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool = False, + issue_domain: str | None = None, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -142,11 +142,11 @@ def async_create_issue( issue_registry.async_get_or_create( domain, issue_id, - issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, data=data, is_fixable=is_fixable, is_persistent=is_persistent, + issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -163,6 +163,7 @@ def create_issue( data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool = False, + issue_domain: str | None = None, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -180,6 +181,7 @@ def create_issue( data=data, is_fixable=is_fixable, is_persistent=is_persistent, + issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index f9a15e0f165..a8843011023 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -106,11 +106,11 @@ class IssueRegistry: domain: str, issue_id: str, *, - issue_domain: str | None = None, breaks_in_ha_version: str | None = None, data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool, + issue_domain: str | None = None, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, From 07ba3c1383df5c6a57ef7e4486f36875232e7256 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 13:27:05 +0200 Subject: [PATCH 456/903] Add update checks to pylint plugin (#76912) --- pylint/plugins/hass_enforce_type_hints.py | 63 +++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 99d06c380fd..1849d38fbc3 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2041,6 +2041,69 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "update": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="UpdateEntity", + matches=[ + TypeHintMatch( + function_name="auto_update", + return_type="bool", + ), + TypeHintMatch( + function_name="installed_version", + return_type=["str", None], + ), + TypeHintMatch( + function_name="device_class", + return_type=["UpdateDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="in_progress", + return_type=["bool", "int", None], + ), + TypeHintMatch( + function_name="latest_version", + return_type=["str", None], + ), + TypeHintMatch( + function_name="release_summary", + return_type=["str", None], + ), + TypeHintMatch( + function_name="release_url", + return_type=["str", None], + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="title", + return_type=["str", None], + ), + TypeHintMatch( + function_name="install", + arg_types={1: "str | None", 2: "bool"}, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="release_notes", + return_type=["str", None], + has_async_counterpart=True, + ), + ], + ), + ], "water_heater": [ ClassTypeHintMatch( base_class="Entity", From 1aef60c81cfdc73231f9c6ddfed33e62377da6eb Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 18 Aug 2022 07:33:38 -0400 Subject: [PATCH 457/903] Add screen on/off switch to Fully Kiosk Browser integration (#76957) --- homeassistant/components/fully_kiosk/switch.py | 7 +++++++ tests/components/fully_kiosk/test_switch.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/fully_kiosk/switch.py b/homeassistant/components/fully_kiosk/switch.py index 7dfcc1e71ac..581700c87d6 100644 --- a/homeassistant/components/fully_kiosk/switch.py +++ b/homeassistant/components/fully_kiosk/switch.py @@ -66,6 +66,13 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = ( off_action=lambda fully: fully.disableMotionDetection(), is_on_fn=lambda data: data["settings"].get("motionDetection"), ), + FullySwitchEntityDescription( + key="screenOn", + name="Screen", + on_action=lambda fully: fully.screenOn(), + off_action=lambda fully: fully.screenOff(), + is_on_fn=lambda data: data.get("screenOn"), + ), ) diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py index 6b6d2829790..8da01ff2fe9 100644 --- a/tests/components/fully_kiosk/test_switch.py +++ b/tests/components/fully_kiosk/test_switch.py @@ -61,6 +61,17 @@ async def test_switches( await call_service(hass, "turn_off", "switch.amazon_fire_motion_detection") assert len(mock_fully_kiosk.disableMotionDetection.mock_calls) == 1 + entity = hass.states.get("switch.amazon_fire_screen") + assert entity + assert entity.state == "on" + entry = entity_registry.async_get("switch.amazon_fire_screen") + assert entry + assert entry.unique_id == "abcdef-123456-screenOn" + await call_service(hass, "turn_off", "switch.amazon_fire_screen") + assert len(mock_fully_kiosk.screenOff.mock_calls) == 1 + await call_service(hass, "turn_on", "switch.amazon_fire_screen") + assert len(mock_fully_kiosk.screenOn.mock_calls) == 1 + assert entry.device_id device_entry = device_registry.async_get(entry.device_id) assert device_entry From c212fe7ca51bd87ea66734ff2433a40f1c470c7d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Aug 2022 14:06:50 +0200 Subject: [PATCH 458/903] Adjust version comparison in HA Cloud account linking (#76978) --- homeassistant/components/cloud/account_link.py | 5 ++++- tests/components/cloud/test_account_link.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 19819307cf0..3cf021c24fe 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -18,6 +18,9 @@ CACHE_TIMEOUT = 3600 _LOGGER = logging.getLogger(__name__) CURRENT_VERSION = AwesomeVersion(HA_VERSION) +CURRENT_PLAIN_VERSION = AwesomeVersion( + CURRENT_VERSION.string.removesuffix(f"{CURRENT_VERSION.modifier}") +) @callback @@ -35,7 +38,7 @@ async def async_provide_implementation(hass: HomeAssistant, domain: str): for service in services: if ( service["service"] == domain - and CURRENT_VERSION >= service["min_version"] + and CURRENT_PLAIN_VERSION >= service["min_version"] and ( service.get("accepts_new_authorizations", True) or ( diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index ad914436c07..6928e2b7a11 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -57,6 +57,7 @@ async def test_setup_provide_implementation(hass): return_value=[ {"service": "test", "min_version": "0.1.0"}, {"service": "too_new", "min_version": "1000000.0.0"}, + {"service": "dev", "min_version": "2022.9.0"}, { "service": "deprecated", "min_version": "0.1.0", @@ -73,6 +74,8 @@ async def test_setup_provide_implementation(hass): "accepts_new_authorizations": False, }, ], + ), patch( + "homeassistant.components.cloud.account_link.HA_VERSION", "2022.9.0.dev20220817" ): assert ( await config_entry_oauth2_flow.async_get_implementations( @@ -101,6 +104,10 @@ async def test_setup_provide_implementation(hass): await config_entry_oauth2_flow.async_get_implementations(hass, "legacy") ) + dev_implementations = await config_entry_oauth2_flow.async_get_implementations( + hass, "dev" + ) + assert "cloud" in implementations assert implementations["cloud"].domain == "cloud" assert implementations["cloud"].service == "test" @@ -111,6 +118,11 @@ async def test_setup_provide_implementation(hass): assert legacy_implementations["cloud"].service == "legacy" assert legacy_implementations["cloud"].hass is hass + assert "cloud" in dev_implementations + assert dev_implementations["cloud"].domain == "cloud" + assert dev_implementations["cloud"].service == "dev" + assert dev_implementations["cloud"].hass is hass + async def test_get_services_cached(hass): """Test that we cache services.""" From f0deaa33a0eaff647262924afa4fb292b9d5ba98 Mon Sep 17 00:00:00 2001 From: Yasser Saleemi Date: Thu, 18 Aug 2022 13:07:58 +0100 Subject: [PATCH 459/903] Include moonsighting calc for islamic_prayer_times (#75595) --- homeassistant/components/islamic_prayer_times/const.py | 2 +- homeassistant/components/islamic_prayer_times/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py index ee7512c2d7a..86f953cc856 100644 --- a/homeassistant/components/islamic_prayer_times/const.py +++ b/homeassistant/components/islamic_prayer_times/const.py @@ -15,7 +15,7 @@ SENSOR_TYPES = { CONF_CALC_METHOD = "calculation_method" -CALC_METHODS = ["isna", "karachi", "mwl", "makkah"] +CALC_METHODS = ["isna", "karachi", "mwl", "makkah", "moonsighting"] DEFAULT_CALC_METHOD = "isna" DATA_UPDATED = "Islamic_prayer_data_updated" diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 455f3bab675..a065ca17ab4 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -2,7 +2,7 @@ "domain": "islamic_prayer_times", "name": "Islamic Prayer Times", "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", - "requirements": ["prayer_times_calculator==0.0.5"], + "requirements": ["prayer_times_calculator==0.0.6"], "codeowners": ["@engrbm87"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 785428e4c9e..96dfac44800 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1283,7 +1283,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer_times_calculator==0.0.5 +prayer_times_calculator==0.0.6 # homeassistant.components.progettihwsw progettihwsw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b499659dc44..2db72263205 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -904,7 +904,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer_times_calculator==0.0.5 +prayer_times_calculator==0.0.6 # homeassistant.components.progettihwsw progettihwsw==0.1.1 From 60c8d95a77959168c2c217a633479ccf5066cbcf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 14:21:05 +0200 Subject: [PATCH 460/903] Remove white_value support from light (#76926) --- homeassistant/components/fibaro/__init__.py | 2 +- homeassistant/components/flux/switch.py | 2 - homeassistant/components/light/__init__.py | 51 ------- .../components/light/reproduce_state.py | 4 - homeassistant/components/light/services.yaml | 8 -- .../components/light/significant_change.py | 16 +-- .../components/mqtt/light/schema_json.py | 3 +- homeassistant/const.py | 1 - pylint/plugins/hass_enforce_type_hints.py | 5 - tests/components/group/test_light.py | 2 - tests/components/light/common.py | 9 -- tests/components/light/test_init.py | 126 ++---------------- .../components/light/test_reproduce_state.py | 16 +-- .../light/test_significant_change.py | 9 -- tests/components/mqtt/test_discovery.py | 2 + tests/components/switch/test_light.py | 1 - tests/components/switch_as_x/test_light.py | 2 - tests/components/tasmota/test_light.py | 36 +---- tests/components/template/test_light.py | 13 -- .../custom_components/test/light.py | 2 - 20 files changed, 19 insertions(+), 291 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 9431cd162bc..ece4d38d726 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -24,7 +24,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_URL, CONF_USERNAME, - CONF_WHITE_VALUE, Platform, ) from homeassistant.core import HomeAssistant @@ -45,6 +44,7 @@ CONF_DIMMING = "dimming" CONF_GATEWAYS = "gateways" CONF_PLUGINS = "plugins" CONF_RESET_COLOR = "reset_color" +CONF_WHITE_VALUE = "white_value" FIBARO_CONTROLLER = "fibaro_controller" FIBARO_DEVICES = "fibaro_devices" PLATFORMS = [ diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 4b9f5ad5285..2d175702047 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -15,7 +15,6 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION, @@ -101,7 +100,6 @@ async def async_set_lights_xy(hass, lights, x_val, y_val, brightness, transition service_data[ATTR_XY_COLOR] = [x_val, y_val] if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness - service_data[ATTR_WHITE_VALUE] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition await hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 099f917bc46..33ec3119b95 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -59,7 +59,6 @@ SUPPORT_EFFECT = 4 SUPPORT_FLASH = 8 SUPPORT_COLOR = 16 # Deprecated, replaced by color modes SUPPORT_TRANSITION = 32 -SUPPORT_WHITE_VALUE = 128 # Deprecated, replaced by color modes # Color mode of the light ATTR_COLOR_MODE = "color_mode" @@ -202,7 +201,6 @@ ATTR_KELVIN = "kelvin" ATTR_MIN_MIREDS = "min_mireds" ATTR_MAX_MIREDS = "max_mireds" ATTR_COLOR_NAME = "color_name" -ATTR_WHITE_VALUE = "white_value" ATTR_WHITE = "white" # Brightness of the light, 0..255 or percentage @@ -274,7 +272,6 @@ LIGHT_TURN_ON_SCHEMA = { vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) ), vol.Exclusive(ATTR_WHITE, COLOR_GROUP): VALID_BRIGHTNESS, - ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), ATTR_FLASH: VALID_FLASH, ATTR_EFFECT: cv.string, } @@ -341,8 +338,6 @@ def filter_turn_on_params(light, params): params.pop(ATTR_FLASH, None) if not supported_features & LightEntityFeature.TRANSITION: params.pop(ATTR_TRANSITION, None) - if not supported_features & SUPPORT_WHITE_VALUE: - params.pop(ATTR_WHITE_VALUE, None) supported_color_modes = ( light._light_internal_supported_color_modes # pylint:disable=protected-access @@ -421,16 +416,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: light._light_internal_supported_color_modes # pylint: disable=protected-access ) supported_color_modes = light.supported_color_modes - # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W - # for legacy lights - if ATTR_RGBW_COLOR in params: - if ( - ColorMode.RGBW in legacy_supported_color_modes - and not supported_color_modes - ): - rgbw_color = params.pop(ATTR_RGBW_COLOR) - params[ATTR_RGB_COLOR] = rgbw_color[0:3] - params[ATTR_WHITE_VALUE] = rgbw_color[3] # If a color temperature is specified, emulate it if not supported by the light if ATTR_COLOR_TEMP in params: @@ -544,9 +529,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_WHITE] = params.pop(ATTR_BRIGHTNESS, params[ATTR_WHITE]) # Remove deprecated white value if the light supports color mode - if supported_color_modes: - params.pop(ATTR_WHITE_VALUE, None) - if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0: await async_handle_light_off_service(light, call) else: @@ -786,12 +768,6 @@ class LightEntity(ToggleEntity): # Add warning in 2021.6, remove in 2021.10 supported = self._light_internal_supported_color_modes - if ( - ColorMode.RGBW in supported - and self.white_value is not None - and self.hs_color is not None - ): - return ColorMode.RGBW if ColorMode.HS in supported and self.hs_color is not None: return ColorMode.HS if ColorMode.COLOR_TEMP in supported and self.color_temp is not None: @@ -828,19 +804,6 @@ class LightEntity(ToggleEntity): def _light_internal_rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the rgbw color value [int, int, int, int].""" rgbw_color = self.rgbw_color - if ( - rgbw_color is None - and self.hs_color is not None - and self.white_value is not None - ): - # Backwards compatibility for rgbw_color added in 2021.4 - # Add warning in 2021.6, remove in 2021.10 - r, g, b = color_util.color_hs_to_RGB( # pylint: disable=invalid-name - *self.hs_color - ) - w = self.white_value # pylint: disable=invalid-name - rgbw_color = (r, g, b, w) - return rgbw_color @property @@ -867,11 +830,6 @@ class LightEntity(ToggleEntity): # https://developers.meethue.com/documentation/core-concepts return self._attr_max_mireds - @property - def white_value(self) -> int | None: - """Return the white value of this light between 0..255.""" - return None - @property def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" @@ -982,13 +940,6 @@ class LightEntity(ToggleEntity): # Add warning in 2021.6, remove in 2021.10 data[ATTR_COLOR_TEMP] = self.color_temp - if supported_features & SUPPORT_WHITE_VALUE and not self.supported_color_modes: - # Backwards compatibility - # Add warning in 2021.6, remove in 2021.10 - data[ATTR_WHITE_VALUE] = self.white_value - if self.hs_color is not None: - data.update(self._light_internal_convert_color(ColorMode.HS)) - if supported_features & LightEntityFeature.EFFECT: data[ATTR_EFFECT] = self.effect @@ -1009,8 +960,6 @@ class LightEntity(ToggleEntity): supported_color_modes.add(ColorMode.COLOR_TEMP) if supported_features & SUPPORT_COLOR: supported_color_modes.add(ColorMode.HS) - if supported_features & SUPPORT_WHITE_VALUE: - supported_color_modes.add(ColorMode.RGBW) if supported_features & SUPPORT_BRIGHTNESS and not supported_color_modes: supported_color_modes = {ColorMode.BRIGHTNESS} diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 5e60c616500..46adcc1fa2e 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -31,7 +31,6 @@ from . import ( ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN, ColorMode, @@ -46,7 +45,6 @@ ATTR_GROUP = [ ATTR_BRIGHTNESS_PCT, ATTR_EFFECT, ATTR_FLASH, - ATTR_WHITE_VALUE, ATTR_TRANSITION, ] @@ -157,8 +155,6 @@ async def _async_reproduce_state( state.attributes.get(ATTR_COLOR_MODE, ColorMode.UNKNOWN) != ColorMode.UNKNOWN ): - # Remove deprecated white value if we got a valid color mode - service_data.pop(ATTR_WHITE_VALUE, None) color_mode = state.attributes[ATTR_COLOR_MODE] if color_mode_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode): if color_mode_attr.state_attr not in state.attributes: diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 06349c72035..b7843a2f0ec 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -531,14 +531,6 @@ toggle: max: 6500 step: 100 unit_of_measurement: K - white_value: - name: White level - description: Number indicating level of white. - advanced: true - selector: - number: - min: 0 - max: 255 brightness: name: Brightness value description: Number indicating brightness, where 0 turns the light diff --git a/homeassistant/components/light/significant_change.py b/homeassistant/components/light/significant_change.py index 79f447f5794..dc8f711b579 100644 --- a/homeassistant/components/light/significant_change.py +++ b/homeassistant/components/light/significant_change.py @@ -6,13 +6,7 @@ from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.significant_change import check_absolute_change -from . import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_EFFECT, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, -) +from . import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR @callback @@ -56,12 +50,4 @@ def async_check_significant_change( ): return True - if check_absolute_change( - # Range 0..255 - old_attrs.get(ATTR_WHITE_VALUE), - new_attrs.get(ATTR_WHITE_VALUE), - 5, - ): - return True - return False diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 910d48f750d..85a0bc335cd 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -38,7 +38,6 @@ from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, - CONF_WHITE_VALUE, CONF_XY, STATE_ON, ) @@ -97,6 +96,8 @@ CONF_FLASH_TIME_SHORT = "flash_time_short" CONF_MAX_MIREDS = "max_mireds" CONF_MIN_MIREDS = "min_mireds" +CONF_WHITE_VALUE = "white_value" + def valid_color_configuration(config): """Test color_mode is not combined with deprecated config.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index 7542ce0d77e..750b014e0da 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -262,7 +262,6 @@ CONF_WHILE: Final = "while" CONF_WHITELIST: Final = "whitelist" CONF_ALLOWLIST_EXTERNAL_DIRS: Final = "allowlist_external_dirs" LEGACY_CONF_WHITELIST_EXTERNAL_DIRS: Final = "whitelist_external_dirs" -CONF_WHITE_VALUE: Final = "white_value" CONF_XY: Final = "xy" CONF_ZONE: Final = "zone" diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 1849d38fbc3..0791ebcd9b2 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1378,10 +1378,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="max_mireds", return_type="int", ), - TypeHintMatch( - function_name="white_value", - return_type=["int", None], - ), TypeHintMatch( function_name="effect_list", return_type=["list[str]", None], @@ -1421,7 +1417,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { "transition": "float | None", "xy_color": "tuple[float, float] | None", "white": "int | None", - "white_value": "int | None", }, kwargs_type="Any", return_type=None, diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 5329d3074b4..be50f29fe5c 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -25,7 +25,6 @@ from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, SERVICE_TOGGLE, @@ -78,7 +77,6 @@ async def test_default_state(hass): assert state.attributes.get(ATTR_BRIGHTNESS) is None assert state.attributes.get(ATTR_HS_COLOR) is None assert state.attributes.get(ATTR_COLOR_TEMP) is None - assert state.attributes.get(ATTR_WHITE_VALUE) is None assert state.attributes.get(ATTR_EFFECT_LIST) is None assert state.attributes.get(ATTR_EFFECT) is None diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 0c16e0f2703..4f83ffacbdc 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -18,7 +18,6 @@ from homeassistant.components.light import ( ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN, ) @@ -46,7 +45,6 @@ def turn_on( hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, @@ -68,7 +66,6 @@ def turn_on( hs_color, color_temp, kelvin, - white_value, profile, flash, effect, @@ -90,7 +87,6 @@ async def async_turn_on( hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, @@ -113,7 +109,6 @@ async def async_turn_on( (ATTR_HS_COLOR, hs_color), (ATTR_COLOR_TEMP, color_temp), (ATTR_KELVIN, kelvin), - (ATTR_WHITE_VALUE, white_value), (ATTR_FLASH, flash), (ATTR_EFFECT, effect), (ATTR_COLOR_NAME, color_name), @@ -158,7 +153,6 @@ def toggle( hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, @@ -177,7 +171,6 @@ def toggle( hs_color, color_temp, kelvin, - white_value, profile, flash, effect, @@ -196,7 +189,6 @@ async def async_toggle( hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, @@ -216,7 +208,6 @@ async def async_toggle( (ATTR_HS_COLOR, hs_color), (ATTR_COLOR_TEMP, color_temp), (ATTR_KELVIN, kelvin), - (ATTR_WHITE_VALUE, white_value), (ATTR_FLASH, flash), (ATTR_EFFECT, effect), (ATTR_COLOR_NAME, color_name), diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index ae9b00baeaa..1f21981340f 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -48,7 +48,6 @@ async def test_methods(hass): light.ATTR_XY_COLOR: "xy_color_val", light.ATTR_PROFILE: "profile_val", light.ATTR_COLOR_NAME: "color_name_val", - light.ATTR_WHITE_VALUE: "white_val", }, blocking=True, ) @@ -65,7 +64,6 @@ async def test_methods(hass): assert call.data.get(light.ATTR_XY_COLOR) == "xy_color_val" assert call.data.get(light.ATTR_PROFILE) == "profile_val" assert call.data.get(light.ATTR_COLOR_NAME) == "color_name_val" - assert call.data.get(light.ATTR_WHITE_VALUE) == "white_val" # Test turn_off turn_off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF) @@ -125,7 +123,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): light.SUPPORT_COLOR | light.LightEntityFeature.EFFECT | light.LightEntityFeature.TRANSITION - | light.SUPPORT_WHITE_VALUE ) ent3.supported_features = ( light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION @@ -220,7 +217,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_EFFECT: "fun_effect", light.ATTR_RGB_COLOR: (255, 255, 255), - light.ATTR_WHITE_VALUE: 255, }, blocking=True, ) @@ -246,7 +242,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): assert data == { light.ATTR_EFFECT: "fun_effect", light.ATTR_HS_COLOR: (0, 0), - light.ATTR_WHITE_VALUE: 255, } _, data = ent3.last_call("turn_on") @@ -271,7 +266,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_BRIGHTNESS: 0, light.ATTR_RGB_COLOR: (255, 255, 255), - light.ATTR_WHITE_VALUE: 0, }, blocking=True, ) @@ -427,13 +421,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): }, blocking=True, ) - with pytest.raises(vol.MultipleInvalid): - await hass.services.async_call( - light.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_WHITE_VALUE: "high"}, - blocking=True, - ) _, data = ent1.last_call("turn_on") assert data == {} @@ -1104,8 +1091,6 @@ async def test_light_backwards_compatibility_supported_color_modes( platform.ENTITIES.append(platform.MockLight("Test_2", light_state)) platform.ENTITIES.append(platform.MockLight("Test_3", light_state)) platform.ENTITIES.append(platform.MockLight("Test_4", light_state)) - platform.ENTITIES.append(platform.MockLight("Test_5", light_state)) - platform.ENTITIES.append(platform.MockLight("Test_6", light_state)) entity0 = platform.ENTITIES[0] @@ -1120,22 +1105,9 @@ async def test_light_backwards_compatibility_supported_color_modes( entity4 = platform.ENTITIES[4] entity4.supported_features = ( - light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE - ) - - entity5 = platform.ENTITIES[5] - entity5.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) - entity6 = platform.ENTITIES[6] - entity6.supported_features = ( - light.SUPPORT_BRIGHTNESS - | light.SUPPORT_COLOR - | light.SUPPORT_COLOR_TEMP - | light.SUPPORT_WHITE_VALUE - ) - assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1168,16 +1140,6 @@ async def test_light_backwards_compatibility_supported_color_modes( assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN state = hass.states.get(entity4.entity_id) - assert state.attributes["supported_color_modes"] == [ - light.ColorMode.HS, - light.ColorMode.RGBW, - ] - if light_state == STATE_OFF: - assert "color_mode" not in state.attributes - else: - assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN - - state = hass.states.get(entity5.entity_id) assert state.attributes["supported_color_modes"] == [ light.ColorMode.COLOR_TEMP, light.ColorMode.HS, @@ -1187,17 +1149,6 @@ async def test_light_backwards_compatibility_supported_color_modes( else: assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN - state = hass.states.get(entity6.entity_id) - assert state.attributes["supported_color_modes"] == [ - light.ColorMode.COLOR_TEMP, - light.ColorMode.HS, - light.ColorMode.RGBW, - ] - if light_state == STATE_OFF: - assert "color_mode" not in state.attributes - else: - assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN - async def test_light_backwards_compatibility_color_mode( hass, enable_custom_integrations @@ -1211,8 +1162,6 @@ async def test_light_backwards_compatibility_color_mode( platform.ENTITIES.append(platform.MockLight("Test_2", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_3", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_4", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_5", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_6", STATE_ON)) entity0 = platform.ENTITIES[0] @@ -1230,17 +1179,10 @@ async def test_light_backwards_compatibility_color_mode( entity4 = platform.ENTITIES[4] entity4.supported_features = ( - light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE - ) - entity4.hs_color = (240, 100) - entity4.white_value = 100 - - entity5 = platform.ENTITIES[5] - entity5.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) - entity5.hs_color = (240, 100) - entity5.color_temp = 100 + entity4.hs_color = (240, 100) + entity4.color_temp = 100 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1265,13 +1207,6 @@ async def test_light_backwards_compatibility_color_mode( assert state.attributes["color_mode"] == light.ColorMode.HS state = hass.states.get(entity4.entity_id) - assert state.attributes["supported_color_modes"] == [ - light.ColorMode.HS, - light.ColorMode.RGBW, - ] - assert state.attributes["color_mode"] == light.ColorMode.RGBW - - state = hass.states.get(entity5.entity_id) assert state.attributes["supported_color_modes"] == [ light.ColorMode.COLOR_TEMP, light.ColorMode.HS, @@ -1281,38 +1216,26 @@ async def test_light_backwards_compatibility_color_mode( async def test_light_service_call_rgbw(hass, enable_custom_integrations): - """Test backwards compatibility for rgbw functionality in service calls.""" + """Test rgbw functionality in service calls.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) - platform.ENTITIES.append(platform.MockLight("Test_legacy_white_value", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) entity0 = platform.ENTITIES[0] - entity0.supported_features = ( - light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE - ) - - entity1 = platform.ENTITIES[1] - entity1.supported_color_modes = {light.ColorMode.RGBW} + entity0.supported_color_modes = {light.ColorMode.RGBW} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.attributes["supported_color_modes"] == [ - light.ColorMode.HS, - light.ColorMode.RGBW, - ] - - state = hass.states.get(entity1.entity_id) assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBW] await hass.services.async_call( "light", "turn_on", { - "entity_id": [entity0.entity_id, entity1.entity_id], + "entity_id": [entity0.entity_id, entity0.entity_id], "brightness_pct": 100, "rgbw_color": (10, 20, 30, 40), }, @@ -1320,8 +1243,6 @@ async def test_light_service_call_rgbw(hass, enable_custom_integrations): ) _, data = entity0.last_call("turn_on") - assert data == {"brightness": 255, "hs_color": (210.0, 66.667), "white_value": 40} - _, data = entity1.last_call("turn_on") assert data == {"brightness": 255, "rgbw_color": (10, 20, 30, 40)} @@ -1330,47 +1251,21 @@ async def test_light_state_rgbw(hass, enable_custom_integrations): platform = getattr(hass.components, "test.light") platform.init(empty=True) - platform.ENTITIES.append(platform.MockLight("Test_legacy_white_value", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) entity0 = platform.ENTITIES[0] - legacy_supported_features = ( - light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE - ) - entity0.supported_features = legacy_supported_features - entity0.hs_color = (210.0, 66.667) + entity0.supported_color_modes = {light.ColorMode.RGBW} + entity0.color_mode = light.ColorMode.RGBW + entity0.hs_color = "Invalid" # Should be ignored entity0.rgb_color = "Invalid" # Should be ignored + entity0.rgbw_color = (1, 2, 3, 4) entity0.rgbww_color = "Invalid" # Should be ignored - entity0.white_value = 40 entity0.xy_color = "Invalid" # Should be ignored - entity1 = platform.ENTITIES[1] - entity1.supported_color_modes = {light.ColorMode.RGBW} - entity1.color_mode = light.ColorMode.RGBW - entity1.hs_color = "Invalid" # Should be ignored - entity1.rgb_color = "Invalid" # Should be ignored - entity1.rgbw_color = (1, 2, 3, 4) - entity1.rgbww_color = "Invalid" # Should be ignored - entity1.white_value = "Invalid" # Should be ignored - entity1.xy_color = "Invalid" # Should be ignored - assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.attributes == { - "color_mode": light.ColorMode.RGBW, - "friendly_name": "Test_legacy_white_value", - "supported_color_modes": [light.ColorMode.HS, light.ColorMode.RGBW], - "supported_features": legacy_supported_features, - "hs_color": (210.0, 66.667), - "rgb_color": (84, 169, 255), - "rgbw_color": (84, 169, 255, 40), - "white_value": 40, - "xy_color": (0.173, 0.207), - } - - state = hass.states.get(entity1.entity_id) assert state.attributes == { "color_mode": light.ColorMode.RGBW, "friendly_name": "Test_rgbw", @@ -1397,7 +1292,6 @@ async def test_light_state_rgbww(hass, enable_custom_integrations): entity0.rgb_color = "Invalid" # Should be ignored entity0.rgbw_color = "Invalid" # Should be ignored entity0.rgbww_color = (1, 2, 3, 4, 5) - entity0.white_value = "Invalid" # Should be ignored entity0.xy_color = "Invalid" # Should be ignored assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) @@ -2264,7 +2158,6 @@ async def test_services_filter_parameters( light.ATTR_EFFECT: "fun_effect", light.ATTR_FLASH: "short", light.ATTR_TRANSITION: 10, - light.ATTR_WHITE_VALUE: 0, }, blocking=True, ) @@ -2353,7 +2246,6 @@ async def test_services_filter_parameters( light.ATTR_EFFECT: "fun_effect", light.ATTR_FLASH: "short", light.ATTR_TRANSITION: 10, - light.ATTR_WHITE_VALUE: 0, }, blocking=True, ) diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 6f35d083d56..1f69e94658d 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -9,7 +9,6 @@ from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service VALID_BRIGHTNESS = {"brightness": 180} -VALID_WHITE_VALUE = {"white_value": 200} VALID_FLASH = {"flash": "short"} VALID_EFFECT = {"effect": "random"} VALID_TRANSITION = {"transition": 15} @@ -28,7 +27,6 @@ async def test_reproducing_states(hass, caplog): """Test reproducing Light states.""" hass.states.async_set("light.entity_off", "off", {}) hass.states.async_set("light.entity_bright", "on", VALID_BRIGHTNESS) - hass.states.async_set("light.entity_white", "on", VALID_WHITE_VALUE) hass.states.async_set("light.entity_flash", "on", VALID_FLASH) hass.states.async_set("light.entity_effect", "on", VALID_EFFECT) hass.states.async_set("light.entity_trans", "on", VALID_TRANSITION) @@ -49,7 +47,6 @@ async def test_reproducing_states(hass, caplog): [ State("light.entity_off", "off"), State("light.entity_bright", "on", VALID_BRIGHTNESS), - State("light.entity_white", "on", VALID_WHITE_VALUE), State("light.entity_flash", "on", VALID_FLASH), State("light.entity_effect", "on", VALID_EFFECT), State("light.entity_trans", "on", VALID_TRANSITION), @@ -79,8 +76,7 @@ async def test_reproducing_states(hass, caplog): [ State("light.entity_xy", "off"), State("light.entity_off", "on", VALID_BRIGHTNESS), - State("light.entity_bright", "on", VALID_WHITE_VALUE), - State("light.entity_white", "on", VALID_FLASH), + State("light.entity_bright", "on", VALID_FLASH), State("light.entity_flash", "on", VALID_EFFECT), State("light.entity_effect", "on", VALID_TRANSITION), State("light.entity_trans", "on", VALID_COLOR_NAME), @@ -93,7 +89,7 @@ async def test_reproducing_states(hass, caplog): ], ) - assert len(turn_on_calls) == 12 + assert len(turn_on_calls) == 11 expected_calls = [] @@ -101,14 +97,10 @@ async def test_reproducing_states(hass, caplog): expected_off["entity_id"] = "light.entity_off" expected_calls.append(expected_off) - expected_bright = dict(VALID_WHITE_VALUE) + expected_bright = dict(VALID_FLASH) expected_bright["entity_id"] = "light.entity_bright" expected_calls.append(expected_bright) - expected_white = dict(VALID_FLASH) - expected_white["entity_id"] = "light.entity_white" - expected_calls.append(expected_white) - expected_flash = dict(VALID_EFFECT) expected_flash["entity_id"] = "light.entity_flash" expected_calls.append(expected_flash) @@ -181,7 +173,6 @@ async def test_filter_color_modes(hass, caplog, color_mode): """Test filtering of parameters according to color mode.""" hass.states.async_set("light.entity", "off", {}) all_colors = { - **VALID_WHITE_VALUE, **VALID_COLOR_NAME, **VALID_COLOR_TEMP, **VALID_HS_COLOR, @@ -210,7 +201,6 @@ async def test_filter_color_modes(hass, caplog, color_mode): light.ColorMode.UNKNOWN: { **VALID_BRIGHTNESS, **VALID_HS_COLOR, - **VALID_WHITE_VALUE, }, light.ColorMode.WHITE: { **VALID_BRIGHTNESS, diff --git a/tests/components/light/test_significant_change.py b/tests/components/light/test_significant_change.py index f935ec8df1a..1ce477de4c6 100644 --- a/tests/components/light/test_significant_change.py +++ b/tests/components/light/test_significant_change.py @@ -4,7 +4,6 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - ATTR_WHITE_VALUE, ) from homeassistant.components.light.significant_change import ( async_check_significant_change, @@ -32,14 +31,6 @@ async def test_significant_change(): None, "on", {ATTR_COLOR_TEMP: 60}, "on", {ATTR_COLOR_TEMP: 65} ) - # White value - assert not async_check_significant_change( - None, "on", {ATTR_WHITE_VALUE: 60}, "on", {ATTR_WHITE_VALUE: 64} - ) - assert async_check_significant_change( - None, "on", {ATTR_WHITE_VALUE: 60}, "on", {ATTR_WHITE_VALUE: 65} - ) - # Effect for eff1, eff2, expected in ( (None, None, False), diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index e4c6f44883a..2bc330f8495 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1181,6 +1181,8 @@ ABBREVIATIONS_WHITE_LIST = [ "CONF_SCHEMA", "CONF_SWING_MODE_LIST", "CONF_TEMP_STEP", + # Removed + "CONF_WHITE_VALUE", ] diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index b37b6cf4471..f3d5cac9238 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -32,7 +32,6 @@ async def test_default_state(hass): assert state.attributes.get("brightness") is None assert state.attributes.get("hs_color") is None assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("effect_list") is None assert state.attributes.get("effect") is None assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ColorMode.ONOFF] diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 7026a6dea76..b5976f17841 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -7,7 +7,6 @@ from homeassistant.components.light import ( ATTR_EFFECT_LIST, ATTR_HS_COLOR, ATTR_SUPPORTED_COLOR_MODES, - ATTR_WHITE_VALUE, DOMAIN as LIGHT_DOMAIN, ColorMode, ) @@ -50,7 +49,6 @@ async def test_default_state(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_BRIGHTNESS) is None assert state.attributes.get(ATTR_HS_COLOR) is None assert state.attributes.get(ATTR_COLOR_TEMP) is None - assert state.attributes.get(ATTR_WHITE_VALUE) is None assert state.attributes.get(ATTR_EFFECT_LIST) is None assert state.attributes.get(ATTR_EFFECT) is None assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ColorMode.ONOFF] diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 1c51975b1f8..0b89d91831a 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -574,7 +574,6 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert "white_value" not in state.attributes # Setting white > 0 should clear the color assert "rgb_color" not in state.attributes assert state.attributes.get("color_mode") == "color_temp" @@ -593,7 +592,6 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON # Setting white to 0 should clear the color_temp - assert "white_value" not in state.attributes assert "color_temp" not in state.attributes assert state.attributes.get("hs_color") == (30, 100) assert state.attributes.get("color_mode") == "hs" @@ -686,7 +684,6 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert "white_value" not in state.attributes # Setting white > 0 should clear the color assert "rgb_color" not in state.attributes assert state.attributes.get("color_mode") == "color_temp" @@ -704,8 +701,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm ) state = hass.states.get("light.test") assert state.state == STATE_ON - # Setting white to 0 should clear the white_value and color_temp - assert not state.attributes.get("white_value") + # Setting white to 0 should clear the color_temp assert not state.attributes.get("color_temp") assert state.attributes.get("color_mode") == "hs" @@ -904,16 +900,6 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota) ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", white_value=128) - # white_value should be ignored - mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", - 0, - False, - ) - mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", @@ -1011,16 +997,6 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", white_value=128) - # white_value should be ignored - mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", - 0, - False, - ) - mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", @@ -1096,16 +1072,6 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", white_value=128) - # white_value should be ignored - mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", - 0, - False, - ) - mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 9b0ad613120..1199a318626 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -90,19 +90,6 @@ OPTIMISTIC_HS_COLOR_LIGHT_CONFIG = { } -OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG = { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, - "set_white_value": { - "service": "test.automation", - "data_template": { - "action": "set_white_value", - "caller": "{{ this.entity_id }}", - "white_value": "{{white_value}}", - }, - }, -} - - async def async_setup_light(hass, count, light_config): """Do setup of light integration.""" config = {"light": {"platform": "template", "lights": light_config}} diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 1b33b1cafbf..a4b5a182edc 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -49,7 +49,6 @@ class MockLight(MockToggleEntity, LightEntity): rgbw_color = None rgbww_color = None xy_color = None - white_value = None def turn_on(self, **kwargs): """Turn the entity on.""" @@ -63,7 +62,6 @@ class MockLight(MockToggleEntity, LightEntity): "rgbw_color", "rgbww_color", "color_temp", - "white_value", ]: setattr(self, key, value) if key == "white": From c7301a449bc1b121dac2e1623768964ca14fa685 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 14:48:23 +0200 Subject: [PATCH 461/903] Add switch checks to pylint plugin (#76909) --- pylint/plugins/hass_enforce_type_hints.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 0791ebcd9b2..19a69f8808f 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2036,6 +2036,25 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "switch": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="SwitchEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["SwitchDeviceClass", "str", None], + ), + ], + ), + ], "update": [ ClassTypeHintMatch( base_class="Entity", From 24f1287bf93abd15a8c16fcf06d2791dbc02bec8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 15:39:56 +0200 Subject: [PATCH 462/903] Improve type hints in homeassistant scene (#76930) --- .../components/homeassistant/scene.py | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 21c364ba65b..6cf480b19a3 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -1,8 +1,9 @@ """Allow users to set and activate scenes.""" from __future__ import annotations +from collections.abc import Mapping, ValuesView import logging -from typing import Any, NamedTuple +from typing import Any, NamedTuple, cast import voluptuous as vol @@ -34,16 +35,16 @@ from homeassistant.helpers import ( config_validation as cv, entity_platform, ) -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddEntitiesCallback, EntityPlatform from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.state import async_reproduce_state from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import async_get_integration -def _convert_states(states): +def _convert_states(states: dict[str, Any]) -> dict[str, State]: """Convert state definitions to State objects.""" - result = {} + result: dict[str, State] = {} for entity_id, info in states.items(): entity_id = cv.entity_id(entity_id) @@ -68,7 +69,7 @@ def _convert_states(states): return result -def _ensure_no_intersection(value): +def _ensure_no_intersection(value: dict[str, Any]) -> dict[str, Any]: """Validate that entities and snapshot_entities do not overlap.""" if ( CONF_SNAPSHOT not in value @@ -143,11 +144,12 @@ def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: if DATA_PLATFORM not in hass.data: return [] - platform = hass.data[DATA_PLATFORM] + platform: EntityPlatform = hass.data[DATA_PLATFORM] + scene_entities = cast(ValuesView[HomeAssistantScene], platform.entities.values()) return [ scene_entity.entity_id - for scene_entity in platform.entities.values() + for scene_entity in scene_entities if entity_id in scene_entity.scene_config.states ] @@ -158,12 +160,12 @@ def entities_in_scene(hass: HomeAssistant, entity_id: str) -> list[str]: if DATA_PLATFORM not in hass.data: return [] - platform = hass.data[DATA_PLATFORM] + platform: EntityPlatform = hass.data[DATA_PLATFORM] if (entity := platform.entities.get(entity_id)) is None: return [] - return list(entity.scene_config.states) + return list(cast(HomeAssistantScene, entity).scene_config.states) async def async_setup_platform( @@ -270,9 +272,12 @@ async def async_setup_platform( ) -def _process_scenes_config(hass, async_add_entities, config): +def _process_scenes_config( + hass, async_add_entities: AddEntitiesCallback, config: dict[str, Any] +) -> None: """Process multiple scenes and add them.""" # Check empty list + scene_config: list[dict[str, Any]] if not (scene_config := config[STATES]): return @@ -293,31 +298,33 @@ def _process_scenes_config(hass, async_add_entities, config): class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" - def __init__(self, hass, scene_config, from_service=False): + def __init__( + self, hass: HomeAssistant, scene_config: SceneConfig, from_service: bool = False + ) -> None: """Initialize the scene.""" self.hass = hass self.scene_config = scene_config self.from_service = from_service @property - def name(self): + def name(self) -> str: """Return the name of the scene.""" return self.scene_config.name @property - def icon(self): + def icon(self) -> str | None: """Return the icon of the scene.""" return self.scene_config.icon @property - def unique_id(self): + def unique_id(self) -> str | None: """Return unique ID.""" return self.scene_config.id @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any]: """Return the scene state attributes.""" - attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)} + attributes: dict[str, Any] = {ATTR_ENTITY_ID: list(self.scene_config.states)} if (unique_id := self.unique_id) is not None: attributes[CONF_ID] = unique_id return attributes From 65eb1584f765dcc2ec502bd8a9fa8d2f23d47cfd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 15:56:52 +0200 Subject: [PATCH 463/903] Improve entity type hints [a] (#76986) --- homeassistant/components/adax/climate.py | 4 +- homeassistant/components/ads/binary_sensor.py | 2 +- homeassistant/components/ads/sensor.py | 2 +- homeassistant/components/ads/switch.py | 8 ++-- .../components/advantage_air/climate.py | 11 ++--- .../components/advantage_air/switch.py | 6 ++- homeassistant/components/agent_dvr/camera.py | 12 +++--- homeassistant/components/airtouch4/climate.py | 16 ++++---- .../components/alarmdecoder/binary_sensor.py | 2 +- .../components/alarmdecoder/sensor.py | 2 +- .../components/ambiclimate/climate.py | 2 +- .../components/androidtv/media_player.py | 36 ++++++++-------- .../components/anel_pwrctrl/switch.py | 7 ++-- .../components/apcupsd/binary_sensor.py | 2 +- homeassistant/components/apcupsd/sensor.py | 2 +- .../components/apple_tv/media_player.py | 41 +++++++++++-------- homeassistant/components/apple_tv/remote.py | 8 ++-- homeassistant/components/aqualogic/sensor.py | 2 +- homeassistant/components/aqualogic/switch.py | 8 ++-- .../components/aquostv/media_player.py | 22 +++++----- .../components/arcam_fmj/media_player.py | 31 ++++++++------ .../components/arest/binary_sensor.py | 4 +- homeassistant/components/arest/sensor.py | 2 +- homeassistant/components/arest/switch.py | 13 +++--- homeassistant/components/atag/climate.py | 6 ++- homeassistant/components/atag/water_heater.py | 8 ++-- homeassistant/components/aten_pe/switch.py | 7 ++-- homeassistant/components/atome/sensor.py | 2 +- .../components/august/binary_sensor.py | 2 +- homeassistant/components/august/camera.py | 4 +- .../components/aurora_abb_powerone/sensor.py | 2 +- 31 files changed, 152 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 258e50f1047..85532d9aadb 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -80,7 +80,7 @@ class AdaxDevice(ClimateEntity): manufacturer="Adax", ) - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" if hvac_mode == HVACMode.HEAT: temperature = max(self.min_temp, self.target_temperature or self.min_temp) @@ -140,7 +140,7 @@ class LocalAdaxDevice(ClimateEntity): manufacturer="Adax", ) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 3ec1e8b1da4..b20ef010f1f 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -53,7 +53,7 @@ class AdsBinarySensor(AdsEntity, BinarySensorEntity): super().__init__(ads_hub, name, ads_var) self._attr_device_class = device_class or BinarySensorDeviceClass.MOVING - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register device notification.""" await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL) diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 7b6b1304987..172f8ee70df 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -70,7 +70,7 @@ class AdsSensor(AdsEntity, SensorEntity): self._ads_type = ads_type self._factor = factor - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register device notification.""" await self.async_initialize_device( self._ads_var, diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index d5ed25bad20..3f597fb9f5c 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -1,6 +1,8 @@ """Support for ADS switch platform.""" from __future__ import annotations +from typing import Any + import pyads import voluptuous as vol @@ -41,7 +43,7 @@ def setup_platform( class AdsSwitch(AdsEntity, SwitchEntity): """Representation of an ADS switch device.""" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register device notification.""" await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL) @@ -50,10 +52,10 @@ class AdsSwitch(AdsEntity, SwitchEntity): """Return True if the entity is on.""" return self._state_dict[STATE_KEY_STATE] - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._ads_hub.write_by_name(self._ads_var, True, pyads.PLCTYPE_BOOL) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._ads_hub.write_by_name(self._ads_var, False, pyads.PLCTYPE_BOOL) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index e69dc06dd7d..d889bf35642 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -114,7 +115,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): """Return the current fan modes.""" return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"]) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC Mode and State.""" if hvac_mode == HVACMode.OFF: await self.async_change( @@ -132,13 +133,13 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): } ) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set the Fan Mode.""" await self.async_change( {self.ac_key: {"info": {"fan": HASS_FAN_MODES.get(fan_mode)}}} ) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set the Temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) await self.async_change({self.ac_key: {"info": {"setTemp": temp}}}) @@ -179,7 +180,7 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): """Return the target temperature.""" return self._zone["setTemp"] - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC Mode and State.""" if hvac_mode == HVACMode.OFF: await self.async_change( @@ -198,7 +199,7 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): } ) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set the Temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) await self.async_change( diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index d9d46427599..52992ae9531 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -1,4 +1,6 @@ """Switch platform for Advantage Air integration.""" +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -44,13 +46,13 @@ class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity): """Return the fresh air status.""" return self._ac["freshAirStatus"] == ADVANTAGE_AIR_STATE_ON - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn fresh air on.""" await self.async_change( {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_ON}}} ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn fresh air off.""" await self.async_change( {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_OFF}}} diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index e99f1ecf223..2e8568b22b7 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -91,7 +91,7 @@ class AgentCamera(MjpegCamera): sw_version=device.client.version, ) - async def async_update(self): + async def async_update(self) -> None: """Update our state from the Agent API.""" try: await self.device.update() @@ -148,7 +148,7 @@ class AgentCamera(MjpegCamera): return self.device.online @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" return self.device.detector_active @@ -160,11 +160,11 @@ class AgentCamera(MjpegCamera): """Disable alerts.""" await self.device.alerts_off() - async def async_enable_motion_detection(self): + async def async_enable_motion_detection(self) -> None: """Enable motion detection.""" await self.device.detector_on() - async def async_disable_motion_detection(self): + async def async_disable_motion_detection(self) -> None: """Disable motion detection.""" await self.device.detector_off() @@ -176,7 +176,7 @@ class AgentCamera(MjpegCamera): """Stop recording.""" await self.device.record_stop() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Enable the camera.""" await self.device.enable() @@ -184,6 +184,6 @@ class AgentCamera(MjpegCamera): """Take a snapshot.""" await self.device.snapshot() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Disable the camera.""" await self.device.disable() diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index f660c06082c..370a061e901 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -154,7 +154,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): modes.append(HVACMode.OFF) return modes - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" if hvac_mode not in HA_STATE_TO_AT: raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") @@ -170,7 +170,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): _LOGGER.debug("Setting operation mode of %s to %s", self._ac_number, hvac_mode) self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan mode: {fan_mode}") @@ -182,14 +182,14 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): self._unit = self._airtouch.GetAcs()[self._ac_number] self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on.""" _LOGGER.debug("Turning %s on", self.unique_id) # in case ac is not on. Airtouch turns itself off if no groups are turned on # (even if groups turned back on) await self._airtouch.TurnAcOn(self._ac_number) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off.""" _LOGGER.debug("Turning %s off", self.unique_id) await self._airtouch.TurnAcOff(self._ac_number) @@ -266,7 +266,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): return HVACMode.FAN_ONLY - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" if hvac_mode not in HA_STATE_TO_AT: raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") @@ -304,7 +304,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): ) self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan mode: {fan_mode}") @@ -315,7 +315,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): ) self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on.""" _LOGGER.debug("Turning %s on", self.unique_id) await self._airtouch.TurnGroupOn(self._group_number) @@ -330,7 +330,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): await self.coordinator.async_request_refresh() self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off.""" _LOGGER.debug("Turning %s off", self.unique_id) await self._airtouch.TurnGroupOff(self._group_number) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 24eeffde691..47e6066400c 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -88,7 +88,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): CONF_ZONE_NUMBER: self._zone_number, } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect(self.hass, SIGNAL_ZONE_FAULT, self._fault_callback) diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 90d0606d681..f0ffc7e7158 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -24,7 +24,7 @@ class AlarmDecoderSensor(SensorEntity): _attr_name = "Alarm Panel Display" _attr_should_poll = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 50135693ff4..99fefbb180e 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -170,7 +170,7 @@ class AmbiclimateEntity(ClimateEntity): return await self._heater.set_target_temperature(temperature) - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" if hvac_mode == HVACMode.HEAT: await self._heater.turn_on() diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 0d43ada7bc0..696fab5788f 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -298,7 +298,7 @@ class ADBDevice(MediaPlayerEntity): self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND) self.turn_on_command = options.get(CONF_TURN_ON_COMMAND) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Set config parameter when add to hass.""" await super().async_added_to_hass() self._process_config() @@ -321,7 +321,7 @@ class ADBDevice(MediaPlayerEntity): """Take a screen capture from the device.""" return await self.aftv.adb_screencap() - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch current playing image.""" if not self._screencap or self.state in (STATE_OFF, None) or not self.available: return None, None @@ -337,22 +337,22 @@ class ADBDevice(MediaPlayerEntity): return None, None @adb_decorator() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self.aftv.media_play() @adb_decorator() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self.aftv.media_pause() @adb_decorator() - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Send play/pause command.""" await self.aftv.media_play_pause() @adb_decorator() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the device.""" if self.turn_on_command: await self.aftv.adb_shell(self.turn_on_command) @@ -360,7 +360,7 @@ class ADBDevice(MediaPlayerEntity): await self.aftv.turn_on() @adb_decorator() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the device.""" if self.turn_off_command: await self.aftv.adb_shell(self.turn_off_command) @@ -368,17 +368,17 @@ class ADBDevice(MediaPlayerEntity): await self.aftv.turn_off() @adb_decorator() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command (results in rewind).""" await self.aftv.media_previous_track() @adb_decorator() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command (results in fast-forward).""" await self.aftv.media_next_track() @adb_decorator() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source. If the source starts with a '!', then it will close the app instead of @@ -469,7 +469,7 @@ class AndroidTVDevice(ADBDevice): ) @adb_decorator(override_available=True) - async def async_update(self): + async def async_update(self) -> None: """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. if not self._attr_available: @@ -514,12 +514,12 @@ class AndroidTVDevice(ADBDevice): self._attr_source_list = None @adb_decorator() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" await self.aftv.media_stop() @adb_decorator() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" is_muted = await self.aftv.is_volume_muted() @@ -528,17 +528,17 @@ class AndroidTVDevice(ADBDevice): await self.aftv.mute_volume() @adb_decorator() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" await self.aftv.set_volume_level(volume) @adb_decorator() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Send volume down command.""" self._attr_volume_level = await self.aftv.volume_down(self._attr_volume_level) @adb_decorator() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Send volume up command.""" self._attr_volume_level = await self.aftv.volume_up(self._attr_volume_level) @@ -558,7 +558,7 @@ class FireTVDevice(ADBDevice): ) @adb_decorator(override_available=True) - async def async_update(self): + async def async_update(self) -> None: """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. if not self._attr_available: @@ -600,6 +600,6 @@ class FireTVDevice(ADBDevice): self._attr_source_list = None @adb_decorator() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop (back) command.""" await self.aftv.back() diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index f4a5ca32c3c..19d1a2deaff 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from anel_pwrctrl import DeviceMaster import voluptuous as vol @@ -78,16 +79,16 @@ class PwrCtrlSwitch(SwitchEntity): self._attr_unique_id = f"{port.device.host}-{port.get_index()}" self._attr_name = port.label - def update(self): + def update(self) -> None: """Trigger update for all switches on the parent device.""" self._parent_device.update() self._attr_is_on = self._port.get_state() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._port.on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._port.off() diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 7ef491a5367..25d50c06c97 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -38,6 +38,6 @@ class OnlineStatus(BinarySensorEntity): self._data = data self._attr_name = config[CONF_NAME] - def update(self): + def update(self) -> None: """Get the status report from APCUPSd and set this entity's state.""" self._attr_is_on = int(self._data.status[KEY_STATUS], 16) & VALUE_ONLINE > 0 diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index b7e7366796b..0f85c4c6854 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -460,7 +460,7 @@ class APCUPSdSensor(SensorEntity): self._data = data self._attr_name = f"{SENSOR_PREFIX}{description.name}" - def update(self): + def update(self) -> None: """Get the latest status and use it to update our sensor state.""" key = self.entity_description.key.upper() if key not in self._data.status: diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 3a495e053eb..771b27a6dc3 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,5 +1,8 @@ """Support for Apple TV media player.""" +from __future__ import annotations + import logging +from typing import Any from pyatv import exceptions from pyatv.const import ( @@ -276,7 +279,9 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return dt_util.utcnow() return None - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" # If input (file) has a file format supported by pyatv, then stream it with # RAOP. Otherwise try to play it with regular AirPlay. @@ -314,7 +319,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return self.atv.metadata.artwork_id return None - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch media image of current playing image.""" state = self.state if self._playing and state not in [STATE_OFF, STATE_IDLE]: @@ -391,8 +396,8 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_browse_media( self, - media_content_type=None, - media_content_id=None, + media_content_type: str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" if media_content_id == "apps" or ( @@ -427,12 +432,12 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return cur_item - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" if self._is_feature_available(FeatureName.TurnOn): await self.atv.power.turn_on() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" if (self._is_feature_available(FeatureName.TurnOff)) and ( not self._is_feature_available(FeatureName.PowerState) @@ -440,58 +445,58 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): ): await self.atv.power.turn_off() - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Pause media on media player.""" if self._playing: await self.atv.remote_control.play_pause() - async def async_media_play(self): + async def async_media_play(self) -> None: """Play media.""" if self.atv: await self.atv.remote_control.play() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Stop the media player.""" if self.atv: await self.atv.remote_control.stop() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause the media player.""" if self.atv: await self.atv.remote_control.pause() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" if self.atv: await self.atv.remote_control.next() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" if self.atv: await self.atv.remote_control.previous() - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" if self.atv: await self.atv.remote_control.set_position(position) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Turn volume up for media player.""" if self.atv: await self.atv.audio.volume_up() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Turn volume down for media player.""" if self.atv: await self.atv.audio.volume_down() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" if self.atv: # pyatv expects volume in percent await self.atv.audio.set_volume(volume * 100.0) - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: str) -> None: """Set repeat mode.""" if self.atv: mode = { @@ -500,7 +505,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): }.get(repeat, RepeatState.Off) await self.atv.remote_control.set_repeat(mode) - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" if self.atv: await self.atv.remote_control.set_shuffle( diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index 2e8cfc4f6b5..f3be6977891 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -1,6 +1,8 @@ """Remote control support for Apple TV.""" import asyncio +from collections.abc import Iterable import logging +from typing import Any from homeassistant.components.remote import ( ATTR_DELAY_SECS, @@ -40,15 +42,15 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): """Return true if device is on.""" return self.atv is not None - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self.manager.connect() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.manager.disconnect() - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 5e6e35cce76..d575beb0367 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -144,7 +144,7 @@ class AquaLogicSensor(SensorEntity): self._processor = processor self._attr_name = f"AquaLogic {description.name}" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index 953b0c9b527..e04bc8595fa 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -1,6 +1,8 @@ """Support for AquaLogic switches.""" from __future__ import annotations +from typing import Any + from aqualogic.core import States import voluptuous as vol @@ -82,19 +84,19 @@ class AquaLogicSwitch(SwitchEntity): state = panel.get_state(self._state_name) return state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if (panel := self._processor.panel) is None: return panel.set_state(self._state_name, True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if (panel := self._processor.panel) is None: return panel.set_state(self._state_name, False) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect(self.hass, UPDATE_TOPIC, self.async_write_ha_state) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index ccbae7151b2..c9465424381 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -131,7 +131,7 @@ class SharpAquosTVDevice(MediaPlayerEntity): self._attr_state = state @_retry - def update(self): + def update(self) -> None: """Retrieve the latest data.""" if self._remote.power() == 1: self._attr_state = STATE_ON @@ -153,7 +153,7 @@ class SharpAquosTVDevice(MediaPlayerEntity): self._attr_volume_level = self._remote.volume() / 60 @_retry - def turn_off(self): + def turn_off(self) -> None: """Turn off tvplayer.""" self._remote.power(0) @@ -168,46 +168,46 @@ class SharpAquosTVDevice(MediaPlayerEntity): self._remote.volume(int(self.volume_level * 60) - 2) @_retry - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set Volume media player.""" self._remote.volume(int(volume * 60)) @_retry - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" self._remote.mute(0) @_retry - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._remote.power(1) @_retry - def media_play_pause(self): + def media_play_pause(self) -> None: """Simulate play pause media player.""" self._remote.remote_button(40) @_retry - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._remote.remote_button(16) @_retry - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._remote.remote_button(16) @_retry - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self._remote.remote_button(21) @_retry - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the previous track command.""" self._remote.remote_button(19) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" for key, value in SOURCES.items(): if source == value: diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 731eb0b0352..f995b79df04 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -1,5 +1,8 @@ """Arcam media player.""" +from __future__ import annotations + import logging +from typing import Any from arcam.fmj import SourceCodes from arcam.fmj.state import State @@ -107,7 +110,7 @@ class ArcamFmj(MediaPlayerEntity): name=self._device_name, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Once registered, add listener for events.""" await self._state.start() await self._state.update() @@ -139,17 +142,17 @@ class ArcamFmj(MediaPlayerEntity): async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STOPPED, _stopped) ) - async def async_update(self): + async def async_update(self) -> None: """Force update of state.""" _LOGGER.debug("Update state %s", self.name) await self._state.update() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" await self._state.set_mute(mute) self.async_write_ha_state() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select a specific source.""" try: value = SourceCodes[source] @@ -160,7 +163,7 @@ class ArcamFmj(MediaPlayerEntity): await self._state.set_source(value) self.async_write_ha_state() - async def async_select_sound_mode(self, sound_mode): + async def async_select_sound_mode(self, sound_mode: str) -> None: """Select a specific source.""" try: await self._state.set_decode_mode(sound_mode) @@ -170,22 +173,22 @@ class ArcamFmj(MediaPlayerEntity): self.async_write_ha_state() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._state.set_volume(round(volume * 99.0)) self.async_write_ha_state() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Turn volume up for media player.""" await self._state.inc_volume() self.async_write_ha_state() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Turn volume up for media player.""" await self._state.dec_volume() self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" if self._state.get_power() is not None: _LOGGER.debug("Turning on device using connection") @@ -194,11 +197,13 @@ class ArcamFmj(MediaPlayerEntity): _LOGGER.debug("Firing event to turn on device") self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id}) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self._state.set_power(False) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" if media_content_id not in (None, "root"): raise BrowseError( @@ -231,7 +236,9 @@ class ArcamFmj(MediaPlayerEntity): return root - async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play media.""" if media_id.startswith("preset:"): diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 92309f5620f..5d65a23335a 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -86,7 +86,7 @@ class ArestBinarySensor(BinarySensorEntity): if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode of %s", resource) - def update(self): + def update(self) -> None: """Get the latest data from aREST API.""" self.arest.update() self._attr_is_on = bool(self.arest.data.get("state")) @@ -102,7 +102,7 @@ class ArestData: self.data = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the latest data from aREST device.""" try: response = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index e4d3fff8c74..5c95fd63c3b 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -159,7 +159,7 @@ class ArestSensor(SensorEntity): if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode of %s", resource) - def update(self): + def update(self) -> None: """Get the latest data from aREST API.""" self.arest.update() self._attr_available = self.arest.available diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index f8c45313d52..6efa24c5a0f 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus import logging +from typing import Any import requests import voluptuous as vol @@ -122,7 +123,7 @@ class ArestSwitchFunction(ArestSwitchBase): except ValueError: _LOGGER.error("Response invalid") - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" request = requests.get( f"{self._resource}/{self._func}", timeout=10, params={"params": "1"} @@ -133,7 +134,7 @@ class ArestSwitchFunction(ArestSwitchBase): else: _LOGGER.error("Can't turn on function %s at %s", self._func, self._resource) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" request = requests.get( f"{self._resource}/{self._func}", timeout=10, params={"params": "0"} @@ -146,7 +147,7 @@ class ArestSwitchFunction(ArestSwitchBase): "Can't turn off function %s at %s", self._func, self._resource ) - def update(self): + def update(self) -> None: """Get the latest data from aREST API and update the state.""" try: request = requests.get(f"{self._resource}/{self._func}", timeout=10) @@ -171,7 +172,7 @@ class ArestSwitchPin(ArestSwitchBase): _LOGGER.error("Can't set mode") self._attr_available = False - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" turn_on_payload = int(not self.invert) request = requests.get( @@ -182,7 +183,7 @@ class ArestSwitchPin(ArestSwitchBase): else: _LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" turn_off_payload = int(self.invert) request = requests.get( @@ -193,7 +194,7 @@ class ArestSwitchPin(ArestSwitchBase): else: _LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource) - def update(self): + def update(self) -> None: """Get the latest data from aREST API and update the state.""" try: request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index cf5624005ed..e6e9d8503e8 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -1,6 +1,8 @@ """Initialization of ATAG One climate platform.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( PRESET_AWAY, @@ -78,12 +80,12 @@ class AtagThermostat(AtagEntity, ClimateEntity): preset = self.coordinator.data.climate.preset_mode return PRESET_INVERTED.get(preset) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE)) self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" await self.coordinator.data.climate.set_hvac_mode(hvac_mode) self.async_write_ha_state() diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index 1dc38401574..009f84a72ef 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -1,4 +1,6 @@ """ATAG water heater component.""" +from typing import Any + from homeassistant.components.water_heater import ( STATE_ECO, STATE_PERFORMANCE, @@ -42,7 +44,7 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity): operation = self.coordinator.data.dhw.current_operation return operation if operation in self.operation_list else STATE_OFF - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)): self.async_write_ha_state() @@ -53,11 +55,11 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity): return self.coordinator.data.dhw.target_temperature @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self.coordinator.data.dhw.max_temp @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" return self.coordinator.data.dhw.min_temp diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 92a58f37c8c..d49201f6d7b 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from atenpdu import AtenPE, AtenPEError import voluptuous as vol @@ -101,17 +102,17 @@ class AtenSwitch(SwitchEntity): self._attr_unique_id = f"{mac}-{outlet}" self._attr_name = name or f"Outlet {outlet}" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._device.setOutletStatus(self._outlet, "on") self._attr_is_on = True - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.setOutletStatus(self._outlet, "off") self._attr_is_on = False - async def async_update(self): + async def async_update(self) -> None: """Process update from entity.""" status = await self._device.displayOutletStatus(self._outlet) if status == "on": diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index be769eae49b..d3df8b4e684 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -269,7 +269,7 @@ class AtomeSensor(SensorEntity): self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR self._attr_state_class = SensorStateClass.TOTAL_INCREASING - def update(self): + def update(self) -> None: """Update device state.""" update_function = getattr(self._data, f"update_{self._sensor_type}_usage") update_function() diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index a33d1cb96dc..6f45f626180 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -288,7 +288,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): self._check_for_off_update_listener() self._check_for_off_update_listener = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed.""" self._schedule_update_to_recheck_turn_off_sensor() await super().async_added_to_hass() diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index c5ab5fc3cfa..32b23e1329c 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -43,12 +43,12 @@ class AugustCamera(AugustEntityMixin, Camera): self._attr_unique_id = f"{self._device_id:s}_camera" @property - def is_recording(self): + def is_recording(self) -> bool: """Return true if the device is recording.""" return self._device.has_subscription @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" return True diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 188f1c789a2..eeb72e4d485 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -82,7 +82,7 @@ class AuroraSensor(AuroraEntity, SensorEntity): self.entity_description = entity_description self.available_prev = True - def update(self): + def update(self) -> None: """Fetch new state data for the sensor. This is the only method that should fetch new data for Home Assistant. From 7a497c1e6e5a0d44b9418a754470ca9dd35e9719 Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:40:04 +0200 Subject: [PATCH 464/903] Add Landis+Gyr Heat Meter integration (#73363) * Add Landis+Gyr Heat Meter integration * Add contant for better sensor config * Add test for init * Refactor some of the PR suggestions in config_flow * Apply small fix * Correct total_increasing to total * Add test for restore state * Add MWh entity that can be added as gas on the energy dashoard * Remove GJ as unit * Round MWh to 5 iso 3 digits * Update homeassistant/components/landisgyr_heat_meter/const.py * Update CODEOWNERS Co-authored-by: Erik Montnemery --- CODEOWNERS | 2 + .../landisgyr_heat_meter/__init__.py | 56 +++++ .../landisgyr_heat_meter/config_flow.py | 136 +++++++++++ .../components/landisgyr_heat_meter/const.py | 192 +++++++++++++++ .../landisgyr_heat_meter/manifest.json | 13 + .../components/landisgyr_heat_meter/sensor.py | 108 +++++++++ .../landisgyr_heat_meter/strings.json | 23 ++ .../landisgyr_heat_meter/translations/en.json | 24 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../landisgyr_heat_meter/__init__.py | 1 + .../landisgyr_heat_meter/test_config_flow.py | 227 ++++++++++++++++++ .../landisgyr_heat_meter/test_init.py | 22 ++ .../landisgyr_heat_meter/test_sensor.py | 200 +++++++++++++++ 15 files changed, 1011 insertions(+) create mode 100644 homeassistant/components/landisgyr_heat_meter/__init__.py create mode 100644 homeassistant/components/landisgyr_heat_meter/config_flow.py create mode 100644 homeassistant/components/landisgyr_heat_meter/const.py create mode 100644 homeassistant/components/landisgyr_heat_meter/manifest.json create mode 100644 homeassistant/components/landisgyr_heat_meter/sensor.py create mode 100644 homeassistant/components/landisgyr_heat_meter/strings.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/en.json create mode 100644 tests/components/landisgyr_heat_meter/__init__.py create mode 100644 tests/components/landisgyr_heat_meter/test_config_flow.py create mode 100644 tests/components/landisgyr_heat_meter/test_init.py create mode 100644 tests/components/landisgyr_heat_meter/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 1f3ce12e63a..26d1a8da2f8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -587,6 +587,8 @@ build.json @home-assistant/supervisor /tests/components/lacrosse_view/ @IceBotYT /homeassistant/components/lametric/ @robbiet480 @frenck /tests/components/lametric/ @robbiet480 @frenck +/homeassistant/components/landisgyr_heat_meter/ @vpathuis +/tests/components/landisgyr_heat_meter/ @vpathuis /homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol /tests/components/launch_library/ @ludeeus @DurgNomis-drol /homeassistant/components/laundrify/ @xLarry diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py new file mode 100644 index 00000000000..b5a9a3fba79 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -0,0 +1,56 @@ +"""The Landis+Gyr Heat Meter integration.""" +from __future__ import annotations + +import logging + +from ultraheat_api import HeatMeterService, UltraheatReader + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up heat meter from a config entry.""" + + _LOGGER.debug("Initializing %s integration on %s", DOMAIN, entry.data[CONF_DEVICE]) + reader = UltraheatReader(entry.data[CONF_DEVICE]) + + api = HeatMeterService(reader) + + async def async_update_data(): + """Fetch data from the API.""" + _LOGGER.info("Polling on %s", entry.data[CONF_DEVICE]) + return await hass.async_add_executor_job(api.read) + + # No automatic polling and no initial refresh of data is being done at this point, + # to prevent battery drain. The user will have to do it manually. + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="ultraheat_gateway", + update_method=async_update_data, + update_interval=None, + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py new file mode 100644 index 00000000000..e3dbbb7433b --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow for Landis+Gyr Heat Meter integration.""" +from __future__ import annotations + +import logging +import os + +import async_timeout +import serial +import serial.tools.list_ports +from ultraheat_api import HeatMeterService, UltraheatReader +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_DEVICE +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CONF_MANUAL_PATH = "Enter Manually" + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ultraheat Heat Meter.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Step when setting up serial configuration.""" + errors = {} + + if user_input is not None: + if user_input[CONF_DEVICE] == CONF_MANUAL_PATH: + return await self.async_step_setup_serial_manual_path() + + dev_path = await self.hass.async_add_executor_job( + get_serial_by_id, user_input[CONF_DEVICE] + ) + + try: + return await self.validate_and_create_entry(dev_path) + except CannotConnect: + errors["base"] = "cannot_connect" + + ports = await self.get_ports() + + schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(ports)}) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_setup_serial_manual_path(self, user_input=None): + """Set path manually.""" + errors = {} + + if user_input is not None: + dev_path = user_input[CONF_DEVICE] + try: + return await self.validate_and_create_entry(dev_path) + except CannotConnect: + errors["base"] = "cannot_connect" + + schema = vol.Schema({vol.Required(CONF_DEVICE): str}) + return self.async_show_form( + step_id="setup_serial_manual_path", + data_schema=schema, + errors=errors, + ) + + async def validate_and_create_entry(self, dev_path): + """Try to connect to the device path and return an entry.""" + model, device_number = await self.validate_ultraheat(dev_path) + + await self.async_set_unique_id(device_number) + self._abort_if_unique_id_configured() + data = { + CONF_DEVICE: dev_path, + "model": model, + "device_number": device_number, + } + return self.async_create_entry( + title=model, + data=data, + ) + + async def validate_ultraheat(self, port: str): + """Validate the user input allows us to connect.""" + + reader = UltraheatReader(port) + heat_meter = HeatMeterService(reader) + try: + async with async_timeout.timeout(10): + # validate and retrieve the model and device number for a unique id + data = await self.hass.async_add_executor_job(heat_meter.read) + _LOGGER.debug("Got data from Ultraheat API: %s", data) + + except Exception as err: + _LOGGER.warning("Failed read data from: %s. %s", port, err) + raise CannotConnect(f"Error communicating with device: {err}") from err + + _LOGGER.debug("Successfully connected to %s", port) + return data.model, data.device_number + + async def get_ports(self) -> dict: + """Get the available ports.""" + ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) + formatted_ports = {} + for port in ports: + formatted_ports[ + port.device + ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + ( + f" - {port.manufacturer}" if port.manufacturer else "" + ) + formatted_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH + return formatted_ports + + +def get_serial_by_id(dev_path: str) -> str: + """Return a /dev/serial/by-id match for given device if available.""" + by_id = "/dev/serial/by-id" + if not os.path.isdir(by_id): + return dev_path + + for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()): + if os.path.realpath(path) == dev_path: + return path + return dev_path + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py new file mode 100644 index 00000000000..70008890d1f --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -0,0 +1,192 @@ +"""Constants for the Landis+Gyr Heat Meter integration.""" + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ENERGY_MEGA_WATT_HOUR, TEMP_CELSIUS, VOLUME_CUBIC_METERS +from homeassistant.helpers.entity import EntityCategory + +DOMAIN = "landisgyr_heat_meter" + +GJ_TO_MWH = 0.277778 # conversion factor + +HEAT_METER_SENSOR_TYPES = ( + SensorEntityDescription( + key="heat_usage", + icon="mdi:fire", + name="Heat usage", + native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="volume_usage_m3", + icon="mdi:fire", + name="Volume usage", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=SensorStateClass.TOTAL, + ), + # Diagnostic entity for debugging, this will match the value in GJ indicated on the meter's display + SensorEntityDescription( + key="heat_usage_gj", + icon="mdi:fire", + name="Heat usage GJ", + native_unit_of_measurement="GJ", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="heat_previous_year", + icon="mdi:fire", + name="Heat usage previous year", + native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="volume_previous_year_m3", + icon="mdi:fire", + name="Volume usage previous year", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="ownership_number", + name="Ownership number", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="error_number", + name="Error number", + icon="mdi:home-alert", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="device_number", + name="Device number", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="measurement_period_minutes", + name="Measurement period minutes", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="power_max_kw", + name="Power max", + native_unit_of_measurement="kW", + icon="mdi:power-plug-outline", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="power_max_previous_year_kw", + name="Power max previous year", + native_unit_of_measurement="kW", + icon="mdi:power-plug-outline", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flowrate_max_m3ph", + name="Flowrate max", + native_unit_of_measurement="m3ph", + icon="mdi:water-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flowrate_max_previous_year_m3ph", + name="Flowrate max previous year", + native_unit_of_measurement="m3ph", + icon="mdi:water-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="return_temperature_max_c", + name="Return temperature max", + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="return_temperature_max_previous_year_c", + name="Return temperature max previous year", + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flow_temperature_max_c", + name="Flow temperature max", + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flow_temperature_max_previous_year_c", + name="Flow temperature max previous year", + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="operating_hours", + name="Operating hours", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flow_hours", + name="Flow hours", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="fault_hours", + name="Fault hours", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="fault_hours_previous_year", + name="Fault hours previous year", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="yearly_set_day", + name="Yearly set day", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="monthly_set_day", + name="Monthly set day", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="meter_date_time", + name="Meter date time", + icon="mdi:clock-outline", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="measuring_range_m3ph", + name="Measuring range", + native_unit_of_measurement="m3ph", + icon="mdi:water-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="settings_and_firmware", + name="Settings and firmware", + entity_category=EntityCategory.DIAGNOSTIC, + ), +) diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json new file mode 100644 index 00000000000..359ca1acea6 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "landisgyr_heat_meter", + "name": "Landis+Gyr Heat Meter", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter", + "requirements": ["ultraheat-api==0.4.1"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@vpathuis"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py new file mode 100644 index 00000000000..1d38b1f5816 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -0,0 +1,108 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from dataclasses import asdict +import logging + +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + RestoreSensor, + SensorDeviceClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util + +from . import DOMAIN +from .const import GJ_TO_MWH, HEAT_METER_SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the sensor platform.""" + _LOGGER.info("The Landis+Gyr Heat Meter sensor platform is being set up!") + + unique_id = entry.entry_id + coordinator = hass.data[DOMAIN][entry.entry_id] + + model = entry.data["model"] + + device = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="Landis & Gyr", + model=model, + name="Landis+Gyr Heat Meter", + ) + + sensors = [] + + for description in HEAT_METER_SENSOR_TYPES: + sensors.append(HeatMeterSensor(coordinator, unique_id, description, device)) + + async_add_entities(sensors) + + +class HeatMeterSensor(CoordinatorEntity, RestoreSensor): + """Representation of a Sensor.""" + + def __init__(self, coordinator, unique_id, description, device): + """Set up the sensor with the initial values.""" + super().__init__(coordinator) + self.key = description.key + self._attr_unique_id = f"{DOMAIN}_{unique_id}_{description.key}" + self._attr_name = "Heat Meter " + description.name + if hasattr(description, "icon"): + self._attr_icon = description.icon + if hasattr(description, "entity_category"): + self._attr_entity_category = description.entity_category + if hasattr(description, ATTR_STATE_CLASS): + self._attr_state_class = description.state_class + if hasattr(description, ATTR_DEVICE_CLASS): + self._attr_device_class = description.device_class + if hasattr(description, ATTR_UNIT_OF_MEASUREMENT): + self._attr_native_unit_of_measurement = ( + description.native_unit_of_measurement + ) + self._attr_device_info = device + self._attr_should_poll = bool(self.key in ("heat_usage", "heat_previous_year")) + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + state = await self.async_get_last_sensor_data() + if state: + self._attr_native_value = state.native_value + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if self.key in asdict(self.coordinator.data): + if self.device_class == SensorDeviceClass.TIMESTAMP: + self._attr_native_value = dt_util.as_utc( + asdict(self.coordinator.data)[self.key] + ) + else: + self._attr_native_value = asdict(self.coordinator.data)[self.key] + + if self.key == "heat_usage": + self._attr_native_value = convert_gj_to_mwh( + self.coordinator.data.heat_usage_gj + ) + + if self.key == "heat_previous_year": + self._attr_native_value = convert_gj_to_mwh( + self.coordinator.data.heat_previous_year_gj + ) + + self.async_write_ha_state() + + +def convert_gj_to_mwh(gigajoule) -> float: + """Convert GJ to MWh using the conversion value.""" + return round(gigajoule * GJ_TO_MWH, 5) diff --git a/homeassistant/components/landisgyr_heat_meter/strings.json b/homeassistant/components/landisgyr_heat_meter/strings.json new file mode 100644 index 00000000000..61e170af2b3 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device": "Select device" + } + }, + "setup_serial_manual_path": { + "data": { + "device": "[%key:common::config_flow::data::usb_path%]" + } + } + }, + "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%]" + } + } +} diff --git a/homeassistant/components/landisgyr_heat_meter/translations/en.json b/homeassistant/components/landisgyr_heat_meter/translations/en.json new file mode 100644 index 00000000000..6915e8cb36a --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/en.json @@ -0,0 +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": { + "device": "Select device" + } + }, + "setup_serial_manual_path": { + "data": { + "device": "USB-device path" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 42b426b8864..2ce09bfcafa 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -197,6 +197,7 @@ FLOWS = { "kulersky", "lacrosse_view", "lametric", + "landisgyr_heat_meter", "launch_library", "laundrify", "lg_soundbar", diff --git a/requirements_all.txt b/requirements_all.txt index 96dfac44800..ecaaf30280c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2383,6 +2383,9 @@ twitchAPI==2.5.2 # homeassistant.components.ukraine_alarm uasiren==0.0.1 +# homeassistant.components.landisgyr_heat_meter +ultraheat-api==0.4.1 + # homeassistant.components.unifiprotect unifi-discovery==1.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2db72263205..2b72beee027 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1614,6 +1614,9 @@ twitchAPI==2.5.2 # homeassistant.components.ukraine_alarm uasiren==0.0.1 +# homeassistant.components.landisgyr_heat_meter +ultraheat-api==0.4.1 + # homeassistant.components.unifiprotect unifi-discovery==1.1.5 diff --git a/tests/components/landisgyr_heat_meter/__init__.py b/tests/components/landisgyr_heat_meter/__init__.py new file mode 100644 index 00000000000..0ee6eb22510 --- /dev/null +++ b/tests/components/landisgyr_heat_meter/__init__.py @@ -0,0 +1 @@ +"""Tests for the Landis+Gyr Heat Meter component.""" diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py new file mode 100644 index 00000000000..b51d4493879 --- /dev/null +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -0,0 +1,227 @@ +"""Test the Landis + Gyr Heat Meter config flow.""" +from dataclasses import dataclass +from unittest.mock import MagicMock, patch + +import serial.tools.list_ports + +from homeassistant import config_entries +from homeassistant.components.landisgyr_heat_meter import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +def mock_serial_port(): + """Mock of a serial port.""" + port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") + port.serial_number = "1234" + port.manufacturer = "Virtual serial port" + port.device = "/dev/ttyUSB1234" + port.description = "Some serial port" + + return port + + +@dataclass +class MockUltraheatRead: + """Mock of the response from the read method of the Ultraheat API.""" + + model: str + device_number: str + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: + """Test manual entry.""" + + mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") + + 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" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "Enter Manually"} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {} + + with patch( + "homeassistant.components.landisgyr_heat_meter.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "/dev/ttyUSB0"} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "LUGCUH50" + assert result["data"] == { + "device": "/dev/ttyUSB0", + "model": "LUGCUH50", + "device_number": "123456789", + } + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) +async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> None: + """Test select from list entry.""" + + mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") + port = mock_serial_port() + + 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" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "LUGCUH50" + assert result["data"] == { + "device": port.device, + "model": "LUGCUH50", + "device_number": "123456789", + } + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: + """Test manual entry fails.""" + + mock_heat_meter().read.side_effect = Exception + + 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" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "Enter Manually"} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {} + + with patch( + "homeassistant.components.landisgyr_heat_meter.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "/dev/ttyUSB0"} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {"base": "cannot_connect"} + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) +async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) -> None: + """Test select from list entry fails.""" + + mock_heat_meter().read.side_effect = Exception + port = mock_serial_port() + + 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" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) +async def test_get_serial_by_id_realpath( + mock_port, mock_heat_meter, hass: HomeAssistant +) -> None: + """Test getting the serial path name.""" + + mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") + port = mock_serial_port() + + 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" + assert result["errors"] == {} + + scandir = [MagicMock(), MagicMock()] + scandir[0].path = "/dev/ttyUSB1234" + scandir[0].is_symlink.return_value = True + scandir[1].path = "/dev/ttyUSB5678" + scandir[1].is_symlink.return_value = True + + with patch("os.path") as path: + with patch("os.scandir", return_value=scandir): + path.isdir.return_value = True + path.realpath.side_effect = ["/dev/ttyUSB1234", "/dev/ttyUSB5678"] + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "LUGCUH50" + assert result["data"] == { + "device": port.device, + "model": "LUGCUH50", + "device_number": "123456789", + } + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) +async def test_get_serial_by_id_dev_path( + mock_port, mock_heat_meter, hass: HomeAssistant +) -> None: + """Test getting the serial path name with no realpath result.""" + + mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") + port = mock_serial_port() + + 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" + assert result["errors"] == {} + + scandir = [MagicMock()] + scandir[0].path.return_value = "/dev/serial/by-id/USB5678" + scandir[0].is_symlink.return_value = True + + with patch("os.path") as path: + with patch("os.scandir", return_value=scandir): + path.isdir.return_value = True + path.realpath.side_effect = ["/dev/ttyUSB5678"] + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "LUGCUH50" + assert result["data"] == { + "device": port.device, + "model": "LUGCUH50", + "device_number": "123456789", + } diff --git a/tests/components/landisgyr_heat_meter/test_init.py b/tests/components/landisgyr_heat_meter/test_init.py new file mode 100644 index 00000000000..b3630fc4872 --- /dev/null +++ b/tests/components/landisgyr_heat_meter/test_init.py @@ -0,0 +1,22 @@ +"""Test the Landis + Gyr Heat Meter init.""" + +from homeassistant.const import CONF_DEVICE + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test removing config entry.""" + entry = MockConfigEntry( + domain="landisgyr_heat_meter", + title="LUGCUH50", + data={CONF_DEVICE: "/dev/1234"}, + ) + + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "landisgyr_heat_meter" in hass.config.components + + assert await hass.config_entries.async_remove(entry.entry_id) diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py new file mode 100644 index 00000000000..505efb446b8 --- /dev/null +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -0,0 +1,200 @@ +"""The tests for the Landis+Gyr Heat Meter sensor platform.""" +from dataclasses import dataclass +import datetime +from unittest.mock import patch + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.landisgyr_heat_meter.const import DOMAIN +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + ENERGY_MEGA_WATT_HOUR, + VOLUME_CUBIC_METERS, +) +from homeassistant.core import CoreState, State +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import MockConfigEntry, mock_restore_cache_with_extra_data + + +@dataclass +class MockHeatMeterResponse: + """Mock for HeatMeterResponse.""" + + heat_usage_gj: int + volume_usage_m3: int + heat_previous_year_gj: int + device_number: str + meter_date_time: datetime.datetime + + +@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService") +async def test_create_sensors(mock_heat_meter, hass): + """Test sensor.""" + entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "123456789", + } + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + mock_heat_meter_response = MockHeatMeterResponse( + heat_usage_gj=123, + volume_usage_m3=456, + heat_previous_year_gj=111, + device_number="devicenr_789", + meter_date_time=dt_util.as_utc(datetime.datetime(2022, 5, 19, 19, 41, 17)), + ) + + mock_heat_meter().read.return_value = mock_heat_meter_response + + await hass.config_entries.async_setup(mock_entry.entry_id) + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.async_block_till_done() + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "sensor.heat_meter_heat_usage"}, + blocking=True, + ) + await hass.async_block_till_done() + + # check if 26 attributes have been created + assert len(hass.states.async_all()) == 26 + entity_reg = entity_registry.async_get(hass) + + state = hass.states.get("sensor.heat_meter_heat_usage") + assert state + assert state.state == "34.16669" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_MEGA_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + + state = hass.states.get("sensor.heat_meter_volume_usage") + assert state + assert state.state == "456" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + + state = hass.states.get("sensor.heat_meter_device_number") + assert state + assert state.state == "devicenr_789" + assert state.attributes.get(ATTR_STATE_CLASS) is None + entity_registry_entry = entity_reg.async_get("sensor.heat_meter_device_number") + assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.heat_meter_meter_date_time") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" + assert state.attributes.get(ATTR_STATE_CLASS) is None + entity_registry_entry = entity_reg.async_get("sensor.heat_meter_meter_date_time") + assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC + + +@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService") +async def test_restore_state(mock_heat_meter, hass): + """Test sensor restore state.""" + # Home assistant is not running yet + hass.state = CoreState.not_running + last_reset = "2022-07-01T00:00:00.000000+00:00" + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "sensor.heat_meter_heat_usage", + "34167", + attributes={ + ATTR_LAST_RESET: last_reset, + ATTR_UNIT_OF_MEASUREMENT: ENERGY_MEGA_WATT_HOUR, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + }, + ), + { + "native_value": 34167, + "native_unit_of_measurement": ENERGY_MEGA_WATT_HOUR, + "icon": "mdi:fire", + "last_reset": last_reset, + }, + ), + ( + State( + "sensor.heat_meter_volume_usage", + "456", + attributes={ + ATTR_LAST_RESET: last_reset, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + }, + ), + { + "native_value": 456, + "native_unit_of_measurement": VOLUME_CUBIC_METERS, + "icon": "mdi:fire", + "last_reset": last_reset, + }, + ), + ( + State( + "sensor.heat_meter_device_number", + "devicenr_789", + attributes={ + ATTR_LAST_RESET: last_reset, + }, + ), + { + "native_value": "devicenr_789", + "native_unit_of_measurement": None, + "last_reset": last_reset, + }, + ), + ], + ) + entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "123456789", + } + + # create and add entry + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data) + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.async_block_till_done() + + # restore from cache + state = hass.states.get("sensor.heat_meter_heat_usage") + assert state + assert state.state == "34167" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_MEGA_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + + state = hass.states.get("sensor.heat_meter_volume_usage") + assert state + assert state.state == "456" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + + state = hass.states.get("sensor.heat_meter_device_number") + assert state + print("STATE IS: ", state) + assert state.state == "devicenr_789" + assert state.attributes.get(ATTR_STATE_CLASS) is None From 88a5b90489af4ab98db382adaaefba41f4249b6f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 16:52:41 +0200 Subject: [PATCH 465/903] Minor improvement of zha test (#76993) --- tests/components/zha/test_config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 84290595f12..9a98b5e0caa 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -775,6 +775,7 @@ async def test_migration_ti_cc_to_znp(old_type, new_type, hass, config_entry): async def test_hardware_not_onboarded(hass): """Test hardware flow.""" data = { + "name": "Yellow", "radio_type": "efr32", "port": { "path": "/dev/ttyAMA1", @@ -790,7 +791,7 @@ async def test_hardware_not_onboarded(hass): ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "/dev/ttyAMA1" + assert result["title"] == "Yellow" assert result["data"] == { CONF_DEVICE: { CONF_BAUDRATE: 115200, From 21ebd1f612a42c72b506c32d5dc416a54611c382 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 16:58:44 +0200 Subject: [PATCH 466/903] Simplify ZHA config entry title (#76991) --- homeassistant/components/zha/config_flow.py | 2 +- tests/components/zha/test_config_flow.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 69da95e8528..94723e38d58 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -128,7 +128,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_zha_device") self._device_path = dev_path - self._title = usb.human_readable_device_name( + self._title = description or usb.human_readable_device_name( dev_path, serial_number, manufacturer, diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 9a98b5e0caa..a769303a4c4 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -237,7 +237,7 @@ async def test_discovery_via_usb(detect_mock, hass): await hass.async_block_till_done() assert result2["type"] == FlowResultType.CREATE_ENTRY - assert "zigbee radio" in result2["title"] + assert result2["title"] == "zigbee radio" assert result2["data"] == { "device": { "baudrate": 115200, @@ -273,10 +273,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): await hass.async_block_till_done() assert result2["type"] == FlowResultType.CREATE_ENTRY - assert ( - "zigate radio - /dev/ttyZIGBEE, s/n: 1234 - test - 6015:0403" - in result2["title"] - ) + assert result2["title"] == "zigate radio" assert result2["data"] == { "device": { "path": "/dev/ttyZIGBEE", From 92a9011953866ace16f07e06a0b17be2841f91f6 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Thu, 18 Aug 2022 11:17:58 -0400 Subject: [PATCH 467/903] Code quality changes for LaCrosse View (#76265) --- .../components/lacrosse_view/__init__.py | 54 ++------ .../components/lacrosse_view/config_flow.py | 44 +++++-- .../components/lacrosse_view/coordinator.py | 79 ++++++++++++ .../components/lacrosse_view/sensor.py | 122 +++++++++++------- .../components/lacrosse_view/strings.json | 3 +- .../lacrosse_view/translations/en.json | 3 +- tests/components/lacrosse_view/__init__.py | 11 ++ .../lacrosse_view/test_config_flow.py | 121 ++++++++++++++++- tests/components/lacrosse_view/test_init.py | 45 ++++++- tests/components/lacrosse_view/test_sensor.py | 32 ++++- 10 files changed, 401 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/lacrosse_view/coordinator.py diff --git a/homeassistant/components/lacrosse_view/__init__.py b/homeassistant/components/lacrosse_view/__init__.py index 0d3147f43a5..46239485eb3 100644 --- a/homeassistant/components/lacrosse_view/__init__.py +++ b/homeassistant/components/lacrosse_view/__init__.py @@ -1,18 +1,16 @@ """The LaCrosse View integration.""" from __future__ import annotations -from datetime import datetime, timedelta - -from lacrosse_view import LaCrosse, Location, LoginError, Sensor +from lacrosse_view import LaCrosse, LoginError from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, LOGGER, SCAN_INTERVAL +from .const import DOMAIN +from .coordinator import LaCrosseUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -20,52 +18,22 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LaCrosse View from a config entry.""" - async def get_data() -> list[Sensor]: - """Get the data from the LaCrosse View.""" - now = datetime.utcnow() - - if hass.data[DOMAIN][entry.entry_id]["last_update"] < now - timedelta( - minutes=59 - ): # Get new token - hass.data[DOMAIN][entry.entry_id]["last_update"] = now - await api.login(entry.data["username"], entry.data["password"]) - - # Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request) - yesterday = now - timedelta(days=1) - yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0) - yesterday_timestamp = datetime.timestamp(yesterday) - - return await api.get_sensors( - location=Location(id=entry.data["id"], name=entry.data["name"]), - tz=hass.config.time_zone, - start=str(int(yesterday_timestamp)), - end=str(int(datetime.timestamp(now))), - ) - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - "api": LaCrosse(async_get_clientsession(hass)), - "last_update": datetime.utcnow(), - } - api: LaCrosse = hass.data[DOMAIN][entry.entry_id]["api"] + api = LaCrosse(async_get_clientsession(hass)) try: await api.login(entry.data["username"], entry.data["password"]) except LoginError as error: - raise ConfigEntryNotReady from error + raise ConfigEntryAuthFailed from error - coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name="LaCrosse View", - update_method=get_data, - update_interval=timedelta(seconds=SCAN_INTERVAL), - ) + coordinator = LaCrosseUpdateCoordinator(hass, api, entry) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id]["coordinator"] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "coordinator": coordinator, + } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/lacrosse_view/config_flow.py b/homeassistant/components/lacrosse_view/config_flow.py index b5b89828e9b..2b694860bc8 100644 --- a/homeassistant/components/lacrosse_view/config_flow.py +++ b/homeassistant/components/lacrosse_view/config_flow.py @@ -1,7 +1,7 @@ """Config flow for LaCrosse View integration.""" from __future__ import annotations -import logging +from collections.abc import Mapping from typing import Any from lacrosse_view import LaCrosse, Location, LoginError @@ -13,9 +13,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, LOGGER STEP_USER_DATA_SCHEMA = vol.Schema( { @@ -47,8 +45,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for LaCrosse View.""" VERSION = 1 - data: dict[str, str] = {} - locations: list[Location] = [] + + def __init__(self) -> None: + """Initialize the config flow.""" + self.data: dict[str, str] = {} + self.locations: list[Location] = [] + self._reauth_entry: config_entries.ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -68,11 +70,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except NoLocations: errors["base"] = "no_locations" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: self.data = user_input self.locations = info + + # Check if we are reauthenticating + if self._reauth_entry is not None: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=self._reauth_entry.data | self.data + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") return await self.async_step_location() return self.async_show_form( @@ -98,11 +108,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): location_id = user_input["location"] - for location in self.locations: - if location.id == location_id: - location_name = location.name + location_name = next( + location.name for location in self.locations if location.id == location_id + ) await self.async_set_unique_id(location_id) + self._abort_if_unique_id_configured() return self.async_create_entry( @@ -115,9 +126,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Reauth in case of a password change or other error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() class InvalidAuth(HomeAssistantError): @@ -126,3 +140,7 @@ class InvalidAuth(HomeAssistantError): class NoLocations(HomeAssistantError): """Error to indicate there are no locations.""" + + +class NonExistentEntry(HomeAssistantError): + """Error to indicate that the entry does not exist when it should.""" diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py new file mode 100644 index 00000000000..5361f94d04f --- /dev/null +++ b/homeassistant/components/lacrosse_view/coordinator.py @@ -0,0 +1,79 @@ +"""DataUpdateCoordinator for LaCrosse View.""" +from __future__ import annotations + +from datetime import datetime, timedelta + +from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import LOGGER, SCAN_INTERVAL + + +class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): + """DataUpdateCoordinator for LaCrosse View.""" + + username: str + password: str + name: str + id: str + hass: HomeAssistant + + def __init__( + self, + hass: HomeAssistant, + api: LaCrosse, + entry: ConfigEntry, + ) -> None: + """Initialize DataUpdateCoordinator for LaCrosse View.""" + self.api = api + self.last_update = datetime.utcnow() + self.username = entry.data["username"] + self.password = entry.data["password"] + self.hass = hass + self.name = entry.data["name"] + self.id = entry.data["id"] + super().__init__( + hass, + LOGGER, + name="LaCrosse View", + update_interval=timedelta(seconds=SCAN_INTERVAL), + ) + + async def _async_update_data(self) -> list[Sensor]: + """Get the data for LaCrosse View.""" + now = datetime.utcnow() + + if self.last_update < now - timedelta(minutes=59): # Get new token + self.last_update = now + try: + await self.api.login(self.username, self.password) + except LoginError as error: + raise ConfigEntryAuthFailed from error + + # Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request) + yesterday = now - timedelta(days=1) + yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0) + yesterday_timestamp = datetime.timestamp(yesterday) + + try: + sensors = await self.api.get_sensors( + location=Location(id=self.id, name=self.name), + tz=self.hass.config.time_zone, + start=str(int(yesterday_timestamp)), + end=str(int(datetime.timestamp(now))), + ) + except HTTPError as error: + raise ConfigEntryNotReady from error + + # Verify that we have permission to read the sensors + for sensor in sensors: + if not sensor.permissions.get("read", False): + raise ConfigEntryAuthFailed( + f"This account does not have permission to read {sensor.name}" + ) + + return sensors diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 8ccbe0514b4..46c4671a109 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -3,16 +3,23 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from re import sub from lacrosse_view import Sensor from homeassistant.components.sensor import ( + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + PRECIPITATION_INCHES, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -27,7 +34,7 @@ from .const import DOMAIN, LOGGER class LaCrosseSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[..., float] + value_fn: Callable[[Sensor, str], float] @dataclass @@ -37,20 +44,51 @@ class LaCrosseSensorEntityDescription( """Description for LaCrosse View sensor.""" +def get_value(sensor: Sensor, field: str) -> float: + """Get the value of a sensor field.""" + return float(sensor.data[field]["values"][-1]["s"]) + + PARALLEL_UPDATES = 0 -ICON_LIST = { - "Temperature": "mdi:thermometer", - "Humidity": "mdi:water-percent", - "HeatIndex": "mdi:thermometer", - "WindSpeed": "mdi:weather-windy", - "Rain": "mdi:water", -} -UNIT_LIST = { - "degrees_celsius": "°C", - "degrees_fahrenheit": "°F", - "relative_humidity": "%", - "kilometers_per_hour": "km/h", - "inches": "in", +SENSOR_DESCRIPTIONS = { + "Temperature": LaCrosseSensorEntityDescription( + key="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Temperature", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=TEMP_CELSIUS, + ), + "Humidity": LaCrosseSensorEntityDescription( + key="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + name="Humidity", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=PERCENTAGE, + ), + "HeatIndex": LaCrosseSensorEntityDescription( + key="HeatIndex", + device_class=SensorDeviceClass.TEMPERATURE, + name="Heat Index", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=TEMP_FAHRENHEIT, + ), + "WindSpeed": LaCrosseSensorEntityDescription( + key="WindSpeed", + name="Wind Speed", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + ), + "Rain": LaCrosseSensorEntityDescription( + key="Rain", + name="Rain", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=PRECIPITATION_INCHES, + ), } @@ -66,35 +104,26 @@ async def async_setup_entry( sensors: list[Sensor] = coordinator.data sensor_list = [] - for i, sensor in enumerate(sensors): - if not sensor.permissions.get("read"): - LOGGER.warning( - "No permission to read sensor %s, are you sure you're signed into the right account?", - sensor.name, - ) - continue + for sensor in sensors: for field in sensor.sensor_field_names: + description = SENSOR_DESCRIPTIONS.get(field) + if description is None: + message = ( + f"Unsupported sensor field: {field}\nPlease create an issue on " + "GitHub. https://github.com/home-assistant/core/issues/new?assignees=&la" + "bels=&template=bug_report.yml&integration_name=LaCrosse%20View&integrat" + "ion_link=https://www.home-assistant.io/integrations/lacrosse_view/&addi" + f"tional_information=Field:%20{field}%0ASensor%20Model:%20{sensor.model}&" + f"title=LaCrosse%20View%20Unsupported%20sensor%20field:%20{field}" + ) + + LOGGER.warning(message) + continue sensor_list.append( LaCrosseViewSensor( coordinator=coordinator, - description=LaCrosseSensorEntityDescription( - key=str(i), - device_class="temperature" if field == "Temperature" else None, - # The regex is to convert CamelCase to Human Case - # e.g. "RelativeHumidity" -> "Relative Humidity" - name=f"{sensor.name} {sub(r'(? None: """Initialize.""" super().__init__(coordinator) - sensor = self.coordinator.data[int(description.key)] self.entity_description = description - self._attr_unique_id = f"{sensor.location.id}-{description.key}-{field}" + self._attr_unique_id = f"{sensor.sensor_id}-{description.key}" self._attr_name = f"{sensor.location.name} {description.name}" - self._attr_icon = ICON_LIST.get(field, "mdi:thermometer") self._attr_device_info = { "identifiers": {(DOMAIN, sensor.sensor_id)}, "name": sensor.name.split(" ")[0], @@ -129,8 +156,11 @@ class LaCrosseViewSensor( "model": sensor.model, "via_device": (DOMAIN, sensor.location.id), } + self._sensor = sensor @property - def native_value(self) -> float: + def native_value(self) -> float | str: """Return the sensor value.""" - return self.entity_description.value_fn() + return self.entity_description.value_fn( + self._sensor, self.entity_description.key + ) diff --git a/homeassistant/components/lacrosse_view/strings.json b/homeassistant/components/lacrosse_view/strings.json index 76f1971518a..160517793d8 100644 --- a/homeassistant/components/lacrosse_view/strings.json +++ b/homeassistant/components/lacrosse_view/strings.json @@ -14,7 +14,8 @@ "no_locations": "No locations found" }, "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/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json index a2a7fd23272..9fc180b0754 100644 --- a/homeassistant/components/lacrosse_view/translations/en.json +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication", diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py index ea01e7a72e3..bd4ccb17b17 100644 --- a/tests/components/lacrosse_view/__init__.py +++ b/tests/components/lacrosse_view/__init__.py @@ -30,3 +30,14 @@ TEST_NO_PERMISSION_SENSOR = Sensor( permissions={"read": False}, model="Test", ) +TEST_UNSUPPORTED_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["SomeUnsupportedField"], + location=Location(id="1", name="Test"), + data={"SomeUnsupportedField": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py index dc55f02bff8..8325cec9209 100644 --- a/tests/components/lacrosse_view/test_config_flow.py +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -6,7 +6,9 @@ from lacrosse_view import Location, LoginError from homeassistant import config_entries from homeassistant.components.lacrosse_view.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, FlowResultType +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant) -> None: @@ -20,6 +22,8 @@ async def test_form(hass: HomeAssistant) -> None: with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( "lacrosse_view.LaCrosse.get_locations", return_value=[Location(id=1, name="Test")], + ), patch( + "homeassistant.components.lacrosse_view.async_setup_entry", return_value=True ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -46,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Test" assert result3["data"] == { "username": "test-username", @@ -161,3 +165,116 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} + + +async def test_already_configured_device(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", + }, + unique_id="1", + ) + mock_config_entry.add_to_hass(hass) + + # Now that we did the config once, let's try to do it again, this should raise the abort for already configured device + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "location" + assert result2["errors"] is None + + with patch( + "homeassistant.components.lacrosse_view.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "location": "1", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test reauthentication.""" + data = { + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", + } + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + unique_id="1", + title="Test", + ) + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "title_placeholders": {"name": mock_config_entry.title}, + "unique_id": mock_config_entry.unique_id, + }, + data=data, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + new_username = "new-username" + new_password = "new-password" + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": new_username, + "password": new_password, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + assert hass.config_entries.async_entries()[0].data == { + "username": new_username, + "password": new_password, + "id": "1", + "name": "Test", + } diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index a719536f737..600fe1c9d24 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -47,11 +47,14 @@ async def test_login_error(hass: HomeAssistant) -> None: assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_RETRY + assert entries[0].state == ConfigEntryState.SETUP_ERROR + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert flows + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" async def test_http_error(hass: HomeAssistant) -> None: @@ -65,7 +68,6 @@ async def test_http_error(hass: HomeAssistant) -> None: assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 @@ -100,3 +102,40 @@ async def test_new_token(hass: HomeAssistant) -> None: await hass.async_block_till_done() login.assert_called_once() + + +async def test_failed_token(hass: HomeAssistant) -> None: + """Test if a reauth flow occurs when token refresh fails.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + login.assert_called_once() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + one_hour_after = datetime.utcnow() + timedelta(hours=1) + + with patch( + "lacrosse_view.LaCrosse.login", side_effect=LoginError("Test") + ), freeze_time(one_hour_after): + async_fire_time_changed(hass, one_hour_after) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert flows + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 57197662cc9..0e102c2f3ef 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -5,7 +5,12 @@ from homeassistant.components.lacrosse_view import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from . import MOCK_ENTRY_DATA, TEST_NO_PERMISSION_SENSOR, TEST_SENSOR +from . import ( + MOCK_ENTRY_DATA, + TEST_NO_PERMISSION_SENSOR, + TEST_SENSOR, + TEST_UNSUPPORTED_SENSOR, +) from tests.common import MockConfigEntry @@ -26,7 +31,7 @@ async def test_entities_added(hass: HomeAssistant) -> None: assert entries assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - assert hass.states.get("sensor.test_test_temperature") + assert hass.states.get("sensor.test_temperature") async def test_sensor_permission(hass: HomeAssistant, caplog) -> None: @@ -36,6 +41,25 @@ async def test_sensor_permission(hass: HomeAssistant, caplog) -> None: with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_NO_PERMISSION_SENSOR] + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.SETUP_ERROR + assert not hass.states.get("sensor.test_temperature") + assert "This account does not have permission to read Test" in caplog.text + + +async def test_field_not_supported(hass: HomeAssistant, caplog) -> None: + """Test if it raises a warning when the field is not supported.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_UNSUPPORTED_SENSOR] ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -45,5 +69,5 @@ async def test_sensor_permission(hass: HomeAssistant, caplog) -> None: assert entries assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - assert hass.states.get("sensor.test_test_temperature") is None - assert "No permission to read sensor" in caplog.text + assert hass.states.get("sensor.test_some_unsupported_field") is None + assert "Unsupported sensor field" in caplog.text From 6e92931087b27b04854d4a806136abc52a15c9d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Aug 2022 12:02:12 -0400 Subject: [PATCH 468/903] Add file selector and file upload integration (#76672) --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/file_upload/__init__.py | 182 ++++++++++++++++++ .../components/file_upload/manifest.json | 8 + .../components/frontend/manifest.json | 1 + homeassistant/helpers/selector.py | 40 +++- mypy.ini | 10 + script/hassfest/manifest.py | 1 + tests/components/file_upload/__init__.py | 1 + tests/components/file_upload/test_init.py | 66 +++++++ tests/components/image/__init__.py | 3 + tests/components/image/test_init.py | 7 +- tests/helpers/test_selector.py | 16 ++ 13 files changed, 332 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/file_upload/__init__.py create mode 100644 homeassistant/components/file_upload/manifest.json create mode 100644 tests/components/file_upload/__init__.py create mode 100644 tests/components/file_upload/test_init.py diff --git a/.strict-typing b/.strict-typing index d9cc4ffb55a..f8a0579433b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -98,6 +98,7 @@ homeassistant.components.energy.* homeassistant.components.evil_genius_labs.* homeassistant.components.fan.* homeassistant.components.fastdotcom.* +homeassistant.components.file_upload.* homeassistant.components.filesize.* homeassistant.components.fitbit.* homeassistant.components.flunearyou.* diff --git a/CODEOWNERS b/CODEOWNERS index 26d1a8da2f8..f952ae22d9b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -329,6 +329,8 @@ build.json @home-assistant/supervisor /tests/components/fibaro/ @rappenze /homeassistant/components/file/ @fabaff /tests/components/file/ @fabaff +/homeassistant/components/file_upload/ @home-assistant/core +/tests/components/file_upload/ @home-assistant/core /homeassistant/components/filesize/ @gjohansson-ST /tests/components/filesize/ @gjohansson-ST /homeassistant/components/filter/ @dgomes diff --git a/homeassistant/components/file_upload/__init__.py b/homeassistant/components/file_upload/__init__.py new file mode 100644 index 00000000000..9f548e14459 --- /dev/null +++ b/homeassistant/components/file_upload/__init__.py @@ -0,0 +1,182 @@ +"""The File Upload integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterator +from contextlib import contextmanager +from dataclasses import dataclass +from pathlib import Path +import shutil +import tempfile + +from aiohttp import web +import voluptuous as vol + +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import raise_if_invalid_filename +from homeassistant.util.ulid import ulid_hex + +DOMAIN = "file_upload" + +# If increased, change upload view to streaming +# https://docs.aiohttp.org/en/stable/web_quickstart.html#file-uploads +MAX_SIZE = 1024 * 1024 * 10 +TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" + + +@contextmanager +def process_uploaded_file(hass: HomeAssistant, file_id: str) -> Iterator[Path]: + """Get an uploaded file. + + File is removed at the end of the context. + """ + if DOMAIN not in hass.data: + raise ValueError("File does not exist") + + file_upload_data: FileUploadData = hass.data[DOMAIN] + + if not file_upload_data.has_file(file_id): + raise ValueError("File does not exist") + + try: + yield file_upload_data.file_path(file_id) + finally: + file_upload_data.files.pop(file_id) + shutil.rmtree(file_upload_data.file_dir(file_id)) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up File Upload.""" + hass.http.register_view(FileUploadView) + return True + + +@dataclass(frozen=True) +class FileUploadData: + """File upload data.""" + + temp_dir: Path + files: dict[str, str] + + @classmethod + async def create(cls, hass: HomeAssistant) -> FileUploadData: + """Initialize the file upload data.""" + + def _create_temp_dir() -> Path: + """Create temporary directory.""" + temp_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME + + # If it exists, it's an old one and Home Assistant didn't shut down correctly. + if temp_dir.exists(): + shutil.rmtree(temp_dir) + + temp_dir.mkdir(0o700) + return temp_dir + + temp_dir = await hass.async_add_executor_job(_create_temp_dir) + + def cleanup_unused_files(ev: Event) -> None: + """Clean up unused files.""" + shutil.rmtree(temp_dir) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_unused_files) + + return cls(temp_dir, {}) + + def has_file(self, file_id: str) -> bool: + """Return if file exists.""" + return file_id in self.files + + def file_dir(self, file_id: str) -> Path: + """Return the file directory.""" + return self.temp_dir / file_id + + def file_path(self, file_id: str) -> Path: + """Return the file path.""" + return self.file_dir(file_id) / self.files[file_id] + + +class FileUploadView(HomeAssistantView): + """HTTP View to upload files.""" + + url = "/api/file_upload" + name = "api:file_upload" + + _upload_lock: asyncio.Lock | None = None + + @callback + def _get_upload_lock(self) -> asyncio.Lock: + """Get upload lock.""" + if self._upload_lock is None: + self._upload_lock = asyncio.Lock() + + return self._upload_lock + + async def post(self, request: web.Request) -> web.Response: + """Upload a file.""" + async with self._get_upload_lock(): + return await self._upload_file(request) + + async def _upload_file(self, request: web.Request) -> web.Response: + """Handle uploaded file.""" + # Increase max payload + request._client_max_size = MAX_SIZE # pylint: disable=protected-access + + data = await request.post() + file_field = data.get("file") + + if not isinstance(file_field, web.FileField): + raise vol.Invalid("Expected a file") + + try: + raise_if_invalid_filename(file_field.filename) + except ValueError as err: + raise web.HTTPBadRequest from err + + hass: HomeAssistant = request.app["hass"] + file_id = ulid_hex() + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = await FileUploadData.create(hass) + + file_upload_data: FileUploadData = hass.data[DOMAIN] + file_dir = file_upload_data.file_dir(file_id) + + def _sync_work() -> None: + file_dir.mkdir() + + # MyPy forgets about the isinstance check because we're in a function scope + assert isinstance(file_field, web.FileField) + + with (file_dir / file_field.filename).open("wb") as target_fileobj: + shutil.copyfileobj(file_field.file, target_fileobj) + + await hass.async_add_executor_job(_sync_work) + + file_upload_data.files[file_id] = file_field.filename + + return self.json({"file_id": file_id}) + + @RequestDataValidator({vol.Required("file_id"): str}) + async def delete(self, request: web.Request, data: dict[str, str]) -> web.Response: + """Delete a file.""" + hass: HomeAssistant = request.app["hass"] + + if DOMAIN not in hass.data: + raise web.HTTPNotFound() + + file_id = data["file_id"] + file_upload_data: FileUploadData = hass.data[DOMAIN] + + if file_upload_data.files.pop(file_id, None) is None: + raise web.HTTPNotFound() + + await hass.async_add_executor_job( + lambda: shutil.rmtree(file_upload_data.file_dir(file_id)) + ) + + return self.json_message("File deleted") diff --git a/homeassistant/components/file_upload/manifest.json b/homeassistant/components/file_upload/manifest.json new file mode 100644 index 00000000000..6e190ba3712 --- /dev/null +++ b/homeassistant/components/file_upload/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "file_upload", + "name": "File Upload", + "documentation": "https://www.home-assistant.io/integrations/file_upload", + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 207b57babb2..f1e6f31fd4b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -9,6 +9,7 @@ "config", "device_automation", "diagnostics", + "file_upload", "http", "lovelace", "onboarding", diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index ccb7ac67dfb..deacc821672 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Sequence from typing import Any, TypedDict, cast +from uuid import UUID import voluptuous as vol @@ -10,7 +11,7 @@ from homeassistant.backports.enum import StrEnum from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.util import decorator -from homeassistant.util.yaml.dumper import add_representer, represent_odict +from homeassistant.util.yaml import dumper from . import config_validation as cv @@ -888,9 +889,42 @@ class TimeSelector(Selector): return cast(str, data) -add_representer( +class FileSelectorConfig(TypedDict): + """Class to represent a file selector config.""" + + accept: str # required + + +@SELECTORS.register("file") +class FileSelector(Selector): + """Selector of a file.""" + + selector_type = "file" + + CONFIG_SCHEMA = vol.Schema( + { + # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept + vol.Required("accept"): str, + } + ) + + def __init__(self, config: FileSelectorConfig | None = None) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + if not isinstance(data, str): + raise vol.Invalid("Value should be a string") + + UUID(data) + + return data + + +dumper.add_representer( Selector, - lambda dumper, value: represent_odict( + lambda dumper, value: dumper.represent_odict( dumper, "tag:yaml.org,2002:map", value.serialize() ), ) diff --git a/mypy.ini b/mypy.ini index 570004a14dd..051c1065423 100644 --- a/mypy.ini +++ b/mypy.ini @@ -739,6 +739,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.file_upload.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.filesize.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 1e6bd03f457..338682bbe76 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -51,6 +51,7 @@ NO_IOT_CLASS = [ "discovery", "downloader", "ffmpeg", + "file_upload", "frontend", "hardkernel", "hardware", diff --git a/tests/components/file_upload/__init__.py b/tests/components/file_upload/__init__.py new file mode 100644 index 00000000000..2630811ffc5 --- /dev/null +++ b/tests/components/file_upload/__init__.py @@ -0,0 +1 @@ +"""Tests for the File Upload integration.""" diff --git a/tests/components/file_upload/test_init.py b/tests/components/file_upload/test_init.py new file mode 100644 index 00000000000..ba3485c96e1 --- /dev/null +++ b/tests/components/file_upload/test_init.py @@ -0,0 +1,66 @@ +"""Test the File Upload integration.""" +from pathlib import Path +from random import getrandbits +from unittest.mock import patch + +import pytest + +from homeassistant.components import file_upload +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.components.image import TEST_IMAGE + + +@pytest.fixture +async def uploaded_file_dir(hass: HomeAssistant, hass_client) -> Path: + """Test uploading and using a file.""" + assert await async_setup_component(hass, "file_upload", {}) + client = await hass_client() + + with patch( + # Patch temp dir name to avoid tests fail running in parallel + "homeassistant.components.file_upload.TEMP_DIR_NAME", + file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", + ), TEST_IMAGE.open("rb") as fp: + res = await client.post("/api/file_upload", data={"file": fp}) + + assert res.status == 200 + response = await res.json() + + file_dir = hass.data[file_upload.DOMAIN].file_dir(response["file_id"]) + assert file_dir.is_dir() + return file_dir + + +async def test_using_file(hass: HomeAssistant, uploaded_file_dir): + """Test uploading and using a file.""" + # Test we can use it + with file_upload.process_uploaded_file(hass, uploaded_file_dir.name) as file_path: + assert file_path.is_file() + assert file_path.parent == uploaded_file_dir + assert file_path.read_bytes() == TEST_IMAGE.read_bytes() + + # Test it's removed + assert not uploaded_file_dir.exists() + + +async def test_removing_file(hass: HomeAssistant, hass_client, uploaded_file_dir): + """Test uploading and using a file.""" + client = await hass_client() + + response = await client.delete( + "/api/file_upload", json={"file_id": uploaded_file_dir.name} + ) + assert response.status == 200 + + # Test it's removed + assert not uploaded_file_dir.exists() + + +async def test_removed_on_stop(hass: HomeAssistant, hass_client, uploaded_file_dir): + """Test uploading and using a file.""" + await hass.async_stop() + + # Test it's removed + assert not uploaded_file_dir.exists() diff --git a/tests/components/image/__init__.py b/tests/components/image/__init__.py index 8bf90c4f516..b04214669aa 100644 --- a/tests/components/image/__init__.py +++ b/tests/components/image/__init__.py @@ -1 +1,4 @@ """Tests for the Image integration.""" +import pathlib + +TEST_IMAGE = pathlib.Path(__file__).parent / "logo.png" diff --git a/tests/components/image/test_init.py b/tests/components/image/test_init.py index ab73bb71286..d62717cb894 100644 --- a/tests/components/image/test_init.py +++ b/tests/components/image/test_init.py @@ -9,11 +9,12 @@ from homeassistant.components.websocket_api import const as ws_const from homeassistant.setup import async_setup_component from homeassistant.util import dt as util_dt +from . import TEST_IMAGE + async def test_upload_image(hass, hass_client, hass_ws_client): """Test we can upload an image.""" now = util_dt.utcnow() - test_image = pathlib.Path(__file__).parent / "logo.png" with tempfile.TemporaryDirectory() as tempdir, patch.object( hass.config, "path", return_value=tempdir @@ -22,7 +23,7 @@ async def test_upload_image(hass, hass_client, hass_ws_client): ws_client: ClientWebSocketResponse = await hass_ws_client() client: ClientSession = await hass_client() - with test_image.open("rb") as fp: + with TEST_IMAGE.open("rb") as fp: res = await client.post("/api/image/upload", data={"file": fp}) assert res.status == 200 @@ -36,7 +37,7 @@ async def test_upload_image(hass, hass_client, hass_ws_client): tempdir = pathlib.Path(tempdir) item_folder: pathlib.Path = tempdir / item["id"] - assert (item_folder / "original").read_bytes() == test_image.read_bytes() + assert (item_folder / "original").read_bytes() == TEST_IMAGE.read_bytes() # fetch non-existing image res = await client.get("/api/image/serve/non-existing/256x256") diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 4cb924a520e..d1018299d96 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -646,3 +646,19 @@ def test_datetime_selector_schema(schema, valid_selections, invalid_selections): def test_template_selector_schema(schema, valid_selections, invalid_selections): """Test template selector.""" _test_selector("template", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {"accept": "image/*"}, + ("0182a1b99dbc5ae24aecd90c346605fa",), + (None, "not-a-uuid", "abcd", 1), + ), + ), +) +def test_file_selector_schema(schema, valid_selections, invalid_selections): + """Test file selector.""" + + _test_selector("file", schema, valid_selections, invalid_selections) From f5487b3a7ea5bc58f532eb790b614f687776010f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 18 Aug 2022 18:03:10 +0200 Subject: [PATCH 469/903] Bump pyhaversion from 22.4.1 to 22.8.0 (#76994) --- homeassistant/components/version/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index cd513b16e33..52ca1ca74f8 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,7 +2,7 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==22.4.1"], + "requirements": ["pyhaversion==22.8.0"], "codeowners": ["@ludeeus"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ecaaf30280c..2a25c6da9df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1554,7 +1554,7 @@ pygtfs==0.1.6 pygti==0.9.3 # homeassistant.components.version -pyhaversion==22.4.1 +pyhaversion==22.8.0 # homeassistant.components.heos pyheos==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b72beee027..d9cfea5b10b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1076,7 +1076,7 @@ pyfttt==0.3 pygti==0.9.3 # homeassistant.components.version -pyhaversion==22.4.1 +pyhaversion==22.8.0 # homeassistant.components.heos pyheos==0.7.2 From fb5a67fb1fb1bd701d4b4f0c583afcbb0aff1ec2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 19:22:08 +0200 Subject: [PATCH 470/903] Add vacuum checks to pylint plugin (#76560) --- pylint/plugins/hass_enforce_type_hints.py | 136 +++++++++++++++++++++- tests/pylint/test_enforce_type_hints.py | 46 +++++++- 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 19a69f8808f..4eedca487ab 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -55,11 +55,12 @@ class ClassTypeHintMatch: matches: list[TypeHintMatch] +_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\]))" _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" - "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), + # or "dict | list | None" + "a_or_b": re.compile(rf"^(.+) \| {_INNER_MATCH}$"), } -_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\]))" _INNER_MATCH_POSSIBILITIES = [i + 1 for i in range(5)] _TYPE_HINT_MATCHERS.update( { @@ -2118,6 +2119,137 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "vacuum": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="_BaseVacuum", + matches=[ + TypeHintMatch( + function_name="battery_level", + return_type=["int", None], + ), + TypeHintMatch( + function_name="battery_icon", + return_type="str", + ), + TypeHintMatch( + function_name="fan_speed", + return_type=["str", None], + ), + TypeHintMatch( + function_name="fan_speed_list", + return_type="list[str]", + ), + TypeHintMatch( + function_name="stop", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="return_to_base", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="clean_spot", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="locate", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_fan_speed", + named_arg_types={ + "fan_speed": "str", + }, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="send_command", + named_arg_types={ + "command": "str", + "params": "dict[str, Any] | list[Any] | None", + }, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ClassTypeHintMatch( + base_class="VacuumEntity", + matches=[ + TypeHintMatch( + function_name="status", + return_type=["str", None], + ), + TypeHintMatch( + function_name="start_pause", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="async_pause", + return_type=None, + ), + TypeHintMatch( + function_name="async_start", + return_type=None, + ), + ], + ), + ClassTypeHintMatch( + base_class="StateVacuumEntity", + matches=[ + TypeHintMatch( + function_name="state", + return_type=["str", None], + ), + TypeHintMatch( + function_name="start", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="pause", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="async_turn_on", + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="async_turn_off", + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="async_toggle", + kwargs_type="Any", + return_type=None, + ), + ], + ), + ], "water_heater": [ ClassTypeHintMatch( base_class="Entity", diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 1381ed34a7b..ebea738edc4 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -73,7 +73,12 @@ def test_regex_x_of_y_i( @pytest.mark.parametrize( ("string", "expected_a", "expected_b"), - [("DiscoveryInfoType | None", "DiscoveryInfoType", "None")], + [ + ("DiscoveryInfoType | None", "DiscoveryInfoType", "None"), + ("dict | list | None", "dict | list", "None"), + ("dict[str, Any] | list[Any] | None", "dict[str, Any] | list[Any]", "None"), + ("dict[str, Any] | list[Any]", "dict[str, Any]", "list[Any]"), + ], ) def test_regex_a_or_b( hass_enforce_type_hints: ModuleType, string: str, expected_a: str, expected_b: str @@ -967,3 +972,42 @@ def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) - with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_vacuum_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: + """Ensure valid hints are accepted for vacuum entity.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + # Ensure that `dict | list | None` is valid for params + class_node = astroid.extract_node( + """ + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class _BaseVacuum(Entity): + pass + + class VacuumEntity(_BaseVacuum, ToggleEntity): + pass + + class MyVacuum( #@ + VacuumEntity + ): + def send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: + pass + """, + "homeassistant.components.pylint_test.vacuum", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) From bb74730e96dff382c2f79c65e48eda767ad899fc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 21:52:12 +0200 Subject: [PATCH 471/903] Add support for USB dongles to the hardware integration (#76795) * Add support for USB dongles to the hardware integration * Update hardware integrations * Adjust tests * Add USB discovery for SkyConnect 1.0 * Improve test coverage * Apply suggestions from code review Co-authored-by: Paulus Schoutsen * Fix frozen dataclass shizzle * Adjust test Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 + .../components/hardkernel/hardware.py | 1 + homeassistant/components/hardware/models.py | 14 +- .../homeassistant_sky_connect/__init__.py | 35 +++++ .../homeassistant_sky_connect/config_flow.py | 37 +++++ .../homeassistant_sky_connect/const.py | 3 + .../homeassistant_sky_connect/hardware.py | 33 ++++ .../homeassistant_sky_connect/manifest.json | 17 ++ .../homeassistant_yellow/hardware.py | 1 + .../components/raspberry_pi/hardware.py | 1 + homeassistant/components/usb/__init__.py | 53 ++++--- homeassistant/components/zha/config_flow.py | 10 +- script/hassfest/manifest.py | 1 + tests/components/hardkernel/test_hardware.py | 1 + .../homeassistant_sky_connect/__init__.py | 1 + .../homeassistant_sky_connect/conftest.py | 14 ++ .../test_config_flow.py | 147 ++++++++++++++++++ .../test_hardware.py | 85 ++++++++++ .../homeassistant_sky_connect/test_init.py | 101 ++++++++++++ .../homeassistant_yellow/test_hardware.py | 1 + .../components/raspberry_pi/test_hardware.py | 1 + tests/components/usb/test_init.py | 42 +++++ tests/components/zha/test_config_flow.py | 8 +- 23 files changed, 581 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/homeassistant_sky_connect/__init__.py create mode 100644 homeassistant/components/homeassistant_sky_connect/config_flow.py create mode 100644 homeassistant/components/homeassistant_sky_connect/const.py create mode 100644 homeassistant/components/homeassistant_sky_connect/hardware.py create mode 100644 homeassistant/components/homeassistant_sky_connect/manifest.json create mode 100644 tests/components/homeassistant_sky_connect/__init__.py create mode 100644 tests/components/homeassistant_sky_connect/conftest.py create mode 100644 tests/components/homeassistant_sky_connect/test_config_flow.py create mode 100644 tests/components/homeassistant_sky_connect/test_hardware.py create mode 100644 tests/components/homeassistant_sky_connect/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index f952ae22d9b..ea9e47a9423 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -467,6 +467,8 @@ build.json @home-assistant/supervisor /tests/components/homeassistant/ @home-assistant/core /homeassistant/components/homeassistant_alerts/ @home-assistant/core /tests/components/homeassistant_alerts/ @home-assistant/core +/homeassistant/components/homeassistant_sky_connect/ @home-assistant/core +/tests/components/homeassistant_sky_connect/ @home-assistant/core /homeassistant/components/homeassistant_yellow/ @home-assistant/core /tests/components/homeassistant_yellow/ @home-assistant/core /homeassistant/components/homekit/ @bdraco diff --git a/homeassistant/components/hardkernel/hardware.py b/homeassistant/components/hardkernel/hardware.py index 804f105f2ed..ad45e3ac946 100644 --- a/homeassistant/components/hardkernel/hardware.py +++ b/homeassistant/components/hardkernel/hardware.py @@ -34,6 +34,7 @@ def async_info(hass: HomeAssistant) -> HardwareInfo: model=board, revision=None, ), + dongles=None, name=BOARD_NAMES.get(board, f"Unknown hardkernel Odroid model '{board}'"), url=None, ) diff --git a/homeassistant/components/hardware/models.py b/homeassistant/components/hardware/models.py index 067c2d955df..8f9819a853d 100644 --- a/homeassistant/components/hardware/models.py +++ b/homeassistant/components/hardware/models.py @@ -17,12 +17,24 @@ class BoardInfo: revision: str | None -@dataclass +@dataclass(frozen=True) +class USBInfo: + """USB info type.""" + + vid: str + pid: str + serial_number: str | None + manufacturer: str | None + description: str | None + + +@dataclass(frozen=True) class HardwareInfo: """Hardware info type.""" name: str | None board: BoardInfo | None + dongles: list[USBInfo] | None url: str | None diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py new file mode 100644 index 00000000000..981e96ccdee --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -0,0 +1,35 @@ +"""The Home Assistant Sky Connect integration.""" +from __future__ import annotations + +from homeassistant.components import usb +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Home Assistant Sky Connect config entry.""" + usb_info = usb.UsbServiceInfo( + device=entry.data["device"], + vid=entry.data["vid"], + pid=entry.data["pid"], + serial_number=entry.data["serial_number"], + manufacturer=entry.data["manufacturer"], + description=entry.data["description"], + ) + if not usb.async_is_plugged_in(hass, entry.data): + # The USB dongle is not plugged in + raise ConfigEntryNotReady + + await hass.config_entries.flow.async_init( + "zha", + context={"source": "usb"}, + data=usb_info, + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return True diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py new file mode 100644 index 00000000000..21cc5e3ace4 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow for the Home Assistant Sky Connect integration.""" +from __future__ import annotations + +from homeassistant.components import usb +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Home Assistant Sky Connect.""" + + VERSION = 1 + + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + """Handle usb discovery.""" + device = discovery_info.device + vid = discovery_info.vid + pid = discovery_info.pid + serial_number = discovery_info.serial_number + manufacturer = discovery_info.manufacturer + description = discovery_info.description + unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" + if await self.async_set_unique_id(unique_id): + self._abort_if_unique_id_configured(updates={"device": device}) + return self.async_create_entry( + title="Home Assistant Sky Connect", + data={ + "device": device, + "vid": vid, + "pid": pid, + "serial_number": serial_number, + "manufacturer": manufacturer, + "description": description, + }, + ) diff --git a/homeassistant/components/homeassistant_sky_connect/const.py b/homeassistant/components/homeassistant_sky_connect/const.py new file mode 100644 index 00000000000..1deb8fd4603 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/const.py @@ -0,0 +1,3 @@ +"""Constants for the Home Assistant Sky Connect integration.""" + +DOMAIN = "homeassistant_sky_connect" diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py new file mode 100644 index 00000000000..3c1993bfd8b --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -0,0 +1,33 @@ +"""The Home Assistant Sky Connect hardware platform.""" +from __future__ import annotations + +from homeassistant.components.hardware.models import HardwareInfo, USBInfo +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN + +DONGLE_NAME = "Home Assistant Sky Connect" + + +@callback +def async_info(hass: HomeAssistant) -> HardwareInfo: + """Return board info.""" + entries = hass.config_entries.async_entries(DOMAIN) + + dongles = [ + USBInfo( + vid=entry.data["vid"], + pid=entry.data["pid"], + serial_number=entry.data["serial_number"], + manufacturer=entry.data["manufacturer"], + description=entry.data["description"], + ) + for entry in entries + ] + + return HardwareInfo( + board=None, + dongles=dongles, + name=DONGLE_NAME, + url=None, + ) diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json new file mode 100644 index 00000000000..5ccb8bd5331 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "homeassistant_sky_connect", + "name": "Home Assistant Sky Connect", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", + "dependencies": ["hardware", "usb"], + "codeowners": ["@home-assistant/core"], + "integration_type": "hardware", + "usb": [ + { + "vid": "10C4", + "pid": "EA60", + "description": "*skyconnect v1.0*", + "known_devices": ["SkyConnect v1.0"] + } + ] +} diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py index aa1fe4b745b..01aee032a22 100644 --- a/homeassistant/components/homeassistant_yellow/hardware.py +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -29,6 +29,7 @@ def async_info(hass: HomeAssistant) -> HardwareInfo: model=MODEL, revision=None, ), + dongles=None, name=BOARD_NAME, url=None, ) diff --git a/homeassistant/components/raspberry_pi/hardware.py b/homeassistant/components/raspberry_pi/hardware.py index 343ba69d76b..cd1b56ba789 100644 --- a/homeassistant/components/raspberry_pi/hardware.py +++ b/homeassistant/components/raspberry_pi/hardware.py @@ -49,6 +49,7 @@ def async_info(hass: HomeAssistant) -> HardwareInfo: model=MODELS.get(board), revision=None, ), + dongles=None, name=BOARD_NAMES.get(board, f"Unknown Raspberry Pi model '{board}'"), url=None, ) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 5783401df13..83c7a6a8a45 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -1,7 +1,7 @@ """The USB Discovery integration.""" from __future__ import annotations -from collections.abc import Coroutine +from collections.abc import Coroutine, Mapping import dataclasses import fnmatch import logging @@ -97,6 +97,27 @@ def _fnmatch_lower(name: str | None, pattern: str) -> bool: return fnmatch.fnmatch(name.lower(), pattern) +def _is_matching(device: USBDevice, matcher: Mapping[str, str]) -> bool: + """Return True if a device matches.""" + if "vid" in matcher and device.vid != matcher["vid"]: + return False + if "pid" in matcher and device.pid != matcher["pid"]: + return False + if "serial_number" in matcher and not _fnmatch_lower( + device.serial_number, matcher["serial_number"] + ): + return False + if "manufacturer" in matcher and not _fnmatch_lower( + device.manufacturer, matcher["manufacturer"] + ): + return False + if "description" in matcher and not _fnmatch_lower( + device.description, matcher["description"] + ): + return False + return True + + class USBDiscovery: """Manage USB Discovery.""" @@ -179,23 +200,8 @@ class USBDiscovery: self.seen.add(device_tuple) matched = [] for matcher in self.usb: - if "vid" in matcher and device.vid != matcher["vid"]: - continue - if "pid" in matcher and device.pid != matcher["pid"]: - continue - if "serial_number" in matcher and not _fnmatch_lower( - device.serial_number, matcher["serial_number"] - ): - continue - if "manufacturer" in matcher and not _fnmatch_lower( - device.manufacturer, matcher["manufacturer"] - ): - continue - if "description" in matcher and not _fnmatch_lower( - device.description, matcher["description"] - ): - continue - matched.append(matcher) + if _is_matching(device, matcher): + matched.append(matcher) if not matched: return @@ -265,3 +271,14 @@ async def websocket_usb_scan( if not usb_discovery.observer_active: await usb_discovery.async_request_scan_serial() connection.send_result(msg["id"]) + + +@callback +def async_is_plugged_in(hass: HomeAssistant, matcher: Mapping) -> bool: + """Return True is a USB device is present.""" + usb_discovery: USBDiscovery = hass.data[DOMAIN] + for device_tuple in usb_discovery.seen: + device = USBDevice(*device_tuple) + if _is_matching(device, matcher): + return True + return False diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 94723e38d58..4b90fdb3ad0 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -138,11 +138,11 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._set_confirm_only() self.context["title_placeholders"] = {CONF_NAME: self._title} - return await self.async_step_confirm() + return await self.async_step_confirm_usb() - async def async_step_confirm(self, user_input=None): - """Confirm a discovery.""" - if user_input is not None: + async def async_step_confirm_usb(self, user_input=None): + """Confirm a USB discovery.""" + if user_input is not None or not onboarding.async_is_onboarded(self.hass): auto_detected_data = await detect_radios(self._device_path) if auto_detected_data is None: # This path probably will not happen now that we have @@ -155,7 +155,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="confirm", + step_id="confirm_usb", description_placeholders={CONF_NAME: self._title}, ) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 338682bbe76..b0b4f5b0582 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -58,6 +58,7 @@ NO_IOT_CLASS = [ "history", "homeassistant", "homeassistant_alerts", + "homeassistant_sky_connect", "homeassistant_yellow", "image", "input_boolean", diff --git a/tests/components/hardkernel/test_hardware.py b/tests/components/hardkernel/test_hardware.py index 1c71959719c..5f33cb417f2 100644 --- a/tests/components/hardkernel/test_hardware.py +++ b/tests/components/hardkernel/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "odroid-n2", "revision": None, }, + "dongles": None, "name": "Home Assistant Blue / Hardkernel Odroid-N2", "url": None, } diff --git a/tests/components/homeassistant_sky_connect/__init__.py b/tests/components/homeassistant_sky_connect/__init__.py new file mode 100644 index 00000000000..90cd1594710 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant Sky Connect integration.""" diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py new file mode 100644 index 00000000000..cc606c9b988 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -0,0 +1,14 @@ +"""Test fixtures for the Home Assistant Sky Connect integration.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_zha(): + """Mock the zha integration.""" + with patch( + "homeassistant.components.zha.async_setup_entry", + return_value=True, + ): + yield diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py new file mode 100644 index 00000000000..1db305f3ad0 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -0,0 +1,147 @@ +"""Test the Home Assistant Sky Connect config flow.""" +import copy +from unittest.mock import patch + +from homeassistant.components import homeassistant_sky_connect, usb +from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +USB_DATA = usb.UsbServiceInfo( + device="bla_device", + vid="bla_vid", + pid="bla_pid", + serial_number="bla_serial_number", + manufacturer="bla_manufacturer", + description="bla_description", +) + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + # mock_integration(hass, MockModule("hassio")) + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=USB_DATA + ) + + expected_data = { + "device": USB_DATA.device, + "vid": USB_DATA.vid, + "pid": USB_DATA.pid, + "serial_number": USB_DATA.serial_number, + "manufacturer": USB_DATA.manufacturer, + "description": USB_DATA.description, + } + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Home Assistant Sky Connect" + assert result["data"] == expected_data + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == expected_data + assert config_entry.options == {} + assert config_entry.title == "Home Assistant Sky Connect" + assert ( + config_entry.unique_id + == f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}" + ) + + +async def test_config_flow_unique_id(hass: HomeAssistant) -> None: + """Test only a single entry is allowed for a dongle.""" + # Setup an existing config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=USB_DATA + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + mock_setup_entry.assert_not_called() + + +async def test_config_flow_multiple_entries(hass: HomeAssistant) -> None: + """Test multiple entries are allowed.""" + # Setup an existing config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + config_entry.add_to_hass(hass) + + usb_data = copy.copy(USB_DATA) + usb_data.serial_number = "bla_serial_number_2" + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_config_flow_update_device(hass: HomeAssistant) -> None: + """Test updating device path.""" + # Setup an existing config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + config_entry.add_to_hass(hass) + + usb_data = copy.copy(USB_DATA) + usb_data.device = "bla_device_2" + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + assert await hass.config_entries.async_setup(config_entry.entry_id) + assert len(mock_setup_entry.mock_calls) == 1 + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.homeassistant_sky_connect.async_unload_entry", + wraps=homeassistant_sky_connect.async_unload_entry, + ) as mock_unload_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_unload_entry.mock_calls) == 1 diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py new file mode 100644 index 00000000000..f4e48d56a67 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -0,0 +1,85 @@ +"""Test the Home Assistant Sky Connect hardware platform.""" +from unittest.mock import patch + +from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +CONFIG_ENTRY_DATA = { + "device": "bla_device", + "vid": "bla_vid", + "pid": "bla_pid", + "serial_number": "bla_serial_number", + "manufacturer": "bla_manufacturer", + "description": "bla_description", +} + +CONFIG_ENTRY_DATA_2 = { + "device": "bla_device_2", + "vid": "bla_vid_2", + "pid": "bla_pid_2", + "serial_number": "bla_serial_number_2", + "manufacturer": "bla_manufacturer_2", + "description": "bla_description_2", +} + + +async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get the board info.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id="unique_1", + ) + config_entry.add_to_hass(hass) + config_entry_2 = MockConfigEntry( + data=CONFIG_ENTRY_DATA_2, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id="unique_2", + ) + config_entry_2.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == { + "hardware": [ + { + "board": None, + "dongles": [ + { + "vid": "bla_vid", + "pid": "bla_pid", + "serial_number": "bla_serial_number", + "manufacturer": "bla_manufacturer", + "description": "bla_description", + }, + { + "vid": "bla_vid_2", + "pid": "bla_pid_2", + "serial_number": "bla_serial_number_2", + "manufacturer": "bla_manufacturer_2", + "description": "bla_description_2", + }, + ], + "name": "Home Assistant Sky Connect", + "url": None, + } + ] + } diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py new file mode 100644 index 00000000000..74c1b9cb14f --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -0,0 +1,101 @@ +"""Test the Home Assistant Sky Connect integration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +CONFIG_ENTRY_DATA = { + "device": "bla_device", + "vid": "bla_vid", + "pid": "bla_pid", + "serial_number": "bla_serial_number", + "manufacturer": "bla_manufacturer", + "description": "bla_description", +} + + +@pytest.mark.parametrize( + "onboarded, num_entries, num_flows", ((False, 1, 0), (True, 0, 1)) +) +async def test_setup_entry( + hass: HomeAssistant, onboarded, num_entries, num_flows +) -> None: + """Test setup of a config entry, including setup of zha.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=onboarded + ), patch( + "zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.mock_calls) == 1 + + assert len(hass.config_entries.async_entries("zha")) == num_entries + assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows + + +async def test_setup_zha(hass: HomeAssistant) -> None: + """Test zha gets the right config.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries("zha")[0] + assert config_entry.data == { + "device": {"baudrate": 115200, "flow_control": None, "path": "bla_device"}, + "radio_type": "znp", + } + assert config_entry.options == {} + assert config_entry.title == "bla_description" + + +async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: + """Test setup of a config entry when the dongle is not plugged in.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=False, + ) as mock_is_plugged_in: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.mock_calls) == 1 + assert config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 28403334ec1..295e44c6ce7 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "yellow", "revision": None, }, + "dongles": None, "name": "Home Assistant Yellow", "url": None, } diff --git a/tests/components/raspberry_pi/test_hardware.py b/tests/components/raspberry_pi/test_hardware.py index a4e938079d3..ad9533e8af5 100644 --- a/tests/components/raspberry_pi/test_hardware.py +++ b/tests/components/raspberry_pi/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "1", "revision": None, }, + "dongles": None, "name": "Raspberry Pi", "url": None, } diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index f4245a0e0d6..0d1ad36a9f4 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -833,3 +833,45 @@ def test_human_readable_device_name(): assert "Silicon Labs" in name assert "10C4" in name assert "8A2A" in name + + +async def test_async_is_plugged_in(hass, hass_ws_client): + """Test async_is_plugged_in.""" + new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] + + mock_comports = [ + MagicMock( + device=slae_sh_device.device, + vid=12345, + pid=12345, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ) + ] + + matcher = { + "vid": "3039", + "pid": "3039", + } + + with patch("pyudev.Context", side_effect=ImportError), patch( + "homeassistant.components.usb.async_get_usb", return_value=new_usb + ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert not usb.async_is_plugged_in(hass, matcher) + + with patch( + "homeassistant.components.usb.comports", return_value=mock_comports + ), patch.object(hass.config_entries.flow, "async_init"): + ws_client = await hass_ws_client(hass) + await ws_client.send_json({"id": 1, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + assert usb.async_is_plugged_in(hass, matcher) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index a769303a4c4..82c2fde7c1e 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -228,7 +228,7 @@ async def test_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "confirm_usb" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -264,7 +264,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "confirm_usb" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -298,7 +298,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "confirm_usb" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -451,7 +451,7 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "confirm_usb" @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) From eec45c1208f6bba1909487865cfecba947741369 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:21:19 +0200 Subject: [PATCH 472/903] Adjust type hints in august sensor entity (#76992) --- homeassistant/components/august/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 8a6d169fcb0..af79cad459e 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -226,7 +226,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): return attributes - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log.""" await super().async_added_to_hass() @@ -234,7 +234,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): if not last_state or last_state.state == STATE_UNAVAILABLE: return - self._attr_state = last_state.state + self._attr_native_value = last_state.state if ATTR_ENTITY_PICTURE in last_state.attributes: self._entity_picture = last_state.attributes[ATTR_ENTITY_PICTURE] if ATTR_OPERATION_REMOTE in last_state.attributes: From b8d8d5540e812bd5b0148e72952a0b9d7ad18872 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 18 Aug 2022 22:35:28 +0200 Subject: [PATCH 473/903] P1 Monitor add water meter support (#74004) --- .../components/p1_monitor/__init__.py | 22 +- homeassistant/components/p1_monitor/const.py | 7 +- .../components/p1_monitor/diagnostics.py | 15 +- .../components/p1_monitor/manifest.json | 2 +- homeassistant/components/p1_monitor/sensor.py | 427 ++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/p1_monitor/conftest.py | 9 +- .../p1_monitor/fixtures/watermeter.json | 10 + .../components/p1_monitor/test_config_flow.py | 14 +- .../components/p1_monitor/test_diagnostics.py | 5 + tests/components/p1_monitor/test_init.py | 2 +- tests/components/p1_monitor/test_sensor.py | 81 +++- 13 files changed, 367 insertions(+), 231 deletions(-) create mode 100644 tests/components/p1_monitor/fixtures/watermeter.json diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index 03055013345..b157f3e8116 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -3,7 +3,14 @@ from __future__ import annotations from typing import TypedDict -from p1monitor import P1Monitor, Phases, Settings, SmartMeter +from p1monitor import ( + P1Monitor, + P1MonitorNoDataError, + Phases, + Settings, + SmartMeter, + WaterMeter, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform @@ -19,6 +26,7 @@ from .const import ( SERVICE_PHASES, SERVICE_SETTINGS, SERVICE_SMARTMETER, + SERVICE_WATERMETER, ) PLATFORMS = [Platform.SENSOR] @@ -55,12 +63,14 @@ class P1MonitorData(TypedDict): smartmeter: SmartMeter phases: Phases settings: Settings + watermeter: WaterMeter | None class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]): """Class to manage fetching P1 Monitor data from single endpoint.""" config_entry: ConfigEntry + has_water_meter: bool | None = None def __init__( self, @@ -84,6 +94,16 @@ class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]): SERVICE_SMARTMETER: await self.p1monitor.smartmeter(), SERVICE_PHASES: await self.p1monitor.phases(), SERVICE_SETTINGS: await self.p1monitor.settings(), + SERVICE_WATERMETER: None, } + if self.has_water_meter or self.has_water_meter is None: + try: + data[SERVICE_WATERMETER] = await self.p1monitor.watermeter() + self.has_water_meter = True + except P1MonitorNoDataError: + LOGGER.debug("No watermeter data received from P1 Monitor") + if self.has_water_meter is None: + self.has_water_meter = False + return data diff --git a/homeassistant/components/p1_monitor/const.py b/homeassistant/components/p1_monitor/const.py index d72927a80f6..045301d38c4 100644 --- a/homeassistant/components/p1_monitor/const.py +++ b/homeassistant/components/p1_monitor/const.py @@ -10,11 +10,6 @@ LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=5) SERVICE_SMARTMETER: Final = "smartmeter" +SERVICE_WATERMETER: Final = "watermeter" SERVICE_PHASES: Final = "phases" SERVICE_SETTINGS: Final = "settings" - -SERVICES: dict[str, str] = { - SERVICE_SMARTMETER: "SmartMeter", - SERVICE_PHASES: "Phases", - SERVICE_SETTINGS: "Settings", -} diff --git a/homeassistant/components/p1_monitor/diagnostics.py b/homeassistant/components/p1_monitor/diagnostics.py index b99cc7b86e1..29f48d47cd9 100644 --- a/homeassistant/components/p1_monitor/diagnostics.py +++ b/homeassistant/components/p1_monitor/diagnostics.py @@ -10,7 +10,13 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from . import P1MonitorDataUpdateCoordinator -from .const import DOMAIN, SERVICE_PHASES, SERVICE_SETTINGS, SERVICE_SMARTMETER +from .const import ( + DOMAIN, + SERVICE_PHASES, + SERVICE_SETTINGS, + SERVICE_SMARTMETER, + SERVICE_WATERMETER, +) TO_REDACT = { CONF_HOST, @@ -23,7 +29,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: P1MonitorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - return { + data = { "entry": { "title": entry.title, "data": async_redact_data(entry.data, TO_REDACT), @@ -34,3 +40,8 @@ async def async_get_config_entry_diagnostics( "settings": asdict(coordinator.data[SERVICE_SETTINGS]), }, } + + if coordinator.has_water_meter: + data["data"]["watermeter"] = asdict(coordinator.data[SERVICE_WATERMETER]) + + return data diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index c94893f61fd..626cff15dfa 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -3,7 +3,7 @@ "name": "P1 Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/p1_monitor", - "requirements": ["p1monitor==1.0.1"], + "requirements": ["p1monitor==2.1.0"], "codeowners": ["@klaasnicolaas"], "quality_scale": "platinum", "iot_class": "local_polling", diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 57f6b0ad99c..757bf249ca1 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, POWER_WATT, VOLUME_CUBIC_METERS, + VOLUME_LITERS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType @@ -33,206 +34,256 @@ from .const import ( SERVICE_PHASES, SERVICE_SETTINGS, SERVICE_SMARTMETER, - SERVICES, + SERVICE_WATERMETER, ) -SENSORS: dict[ - Literal["smartmeter", "phases", "settings"], tuple[SensorEntityDescription, ...] -] = { - SERVICE_SMARTMETER: ( - SensorEntityDescription( - key="gas_consumption", - name="Gas Consumption", - entity_registry_enabled_default=False, - native_unit_of_measurement=VOLUME_CUBIC_METERS, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="power_consumption", - name="Power Consumption", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="energy_consumption_high", - name="Energy Consumption - High Tariff", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="energy_consumption_low", - name="Energy Consumption - Low Tariff", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="power_production", - name="Power Production", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="energy_production_high", - name="Energy Production - High Tariff", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="energy_production_low", - name="Energy Production - Low Tariff", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="energy_tariff_period", - name="Energy Tariff Period", - icon="mdi:calendar-clock", - ), +SENSORS_SMARTMETER: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="gas_consumption", + name="Gas Consumption", + entity_registry_enabled_default=False, + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, ), - SERVICE_PHASES: ( - SensorEntityDescription( - key="voltage_phase_l1", - name="Voltage Phase L1", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="voltage_phase_l2", - name="Voltage Phase L2", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="voltage_phase_l3", - name="Voltage Phase L3", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="current_phase_l1", - name="Current Phase L1", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="current_phase_l2", - name="Current Phase L2", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="current_phase_l3", - name="Current Phase L3", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_consumed_phase_l1", - name="Power Consumed Phase L1", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_consumed_phase_l2", - name="Power Consumed Phase L2", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_consumed_phase_l3", - name="Power Consumed Phase L3", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_produced_phase_l1", - name="Power Produced Phase L1", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_produced_phase_l2", - name="Power Produced Phase L2", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_produced_phase_l3", - name="Power Produced Phase L3", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), + SensorEntityDescription( + key="power_consumption", + name="Power Consumption", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), - SERVICE_SETTINGS: ( - SensorEntityDescription( - key="gas_consumption_price", - name="Gas Consumption Price", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}", - ), - SensorEntityDescription( - key="energy_consumption_price_low", - name="Energy Consumption Price - Low", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", - ), - SensorEntityDescription( - key="energy_consumption_price_high", - name="Energy Consumption Price - High", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", - ), - SensorEntityDescription( - key="energy_production_price_low", - name="Energy Production Price - Low", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", - ), - SensorEntityDescription( - key="energy_production_price_high", - name="Energy Production Price - High", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", - ), + SensorEntityDescription( + key="energy_consumption_high", + name="Energy Consumption - High Tariff", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), -} + SensorEntityDescription( + key="energy_consumption_low", + name="Energy Consumption - Low Tariff", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="power_production", + name="Power Production", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="energy_production_high", + name="Energy Production - High Tariff", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_production_low", + name="Energy Production - Low Tariff", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_tariff_period", + name="Energy Tariff Period", + icon="mdi:calendar-clock", + ), +) + +SENSORS_PHASES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="voltage_phase_l1", + name="Voltage Phase L1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_phase_l2", + name="Voltage Phase L2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_phase_l3", + name="Voltage Phase L3", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="current_phase_l1", + name="Current Phase L1", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="current_phase_l2", + name="Current Phase L2", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="current_phase_l3", + name="Current Phase L3", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_consumed_phase_l1", + name="Power Consumed Phase L1", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_consumed_phase_l2", + name="Power Consumed Phase L2", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_consumed_phase_l3", + name="Power Consumed Phase L3", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_produced_phase_l1", + name="Power Produced Phase L1", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_produced_phase_l2", + name="Power Produced Phase L2", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_produced_phase_l3", + name="Power Produced Phase L3", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), +) + +SENSORS_SETTINGS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="gas_consumption_price", + name="Gas Consumption Price", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}", + ), + SensorEntityDescription( + key="energy_consumption_price_low", + name="Energy Consumption Price - Low", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + ), + SensorEntityDescription( + key="energy_consumption_price_high", + name="Energy Consumption Price - High", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + ), + SensorEntityDescription( + key="energy_production_price_low", + name="Energy Production Price - Low", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + ), + SensorEntityDescription( + key="energy_production_price_high", + name="Energy Production Price - High", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + ), +) + +SENSORS_WATERMETER: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="consumption_day", + name="Consumption Day", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=VOLUME_LITERS, + ), + SensorEntityDescription( + key="consumption_total", + name="Consumption Total", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=VOLUME_CUBIC_METERS, + ), + SensorEntityDescription( + key="pulse_count", + name="Pulse Count", + ), +) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up P1 Monitor Sensors based on a config entry.""" - async_add_entities( + coordinator: P1MonitorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + entities: list[P1MonitorSensorEntity] = [] + entities.extend( P1MonitorSensorEntity( - coordinator=hass.data[DOMAIN][entry.entry_id], + coordinator=coordinator, description=description, - service_key=service_key, - name=entry.title, - service=SERVICES[service_key], + name="SmartMeter", + service_key="smartmeter", + service=SERVICE_SMARTMETER, ) - for service_key, service_sensors in SENSORS.items() - for description in service_sensors + for description in SENSORS_SMARTMETER ) + entities.extend( + P1MonitorSensorEntity( + coordinator=coordinator, + description=description, + name="Phases", + service_key="phases", + service=SERVICE_PHASES, + ) + for description in SENSORS_PHASES + ) + entities.extend( + P1MonitorSensorEntity( + coordinator=coordinator, + description=description, + name="Settings", + service_key="settings", + service=SERVICE_SETTINGS, + ) + for description in SENSORS_SETTINGS + ) + if coordinator.has_water_meter: + entities.extend( + P1MonitorSensorEntity( + coordinator=coordinator, + description=description, + name="WaterMeter", + service_key="watermeter", + service=SERVICE_WATERMETER, + ) + for description in SENSORS_WATERMETER + ) + async_add_entities(entities) class P1MonitorSensorEntity( @@ -245,7 +296,7 @@ class P1MonitorSensorEntity( *, coordinator: P1MonitorDataUpdateCoordinator, description: SensorEntityDescription, - service_key: Literal["smartmeter", "phases", "settings"], + service_key: Literal["smartmeter", "watermeter", "phases", "settings"], name: str, service: str, ) -> None: @@ -253,7 +304,7 @@ class P1MonitorSensorEntity( super().__init__(coordinator=coordinator) self._service_key = service_key - self.entity_id = f"{SENSOR_DOMAIN}.{name}_{description.key}" + self.entity_id = f"{SENSOR_DOMAIN}.{service}_{description.key}" self.entity_description = description self._attr_unique_id = ( f"{coordinator.config_entry.entry_id}_{service_key}_{description.key}" @@ -266,7 +317,7 @@ class P1MonitorSensorEntity( }, configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", manufacturer="P1 Monitor", - name=service, + name=name, ) @property diff --git a/requirements_all.txt b/requirements_all.txt index 2a25c6da9df..322d67a94df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1206,7 +1206,7 @@ orvibo==1.1.1 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==1.0.1 +p1monitor==2.1.0 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9cfea5b10b..8c67a756157 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -842,7 +842,7 @@ openerz-api==0.1.0 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==1.0.1 +p1monitor==2.1.0 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/tests/components/p1_monitor/conftest.py b/tests/components/p1_monitor/conftest.py index dbdf572c6de..5f6713f5228 100644 --- a/tests/components/p1_monitor/conftest.py +++ b/tests/components/p1_monitor/conftest.py @@ -2,7 +2,7 @@ import json from unittest.mock import AsyncMock, MagicMock, patch -from p1monitor import Phases, Settings, SmartMeter +from p1monitor import Phases, Settings, SmartMeter, WaterMeter import pytest from homeassistant.components.p1_monitor.const import DOMAIN @@ -43,7 +43,12 @@ def mock_p1monitor(): json.loads(load_fixture("p1_monitor/settings.json")) ) ) - yield p1monitor_mock + client.watermeter = AsyncMock( + return_value=WaterMeter.from_dict( + json.loads(load_fixture("p1_monitor/watermeter.json")) + ) + ) + yield client @pytest.fixture diff --git a/tests/components/p1_monitor/fixtures/watermeter.json b/tests/components/p1_monitor/fixtures/watermeter.json new file mode 100644 index 00000000000..f69a4edcf88 --- /dev/null +++ b/tests/components/p1_monitor/fixtures/watermeter.json @@ -0,0 +1,10 @@ +[ + { + "TIMEPERIOD_ID": 13, + "TIMESTAMP_UTC": 1656194400, + "TIMESTAMP_lOCAL": "2022-06-26 00:00:00", + "WATERMETER_CONSUMPTION_LITER": 112.0, + "WATERMETER_CONSUMPTION_TOTAL_M3": 1696.14, + "WATERMETER_PULS_COUNT": 112.0 + } +] diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index 42a41789a92..13541e782b4 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -27,17 +27,12 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={ - CONF_NAME: "Name", - CONF_HOST: "example.com", - }, + user_input={CONF_NAME: "Name", CONF_HOST: "example.com"}, ) assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" - assert result2.get("data") == { - CONF_HOST: "example.com", - } + assert result2.get("data") == {CONF_HOST: "example.com"} assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_p1monitor.mock_calls) == 1 @@ -52,10 +47,7 @@ async def test_api_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={ - CONF_NAME: "Name", - CONF_HOST: "example.com", - }, + data={CONF_NAME: "Name", CONF_HOST: "example.com"}, ) assert result.get("type") == FlowResultType.FORM diff --git a/tests/components/p1_monitor/test_diagnostics.py b/tests/components/p1_monitor/test_diagnostics.py index 6b97107c353..1ab9da7adc8 100644 --- a/tests/components/p1_monitor/test_diagnostics.py +++ b/tests/components/p1_monitor/test_diagnostics.py @@ -55,5 +55,10 @@ async def test_diagnostics( "energy_production_price_high": "0.20522", "energy_production_price_low": "0.20522", }, + "watermeter": { + "consumption_day": 112.0, + "consumption_total": 1696.14, + "pulse_count": 112.0, + }, }, } diff --git a/tests/components/p1_monitor/test_init.py b/tests/components/p1_monitor/test_init.py index 1cf9cf21966..d7817faecdf 100644 --- a/tests/components/p1_monitor/test_init.py +++ b/tests/components/p1_monitor/test_init.py @@ -28,7 +28,7 @@ async def test_load_unload_config_entry( @patch( - "homeassistant.components.p1_monitor.P1Monitor.request", + "homeassistant.components.p1_monitor.P1Monitor._request", side_effect=P1MonitorConnectionError, ) async def test_config_entry_not_ready( diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index faaafca5ab8..fe9560c9cb6 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -1,4 +1,7 @@ """Tests for the sensors provided by the P1 Monitor integration.""" +from unittest.mock import MagicMock + +from p1monitor import P1MonitorNoDataError import pytest from homeassistant.components.p1_monitor.const import DOMAIN @@ -17,6 +20,7 @@ from homeassistant.const import ( ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, POWER_WATT, + VOLUME_LITERS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -33,8 +37,8 @@ async def test_smartmeter( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.monitor_power_consumption") - entry = entity_registry.async_get("sensor.monitor_power_consumption") + state = hass.states.get("sensor.smartmeter_power_consumption") + entry = entity_registry.async_get("sensor.smartmeter_power_consumption") assert entry assert state assert entry.unique_id == f"{entry_id}_smartmeter_power_consumption" @@ -45,8 +49,8 @@ async def test_smartmeter( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.monitor_energy_consumption_high") - entry = entity_registry.async_get("sensor.monitor_energy_consumption_high") + state = hass.states.get("sensor.smartmeter_energy_consumption_high") + entry = entity_registry.async_get("sensor.smartmeter_energy_consumption_high") assert entry assert state assert entry.unique_id == f"{entry_id}_smartmeter_energy_consumption_high" @@ -59,8 +63,8 @@ async def test_smartmeter( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.monitor_energy_tariff_period") - entry = entity_registry.async_get("sensor.monitor_energy_tariff_period") + state = hass.states.get("sensor.smartmeter_energy_tariff_period") + entry = entity_registry.async_get("sensor.smartmeter_energy_tariff_period") assert entry assert state assert entry.unique_id == f"{entry_id}_smartmeter_energy_tariff_period" @@ -90,8 +94,8 @@ async def test_phases( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.monitor_voltage_phase_l1") - entry = entity_registry.async_get("sensor.monitor_voltage_phase_l1") + state = hass.states.get("sensor.phases_voltage_phase_l1") + entry = entity_registry.async_get("sensor.phases_voltage_phase_l1") assert entry assert state assert entry.unique_id == f"{entry_id}_phases_voltage_phase_l1" @@ -102,8 +106,8 @@ async def test_phases( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.monitor_current_phase_l1") - entry = entity_registry.async_get("sensor.monitor_current_phase_l1") + state = hass.states.get("sensor.phases_current_phase_l1") + entry = entity_registry.async_get("sensor.phases_current_phase_l1") assert entry assert state assert entry.unique_id == f"{entry_id}_phases_current_phase_l1" @@ -114,8 +118,8 @@ async def test_phases( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.monitor_power_consumed_phase_l1") - entry = entity_registry.async_get("sensor.monitor_power_consumed_phase_l1") + state = hass.states.get("sensor.phases_power_consumed_phase_l1") + entry = entity_registry.async_get("sensor.phases_power_consumed_phase_l1") assert entry assert state assert entry.unique_id == f"{entry_id}_phases_power_consumed_phase_l1" @@ -146,8 +150,8 @@ async def test_settings( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.monitor_energy_consumption_price_low") - entry = entity_registry.async_get("sensor.monitor_energy_consumption_price_low") + state = hass.states.get("sensor.settings_energy_consumption_price_low") + entry = entity_registry.async_get("sensor.settings_energy_consumption_price_low") assert entry assert state assert entry.unique_id == f"{entry_id}_settings_energy_consumption_price_low" @@ -159,8 +163,8 @@ async def test_settings( == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" ) - state = hass.states.get("sensor.monitor_energy_production_price_low") - entry = entity_registry.async_get("sensor.monitor_energy_production_price_low") + state = hass.states.get("sensor.settings_energy_production_price_low") + entry = entity_registry.async_get("sensor.settings_energy_production_price_low") assert entry assert state assert entry.unique_id == f"{entry_id}_settings_energy_production_price_low" @@ -183,9 +187,52 @@ async def test_settings( assert not device_entry.sw_version +async def test_watermeter( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the P1 Monitor - WaterMeter sensors.""" + entry_id = init_integration.entry_id + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + state = hass.states.get("sensor.watermeter_consumption_day") + entry = entity_registry.async_get("sensor.watermeter_consumption_day") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_watermeter_consumption_day" + assert state.state == "112.0" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Consumption Day" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_LITERS + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_watermeter")} + assert device_entry.manufacturer == "P1 Monitor" + assert device_entry.name == "WaterMeter" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + assert not device_entry.model + assert not device_entry.sw_version + + +async def test_no_watermeter( + hass: HomeAssistant, mock_p1monitor: MagicMock, mock_config_entry: MockConfigEntry +) -> None: + """Test the P1 Monitor - Without WaterMeter sensors.""" + mock_p1monitor.watermeter.side_effect = P1MonitorNoDataError + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.states.get("sensor.watermeter_consumption_day") + assert not hass.states.get("sensor.consumption_total") + assert not hass.states.get("sensor.pulse_count") + + @pytest.mark.parametrize( "entity_id", - ("sensor.monitor_gas_consumption",), + ("sensor.smartmeter_gas_consumption",), ) async def test_smartmeter_disabled_by_default( hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str From 7a457391047f00763ee9e86d2415bc2ee6a65235 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:47:56 +0200 Subject: [PATCH 474/903] Adjust type hints in aquostv media player entity (#76990) --- homeassistant/components/aquostv/media_player.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index c9465424381..b0ff674e2de 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -158,13 +158,19 @@ class SharpAquosTVDevice(MediaPlayerEntity): self._remote.power(0) @_retry - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" + if self.volume_level is None: + _LOGGER.debug("Unknown volume in volume_up") + return self._remote.volume(int(self.volume_level * 60) + 2) @_retry - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" + if self.volume_level is None: + _LOGGER.debug("Unknown volume in volume_down") + return self._remote.volume(int(self.volume_level * 60) - 2) @_retry From 009a573324d083abc4dc4076cd7271fc18f11080 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:48:49 +0200 Subject: [PATCH 475/903] Adjust type hints in alpha-vantage sensor entity (#76988) --- homeassistant/components/alpha_vantage/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 534383f0bbf..02c6958e0da 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -128,7 +128,7 @@ class AlphaVantageSensor(SensorEntity): self._attr_native_unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) self._attr_icon = ICONS.get(symbol.get(CONF_CURRENCY, "USD")) - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug("Requesting new data for symbol %s", self._symbol) all_values, _ = self._timeseries.get_intraday(self._symbol) @@ -144,7 +144,7 @@ class AlphaVantageSensor(SensorEntity): ATTR_LOW: values["3. low"], } if isinstance(values, dict) - else None + else {} ) _LOGGER.debug("Received new values for symbol %s", self._symbol) @@ -167,7 +167,7 @@ class AlphaVantageForeignExchange(SensorEntity): self._attr_icon = ICONS.get(self._from_currency, "USD") self._attr_native_unit_of_measurement = self._to_currency - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug( "Requesting new data for forex %s - %s", @@ -187,7 +187,7 @@ class AlphaVantageForeignExchange(SensorEntity): CONF_TO: self._to_currency, } if values is not None - else None + else {} ) _LOGGER.debug( From f323c5e8806108d05a00551662c0cfed952d3014 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:49:59 +0200 Subject: [PATCH 476/903] Adjust type hints in android_ip_webcam switch entity (#76989) --- homeassistant/components/android_ip_webcam/switch.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index 57f78b20e3d..b09b0de4be8 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -1,8 +1,9 @@ """Support for Android IP Webcam settings.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass +from typing import Any from pydroid_ipcam import PyDroidIPCam @@ -21,8 +22,8 @@ from .entity import AndroidIPCamBaseEntity class AndroidIPWebcamSwitchEntityDescriptionMixin: """Mixin for required keys.""" - on_func: Callable[[PyDroidIPCam], None] - off_func: Callable[[PyDroidIPCam], None] + on_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]] + off_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]] @dataclass @@ -159,12 +160,12 @@ class IPWebcamSettingSwitch(AndroidIPCamBaseEntity, SwitchEntity): """Return if settings is on or off.""" return bool(self.cam.current_settings.get(self.entity_description.key)) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" await self.entity_description.on_func(self.cam) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" await self.entity_description.off_func(self.cam) await self.coordinator.async_request_refresh() From a434d755b35701a710886638c200d9e2f4ba86c9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 19 Aug 2022 00:27:31 +0000 Subject: [PATCH 477/903] [ci skip] Translation update --- .../components/almond/translations/es.json | 2 +- .../lacrosse_view/translations/de.json | 3 +- .../lacrosse_view/translations/el.json | 3 +- .../lacrosse_view/translations/es.json | 3 +- .../lacrosse_view/translations/fr.json | 3 +- .../lacrosse_view/translations/pt-BR.json | 3 +- .../components/lametric/translations/de.json | 40 ++++++++++++++- .../components/lametric/translations/el.json | 50 +++++++++++++++++++ .../components/lametric/translations/es.json | 50 +++++++++++++++++++ .../components/lametric/translations/fr.json | 44 ++++++++++++++++ .../components/lametric/translations/ja.json | 48 ++++++++++++++++++ .../components/lametric/translations/no.json | 50 +++++++++++++++++++ .../lametric/translations/pt-BR.json | 50 +++++++++++++++++++ .../components/lametric/translations/ru.json | 50 +++++++++++++++++++ .../lametric/translations/zh-Hant.json | 50 +++++++++++++++++++ .../landisgyr_heat_meter/translations/de.json | 23 +++++++++ .../landisgyr_heat_meter/translations/el.json | 23 +++++++++ .../landisgyr_heat_meter/translations/en.json | 11 ++-- .../landisgyr_heat_meter/translations/es.json | 23 +++++++++ .../landisgyr_heat_meter/translations/fr.json | 23 +++++++++ .../translations/pt-BR.json | 23 +++++++++ .../tankerkoenig/translations/es.json | 2 +- .../xiaomi_ble/translations/es.json | 2 +- .../yalexs_ble/translations/ja.json | 3 ++ 24 files changed, 567 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/lametric/translations/el.json create mode 100644 homeassistant/components/lametric/translations/es.json create mode 100644 homeassistant/components/lametric/translations/fr.json create mode 100644 homeassistant/components/lametric/translations/ja.json create mode 100644 homeassistant/components/lametric/translations/no.json create mode 100644 homeassistant/components/lametric/translations/pt-BR.json create mode 100644 homeassistant/components/lametric/translations/ru.json create mode 100644 homeassistant/components/lametric/translations/zh-Hant.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/de.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/el.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/es.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/fr.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index 7c0a80ef444..7c768deecdd 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para conectarse a Almond proporcionado por el complemento: {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a Almond proporcionado por el complemento: {addon}?", "title": "Almond a trav\u00e9s del complemento Home Assistant" }, "pick_implementation": { diff --git a/homeassistant/components/lacrosse_view/translations/de.json b/homeassistant/components/lacrosse_view/translations/de.json index d9aa1210fa0..362aa1619f8 100644 --- a/homeassistant/components/lacrosse_view/translations/de.json +++ b/homeassistant/components/lacrosse_view/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", diff --git a/homeassistant/components/lacrosse_view/translations/el.json b/homeassistant/components/lacrosse_view/translations/el.json index 1975028e9c9..5be96ab9caa 100644 --- a/homeassistant/components/lacrosse_view/translations/el.json +++ b/homeassistant/components/lacrosse_view/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", diff --git a/homeassistant/components/lacrosse_view/translations/es.json b/homeassistant/components/lacrosse_view/translations/es.json index 1f341b0f44e..9b02b2bbd4f 100644 --- a/homeassistant/components/lacrosse_view/translations/es.json +++ b/homeassistant/components/lacrosse_view/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/lacrosse_view/translations/fr.json b/homeassistant/components/lacrosse_view/translations/fr.json index 689d3cf7cf6..c3ea4492cd3 100644 --- a/homeassistant/components/lacrosse_view/translations/fr.json +++ b/homeassistant/components/lacrosse_view/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_auth": "Authentification non valide", diff --git a/homeassistant/components/lacrosse_view/translations/pt-BR.json b/homeassistant/components/lacrosse_view/translations/pt-BR.json index 29b458e5599..89f951e857b 100644 --- a/homeassistant/components/lacrosse_view/translations/pt-BR.json +++ b/homeassistant/components/lacrosse_view/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", diff --git a/homeassistant/components/lametric/translations/de.json b/homeassistant/components/lametric/translations/de.json index 1ea4d15dcd8..d44ad04ec0a 100644 --- a/homeassistant/components/lametric/translations/de.json +++ b/homeassistant/components/lametric/translations/de.json @@ -1,12 +1,50 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "invalid_discovery_info": "Ung\u00fcltige Suchinformationen erhalten", + "link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt", + "missing_configuration": "Die LaMetric-Integration ist nicht konfiguriert. Bitte folge der Dokumentation.", + "no_devices": "Der autorisierte Benutzer hat keine LaMetric Ger\u00e4te", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" }, "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Ein LaMetric-Ger\u00e4t kann im Home Assistant auf zwei verschiedene Arten eingerichtet werden.\n\nDu kannst alle Ger\u00e4teinformationen und API-Tokens selbst eingeben, oder Home Assistant kann sie von deinem LaMetric.com-Konto importieren.", + "menu_options": { + "manual_entry": "Manuell eintragen", + "pick_implementation": "Import von LaMetric.com (empfohlen)" + } + }, + "manual_entry": { + "data": { + "api_key": "API-Schl\u00fcssel", + "host": "Host" + }, + "data_description": { + "api_key": "Du findest diesen API-Schl\u00fcssel auf der [Ger\u00e4teseite in deinem LaMetric-Entwicklerkonto](https://developer.lametric.com/user/devices).", + "host": "Die IP-Adresse oder der Hostname deines LaMetric TIME in deinem Netzwerk." + } + }, "pick_implementation": { "title": "W\u00e4hle eine Authentifizierungsmethode" + }, + "user_cloud_select_device": { + "data": { + "device": "W\u00e4hle das hinzuzuf\u00fcgende LaMetric-Ger\u00e4t aus" + } } } + }, + "issues": { + "manual_migration": { + "description": "Die LaMetric-Integration wurde modernisiert: Sie wird nun \u00fcber die Benutzeroberfl\u00e4che konfiguriert und eingerichtet und die Kommunikation erfolgt nun lokal.\n\nLeider gibt es keinen automatischen Migrationspfad, so dass du dein LaMetric mit Home Assistant neu einrichten musst. Bitte konsultiere die Dokumentation zur LaMetric-Integration von Home Assistant, um zu erfahren, wie du diese einrichten kannst.\n\nEntferne die alte LaMetric YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um das Problem zu beheben.", + "title": "Manuelle Migration f\u00fcr LaMetric erforderlich" + } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/el.json b/homeassistant/components/lametric/translations/el.json new file mode 100644 index 00000000000..964bfc4b212 --- /dev/null +++ b/homeassistant/components/lametric/translations/el.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.", + "invalid_discovery_info": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2", + "link_local_address": "\u039f\u03b9 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9", + "missing_configuration": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 LaMetric \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "no_devices": "\u039f \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 LaMetric", + "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "\u039c\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae LaMetric \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant \u03bc\u03b5 \u03b4\u03cd\u03bf \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c4\u03c1\u03cc\u03c0\u03bf\u03c5\u03c2. \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf\u03b9 \u03c3\u03b1\u03c2 \u03cc\u03bb\u03b5\u03c2 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03ac API \u03ae \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 Home \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c4\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf LaMetric.com.", + "menu_options": { + "manual_entry": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1", + "pick_implementation": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b1\u03c0\u03cc \u03c4\u03bf LaMetric.com (\u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9)" + } + }, + "manual_entry": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "data_description": { + "api_key": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c3\u03c4\u03b7 [\u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae LaMetric](https://developer.lametric.com/user/devices).", + "host": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c4\u03bf\u03c5 LaMetric TIME \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2." + } + }, + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "user_cloud_select_device": { + "data": { + "device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae LaMetric \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 LaMetric \u03ad\u03c7\u03b5\u03b9 \u03b5\u03ba\u03c3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03b9\u03c3\u03c4\u03b5\u03af: \u03a4\u03ce\u03c1\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b5\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2. \n\n \u0394\u03c5\u03c3\u03c4\u03c5\u03c7\u03ce\u03c2, \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9, \u03c9\u03c2 \u03b5\u03ba \u03c4\u03bf\u03cd\u03c4\u03bf\u03c5, \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b1\u03c0\u03cc \u03b5\u03c3\u03ac\u03c2 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf LaMetric \u03c3\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Home Assistant. \u03a3\u03c5\u03bc\u03b2\u03bf\u03c5\u03bb\u03b5\u03c5\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant LaMetric \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03bb\u03b9\u03ac \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML LaMetric \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/es.json b/homeassistant/components/lametric/translations/es.json new file mode 100644 index 00000000000..e7cbe914a2e --- /dev/null +++ b/homeassistant/components/lametric/translations/es.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "invalid_discovery_info": "Se recibi\u00f3 informaci\u00f3n de descubrimiento no v\u00e1lida", + "link_local_address": "Las direcciones de enlace local no son compatibles", + "missing_configuration": "La integraci\u00f3n de LaMetric no est\u00e1 configurada. Por favor, sigue la documentaci\u00f3n.", + "no_devices": "El usuario autorizado no tiene dispositivos LaMetric", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Un dispositivo LaMetric se puede configurar en Home Assistant de dos maneras diferentes. \n\nPuedes introducir t\u00fa mismo toda la informaci\u00f3n del dispositivo y los tokens API, o Home Assistant puede importarlos desde tu cuenta de LaMetric.com.", + "menu_options": { + "manual_entry": "Introducir manualmente", + "pick_implementation": "Importar desde LaMetric.com (recomendado)" + } + }, + "manual_entry": { + "data": { + "api_key": "Clave API", + "host": "Host" + }, + "data_description": { + "api_key": "Puedes encontrar esta clave API en la [p\u00e1gina de dispositivos en tu cuenta de desarrollador de LaMetric](https://developer.lametric.com/user/devices).", + "host": "La direcci\u00f3n IP o el nombre de host de tu LaMetric TIME en tu red." + } + }, + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "user_cloud_select_device": { + "data": { + "device": "Selecciona el dispositivo LaMetric para a\u00f1adir" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "La integraci\u00f3n de LaMetric se ha modernizado: ahora se configura a trav\u00e9s de la interfaz de usuario y las comunicaciones son locales.\n\nDesafortunadamente, no existe una ruta de migraci\u00f3n autom\u00e1tica posible y, por lo tanto, requiere que vuelvas a configurar tu LaMetric con Home Assistant. Consulta la documentaci\u00f3n de la integraci\u00f3n LaMetric en Home Assistant sobre c\u00f3mo configurarlo. \n\nElimina la configuraci\u00f3n antigua de LaMetric YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se requiere migraci\u00f3n manual para LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/fr.json b/homeassistant/components/lametric/translations/fr.json new file mode 100644 index 00000000000..5bd4a5899f6 --- /dev/null +++ b/homeassistant/components/lametric/translations/fr.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", + "invalid_discovery_info": "Informations de d\u00e9couverte re\u00e7ues non valides", + "link_local_address": "Les adresses de liaison locale ne sont pas prises en charge", + "missing_configuration": "L'int\u00e9gration LaMetric n'est pas configur\u00e9e\u00a0; veuillez suivre la documentation.", + "no_devices": "L'utilisateur autoris\u00e9 ne poss\u00e8de aucun appareil LaMetric", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "Saisir manuellement", + "pick_implementation": "Importer depuis LaMetric.com (recommand\u00e9)" + } + }, + "manual_entry": { + "data": { + "api_key": "Cl\u00e9 d'API", + "host": "H\u00f4te" + } + }, + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "user_cloud_select_device": { + "data": { + "device": "S\u00e9lectionnez l'appareil LaMetric \u00e0 ajouter" + } + } + } + }, + "issues": { + "manual_migration": { + "title": "Migration manuelle requise pour LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ja.json b/homeassistant/components/lametric/translations/ja.json new file mode 100644 index 00000000000..242a43bddb7 --- /dev/null +++ b/homeassistant/components/lametric/translations/ja.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "invalid_discovery_info": "\u7121\u52b9\u306a\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u60c5\u5831\u3092\u53d7\u4fe1\u3057\u305f", + "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093", + "missing_configuration": "LaMetric\u306e\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_devices": "\u8a31\u53ef\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u306f\u3001LaMetric\u30c7\u30d0\u30a4\u30b9\u3092\u6301\u3063\u3066\u3044\u307e\u305b\u3093", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "\u624b\u52d5\u3067\u5165\u529b", + "pick_implementation": "LaMetric.com\u304b\u3089\u306e\u30a4\u30f3\u30dd\u30fc\u30c8((\u63a8\u5968)" + } + }, + "manual_entry": { + "data": { + "api_key": "API\u30ad\u30fc", + "host": "\u30db\u30b9\u30c8" + }, + "data_description": { + "api_key": "\u3053\u306eAPI\u30ad\u30fc\u306f\u3001[LaMetric\u958b\u767a\u8005\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30c7\u30d0\u30a4\u30b9\u30da\u30fc\u30b8](https://developer.lametric.com/user/devices)\u306b\u3042\u308a\u307e\u3059\u3002", + "host": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u3001LaMetric TIME\u306eIP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30db\u30b9\u30c8\u540d\u3002" + } + }, + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, + "user_cloud_select_device": { + "data": { + "device": "\u8ffd\u52a0\u3059\u308bLaMetric\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + } + } + } + }, + "issues": { + "manual_migration": { + "title": "LaMetric\u306b\u5fc5\u8981\u306a\u624b\u52d5\u3067\u306e\u79fb\u884c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/no.json b/homeassistant/components/lametric/translations/no.json new file mode 100644 index 00000000000..79d3591aec1 --- /dev/null +++ b/homeassistant/components/lametric/translations/no.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "invalid_discovery_info": "Ugyldig oppdagelsesinformasjon mottatt", + "link_local_address": "Lokale koblingsadresser st\u00f8ttes ikke", + "missing_configuration": "LaMetric-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "no_devices": "Den autoriserte brukeren har ingen LaMetric-enheter", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "En LaMetric-enhet kan settes opp i Home Assistant p\u00e5 to forskjellige m\u00e5ter. \n\n Du kan skrive inn all enhetsinformasjon og API-tokens selv, eller Home Assistant kan importere dem fra LaMetric.com-kontoen din.", + "menu_options": { + "manual_entry": "G\u00e5 inn manuelt", + "pick_implementation": "Importer fra LaMetric.com (anbefalt)" + } + }, + "manual_entry": { + "data": { + "api_key": "API-n\u00f8kkel", + "host": "Vert" + }, + "data_description": { + "api_key": "Du finner denne API-n\u00f8kkelen p\u00e5 [enhetssiden i LaMetric-utviklerkontoen din](https://developer.lametric.com/user/devices).", + "host": "IP-adressen eller vertsnavnet til LaMetric TIME p\u00e5 nettverket ditt." + } + }, + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "user_cloud_select_device": { + "data": { + "device": "Velg LaMetric-enheten du vil legge til" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "LaMetric-integrasjonen er modernisert: Den er n\u00e5 konfigurert og satt opp via brukergrensesnittet og kommunikasjonen er n\u00e5 lokal. \n\n Dessverre er det ingen automatisk migreringsbane mulig og krever derfor at du konfigurerer LaMetric p\u00e5 nytt med Home Assistant. Se integreringsdokumentasjonen for Home Assistant LaMetric for hvordan du setter den opp. \n\n Fjern den gamle LaMetric YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Manuell migrering kreves for LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/pt-BR.json b/homeassistant/components/lametric/translations/pt-BR.json new file mode 100644 index 00000000000..4153b06e94d --- /dev/null +++ b/homeassistant/components/lametric/translations/pt-BR.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "invalid_discovery_info": "Informa\u00e7\u00f5es de descoberta inv\u00e1lidas recebidas", + "link_local_address": "Endere\u00e7os locais de links n\u00e3o s\u00e3o suportados", + "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.", + "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric", + "no_url_available": "Nenhuma URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre este erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Um dispositivo LaMetric pode ser configurado no Home Assistant de duas maneiras diferentes. \n\n Voc\u00ea mesmo pode inserir todas as informa\u00e7\u00f5es do dispositivo e tokens de API, ou o Home Assistant pode import\u00e1-los de sua conta LaMetric.com.", + "menu_options": { + "manual_entry": "Entre manualmente", + "pick_implementation": "Importar do LaMetric.com (recomendado)" + } + }, + "manual_entry": { + "data": { + "api_key": "Chave API", + "host": "Host" + }, + "data_description": { + "api_key": "Voc\u00ea pode encontrar essa chave de API em [p\u00e1gina de dispositivos em sua conta de desenvolvedor LaMetric](https://developer.lametric.com/user/devices).", + "host": "O endere\u00e7o IP ou nome de host do seu LaMetric TIME em sua rede." + } + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "user_cloud_select_device": { + "data": { + "device": "Selecione o dispositivo LaMetric para adicionar" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "A integra\u00e7\u00e3o LaMetric foi modernizada: agora est\u00e1 configurada e configurada atrav\u00e9s da interface do usu\u00e1rio e as comunica\u00e7\u00f5es agora s\u00e3o locais. \n\n Infelizmente, n\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o autom\u00e1tica poss\u00edvel e, portanto, exige que voc\u00ea reconfigure seu LaMetric com o Home Assistant. Consulte a documenta\u00e7\u00e3o de integra\u00e7\u00e3o do Home Assistant LaMetric sobre como configur\u00e1-lo. \n\n Remova a configura\u00e7\u00e3o antiga do LaMetric YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "Migra\u00e7\u00e3o manual necess\u00e1ria para LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ru.json b/homeassistant/components/lametric/translations/ru.json new file mode 100644 index 00000000000..34a1bb58a62 --- /dev/null +++ b/homeassistant/components/lametric/translations/ru.json @@ -0,0 +1,50 @@ +{ + "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.", + "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_discovery_info": "\u041f\u0440\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u044b\u043b\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0430 \u043d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f.", + "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f LaMetric \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \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_devices": "\u0423 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 LaMetric.", + "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." + }, + "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": { + "choice_enter_manual_or_fetch_cloud": { + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e LaMetric \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant \u0434\u0432\u0443\u043c\u044f \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u043c\u0438 \u0441\u043f\u043e\u0441\u043e\u0431\u0430\u043c\u0438.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0432\u0435\u0441\u0442\u0438 \u0432\u0441\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438 API-\u0442\u043e\u043a\u0435\u043d\u044b \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u043b\u0438 Home Asssistant \u043c\u043e\u0436\u0435\u0442 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445 \u0438\u0437 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 LaMetric.com.", + "menu_options": { + "manual_entry": "\u0412\u0432\u0435\u0441\u0442\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e", + "pick_implementation": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441 LaMetric.com (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)" + } + }, + "manual_entry": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "\u0425\u043e\u0441\u0442" + }, + "data_description": { + "api_key": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u043a\u043b\u044e\u0447 API \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0432 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 LaMetric](https://developer.lametric.com/user/devices).", + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 LaMetric TIME \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438." + } + }, + "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" + }, + "user_cloud_select_device": { + "data": { + "device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e LaMetric" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f LaMetric \u043c\u043e\u0434\u0435\u0440\u043d\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0422\u0435\u043f\u0435\u0440\u044c \u043e\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0438 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0430 \u043e\u0431\u043c\u0435\u043d \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438.\n\n\u041a \u0441\u043e\u0436\u0430\u043b\u0435\u043d\u0438\u044e, \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043c\u043f\u043e\u0440\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u0435\u043d, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0437\u0430\u043d\u043e\u0432\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c LaMetric \u0432 Home Assistant. \u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432\u0441\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e, \u043e\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 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 LaMetric.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e LaMetric \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u043d\u0430 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/zh-Hant.json b/homeassistant/components/lametric/translations/zh-Hant.json new file mode 100644 index 00000000000..e9c2835756c --- /dev/null +++ b/homeassistant/components/lametric/translations/zh-Hant.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "invalid_discovery_info": "\u63a5\u6536\u5230\u7121\u6548\u7684\u63a2\u7d22\u8cc7\u8a0a", + "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", + "missing_configuration": "LaMetric \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_devices": "\u8a8d\u8b49\u4f7f\u7528\u8005\u6c92\u6709\u4efb\u4f55 LaMetric \u88dd\u7f6e", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "\u6709\u5169\u7a2e\u4e0d\u540c\u65b9\u6cd5\u53ef\u4ee5\u5c07 LaMetric \u88dd\u7f6e\u6574\u5408\u9032 Home Assistant\u3002\n\n\u53ef\u4ee5\u81ea\u884c\u8f38\u5165 API \u6b0a\u6756\u8207\u5168\u90e8\u88dd\u7f6e\u8cc7\u8a0a\uff0c\u6216\u8005 Home Asssistant \u53ef\u4ee5\u7531 LaMetric.com \u5e33\u865f\u9032\u884c\u532f\u5165\u3002", + "menu_options": { + "manual_entry": "\u624b\u52d5\u8f38\u5165", + "pick_implementation": "\u7531 LaMetric.com \u532f\u5165\uff08\u5efa\u8b70\uff09" + } + }, + "manual_entry": { + "data": { + "api_key": "API \u91d1\u9470", + "host": "\u4e3b\u6a5f\u7aef" + }, + "data_description": { + "api_key": "\u53ef\u4ee5\u65bc [LaMetric \u958b\u767c\u8005\u5e33\u865f\u7684\u88dd\u7f6e\u9801\u9762 account](https://developer.lametric.com/user/devices) \u4e2d\u627e\u5230 API \u91d1\u9470\u3002", + "host": "\u7db2\u8def\u4e2d LaMetric TIME \u7684 IP \u4f4d\u5740\u6216\u4e3b\u6a5f\u540d\u7a31\u3002" + } + }, + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "user_cloud_select_device": { + "data": { + "device": "\u9078\u64c7\u6240\u8981\u65b0\u589e\u7684 LaMetric \u88dd\u7f6e" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "LaMetric \u6574\u5408\u5df2\u7d93\u9032\u884c\u66f4\u65b0\uff1a\u73fe\u5728\u53ef\u900f\u904e\u4f7f\u7528\u8005\u4ecb\u9762\u9032\u884c\u8a2d\u5b9a\u3001\u4e26\u4e14\u70ba\u672c\u5730\u7aef\u901a\u8a0a\u65b9\u5f0f\u3002\n\n\u4e0d\u5e78\u7684\u3001\u76ee\u524d\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u529f\u80fd\uff0c\u9700\u8981\u65bc Home Assistant \u91cd\u65b0\u8f38\u8a2d\u5b9a LaMetric\u3002\u8acb\u53c3\u95b1 Home Assistant LaMetric \u6574\u5408\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u9032\u884c\u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 LaMetric YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u9700\u8981\u624b\u52d5\u9032\u884c LaMetric \u6574\u5408\u8f49\u79fb" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/de.json b/homeassistant/components/landisgyr_heat_meter/translations/de.json new file mode 100644 index 00000000000..e8a48a02c51 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB-Ger\u00e4te-Pfad" + } + }, + "user": { + "data": { + "device": "Ger\u00e4t w\u00e4hlen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/el.json b/homeassistant/components/landisgyr_heat_meter/translations/el.json new file mode 100644 index 00000000000..1f7d7b27df9 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + } + }, + "user": { + "data": { + "device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/en.json b/homeassistant/components/landisgyr_heat_meter/translations/en.json index 6915e8cb36a..84caa2819a4 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/en.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/en.json @@ -5,19 +5,18 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB Device Path" + } + }, "user": { "data": { "device": "Select device" } - }, - "setup_serial_manual_path": { - "data": { - "device": "USB-device path" - } } } } diff --git a/homeassistant/components/landisgyr_heat_meter/translations/es.json b/homeassistant/components/landisgyr_heat_meter/translations/es.json new file mode 100644 index 00000000000..956cebf852d --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Ruta del dispositivo USB" + } + }, + "user": { + "data": { + "device": "Selecciona el dispositivo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/fr.json b/homeassistant/components/landisgyr_heat_meter/translations/fr.json new file mode 100644 index 00000000000..42d2fe61555 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Chemin du p\u00e9riph\u00e9rique USB" + } + }, + "user": { + "data": { + "device": "S\u00e9lectionner un appareil" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json new file mode 100644 index 00000000000..1ac8f7b38bf --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Caminho do dispositivo USB" + } + }, + "user": { + "data": { + "device": "Selecione o dispositivo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/es.json b/homeassistant/components/tankerkoenig/translations/es.json index ec0f079cbea..27ee462e17b 100644 --- a/homeassistant/components/tankerkoenig/translations/es.json +++ b/homeassistant/components/tankerkoenig/translations/es.json @@ -19,7 +19,7 @@ "stations": "Estaciones" }, "description": "se encontraron {stations_count} estaciones en el radio", - "title": "Selecciona las estaciones a a\u00f1adir" + "title": "Selecciona las estaciones para a\u00f1adir" }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_ble/translations/es.json b/homeassistant/components/xiaomi_ble/translations/es.json index 504c7bc845c..357bb14cdf4 100644 --- a/homeassistant/components/xiaomi_ble/translations/es.json +++ b/homeassistant/components/xiaomi_ble/translations/es.json @@ -20,7 +20,7 @@ "description": "\u00bfQuieres configurar {name}?" }, "confirm_slow": { - "description": "No ha habido una transmisi\u00f3n desde este dispositivo en el \u00faltimo minuto, por lo que no estamos seguros de si este dispositivo usa cifrado o no. Esto puede deberse a que el dispositivo utiliza un intervalo de transmisi\u00f3n lento. Confirma para agregar este dispositivo de todos modos, luego, la pr\u00f3xima vez que se reciba una transmisi\u00f3n, se te pedir\u00e1 que ingreses su clave de enlace si es necesario." + "description": "No ha habido una transmisi\u00f3n desde este dispositivo en el \u00faltimo minuto, por lo que no estamos seguros de si este dispositivo usa cifrado o no. Esto puede deberse a que el dispositivo utiliza un intervalo de transmisi\u00f3n lento. Confirma para a\u00f1adir este dispositivo de todos modos, luego, la pr\u00f3xima vez que se reciba una transmisi\u00f3n, se te pedir\u00e1 que ingreses su clave de enlace si es necesario." }, "get_encryption_key_4_5": { "data": { diff --git a/homeassistant/components/yalexs_ble/translations/ja.json b/homeassistant/components/yalexs_ble/translations/ja.json index b847383ba9b..5e752cb1cfa 100644 --- a/homeassistant/components/yalexs_ble/translations/ja.json +++ b/homeassistant/components/yalexs_ble/translations/ja.json @@ -15,6 +15,9 @@ }, "flow_title": "{name}", "step": { + "integration_discovery_confirm": { + "description": "\u30a2\u30c9\u30ec\u30b9 {address} \u3092\u3001Bluetooth\u7d4c\u7531\u3067 {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "address": "Bluetooth\u30a2\u30c9\u30ec\u30b9", From cd59d3ab81b189cf6d14f91d88d92051e3c43dee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Aug 2022 15:41:07 -1000 Subject: [PATCH 478/903] Add support for multiple Bluetooth adapters (#76963) --- .../components/bluetooth/__init__.py | 119 +++++-- .../components/bluetooth/config_flow.py | 150 +++++---- homeassistant/components/bluetooth/const.py | 28 +- homeassistant/components/bluetooth/manager.py | 30 +- .../components/bluetooth/manifest.json | 2 +- .../components/bluetooth/strings.json | 23 +- .../components/bluetooth/translations/en.json | 21 +- homeassistant/components/bluetooth/util.py | 70 +++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/__init__.py | 28 +- tests/components/bluetooth/conftest.py | 70 ++++ .../components/bluetooth/test_config_flow.py | 305 +++++++++--------- tests/components/bluetooth/test_init.py | 277 ++++++++-------- tests/components/bluetooth/test_scanner.py | 81 ++--- tests/conftest.py | 17 +- 17 files changed, 738 insertions(+), 489 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8bb275c94fd..e659cec60a0 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from asyncio import Future from collections.abc import Callable +import platform from typing import TYPE_CHECKING import async_timeout @@ -11,11 +12,22 @@ from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import discovery_flow +from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.loader import async_get_bluetooth from . import models -from .const import CONF_ADAPTER, DATA_MANAGER, DOMAIN, SOURCE_LOCAL +from .const import ( + ADAPTER_ADDRESS, + ADAPTER_HW_VERSION, + ADAPTER_SW_VERSION, + CONF_ADAPTER, + CONF_DETAILS, + DATA_MANAGER, + DEFAULT_ADDRESS, + DOMAIN, + SOURCE_LOCAL, + AdapterDetails, +) from .manager import BluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher from .models import ( @@ -28,7 +40,7 @@ from .models import ( ProcessAdvertisementCallback, ) from .scanner import HaScanner, create_bleak_scanner -from .util import async_get_bluetooth_adapters +from .util import adapter_human_name, adapter_unique_name, async_default_adapter if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -164,37 +176,88 @@ def async_rediscover_address(hass: HomeAssistant, address: str) -> None: manager.async_rediscover_address(address) -async def _async_has_bluetooth_adapter() -> bool: - """Return if the device has a bluetooth adapter.""" - return bool(await async_get_bluetooth_adapters()) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) + manager = BluetoothManager(hass, integration_matcher) manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.data[DATA_MANAGER] = models.MANAGER = manager - # The config entry is responsible for starting the manager - # if its enabled - if hass.config_entries.async_entries(DOMAIN): - return True - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} - ) + adapters = await manager.async_get_bluetooth_adapters() + + async_migrate_entries(hass, adapters) + await async_discover_adapters(hass, adapters) + + return True + + +@hass_callback +def async_migrate_entries( + hass: HomeAssistant, + adapters: dict[str, AdapterDetails], +) -> None: + """Migrate config entries to support multiple.""" + current_entries = hass.config_entries.async_entries(DOMAIN) + default_adapter = async_default_adapter() + + for entry in current_entries: + if entry.unique_id: + continue + + address = DEFAULT_ADDRESS + adapter = entry.options.get(CONF_ADAPTER, default_adapter) + if adapter in adapters: + address = adapters[adapter][ADAPTER_ADDRESS] + hass.config_entries.async_update_entry( + entry, title=adapter_unique_name(adapter, address), unique_id=address ) - elif await _async_has_bluetooth_adapter(): + + +async def async_discover_adapters( + hass: HomeAssistant, + adapters: dict[str, AdapterDetails], +) -> None: + """Discover adapters and start flows.""" + if platform.system() == "Windows": + # We currently do not have a good way to detect if a bluetooth device is + # available on Windows. We will just assume that it is not unless they + # actively add it. + return + + for adapter, details in adapters.items(): discovery_flow.async_create_flow( hass, DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: adapter, CONF_DETAILS: details}, ) - return True + + +async def async_update_device( + entry: config_entries.ConfigEntry, + manager: BluetoothManager, + adapter: str, + address: str, +) -> None: + """Update device registry entry. + + The physical adapter can change from hci0/hci1 on reboot + or if the user moves around the usb sticks so we need to + update the device with the new location so they can + figure out where the adapter is. + """ + adapters = await manager.async_get_bluetooth_adapters() + details = adapters[adapter] + registry = dr.async_get(manager.hass) + registry.async_get_or_create( + config_entry_id=entry.entry_id, + name=adapter_human_name(adapter, details[ADAPTER_ADDRESS]), + connections={(dr.CONNECTION_BLUETOOTH, details[ADAPTER_ADDRESS])}, + sw_version=details.get(ADAPTER_SW_VERSION), + hw_version=details.get(ADAPTER_HW_VERSION), + ) async def async_setup_entry( @@ -202,7 +265,12 @@ async def async_setup_entry( ) -> bool: """Set up a config entry for a bluetooth scanner.""" manager: BluetoothManager = hass.data[DATA_MANAGER] - adapter: str | None = entry.options.get(CONF_ADAPTER) + address = entry.unique_id + assert address is not None + adapter = await manager.async_get_adapter_from_address(address) + if adapter is None: + raise ConfigEntryNotReady(f"Bluetooth adapter with address {address} not found") + try: bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter) except RuntimeError as err: @@ -211,18 +279,11 @@ async def async_setup_entry( entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) await scanner.async_start() entry.async_on_unload(manager.async_register_scanner(scanner)) - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + await async_update_device(entry, manager, adapter, address) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner return True -async def _async_update_listener( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 1a0be8706bf..2435a1e39ed 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,16 +1,16 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import voluptuous as vol from homeassistant.components import onboarding -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.core import callback +from homeassistant.config_entries import ConfigFlow +from homeassistant.helpers.typing import DiscoveryInfoType -from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN -from .util import async_get_bluetooth_adapters +from .const import ADAPTER_ADDRESS, CONF_ADAPTER, CONF_DETAILS, DOMAIN, AdapterDetails +from .util import adapter_human_name, adapter_unique_name, async_get_bluetooth_adapters if TYPE_CHECKING: from homeassistant.data_entry_flow import FlowResult @@ -21,60 +21,94 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize the config flow.""" + self._adapter: str | None = None + self._details: AdapterDetails | None = None + self._adapters: dict[str, AdapterDetails] = {} + + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle a flow initialized by discovery.""" + self._adapter = cast(str, discovery_info[CONF_ADAPTER]) + self._details = cast(AdapterDetails, discovery_info[CONF_DETAILS]) + await self.async_set_unique_id(self._details[ADAPTER_ADDRESS]) + self._abort_if_unique_id_configured() + self.context["title_placeholders"] = { + "name": adapter_human_name(self._adapter, self._details[ADAPTER_ADDRESS]) + } + return await self.async_step_single_adapter() + + async def async_step_single_adapter( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Select an adapter.""" + adapter = self._adapter + details = self._details + assert adapter is not None + assert details is not None + + address = details[ADAPTER_ADDRESS] + + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=adapter_unique_name(adapter, address), data={} + ) + + return self.async_show_form( + step_id="single_adapter", + description_placeholders={"name": adapter_human_name(adapter, address)}, + ) + + async def async_step_multiple_adapters( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + assert self._adapters is not None + adapter = user_input[CONF_ADAPTER] + address = self._adapters[adapter][ADAPTER_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=adapter_unique_name(adapter, address), data={} + ) + + configured_addresses = self._async_current_ids() + self._adapters = await async_get_bluetooth_adapters() + unconfigured_adapters = [ + adapter + for adapter, details in self._adapters.items() + if details[ADAPTER_ADDRESS] not in configured_addresses + ] + if not unconfigured_adapters: + return self.async_abort(reason="no_adapters") + if len(unconfigured_adapters) == 1: + self._adapter = list(self._adapters)[0] + self._details = self._adapters[self._adapter] + return await self.async_step_single_adapter() + + return self.async_show_form( + step_id="multiple_adapters", + data_schema=vol.Schema( + { + vol.Required(CONF_ADAPTER): vol.In( + { + adapter: adapter_human_name( + adapter, self._adapters[adapter][ADAPTER_ADDRESS] + ) + for adapter in sorted(unconfigured_adapters) + } + ), + } + ), + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" - return await self.async_step_enable_bluetooth() - - async def async_step_enable_bluetooth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user or import.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - - if user_input is not None or not onboarding.async_is_onboarded(self.hass): - return self.async_create_entry(title=DEFAULT_NAME, data={}) - - return self.async_show_form(step_id="enable_bluetooth") - - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import from configuration.yaml.""" - return await self.async_step_enable_bluetooth(user_input) - - @staticmethod - @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> OptionsFlowHandler: - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(OptionsFlow): - """Handle the option flow for bluetooth.""" - - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - if not (adapters := await async_get_bluetooth_adapters()): - return self.async_abort(reason="no_adapters") - - data_schema = vol.Schema( - { - vol.Required( - CONF_ADAPTER, - default=self.config_entry.options.get(CONF_ADAPTER, adapters[0]), - ): vol.In(adapters), - } - ) - return self.async_show_form(step_id="init", data_schema=data_schema) + return await self.async_step_multiple_adapters() diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 04581b841b9..0cd02bcbb8d 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -2,18 +2,27 @@ from __future__ import annotations from datetime import timedelta -from typing import Final +from typing import Final, TypedDict DOMAIN = "bluetooth" -DEFAULT_NAME = "Bluetooth" CONF_ADAPTER = "adapter" +CONF_DETAILS = "details" -MACOS_DEFAULT_BLUETOOTH_ADAPTER = "CoreBluetooth" +WINDOWS_DEFAULT_BLUETOOTH_ADAPTER = "bluetooth" +MACOS_DEFAULT_BLUETOOTH_ADAPTER = "Core Bluetooth" UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} +DEFAULT_ADAPTER_BY_PLATFORM = { + "Windows": WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, + "Darwin": MACOS_DEFAULT_BLUETOOTH_ADAPTER, +} + +# Some operating systems hide the adapter address for privacy reasons (ex MacOS) +DEFAULT_ADDRESS: Final = "00:00:00:00:00:00" + SOURCE_LOCAL: Final = "local" DATA_MANAGER: Final = "bluetooth_manager" @@ -22,3 +31,16 @@ UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 START_TIMEOUT = 12 SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) + + +class AdapterDetails(TypedDict, total=False): + """Adapter details.""" + + address: str + sw_version: str + hw_version: str + + +ADAPTER_ADDRESS: Final = "address" +ADAPTER_SW_VERSION: Final = "sw_version" +ADAPTER_HW_VERSION: Final = "hw_version" diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 0fba2d2aae1..0b588e71681 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -20,7 +20,12 @@ from homeassistant.core import ( from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval -from .const import SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS +from .const import ( + ADAPTER_ADDRESS, + SOURCE_LOCAL, + UNAVAILABLE_TRACK_SECONDS, + AdapterDetails, +) from .match import ( ADDRESS, BluetoothCallbackMatcher, @@ -29,6 +34,7 @@ from .match import ( ) from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher +from .util import async_get_bluetooth_adapters if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -39,7 +45,7 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" -RSSI_SWITCH_THRESHOLD = 10 +RSSI_SWITCH_THRESHOLD = 6 STALE_ADVERTISEMENT_SECONDS = 180 _LOGGER = logging.getLogger(__name__) @@ -132,6 +138,26 @@ class BluetoothManager: ] = [] self.history: dict[str, AdvertisementHistory] = {} self._scanners: list[HaScanner] = [] + self._adapters: dict[str, AdapterDetails] = {} + + def _find_adapter_by_address(self, address: str) -> str | None: + for adapter, details in self._adapters.items(): + if details[ADAPTER_ADDRESS] == address: + return adapter + return None + + async def async_get_bluetooth_adapters(self) -> dict[str, AdapterDetails]: + """Get bluetooth adapters.""" + if not self._adapters: + self._adapters = await async_get_bluetooth_adapters() + return self._adapters + + async def async_get_adapter_from_address(self, address: str) -> str | None: + """Get adapter from address.""" + if adapter := self._find_adapter_by_address(address): + return adapter + self._adapters = await async_get_bluetooth_adapters() + return self._find_adapter_by_address(address) @hass_callback def async_setup(self) -> None: diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f3828db5d10..ff99bd3d97d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.1.3"], + "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.2.0"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index beff2fd8312..269995192a8 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -2,9 +2,6 @@ "config": { "flow_title": "{name}", "step": { - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" - }, "user": { "description": "Choose a device to setup", "data": { @@ -13,20 +10,20 @@ }, "bluetooth_confirm": { "description": "Do you want to setup {name}?" + }, + "multiple_adapters": { + "description": "Select a Bluetooth adapter to setup", + "data": { + "adapter": "Adapter" + } + }, + "single_adapter": { + "description": "Do you want to setup the Bluetooth adapter {name}?" } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "no_adapters": "No Bluetooth adapters found" - } - }, - "options": { - "step": { - "init": { - "data": { - "adapter": "The Bluetooth Adapter to use for scanning" - } - } + "no_adapters": "No unconfigured Bluetooth adapters found" } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 4b53822b771..ac80cfb620e 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -2,15 +2,21 @@ "config": { "abort": { "already_configured": "Service is already configured", - "no_adapters": "No Bluetooth adapters found" + "no_adapters": "No unconfigured Bluetooth adapters found" }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "Select a Bluetooth adapter to setup" + }, + "single_adapter": { + "description": "Do you want to setup the Bluetooth adapter {name}?" }, "user": { "data": { @@ -19,14 +25,5 @@ "description": "Choose a device to setup" } } - }, - "options": { - "step": { - "init": { - "data": { - "adapter": "The Bluetooth Adapter to use for scanning" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 68920050748..3133b2f210d 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -3,25 +3,65 @@ from __future__ import annotations import platform -from .const import MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER +from homeassistant.core import callback + +from .const import ( + DEFAULT_ADAPTER_BY_PLATFORM, + DEFAULT_ADDRESS, + MACOS_DEFAULT_BLUETOOTH_ADAPTER, + UNIX_DEFAULT_BLUETOOTH_ADAPTER, + WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, + AdapterDetails, +) -async def async_get_bluetooth_adapters() -> list[str]: +async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: """Return a list of bluetooth adapters.""" - if platform.system() == "Windows": # We don't have a good way to detect on windows - return [] - if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware - return [MACOS_DEFAULT_BLUETOOTH_ADAPTER] + if platform.system() == "Windows": + return { + WINDOWS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( + address=DEFAULT_ADDRESS, + sw_version=platform.release(), + ) + } + if platform.system() == "Darwin": + return { + MACOS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( + address=DEFAULT_ADDRESS, + sw_version=platform.release(), + ) + } from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel - get_bluetooth_adapters, + get_bluetooth_adapter_details, ) - adapters = await get_bluetooth_adapters() - if ( - UNIX_DEFAULT_BLUETOOTH_ADAPTER in adapters - and adapters[0] != UNIX_DEFAULT_BLUETOOTH_ADAPTER - ): - # The default adapter always needs to be the first in the list - # because that is how bleak works. - adapters.insert(0, adapters.pop(adapters.index(UNIX_DEFAULT_BLUETOOTH_ADAPTER))) + adapters: dict[str, AdapterDetails] = {} + adapter_details = await get_bluetooth_adapter_details() + for adapter, details in adapter_details.items(): + adapter1 = details["org.bluez.Adapter1"] + adapters[adapter] = AdapterDetails( + address=adapter1["Address"], + sw_version=adapter1["Name"], # This is actually the BlueZ version + hw_version=adapter1["Modalias"], + ) return adapters + + +@callback +def async_default_adapter() -> str: + """Return the default adapter for the platform.""" + return DEFAULT_ADAPTER_BY_PLATFORM.get( + platform.system(), UNIX_DEFAULT_BLUETOOTH_ADAPTER + ) + + +@callback +def adapter_human_name(adapter: str, address: str) -> str: + """Return a human readable name for the adapter.""" + return adapter if address == DEFAULT_ADDRESS else f"{adapter} ({address})" + + +@callback +def adapter_unique_name(adapter: str, address: str) -> str: + """Return a unique name for the adapter.""" + return adapter if address == DEFAULT_ADDRESS else address diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8381e204a77..b6e5dbc4119 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.1 -bluetooth-adapters==0.1.3 +bluetooth-adapters==0.2.0 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 322d67a94df..733d82c9668 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.3 +bluetooth-adapters==0.2.0 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c67a756157..2a5a6c3f6cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.3 +bluetooth-adapters==0.2.0 # homeassistant.components.bond bond-async==0.1.22 diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 44da1a60f03..220432c46c2 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -6,8 +6,13 @@ from unittest.mock import patch from bleak.backends.scanner import AdvertisementData, BLEDevice -from homeassistant.components.bluetooth import SOURCE_LOCAL, models +from homeassistant.components.bluetooth import DOMAIN, SOURCE_LOCAL, models +from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS from homeassistant.components.bluetooth.manager import BluetoothManager +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry def _get_manager() -> BluetoothManager: @@ -48,3 +53,24 @@ def patch_discovered_devices(mock_discovered: list[BLEDevice]) -> None: return patch.object( manager, "async_discovered_devices", return_value=mock_discovered ) + + +async def async_setup_with_default_adapter(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Bluetooth integration with a default adapter.""" + return await _async_setup_with_adapter(hass, DEFAULT_ADDRESS) + + +async def async_setup_with_one_adapter(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Bluetooth integration with one adapter.""" + return await _async_setup_with_adapter(hass, "00:00:00:00:00:01") + + +async def _async_setup_with_adapter( + hass: HomeAssistant, address: str +) -> MockConfigEntry: + """Set up the Bluetooth integration with any adapter.""" + entry = MockConfigEntry(domain="bluetooth", unique_id=address) + entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + return entry diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 760500fe7a1..5ddd0fbc15f 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1 +1,71 @@ """Tests for the bluetooth component.""" + +from unittest.mock import patch + +import pytest + + +@pytest.fixture(name="macos_adapter") +def macos_adapter(): + """Fixture that mocks the macos adapter.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Darwin" + ): + yield + + +@pytest.fixture(name="windows_adapter") +def windows_adapter(): + """Fixture that mocks the windows adapter.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", + return_value="Windows", + ): + yield + + +@pytest.fixture(name="one_adapter") +def one_adapter_fixture(): + """Fixture that mocks one adapter on Linux.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + yield + + +@pytest.fixture(name="two_adapters") +def two_adapters_fixture(): + """Fixture that mocks two adapters on Linux.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + "hci1": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:02", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + yield diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 1053133cac9..e16208b3d70 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -5,38 +5,88 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.bluetooth.const import ( CONF_ADAPTER, + CONF_DETAILS, + DEFAULT_ADDRESS, DOMAIN, - MACOS_DEFAULT_BLUETOOTH_ADAPTER, + AdapterDetails, ) from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_async_step_user(hass): - """Test setting up manually.""" +async def test_async_step_user_macos(hass, macos_adapter): + """Test setting up manually with one adapter on MacOS.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data={}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "enable_bluetooth" + assert result["step_id"] == "single_adapter" with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( "homeassistant.components.bluetooth.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Bluetooth" + assert result2["title"] == "Core Bluetooth" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_user_only_allows_one(hass): +async def test_async_step_user_linux_one_adapter(hass, one_adapter): + """Test setting up manually with one adapter on Linux.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "single_adapter" + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "00:00:00:00:00:01" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_user_linux_two_adapters(hass, two_adapters): + """Test setting up manually with two adapters on Linux.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "multiple_adapters" + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_ADAPTER: "hci1"} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "00:00:00:00:00:02" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_user_only_allows_one(hass, macos_adapter): """Test setting up manually with an existing entry.""" - entry = MockConfigEntry(domain=DOMAIN) + entry = MockConfigEntry(domain=DOMAIN, unique_id=DEFAULT_ADDRESS) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -44,34 +94,48 @@ async def test_async_step_user_only_allows_one(hass): data={}, ) assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "no_adapters" async def test_async_step_integration_discovery(hass): """Test setting up from integration discovery.""" + + details = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "enable_bluetooth" + assert result["step_id"] == "single_adapter" with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( "homeassistant.components.bluetooth.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Bluetooth" + assert result2["title"] == "00:00:00:00:00:01" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_integration_discovery_during_onboarding(hass): +async def test_async_step_integration_discovery_during_onboarding_one_adapter( + hass, one_adapter +): """Test setting up from integration discovery during onboarding.""" + details = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( "homeassistant.components.bluetooth.async_setup_entry", return_value=True ) as mock_setup_entry, patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -80,10 +144,77 @@ async def test_async_step_integration_discovery_during_onboarding(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Bluetooth" + assert result["title"] == "00:00:00:00:00:01" + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + +async def test_async_step_integration_discovery_during_onboarding_two_adapters( + hass, two_adapters +): + """Test setting up from integration discovery during onboarding.""" + details1 = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) + details2 = AdapterDetails( + address="00:00:00:00:00:02", sw_version="1.23.5", hw_version="1.2.3" + ) + + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details1}, + ) + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ADAPTER: "hci1", CONF_DETAILS: details2}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "00:00:00:00:00:01" + assert result["data"] == {} + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "00:00:00:00:00:02" + assert result2["data"] == {} + + assert len(mock_setup_entry.mock_calls) == 2 + assert len(mock_onboarding.mock_calls) == 2 + + +async def test_async_step_integration_discovery_during_onboarding(hass, macos_adapter): + """Test setting up from integration discovery during onboarding.""" + details = AdapterDetails( + address=DEFAULT_ADDRESS, sw_version="1.23.5", hw_version="1.2.3" + ) + + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ADAPTER: "Core Bluetooth", CONF_DETAILS: details}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Core Bluetooth" assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_onboarding.mock_calls) == 1 @@ -91,150 +222,16 @@ async def test_async_step_integration_discovery_during_onboarding(hass): async def test_async_step_integration_discovery_already_exists(hass): """Test setting up from integration discovery when an entry already exists.""" - entry = MockConfigEntry(domain=DOMAIN) + details = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) + + entry = MockConfigEntry(domain=DOMAIN, unique_id="00:00:00:00:00:01") entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_async_step_import(hass): - """Test setting up from integration discovery.""" - with patch( - "homeassistant.components.bluetooth.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={}, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Bluetooth" - assert result["data"] == {} - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_async_step_import_already_exists(hass): - """Test setting up from yaml when an entry already exists.""" - entry = MockConfigEntry(domain=DOMAIN) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={}, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Linux") -async def test_options_flow_linux(mock_system, hass, mock_bleak_scanner_start): - """Test options on Linux.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, - unique_id="DOMAIN", - ) - entry.add_to_hass(hass) - - # Verify we can keep it as hci0 - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_ADAPTER: "hci0", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_ADAPTER] == "hci0" - - # Verify we can change it to hci1 - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] - ): - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_ADAPTER: "hci1", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_ADAPTER] == "hci1" - - -@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Darwin") -async def test_options_flow_macos(mock_system, hass, mock_bleak_scanner_start): - """Test options on MacOS.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, - unique_id="DOMAIN", - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_ADAPTER: MACOS_DEFAULT_BLUETOOTH_ADAPTER, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_ADAPTER] == MACOS_DEFAULT_BLUETOOTH_ADAPTER - - -@patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Windows" -) -async def test_options_flow_windows(mock_system, hass, mock_bleak_scanner_start): - """Test options on Windows.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, - unique_id="DOMAIN", - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "no_adapters" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 84c37300dc4..57fcb8402a0 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -19,6 +19,7 @@ from homeassistant.components.bluetooth import ( scanner, ) from homeassistant.components.bluetooth.const import ( + DEFAULT_ADDRESS, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) @@ -28,7 +29,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_manager, inject_advertisement, patch_discovered_devices +from . import ( + _get_manager, + async_setup_with_default_adapter, + inject_advertisement, + patch_discovered_devices, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -52,7 +58,7 @@ async def test_setup_and_stop(hass, mock_bleak_scanner_start, enable_bluetooth): assert len(mock_bleak_scanner_start.mock_calls) == 1 -async def test_setup_and_stop_no_bluetooth(hass, caplog): +async def test_setup_and_stop_no_bluetooth(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth is not available.""" mock_bt = [ {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} @@ -63,10 +69,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): ) as mock_ha_bleak_scanner, patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -76,7 +79,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): assert "Failed to initialize Bluetooth" in caplog.text -async def test_setup_and_stop_broken_bluetooth(hass, caplog): +async def test_setup_and_stop_broken_bluetooth(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth/dbus is broken.""" mock_bt = [] with patch( @@ -85,10 +88,7 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -98,7 +98,7 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): assert len(bluetooth.async_discovered_service_info(hass)) == 0 -async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): +async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth/dbus is hanging.""" mock_bt = [] @@ -111,10 +111,7 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -123,7 +120,7 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): assert "Timed out starting Bluetooth" in caplog.text -async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): +async def test_setup_and_retry_adapter_not_yet_available(hass, caplog, macos_adapter): """Test we retry if the adapter is not yet available.""" mock_bt = [] with patch( @@ -132,10 +129,7 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -159,7 +153,7 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): await hass.async_block_till_done() -async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): +async def test_no_race_during_manual_reload_in_retry_state(hass, caplog, macos_adapter): """Test we can successfully reload when the entry is in a retry state.""" mock_bt = [] with patch( @@ -168,10 +162,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -196,7 +187,9 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): await hass.async_block_till_done() -async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): +async def test_calling_async_discovered_devices_no_bluetooth( + hass, caplog, macos_adapter +): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] with patch( @@ -205,9 +198,7 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -228,9 +219,7 @@ async def test_discovery_match_by_service_uuid( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -256,16 +245,15 @@ async def test_discovery_match_by_service_uuid( assert mock_config_flow.mock_calls[0][1][0] == "switchbot" -async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): +async def test_discovery_match_by_local_name( + hass, mock_bleak_scanner_start, macos_adapter +): """Test bluetooth discovery match by local_name.""" mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -292,7 +280,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, macos_adapter ): """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start.""" mock_bt = [ @@ -305,10 +293,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -371,7 +356,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( async def test_discovery_match_by_service_data_uuid_then_others( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, macos_adapter ): """Test bluetooth discovery match by service_data_uuid and then other fields.""" mock_bt = [ @@ -391,10 +376,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -526,7 +508,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, macos_adapter ): """Test bluetooth discovery matches twice for service_uuid and then manufacturer_id.""" mock_bt = [ @@ -542,10 +524,7 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -600,9 +579,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -631,7 +608,9 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): assert mock_config_flow.mock_calls[1][1][0] == "switchbot" -async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): +async def test_async_discovered_device_api( + hass, mock_bleak_scanner_start, macos_adapter +): """Test the async_discovered_device API.""" mock_bt = [] with patch( @@ -642,10 +621,7 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): ): assert not bluetooth.async_discovered_service_info(hass) assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -738,9 +714,8 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ), patch.object(hass.config_entries.flow, "async_init"): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -821,10 +796,7 @@ async def test_register_callback_by_address( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -913,10 +885,7 @@ async def test_register_callback_survives_reload( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -933,7 +902,7 @@ async def test_register_callback_survives_reload( switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData( local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + service_uuids=["zba20d00-224d-11e6-9fb8-0002a5d5c51b"], manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) @@ -1063,10 +1032,7 @@ async def test_wrapped_instance_with_filter( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1132,10 +1098,7 @@ async def test_wrapped_instance_with_service_uuids( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1184,10 +1147,7 @@ async def test_wrapped_instance_with_broken_callbacks( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ), patch.object(hass.config_entries.flow, "async_init"): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1231,10 +1191,7 @@ async def test_wrapped_instance_changes_uuids( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1283,10 +1240,7 @@ async def test_wrapped_instance_changes_filters( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1335,10 +1289,7 @@ async def test_wrapped_instance_unsupported_filter( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1354,7 +1305,9 @@ async def test_wrapped_instance_unsupported_filter( assert "Only UUIDs filters are supported" in caplog.text -async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): +async def test_async_ble_device_from_address( + hass, mock_bleak_scanner_start, macos_adapter +): """Test the async_ble_device_from_address api.""" mock_bt = [] with patch( @@ -1369,9 +1322,8 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") is None ) - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -1394,26 +1346,14 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): ) -async def test_setup_without_bluetooth_in_configuration_yaml(hass, mock_bluetooth): - """Test setting up without bluetooth in configuration.yaml does not create the config entry.""" - assert await async_setup_component(hass, bluetooth.DOMAIN, {}) - await hass.async_block_till_done() - assert not hass.config_entries.async_entries(bluetooth.DOMAIN) - - -async def test_setup_with_bluetooth_in_configuration_yaml(hass, mock_bluetooth): - """Test setting up with bluetooth in configuration.yaml creates the config entry.""" - assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) - await hass.async_block_till_done() - assert hass.config_entries.async_entries(bluetooth.DOMAIN) - - -async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_bluetooth): +async def test_can_unsetup_bluetooth_single_adapter_macos( + hass, mock_bleak_scanner_start, enable_bluetooth, macos_adapter +): """Test we can setup and unsetup bluetooth.""" - entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS) entry.add_to_hass(hass) - for _ in range(2): + for _ in range(2): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1421,35 +1361,80 @@ async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_blue await hass.async_block_till_done() -async def test_auto_detect_bluetooth_adapters_linux(hass): +async def test_can_unsetup_bluetooth_single_adapter_linux( + hass, mock_bleak_scanner_start, enable_bluetooth, one_adapter +): + """Test we can setup and unsetup bluetooth.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + + for _ in range(2): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_can_unsetup_bluetooth_multiple_adapters( + hass, mock_bleak_scanner_start, enable_bluetooth, two_adapters +): + """Test we can setup and unsetup bluetooth with multiple adapters.""" + entry1 = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry1.add_to_hass(hass) + + entry2 = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:02" + ) + entry2.add_to_hass(hass) + + for _ in range(2): + for entry in (entry1, entry2): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_three_adapters_one_missing( + hass, mock_bleak_scanner_start, enable_bluetooth, two_adapters +): + """Test three adapters but one is missing results in a retry on setup.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:03" + ) + entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_auto_detect_bluetooth_adapters_linux(hass, one_adapter): """Test we auto detect bluetooth adapters on linux.""" - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0"] - ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ): - assert await async_setup_component(hass, bluetooth.DOMAIN, {}) - await hass.async_block_till_done() + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() assert not hass.config_entries.async_entries(bluetooth.DOMAIN) assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 -async def test_auto_detect_bluetooth_adapters_linux_multiple(hass): +async def test_auto_detect_bluetooth_adapters_linux_multiple(hass, two_adapters): """Test we auto detect bluetooth adapters on linux with multiple adapters.""" - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci1", "hci0"] - ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ): - assert await async_setup_component(hass, bluetooth.DOMAIN, {}) - await hass.async_block_till_done() + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() assert not hass.config_entries.async_entries(bluetooth.DOMAIN) - assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2 async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): """Test we auto detect bluetooth adapters on linux with no adapters found.""" - with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()), patch( + with patch( + "bluetooth_adapters.get_bluetooth_adapter_details", return_value={} + ), patch( "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) @@ -1485,3 +1470,23 @@ async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_blu """Test getting the scanner returns the wrapped instance.""" scanner = bluetooth.async_get_scanner(hass) assert isinstance(scanner, models.HaBleakScannerWrapper) + + +async def test_migrate_single_entry_macos( + hass, mock_bleak_scanner_start, macos_adapter +): + """Test we can migrate a single entry on MacOS.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert entry.unique_id == DEFAULT_ADDRESS + + +async def test_migrate_single_entry_linux(hass, mock_bleak_scanner_start, one_adapter): + """Test we can migrate a single entry on Linux.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert entry.unique_id == "00:00:00:00:00:01" diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 032b67662df..bde1dbd1696 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -11,23 +11,20 @@ from dbus_next import InvalidMessageError from homeassistant.components import bluetooth from homeassistant.components.bluetooth.const import ( - CONF_ADAPTER, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, - UNIX_DEFAULT_BLUETOOTH_ADAPTER, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_manager +from . import _get_manager, async_setup_with_one_adapter -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import async_fire_time_changed async def test_config_entry_can_be_reloaded_when_stop_raises( - hass, caplog, enable_bluetooth + hass, caplog, enable_bluetooth, macos_adapter ): """Test we can reload if stopping the scanner raises.""" entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] @@ -44,31 +41,7 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( assert "Error stopping scanner" in caplog.text -async def test_changing_the_adapter_at_runtime(hass): - """Test we can change the adapter at runtime.""" - entry = MockConfigEntry( - domain=bluetooth.DOMAIN, - data={}, - options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start" - ), patch("homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop"): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - entry.options = {CONF_ADAPTER: "hci1"} - - await hass.config_entries.async_reload(entry.entry_id) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - - -async def test_dbus_socket_missing_in_container(hass, caplog): +async def test_dbus_socket_missing_in_container(hass, caplog, one_adapter): """Test we handle dbus being missing in the container.""" with patch( @@ -77,10 +50,8 @@ async def test_dbus_socket_missing_in_container(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=FileNotFoundError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -90,7 +61,7 @@ async def test_dbus_socket_missing_in_container(hass, caplog): assert "docker" in caplog.text -async def test_dbus_socket_missing(hass, caplog): +async def test_dbus_socket_missing(hass, caplog, one_adapter): """Test we handle dbus being missing.""" with patch( @@ -99,10 +70,8 @@ async def test_dbus_socket_missing(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=FileNotFoundError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -112,7 +81,7 @@ async def test_dbus_socket_missing(hass, caplog): assert "docker" not in caplog.text -async def test_dbus_broken_pipe_in_container(hass, caplog): +async def test_dbus_broken_pipe_in_container(hass, caplog, one_adapter): """Test we handle dbus broken pipe in the container.""" with patch( @@ -121,10 +90,8 @@ async def test_dbus_broken_pipe_in_container(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BrokenPipeError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -135,7 +102,7 @@ async def test_dbus_broken_pipe_in_container(hass, caplog): assert "container" in caplog.text -async def test_dbus_broken_pipe(hass, caplog): +async def test_dbus_broken_pipe(hass, caplog, one_adapter): """Test we handle dbus broken pipe.""" with patch( @@ -144,10 +111,8 @@ async def test_dbus_broken_pipe(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BrokenPipeError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -158,17 +123,15 @@ async def test_dbus_broken_pipe(hass, caplog): assert "container" not in caplog.text -async def test_invalid_dbus_message(hass, caplog): +async def test_invalid_dbus_message(hass, caplog, one_adapter): """Test we handle invalid dbus message.""" with patch( "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=InvalidMessageError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -177,7 +140,7 @@ async def test_invalid_dbus_message(hass, caplog): assert "dbus" in caplog.text -async def test_recovery_from_dbus_restart(hass): +async def test_recovery_from_dbus_restart(hass, one_adapter): """Test we can recover when DBus gets restarted out from under us.""" called_start = 0 @@ -213,10 +176,8 @@ async def test_recovery_from_dbus_restart(hass): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", return_value=scanner, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + assert called_start == 1 start_time_monotonic = 1000 diff --git a/tests/conftest.py b/tests/conftest.py index 4c268206805..0c0a654059b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -876,7 +876,7 @@ async def mock_enable_bluetooth( hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Fixture to mock starting the bleak scanner.""" - entry = MockConfigEntry(domain="bluetooth") + entry = MockConfigEntry(domain="bluetooth", unique_id="00:00:00:00:00:01") entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -885,7 +885,20 @@ async def mock_enable_bluetooth( @pytest.fixture(name="mock_bluetooth_adapters") def mock_bluetooth_adapters(): """Fixture to mock bluetooth adapters.""" - with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=[]): + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): yield From 09aaf45f0a879c8ecdad8c0fa98398e59900c71b Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Thu, 18 Aug 2022 22:23:20 -0400 Subject: [PATCH 479/903] Fix lutron caseta Sunnata Keypad support (#75324) Co-authored-by: J. Nick Koston --- .../components/lutron_caseta/__init__.py | 3 +- .../lutron_caseta/device_trigger.py | 31 +++++++++++++++---- .../lutron_caseta/test_device_trigger.py | 4 +-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index d235f294b3a..5653504c98a 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -46,6 +46,7 @@ from .const import ( from .device_trigger import ( DEVICE_TYPE_SUBTYPE_MAP_TO_LIP, LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP, + _lutron_model_to_device_type, ) from .models import LutronCasetaData from .util import serial_to_unique_id @@ -281,7 +282,7 @@ def _async_subscribe_pico_remote_events( else: action = ACTION_RELEASE - type_ = device["type"] + type_ = _lutron_model_to_device_type(device["model"], device["type"]) area, name = _area_and_name_from_name(device["name"]) leap_button_number = device["button_number"] lip_button_number = async_get_lip_button(type_, leap_button_number) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 77cad154c06..b355c3dcc3f 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -39,6 +39,13 @@ def _reverse_dict(forward_dict: dict) -> dict: return {v: k for k, v in forward_dict.items()} +LUTRON_MODEL_TO_TYPE = { + "RRST-W2B-XX": "SunnataKeypad_2Button", + "RRST-W3RL-XX": "SunnataKeypad_3ButtonRaiseLower", + "RRST-W4B-XX": "SunnataKeypad_4Button", +} + + SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE] LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( @@ -379,9 +386,13 @@ async def async_validate_trigger_config( if not device: return config - if not (schema := DEVICE_TYPE_SCHEMA_MAP.get(device["type"])): + if not ( + schema := DEVICE_TYPE_SCHEMA_MAP.get( + _lutron_model_to_device_type(device["model"], device["type"]) + ) + ): raise InvalidDeviceAutomationConfig( - f"Device type {device['type']} not supported: {config[CONF_DEVICE_ID]}" + f"Device model {device['model']} with type {device['type']} not supported: {config[CONF_DEVICE_ID]}" ) return schema(config) @@ -396,7 +407,9 @@ async def async_get_triggers( if not (device := get_button_device_by_dr_id(hass, device_id)): raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(device["type"], {}) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get( + _lutron_model_to_device_type(device["model"], device["type"]), {} + ) for trigger in SUPPORTED_INPUTS_EVENTS_TYPES: for subtype in valid_buttons: @@ -413,10 +426,16 @@ async def async_get_triggers( return triggers -def _device_model_to_type(model: str) -> str: +def _device_model_to_type(device_registry_model: str) -> str: """Convert a lutron_caseta device registry entry model to type.""" - _, device_type = model.split(" ") - return device_type.replace("(", "").replace(")", "") + model, p_device_type = device_registry_model.split(" ") + device_type = p_device_type.replace("(", "").replace(")", "") + return _lutron_model_to_device_type(model, device_type) + + +def _lutron_model_to_device_type(model: str, device_type: str) -> str: + """Get the mapped type based on the lutron model or type.""" + return LUTRON_MODEL_TO_TYPE.get(model, device_type) async def async_attach_trigger( diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 54cd842f0ee..b8c655a23bd 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -64,8 +64,8 @@ MOCK_BUTTON_DEVICES = [ {"Number": 11}, ], "leap_name": "Front Steps_Front Steps Sunnata Keypad", - "type": "SunnataKeypad_3ButtonRaiseLower", - "model": "PJ2-3BRL-GXX-X01", + "type": "SunnataKeypad", + "model": "RRST-W4B-XX", "serial": 43845547, }, ] From 72a4f8af3d9f614653f141e1cccf61a26694e2cd Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 19 Aug 2022 09:07:32 +0300 Subject: [PATCH 480/903] Add config flow to `pushover` (#74500) * Add config flow to `pushover` * Add tests for reauth * add deprecated yaml issue * address comments * fix test error, other fixes * update translations --- CODEOWNERS | 2 + homeassistant/components/pushover/__init__.py | 54 +++++ .../components/pushover/config_flow.py | 106 +++++++++ homeassistant/components/pushover/const.py | 20 ++ .../components/pushover/manifest.json | 4 +- homeassistant/components/pushover/notify.py | 122 ++++++---- .../components/pushover/strings.json | 34 +++ .../components/pushover/translations/en.json | 34 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/pushover/__init__.py | 10 + tests/components/pushover/test_config_flow.py | 218 ++++++++++++++++++ tests/components/pushover/test_init.py | 98 ++++++++ 13 files changed, 660 insertions(+), 46 deletions(-) create mode 100644 homeassistant/components/pushover/config_flow.py create mode 100644 homeassistant/components/pushover/const.py create mode 100644 homeassistant/components/pushover/strings.json create mode 100644 homeassistant/components/pushover/translations/en.json create mode 100644 tests/components/pushover/__init__.py create mode 100644 tests/components/pushover/test_config_flow.py create mode 100644 tests/components/pushover/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index ea9e47a9423..9a0c092eceb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -848,6 +848,8 @@ build.json @home-assistant/supervisor /tests/components/pure_energie/ @klaasnicolaas /homeassistant/components/push/ @dgomes /tests/components/push/ @dgomes +/homeassistant/components/pushover/ @engrbm87 +/tests/components/pushover/ @engrbm87 /homeassistant/components/pvoutput/ @frenck /tests/components/pvoutput/ @frenck /homeassistant/components/pvpc_hourly_pricing/ @azogue diff --git a/homeassistant/components/pushover/__init__.py b/homeassistant/components/pushover/__init__.py index 921d37ed332..fa9a9c5ebd9 100644 --- a/homeassistant/components/pushover/__init__.py +++ b/homeassistant/components/pushover/__init__.py @@ -1 +1,55 @@ """The pushover component.""" +from __future__ import annotations + +from pushover_complete import BadAPIRequestError, PushoverAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_USER_KEY, DATA_HASS_CONFIG, DOMAIN + +PLATFORMS = [Platform.NOTIFY] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the pushover component.""" + + hass.data[DATA_HASS_CONFIG] = config + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up pushover from a config entry.""" + + pushover_api = PushoverAPI(entry.data[CONF_API_KEY]) + try: + await hass.async_add_executor_job( + pushover_api.validate, entry.data[CONF_USER_KEY] + ) + + except BadAPIRequestError as err: + if "application token is invalid" in str(err): + raise ConfigEntryAuthFailed(err) from err + raise ConfigEntryNotReady(err) from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = pushover_api + + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + { + CONF_NAME: entry.data[CONF_NAME], + CONF_USER_KEY: entry.data[CONF_USER_KEY], + "entry_id": entry.entry_id, + }, + hass.data[DATA_HASS_CONFIG], + ) + ) + + return True diff --git a/homeassistant/components/pushover/config_flow.py b/homeassistant/components/pushover/config_flow.py new file mode 100644 index 00000000000..3f12446733e --- /dev/null +++ b/homeassistant/components/pushover/config_flow.py @@ -0,0 +1,106 @@ +"""Config flow for pushover integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from pushover_complete import BadAPIRequestError, PushoverAPI +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_USER_KEY, DEFAULT_NAME, DOMAIN + +USER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_API_KEY): str, + vol.Required(CONF_USER_KEY): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: + """Validate user input.""" + errors = {} + pushover_api = PushoverAPI(data[CONF_API_KEY]) + try: + await hass.async_add_executor_job(pushover_api.validate, data[CONF_USER_KEY]) + except BadAPIRequestError as err: + if "application token is invalid" in str(err): + errors[CONF_API_KEY] = "invalid_api_key" + elif "user key is invalid" in str(err): + errors[CONF_USER_KEY] = "invalid_user_key" + else: + errors["base"] = "cannot_connect" + return errors + + +class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for pushover integration.""" + + _reauth_entry: config_entries.ConfigEntry | None + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Handle import from config.""" + return await self.async_step_user(import_config) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + errors = {} + if user_input is not None and self._reauth_entry: + user_input = {**self._reauth_entry.data, **user_input} + errors = await validate_input(self.hass, user_input) + if not errors: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } + ), + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_USER_KEY]) + self._abort_if_unique_id_configured() + + self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) + + errors = await validate_input(self.hass, user_input) + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=USER_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/pushover/const.py b/homeassistant/components/pushover/const.py new file mode 100644 index 00000000000..af541132297 --- /dev/null +++ b/homeassistant/components/pushover/const.py @@ -0,0 +1,20 @@ +"""Constants for pushover.""" + +from typing import Final + +DOMAIN: Final = "pushover" +DATA_HASS_CONFIG: Final = "pushover_hass_config" +DEFAULT_NAME: Final = "Pushover" + +ATTR_ATTACHMENT: Final = "attachment" +ATTR_URL: Final = "url" +ATTR_URL_TITLE: Final = "url_title" +ATTR_PRIORITY: Final = "priority" +ATTR_RETRY: Final = "retry" +ATTR_SOUND: Final = "sound" +ATTR_HTML: Final = "html" +ATTR_CALLBACK_URL: Final = "callback_url" +ATTR_EXPIRE: Final = "expire" +ATTR_TIMESTAMP: Final = "timestamp" + +CONF_USER_KEY: Final = "user_key" diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 0752fbc7b78..a13de899480 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -1,9 +1,11 @@ { "domain": "pushover", "name": "Pushover", + "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": ["pushover_complete==1.1.1"], - "codeowners": [], + "codeowners": ["@engrbm87"], + "config_flow": true, "iot_class": "cloud_push", "loggers": ["pushover_complete"] } diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 8a18597c2ca..d9073b18a0a 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,7 +1,10 @@ """Pushover platform for notify component.""" -import logging +from __future__ import annotations -from pushover_complete import PushoverAPI +import logging +from typing import Any + +from pushover_complete import BadAPIRequestError, PushoverAPI import voluptuous as vol from homeassistant.components.notify import ( @@ -12,47 +15,82 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from ...exceptions import HomeAssistantError +from .const import ( + ATTR_ATTACHMENT, + ATTR_CALLBACK_URL, + ATTR_EXPIRE, + ATTR_HTML, + ATTR_PRIORITY, + ATTR_RETRY, + ATTR_SOUND, + ATTR_TIMESTAMP, + ATTR_URL, + ATTR_URL_TITLE, + CONF_USER_KEY, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) -ATTR_ATTACHMENT = "attachment" -ATTR_URL = "url" -ATTR_URL_TITLE = "url_title" -ATTR_PRIORITY = "priority" -ATTR_RETRY = "retry" -ATTR_SOUND = "sound" -ATTR_HTML = "html" -ATTR_CALLBACK_URL = "callback_url" -ATTR_EXPIRE = "expire" -ATTR_TIMESTAMP = "timestamp" - -CONF_USER_KEY = "user_key" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Required(CONF_USER_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string} ) -def get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> PushoverNotificationService | None: """Get the Pushover notification service.""" + if discovery_info is None: + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + return None + + pushover_api: PushoverAPI = hass.data[DOMAIN][discovery_info["entry_id"]] return PushoverNotificationService( - hass, config[CONF_USER_KEY], config[CONF_API_KEY] + hass, pushover_api, discovery_info[CONF_USER_KEY] ) class PushoverNotificationService(BaseNotificationService): """Implement the notification service for Pushover.""" - def __init__(self, hass, user_key, api_token): + def __init__( + self, hass: HomeAssistant, pushover: PushoverAPI, user_key: str + ) -> None: """Initialize the service.""" self._hass = hass self._user_key = user_key - self._api_token = api_token - self.pushover = PushoverAPI(self._api_token) + self.pushover = pushover - def send_message(self, message="", **kwargs): + def send_message(self, message: str = "", **kwargs: dict[str, Any]) -> None: """Send a message to a user.""" # Extract params from data dict @@ -87,28 +125,22 @@ class PushoverNotificationService(BaseNotificationService): # Remove attachment key to send without attachment. image = None - targets = kwargs.get(ATTR_TARGET) - - if not isinstance(targets, list): - targets = [targets] - - for target in targets: - try: - self.pushover.send_message( - self._user_key, - message, - target, - title, - url, - url_title, - image, - priority, - retry, - expire, - callback_url, - timestamp, - sound, - html, - ) - except ValueError as val_err: - _LOGGER.error(val_err) + try: + self.pushover.send_message( + self._user_key, + message, + kwargs.get(ATTR_TARGET), + title, + url, + url_title, + image, + priority, + retry, + expire, + callback_url, + timestamp, + sound, + html, + ) + except BadAPIRequestError as err: + raise HomeAssistantError(str(err)) from err diff --git a/homeassistant/components/pushover/strings.json b/homeassistant/components/pushover/strings.json new file mode 100644 index 00000000000..c309a1ec01f --- /dev/null +++ b/homeassistant/components/pushover/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "invalid_user_key": "Invalid user key" + }, + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "user_key": "User key" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Pushover YAML configuration is being removed", + "description": "Configuring Pushover using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/pushover/translations/en.json b/homeassistant/components/pushover/translations/en.json new file mode 100644 index 00000000000..33826000dc3 --- /dev/null +++ b/homeassistant/components/pushover/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "invalid_user_key": "Invalid user key" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "api_key": "API Key", + "name": "Name", + "user_key": "User key" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Pushover using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Pushover YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2ce09bfcafa..c604c5e95cd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -290,6 +290,7 @@ FLOWS = { "prosegur", "ps4", "pure_energie", + "pushover", "pvoutput", "pvpc_hourly_pricing", "qingping", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a5a6c3f6cc..ef6a03ddcd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -918,6 +918,9 @@ pure-python-adb[async]==0.3.0.dev0 # homeassistant.components.pushbullet pushbullet.py==0.11.0 +# homeassistant.components.pushover +pushover_complete==1.1.1 + # homeassistant.components.pvoutput pvo==0.2.2 diff --git a/tests/components/pushover/__init__.py b/tests/components/pushover/__init__.py new file mode 100644 index 00000000000..cb0f5788099 --- /dev/null +++ b/tests/components/pushover/__init__.py @@ -0,0 +1,10 @@ +"""Tests for the pushover component.""" + +from homeassistant.components.pushover.const import CONF_USER_KEY +from homeassistant.const import CONF_API_KEY, CONF_NAME + +MOCK_CONFIG = { + CONF_NAME: "Pushover", + CONF_API_KEY: "MYAPIKEY", + CONF_USER_KEY: "MYUSERKEY", +} diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py new file mode 100644 index 00000000000..68672961a56 --- /dev/null +++ b/tests/components/pushover/test_config_flow.py @@ -0,0 +1,218 @@ +"""Test pushbullet config flow.""" +from unittest.mock import MagicMock, patch + +from pushover_complete import BadAPIRequestError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.pushover.const import CONF_USER_KEY, DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant + +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def mock_pushover(): + """Mock pushover.""" + with patch( + "pushover_complete.PushoverAPI._generic_post", return_value={} + ) as mock_generic_post: + yield mock_generic_post + + +@pytest.fixture(autouse=True) +def pushover_setup_fixture(): + """Patch pushover setup entry.""" + with patch( + "homeassistant.components.pushover.async_setup_entry", return_value=True + ): + yield + + +async def test_flow_user(hass: HomeAssistant) -> None: + """Test user initialized flow.""" + 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=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Pushover" + assert result["data"] == MOCK_CONFIG + + +async def test_flow_user_key_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate user key.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="MYUSERKEY", + ) + + entry.add_to_hass(hass) + + 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=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_name_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="MYUSERKEY", + ) + + entry.add_to_hass(hass) + + new_config = MOCK_CONFIG.copy() + new_config[CONF_USER_KEY] = "NEUSERWKEY" + + 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=new_config, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_invalid_user_key( + hass: HomeAssistant, mock_pushover: MagicMock +) -> None: + """Test user initialized flow with wrong user key.""" + + mock_pushover.side_effect = BadAPIRequestError("400: user key is invalid") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_USER_KEY: "invalid_user_key"} + + +async def test_flow_invalid_api_key( + hass: HomeAssistant, mock_pushover: MagicMock +) -> None: + """Test user initialized flow with wrong api key.""" + + mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + + +async def test_flow_conn_err(hass: HomeAssistant, mock_pushover: MagicMock) -> None: + """Test user initialized flow with conn error.""" + + mock_pushover.side_effect = BadAPIRequestError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_import(hass: HomeAssistant) -> None: + """Test user initialized flow with unreachable server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Pushover" + assert result["data"] == MOCK_CONFIG + + +async def test_reauth_success(hass: HomeAssistant) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "NEWAPIKEY", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_reauth_failed(hass: HomeAssistant, mock_pushover: MagicMock) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "WRONGAPIKEY", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == { + CONF_API_KEY: "invalid_api_key", + } diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py new file mode 100644 index 00000000000..22b38be0b48 --- /dev/null +++ b/tests/components/pushover/test_init.py @@ -0,0 +1,98 @@ +"""Test pushbullet integration.""" +from collections.abc import Awaitable +from typing import Callable +from unittest.mock import MagicMock, patch + +import aiohttp +from pushover_complete import BadAPIRequestError +import pytest + +from homeassistant.components.notify.const import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.pushover.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry +from tests.components.repairs import get_repairs + + +@pytest.fixture(autouse=True) +def mock_pushover(): + """Mock pushover.""" + with patch( + "pushover_complete.PushoverAPI._generic_post", return_value={} + ) as mock_generic_post: + yield mock_generic_post + + +async def test_setup( + hass: HomeAssistant, + hass_ws_client: Callable[ + [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] + ], +) -> None: + """Test integration failed due to an error.""" + assert await async_setup_component( + hass, + NOTIFY_DOMAIN, + { + NOTIFY_DOMAIN: [ + { + "name": "Pushover", + "platform": "pushover", + "api_key": "MYAPIKEY", + "user_key": "MYUSERKEY", + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.config_entries.async_entries(DOMAIN) + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "deprecated_yaml" + + +async def test_async_setup_entry_success(hass: HomeAssistant) -> None: + """Test pushover successful setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + +async def test_async_setup_entry_failed_invalid_api_key( + hass: HomeAssistant, mock_pushover: MagicMock +) -> None: + """Test pushover failed setup due to invalid api key.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_async_setup_entry_failed_conn_error( + hass: HomeAssistant, mock_pushover: MagicMock +) -> None: + """Test pushover failed setup due to conn error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + mock_pushover.side_effect = BadAPIRequestError + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY From 4eb4146e29f711aaf1bbde81c767d4e03ea01f93 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 19 Aug 2022 08:36:46 +0200 Subject: [PATCH 481/903] Remove unneeded charging_status attribute in bmw_connected_drive binary sensor (#74921) * Use `charging_status.value` in attribute for BMW binary sensor * Remove `charging_status` attribute Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/binary_sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d7e28eef41c..87506cc2230 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -180,9 +180,6 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( icon="mdi:ev-station", # device class power: On means power detected, Off means no power value_fn=lambda v: v.fuel_and_battery.charging_status == ChargingState.CHARGING, - attr_fn=lambda v, u: { - "charging_status": str(v.fuel_and_battery.charging_status), - }, ), BMWBinarySensorEntityDescription( key="connection_status", From 1faabb8f401806bec0c93ccb5dca8dc856a99f50 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 19 Aug 2022 08:58:18 +0200 Subject: [PATCH 482/903] Add timeouts to requests calls (#76851) --- homeassistant/components/abode/camera.py | 4 +++- homeassistant/components/facebox/image_processing.py | 7 +++++-- homeassistant/components/llamalab_automate/notify.py | 2 +- homeassistant/components/nest/legacy/camera.py | 2 +- homeassistant/components/opencv/image_processing.py | 2 +- homeassistant/components/uk_transport/sensor.py | 2 +- homeassistant/components/withings/common.py | 1 + 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index d0f428a45fa..c4c2d0dc78d 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -74,7 +74,9 @@ class AbodeCamera(AbodeDevice, Camera): """Attempt to download the most recent capture.""" if self._device.image_url: try: - self._response = requests.get(self._device.image_url, stream=True) + self._response = requests.get( + self._device.image_url, stream=True, timeout=10 + ) self._response.raise_for_status() except requests.HTTPError as err: diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index 21eb91a04a3..5584efb883a 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -68,7 +68,7 @@ def check_box_health(url, username, password): if username: kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: - response = requests.get(url, **kwargs) + response = requests.get(url, **kwargs, timeout=10) if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None @@ -116,7 +116,9 @@ def post_image(url, image, username, password): if username: kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: - response = requests.post(url, json={"base64": encode_image(image)}, **kwargs) + response = requests.post( + url, json={"base64": encode_image(image)}, timeout=10, **kwargs + ) if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None @@ -137,6 +139,7 @@ def teach_file(url, name, file_path, username, password): url, data={FACEBOX_NAME: name, ATTR_ID: file_path}, files={"file": open_file}, + timeout=10, **kwargs, ) if response.status_code == HTTPStatus.UNAUTHORIZED: diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index e2850792906..af0271e107d 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -66,6 +66,6 @@ class AutomateNotificationService(BaseNotificationService): "payload": message, } - response = requests.post(_RESOURCE, json=data) + response = requests.post(_RESOURCE, json=data, timeout=10) if response.status_code != HTTPStatus.OK: _LOGGER.error("Error sending message: %s", response) diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py index 9974ae2daaa..affe912b6fb 100644 --- a/homeassistant/components/nest/legacy/camera.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -142,7 +142,7 @@ class NestCamera(Camera): url = self.device.snapshot_url try: - response = requests.get(url) + response = requests.get(url, timeout=10) except requests.exceptions.RequestException as error: _LOGGER.error("Error getting camera image: %s", error) return None diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index cde5a8264b8..463b5e0eed1 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -87,7 +87,7 @@ def _create_processor_from_config(hass, camera_entity, config): def _get_default_classifier(dest_path): """Download the default OpenCV classifier.""" _LOGGER.info("Downloading default classifier") - req = requests.get(CASCADE_URL, stream=True) + req = requests.get(CASCADE_URL, stream=True, timeout=10) with open(dest_path, "wb") as fil: for chunk in req.iter_content(chunk_size=1024): if chunk: # filter out keep-alive new chunks diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index f3c7c5d84a0..28d0fd71142 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -134,7 +134,7 @@ class UkTransportSensor(SensorEntity): {"app_id": self._api_app_id, "app_key": self._api_app_key}, **params ) - response = requests.get(self._url, params=request_params) + response = requests.get(self._url, params=request_params, timeout=10) if response.status_code != HTTPStatus.OK: _LOGGER.warning("Invalid response from API") elif "error" in response.json(): diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 93c3800e42f..11badca8d8c 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -503,6 +503,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): f"{self.URL}/{path}", params=params, headers={"Authorization": f"Bearer {access_token}"}, + timeout=10, ) return response.json() From 6d49362573e00cba77431438b20b81b39b9bd58f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Aug 2022 09:33:57 +0200 Subject: [PATCH 483/903] Revert rename of confirm step in zha config flow (#77010) * Revert rename of confirm step in zha config flow * Update tests --- homeassistant/components/zha/config_flow.py | 6 +++--- homeassistant/components/zha/strings.json | 3 +++ .../homeassistant_sky_connect/test_config_flow.py | 2 -- tests/components/zha/test_config_flow.py | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 4b90fdb3ad0..9c7ec46a386 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -138,9 +138,9 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._set_confirm_only() self.context["title_placeholders"] = {CONF_NAME: self._title} - return await self.async_step_confirm_usb() + return await self.async_step_confirm() - async def async_step_confirm_usb(self, user_input=None): + async def async_step_confirm(self, user_input=None): """Confirm a USB discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): auto_detected_data = await detect_radios(self._device_path) @@ -155,7 +155,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="confirm_usb", + step_id="confirm", description_placeholders={CONF_NAME: self._title}, ) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 4eb872f4fae..37be80e9b56 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -10,6 +10,9 @@ "confirm": { "description": "Do you want to setup {name}?" }, + "confirm_hardware": { + "description": "Do you want to setup {name}?" + }, "pick_radio": { "data": { "radio_type": "Radio Type" }, "title": "Radio Type", diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 1db305f3ad0..bbde732d201 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -21,8 +21,6 @@ USB_DATA = usb.UsbServiceInfo( async def test_config_flow(hass: HomeAssistant) -> None: """Test the config flow.""" - # mock_integration(hass, MockModule("hassio")) - with patch( "homeassistant.components.homeassistant_sky_connect.async_setup_entry", return_value=True, diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 82c2fde7c1e..a769303a4c4 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -228,7 +228,7 @@ async def test_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm_usb" + assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -264,7 +264,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm_usb" + assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -298,7 +298,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm_usb" + assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -451,7 +451,7 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm_usb" + assert result["step_id"] == "confirm" @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) From dd109839b9ddc8b33d2ef1c0039c51b7e1b67c5e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 19 Aug 2022 01:39:48 -0600 Subject: [PATCH 484/903] Provide slight speedup to Guardian device lookup during service call (#77004) * Provide slight speedup to Guardian device lookup during service call * Messages --- homeassistant/components/guardian/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 909752b5b33..a30536f66c3 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -103,12 +103,16 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) - if device_entry := device_registry.async_get(device_id): - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.entry_id in device_entry.config_entries: - return entry.entry_id + if (device_entry := device_registry.async_get(device_id)) is None: + raise ValueError(f"Invalid Guardian device ID: {device_id}") - raise ValueError(f"No client for device ID: {device_id}") + for entry_id in device_entry.config_entries: + if (entry := hass.config_entries.async_get_entry(entry_id)) is None: + continue + if entry.domain == DOMAIN: + return entry_id + + raise ValueError(f"No config entry for device ID: {device_id}") @callback From dedf063e43d94d9a0842ef9976d611c2a9229c0b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 09:54:13 +0200 Subject: [PATCH 485/903] Improve entity type hints [b] (#77012) --- homeassistant/components/balboa/climate.py | 9 +++-- .../components/bayesian/binary_sensor.py | 4 +- homeassistant/components/bbox/sensor.py | 4 +- .../components/beewi_smartclim/sensor.py | 2 +- homeassistant/components/bitcoin/sensor.py | 2 +- homeassistant/components/bizkaibus/sensor.py | 2 +- .../components/blackbird/media_player.py | 8 ++-- homeassistant/components/blebox/climate.py | 5 ++- homeassistant/components/blebox/light.py | 3 +- homeassistant/components/blebox/switch.py | 5 ++- .../components/blink/binary_sensor.py | 2 +- homeassistant/components/blink/camera.py | 6 +-- homeassistant/components/blink/sensor.py | 2 +- homeassistant/components/blockchain/sensor.py | 2 +- .../components/bloomsky/binary_sensor.py | 2 +- homeassistant/components/bloomsky/sensor.py | 2 +- .../components/bluesound/media_player.py | 38 +++++++++++-------- homeassistant/components/bosch_shc/switch.py | 9 +++-- homeassistant/components/broadlink/remote.py | 14 ++++--- homeassistant/components/broadlink/switch.py | 7 ++-- homeassistant/components/bsblan/climate.py | 4 +- 21 files changed, 73 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index 98acf88649e..cd10ccf2bc9 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -121,7 +122,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): """Return current preset mode.""" return self._client.get_heatmode(True) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature.""" scale = self._client.get_tempscale() newtemp = kwargs[ATTR_TEMPERATURE] @@ -133,7 +134,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): await asyncio.sleep(SET_TEMPERATURE_WAIT) await self._client.send_temp_change(newtemp) - async def async_set_preset_mode(self, preset_mode) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" modelist = self._client.get_heatmode_stringlist() self._async_validate_mode_or_raise(preset_mode) @@ -141,7 +142,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): raise ValueError(f"{preset_mode} is not a valid preset mode") await self._client.change_heatmode(modelist.index(preset_mode)) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" await self._client.change_blower(self._ha_to_balboa_blower_map[fan_mode]) @@ -150,7 +151,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): if mode == self._client.HEATMODE_RNR: raise ValueError(f"{mode} can only be reported but not set") - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode. OFF = Rest diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 216a8360a84..5641480ba98 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -162,7 +162,7 @@ class BayesianBinarySensor(BinarySensorEntity): "state": self._process_state, } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """ Call when entity about to be added. @@ -397,7 +397,7 @@ class BayesianBinarySensor(BinarySensorEntity): ATTR_PROBABILITY_THRESHOLD: self._probability_threshold, } - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the states.""" if not self._callbacks: self._recalculate_and_write_state() diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index b85c75569d4..43ba6956507 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -136,7 +136,7 @@ class BboxUptimeSensor(SensorEntity): self._attr_name = f"{name} {description.name}" self.bbox_data = bbox_data - def update(self): + def update(self) -> None: """Get the latest data from Bbox and update the state.""" self.bbox_data.update() self._attr_native_value = utcnow() - timedelta( @@ -155,7 +155,7 @@ class BboxSensor(SensorEntity): self._attr_name = f"{name} {description.name}" self.bbox_data = bbox_data - def update(self): + def update(self) -> None: """Get the latest data from Bbox and update the state.""" self.bbox_data.update() sensor_type = self.entity_description.key diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 476ffbdbf1a..4d8936859f5 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -73,7 +73,7 @@ class BeewiSmartclimSensor(SensorEntity): self._attr_device_class = self._device self._attr_unique_id = f"{mac}_{device}" - def update(self): + def update(self) -> None: """Fetch new state data from the poller.""" self._poller.update_sensor() self._attr_native_value = None diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index d5e9cc9adad..49691089f35 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -181,7 +181,7 @@ class BitcoinSensor(SensorEntity): self.data = data self._currency = currency - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" self.data.update() stats = self.data.stats diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index d79192af72d..2c2d1b9db29 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -54,7 +54,7 @@ class BizkaibusSensor(SensorEntity): self.data = data self._attr_name = name - def update(self): + def update(self) -> None: """Get the latest data from the webservice.""" self.data.update() with suppress(TypeError): diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index a20c4294438..b0fda2de0f4 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -158,7 +158,7 @@ class BlackbirdZone(MediaPlayerEntity): self._zone_id = zone_id self._attr_name = zone_name - def update(self): + def update(self) -> None: """Retrieve latest state.""" state = self._blackbird.zone_status(self._zone_id) if not state: @@ -183,7 +183,7 @@ class BlackbirdZone(MediaPlayerEntity): _LOGGER.debug("Setting all zones source to %s", idx) self._blackbird.set_all_zone_source(idx) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set input source.""" if source not in self._source_name_id: return @@ -191,12 +191,12 @@ class BlackbirdZone(MediaPlayerEntity): _LOGGER.debug("Setting zone %d source to %s", self._zone_id, idx) self._blackbird.set_zone_source(self._zone_id, idx) - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" _LOGGER.debug("Turning zone %d on", self._zone_id) self._blackbird.set_zone_power(self._zone_id, True) - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" _LOGGER.debug("Turning zone %d off", self._zone_id) self._blackbird.set_zone_power(self._zone_id, False) diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index e279991df20..78f47b1ba46 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -1,5 +1,6 @@ """BleBox climate entity.""" from datetime import timedelta +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -73,7 +74,7 @@ class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): """Return the desired thermostat temperature.""" return self._feature.desired - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the climate entity mode.""" if hvac_mode == HVACMode.HEAT: await self._feature.async_on() @@ -81,7 +82,7 @@ class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): await self._feature.async_off() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set the thermostat temperature.""" value = kwargs[ATTR_TEMPERATURE] await self._feature.async_set_temperature(value) diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index a2ad51cfc9c..8202186d86d 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import blebox_uniapi.light from blebox_uniapi.light import BleboxColorMode @@ -175,6 +176,6 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): f"Turning on with effect '{self.name}' failed: {effect} not in effect list." ) from exc - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._feature.async_off() diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index f9c866244c7..5ae37d6b34d 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -1,5 +1,6 @@ """BleBox switch implementation.""" from datetime import timedelta +from typing import Any from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -35,10 +36,10 @@ class BleBoxSwitchEntity(BleBoxEntity, SwitchEntity): """Return whether switch is on.""" return self._feature.is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" await self._feature.async_turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self._feature.async_turn_off() diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 5b2e490cda9..ddcf58deb6f 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -69,7 +69,7 @@ class BlinkBinarySensor(BinarySensorEntity): model=self._camera.camera_type, ) - def update(self): + def update(self) -> None: """Update sensor state.""" self.data.refresh() state = self._camera.attributes[self.entity_description.key] diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 429452cf4bd..e500eb79e42 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -58,18 +58,18 @@ class BlinkCamera(Camera): """Return the camera attributes.""" return self._camera.attributes - def enable_motion_detection(self): + def enable_motion_detection(self) -> None: """Enable motion detection for the camera.""" self._camera.arm = True self.data.refresh() - def disable_motion_detection(self): + def disable_motion_detection(self) -> None: """Disable motion detection for the camera.""" self._camera.arm = False self.data.refresh() @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the state of the camera.""" return self._camera.arm diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 094fa8aadbb..3940522074b 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -72,7 +72,7 @@ class BlinkSensor(SensorEntity): model=self._camera.camera_type, ) - def update(self): + def update(self) -> None: """Retrieve sensor data from the camera.""" self.data.refresh() try: diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index 92cd7a56e92..4feb7a9fa6a 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -65,6 +65,6 @@ class BlockchainSensor(SensorEntity): self._attr_name = name self.addresses = addresses - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" self._attr_native_value = get_balance(self.addresses) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index e1d9d830efa..7b59039a89e 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -58,7 +58,7 @@ class BloomSkySensor(BinarySensorEntity): self._attr_unique_id = f"{self._device_id}-{sensor_name}" self._attr_device_class = SENSOR_TYPES.get(sensor_name) - def update(self): + def update(self) -> None: """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index f064ce992c6..77978e6324d 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -112,7 +112,7 @@ class BloomSkySensor(SensorEntity): """Return the class of this device, from component DEVICE_CLASSES.""" return SENSOR_DEVICE_CLASS.get(self._sensor_name) - def update(self): + def update(self) -> None: """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 7f1c6b6553f..53f6de14b3c 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -6,6 +6,7 @@ from asyncio import CancelledError from datetime import timedelta from http import HTTPStatus import logging +from typing import Any from urllib import parse import aiohttp @@ -22,6 +23,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( + BrowseMedia, async_process_play_media_url, ) from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC @@ -333,7 +335,7 @@ class BluesoundPlayer(MediaPlayerEntity): ) raise - async def async_update(self): + async def async_update(self) -> None: """Update internal status of the entity.""" if not self._is_online: return @@ -930,12 +932,12 @@ class BluesoundPlayer(MediaPlayerEntity): while sleep > 0: sleep = await self.async_increase_timer() - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable or disable shuffle mode.""" value = "1" if shuffle else "0" return await self.send_bluesound_command(f"/Shuffle?state={value}") - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" if self.is_grouped and not self.is_master: return @@ -958,14 +960,14 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(url) - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command("Clear") - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send media_next command to media player.""" if self.is_grouped and not self.is_master: return @@ -978,7 +980,7 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(cmd) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send media_previous command to media player.""" if self.is_grouped and not self.is_master: return @@ -991,35 +993,37 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(cmd) - async def async_media_play(self): + async def async_media_play(self) -> None: """Send media_play command to media player.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command("Play") - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send media_pause command to media player.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command("Pause") - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command("Pause") - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send media_seek command to media player.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command(f"Play?seek={float(position)}") - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" if self.is_grouped and not self.is_master: return @@ -1036,21 +1040,21 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(url) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" current_vol = self.volume_level if not current_vol or current_vol >= 1: return return await self.async_set_volume_level(current_vol + 0.01) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down the media player.""" current_vol = self.volume_level if not current_vol or current_vol <= 0: return return await self.async_set_volume_level(current_vol - 0.01) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Send volume_up command to media player.""" if volume < 0: volume = 0 @@ -1058,13 +1062,15 @@ class BluesoundPlayer(MediaPlayerEntity): volume = 1 return await self.send_bluesound_command(f"Volume?level={float(volume) * 100}") - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command to media player.""" if mute: return await self.send_bluesound_command("Volume?mute=1") return await self.send_bluesound_command("Volume?mute=0") - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( self.hass, diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index aa49d873f6f..28b68ac091b 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any from boschshcpy import ( SHCCamera360, @@ -183,11 +184,11 @@ class SHCSwitch(SHCEntity, SwitchEntity): == self.entity_description.on_value ) - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" setattr(self._device, self.entity_description.on_key, True) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" setattr(self._device, self.entity_description.on_key, False) @@ -218,10 +219,10 @@ class SHCRoutingSwitch(SHCEntity, SwitchEntity): """Return the state of the switch.""" return self._device.routing.name == "ENABLED" - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._device.routing = True - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._device.routing = False diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index dd0f40d45bc..da72f4fcb06 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -2,9 +2,11 @@ import asyncio from base64 import b64encode from collections import defaultdict +from collections.abc import Iterable from datetime import timedelta from itertools import product import logging +from typing import Any from broadlink.exceptions import ( AuthorizationError, @@ -174,18 +176,18 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): """ return self._flags - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when the remote is added to hass.""" state = await self.async_get_last_state() self._attr_is_on = state is None or state.state != STATE_OFF await super().async_added_to_hass() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the remote.""" self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the remote.""" self._attr_is_on = False self.async_write_ha_state() @@ -198,7 +200,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): self._flags.update(await self._flag_storage.async_load() or {}) self._storage_loaded = True - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a list of commands to a device.""" kwargs[ATTR_COMMAND] = command kwargs = SERVICE_SEND_SCHEMA(kwargs) @@ -255,7 +257,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): if at_least_one_sent: self._flag_storage.async_delay_save(self._get_flags, FLAG_SAVE_DELAY) - async def async_learn_command(self, **kwargs): + async def async_learn_command(self, **kwargs: Any) -> None: """Learn a list of commands from a remote.""" kwargs = SERVICE_LEARN_SCHEMA(kwargs) commands = kwargs[ATTR_COMMAND] @@ -419,7 +421,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): self.hass, notification_id="learn_command" ) - async def async_delete_command(self, **kwargs): + async def async_delete_command(self, **kwargs: Any) -> None: """Delete a list of commands from a remote.""" kwargs = SERVICE_DELETE_SCHEMA(kwargs) commands = kwargs[ATTR_COMMAND] diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index d38898a513f..229949b7ee2 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod import logging +from typing import Any from broadlink.exceptions import BroadlinkException import voluptuous as vol @@ -146,19 +147,19 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): self._command_off = command_off self._attr_name = f"{device.name} Switch" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when the switch is added to hass.""" state = await self.async_get_last_state() self._attr_is_on = state is not None and state.state == STATE_ON await super().async_added_to_hass() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" if await self._async_send_packet(self._command_on): self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" if await self._async_send_packet(self._command_off): self._attr_is_on = False diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 16d8452f10b..7ebcb48f307 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -106,14 +106,14 @@ class BSBLanClimate(ClimateEntity): self._store_hvac_mode = self._attr_hvac_mode await self.async_set_data(preset_mode=preset_mode) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set HVAC mode.""" _LOGGER.debug("Setting HVAC mode to: %s", hvac_mode) # preset should be none when hvac mode is set self._attr_preset_mode = PRESET_NONE await self.async_set_data(hvac_mode=hvac_mode) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" await self.async_set_data(**kwargs) From 4de50fc4718257a8de519ffcd7d8eee2ce91c37f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:09:20 +0200 Subject: [PATCH 486/903] Improve type hint in bsblan climate entity (#77014) --- homeassistant/components/bsblan/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 7ebcb48f307..e83415ebf52 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -85,7 +85,7 @@ class BSBLanClimate(ClimateEntity): ) -> None: """Initialize BSBLan climate device.""" self._attr_available = True - self._store_hvac_mode = None + self._store_hvac_mode: HVACMode | str | None = None self.bsblan = bsblan self._attr_name = self._attr_unique_id = info.device_identification self._attr_device_info = DeviceInfo( @@ -95,7 +95,7 @@ class BSBLanClimate(ClimateEntity): name="BSBLan Device", ) - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" _LOGGER.debug("Setting preset mode to: %s", preset_mode) if preset_mode == PRESET_NONE: From d70bc68b9392392633b91f7bd815531ab3e90225 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:30:34 +0200 Subject: [PATCH 487/903] Improve type hint in brottsplatskartan sensor entity (#77015) --- homeassistant/components/brottsplatskartan/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 171986ffd1c..535fa9f56ad 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -92,10 +92,10 @@ class BrottsplatskartanSensor(SensorEntity): self._brottsplatskartan = bpk self._attr_name = name - def update(self): + def update(self) -> None: """Update device state.""" - incident_counts = defaultdict(int) + incident_counts: defaultdict[str, int] = defaultdict(int) incidents = self._brottsplatskartan.get_incidents() if incidents is False: From 801f7d1d5f95d4ae8b60dbef85ced0b257faf049 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:33:34 +0200 Subject: [PATCH 488/903] Adjust type hints in airtouch4 climate entity (#76987) --- homeassistant/components/airtouch4/climate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 370a061e901..dcc107453d5 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -294,9 +295,11 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): ) return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds] - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" - temp = kwargs.get(ATTR_TEMPERATURE) + if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: + _LOGGER.debug("Argument `temperature` is missing in set_temperature") + return _LOGGER.debug("Setting temp of %s to %s", self._group_number, str(temp)) self._unit = await self._airtouch.SetGroupToTemperature( From 655e2f92ba8f7cfab3dd13b8676d9fcafd82ba1e Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 19 Aug 2022 11:39:14 +0300 Subject: [PATCH 489/903] Add strict typing to mikrotik (#76974) add strict typing to mikrotik --- .strict-typing | 1 + homeassistant/components/mikrotik/__init__.py | 7 +- homeassistant/components/mikrotik/const.py | 3 - homeassistant/components/mikrotik/device.py | 66 ++++++++++++++++++ .../components/mikrotik/device_tracker.py | 2 +- homeassistant/components/mikrotik/hub.py | 67 ++----------------- mypy.ini | 10 +++ 7 files changed, 88 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/mikrotik/device.py diff --git a/.strict-typing b/.strict-typing index f8a0579433b..a215c2187ab 100644 --- a/.strict-typing +++ b/.strict-typing @@ -170,6 +170,7 @@ homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.media_source.* homeassistant.components.metoffice.* +homeassistant.components.mikrotik.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index f72c79c1559..6a158c60fcf 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,15 +1,18 @@ """The Mikrotik component.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from .const import ATTR_MANUFACTURER, DOMAIN, PLATFORMS +from .const import ATTR_MANUFACTURER, DOMAIN from .errors import CannotConnect, LoginError from .hub import MikrotikDataUpdateCoordinator, get_api CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +PLATFORMS = [Platform.DEVICE_TRACKER] + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Mikrotik component.""" @@ -26,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) device_registry = dr.async_get(hass) device_registry.async_get_or_create( diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index bbe129c4a00..911d348365e 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -1,8 +1,6 @@ """Constants used in the Mikrotik components.""" from typing import Final -from homeassistant.const import Platform - DOMAIN: Final = "mikrotik" DEFAULT_NAME: Final = "Mikrotik" DEFAULT_API_PORT: Final = 8728 @@ -40,7 +38,6 @@ MIKROTIK_SERVICES: Final = { IS_CAPSMAN: "/caps-man/interface/print", } -PLATFORMS: Final = [Platform.DEVICE_TRACKER] ATTR_DEVICE_TRACKER: Final = [ "comment", diff --git a/homeassistant/components/mikrotik/device.py b/homeassistant/components/mikrotik/device.py new file mode 100644 index 00000000000..f37ef6fee80 --- /dev/null +++ b/homeassistant/components/mikrotik/device.py @@ -0,0 +1,66 @@ +"""Network client device class.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from homeassistant.util import slugify +import homeassistant.util.dt as dt_util + +from .const import ATTR_DEVICE_TRACKER + + +class Device: + """Represents a network device.""" + + def __init__(self, mac: str, params: dict[str, Any]) -> None: + """Initialize the network device.""" + self._mac = mac + self._params = params + self._last_seen: datetime | None = None + self._attrs: dict[str, Any] = {} + self._wireless_params: dict[str, Any] = {} + + @property + def name(self) -> str: + """Return device name.""" + return self._params.get("host-name", self.mac) + + @property + def ip_address(self) -> str | None: + """Return device primary ip address.""" + return self._params.get("address") + + @property + def mac(self) -> str: + """Return device mac.""" + return self._mac + + @property + def last_seen(self) -> datetime | None: + """Return device last seen.""" + return self._last_seen + + @property + def attrs(self) -> dict[str, Any]: + """Return device attributes.""" + attr_data = self._wireless_params | self._params + for attr in ATTR_DEVICE_TRACKER: + if attr in attr_data: + self._attrs[slugify(attr)] = attr_data[attr] + return self._attrs + + def update( + self, + wireless_params: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + active: bool = False, + ) -> None: + """Update Device params.""" + if wireless_params: + self._wireless_params = wireless_params + if params: + self._params = params + if active: + self._last_seen = dt_util.utcnow() diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 856521f019d..f50c49d5ab6 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -63,7 +63,7 @@ def update_items( coordinator: MikrotikDataUpdateCoordinator, async_add_entities: AddEntitiesCallback, tracked: dict[str, MikrotikDataUpdateCoordinatorTracker], -): +) -> None: """Update tracked device state from the hub.""" new_tracked: list[MikrotikDataUpdateCoordinatorTracker] = [] for mac, device in coordinator.api.devices.items(): diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 914911ee5cc..08320c603f9 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -1,7 +1,7 @@ """The Mikrotik router class.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta import logging import socket import ssl @@ -14,12 +14,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import slugify -import homeassistant.util.dt as dt_util from .const import ( ARP, - ATTR_DEVICE_TRACKER, ATTR_FIRMWARE, ATTR_MODEL, ATTR_SERIAL_NUMBER, @@ -38,66 +35,12 @@ from .const import ( NAME, WIRELESS, ) +from .device import Device from .errors import CannotConnect, LoginError _LOGGER = logging.getLogger(__name__) -class Device: - """Represents a network device.""" - - def __init__(self, mac: str, params: dict[str, Any]) -> None: - """Initialize the network device.""" - self._mac = mac - self._params = params - self._last_seen: datetime | None = None - self._attrs: dict[str, Any] = {} - self._wireless_params: dict[str, Any] = {} - - @property - def name(self) -> str: - """Return device name.""" - return self._params.get("host-name", self.mac) - - @property - def ip_address(self) -> str | None: - """Return device primary ip address.""" - return self._params.get("address") - - @property - def mac(self) -> str: - """Return device mac.""" - return self._mac - - @property - def last_seen(self) -> datetime | None: - """Return device last seen.""" - return self._last_seen - - @property - def attrs(self) -> dict[str, Any]: - """Return device attributes.""" - attr_data = self._wireless_params | self._params - for attr in ATTR_DEVICE_TRACKER: - if attr in attr_data: - self._attrs[slugify(attr)] = attr_data[attr] - return self._attrs - - def update( - self, - wireless_params: dict[str, Any] | None = None, - params: dict[str, Any] | None = None, - active: bool = False, - ) -> None: - """Update Device params.""" - if wireless_params: - self._wireless_params = wireless_params - if params: - self._params = params - if active: - self._last_seen = dt_util.utcnow() - - class MikrotikData: """Handle all communication with the Mikrotik API.""" @@ -248,8 +191,8 @@ class MikrotikData: self, cmd: str, params: dict[str, Any] | None = None ) -> list[dict[str, Any]]: """Retrieve data from Mikrotik API.""" + _LOGGER.debug("Running command %s", cmd) try: - _LOGGER.debug("Running command %s", cmd) if params: return list(self.api(cmd=cmd, **params)) return list(self.api(cmd=cmd)) @@ -273,7 +216,7 @@ class MikrotikData: return [] -class MikrotikDataUpdateCoordinator(DataUpdateCoordinator): +class MikrotikDataUpdateCoordinator(DataUpdateCoordinator[None]): """Mikrotik Hub Object.""" def __init__( @@ -293,7 +236,7 @@ class MikrotikDataUpdateCoordinator(DataUpdateCoordinator): @property def host(self) -> str: """Return the host of this hub.""" - return self.config_entry.data[CONF_HOST] + return str(self.config_entry.data[CONF_HOST]) @property def hostname(self) -> str: diff --git a/mypy.ini b/mypy.ini index 051c1065423..fd609d8099b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1459,6 +1459,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.mikrotik.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true From c3305caabec31ea1a1977f7fac910fd07a68cdbb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 19 Aug 2022 02:41:33 -0600 Subject: [PATCH 490/903] Provide slight speedup to RainMachine device lookup during service call (#76944) Fix --- homeassistant/components/rainmachine/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index a5426552ae2..52de2e1c61a 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -159,11 +159,15 @@ def async_get_controller_for_service_call( device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) - if device_entry := device_registry.async_get(device_id): - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.entry_id in device_entry.config_entries: - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] - return data.controller + if (device_entry := device_registry.async_get(device_id)) is None: + raise ValueError(f"Invalid RainMachine device ID: {device_id}") + + for entry_id in device_entry.config_entries: + if (entry := hass.config_entries.async_get_entry(entry_id)) is None: + continue + if entry.domain == DOMAIN: + data: RainMachineData = hass.data[DOMAIN][entry_id] + return data.controller raise ValueError(f"No controller for device ID: {device_id}") From cbeaea98d16c354032fad2fca257865faaf4d2fb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 19 Aug 2022 04:56:01 -0400 Subject: [PATCH 491/903] Remove deprecated YAML configuration from Skybell (#76940) --- homeassistant/components/skybell/__init__.py | 59 +++++-------------- .../components/skybell/binary_sensor.py | 16 +---- homeassistant/components/skybell/camera.py | 30 +--------- .../components/skybell/config_flow.py | 7 --- homeassistant/components/skybell/const.py | 3 - .../components/skybell/manifest.json | 2 +- homeassistant/components/skybell/sensor.py | 16 ----- homeassistant/components/skybell/strings.json | 6 ++ homeassistant/components/skybell/switch.py | 20 +------ .../components/skybell/translations/en.json | 6 ++ tests/components/skybell/test_config_flow.py | 34 +---------- 11 files changed, 33 insertions(+), 166 deletions(-) diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 9f032327d62..1e272dba27f 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -2,39 +2,22 @@ from __future__ import annotations import asyncio -import os from aioskybell import Skybell from aioskybell.exceptions import SkybellAuthenticationException, SkybellException -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType -from .const import DEFAULT_CACHEDB, DOMAIN +from .const import DOMAIN from .coordinator import SkybellDataUpdateCoordinator -CONFIG_SCHEMA = vol.Schema( - vol.All( - # Deprecated in Home Assistant 2022.6 - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CAMERA, @@ -48,31 +31,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the SkyBell component.""" hass.data.setdefault(DOMAIN, {}) - entry_config = {} - if DOMAIN not in config: - return True - for parameter, value in config[DOMAIN].items(): - if parameter == CONF_USERNAME: - entry_config[CONF_EMAIL] = value - else: - entry_config[parameter] = value - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=entry_config, - ) + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", ) - # Clean up unused cache file since we are using an account specific name - # Remove with import - def clean_cache(): - """Clean old cache filename.""" - if os.path.exists(hass.config.path(DEFAULT_CACHEDB)): - os.remove(hass.config.path(DEFAULT_CACHEDB)) - - await hass.async_add_executor_job(clean_cache) - return True diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index 05f007e9455..6b49307d439 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -2,18 +2,14 @@ from __future__ import annotations from aioskybell.helpers import const as CONST -import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN @@ -33,21 +29,11 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( ), ) -# Deprecated in Home Assistant 2022.6 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_ENTITY_NAMESPACE, default=DOMAIN): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)] - ), - } -) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Skybell switch.""" + """Set up Skybell binary sensor.""" async_add_entities( SkybellBinarySensor(coordinator, sensor) for sensor in BINARY_SENSOR_TYPES diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 5bbcea833c2..b9aba0e82ac 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -3,43 +3,19 @@ from __future__ import annotations from aiohttp import web from haffmpeg.camera import CameraMjpeg -import voluptuous as vol -from homeassistant.components.camera import ( - PLATFORM_SCHEMA, - Camera, - CameraEntityDescription, -) +from homeassistant.components.camera import Camera, CameraEntityDescription from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - CONF_ACTIVITY_NAME, - CONF_AVATAR_NAME, - DOMAIN, - IMAGE_ACTIVITY, - IMAGE_AVATAR, -) +from .const import DOMAIN from .coordinator import SkybellDataUpdateCoordinator from .entity import SkybellEntity -# Deprecated in Home Assistant 2022.6 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=[IMAGE_AVATAR]): vol.All( - cv.ensure_list, [vol.In([IMAGE_AVATAR, IMAGE_ACTIVITY])] - ), - vol.Optional(CONF_ACTIVITY_NAME): cv.string, - vol.Optional(CONF_AVATAR_NAME): cv.string, - } -) - CAMERA_TYPES: tuple[CameraEntityDescription, ...] = ( CameraEntityDescription(key="activity", name="Last activity"), CameraEntityDescription(key="avatar", name="Camera"), @@ -49,7 +25,7 @@ CAMERA_TYPES: tuple[CameraEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Skybell switch.""" + """Set up Skybell camera.""" entities = [] for description in CAMERA_TYPES: for coordinator in hass.data[DOMAIN][entry.entry_id]: diff --git a/homeassistant/components/skybell/config_flow.py b/homeassistant/components/skybell/config_flow.py index 7b7b43788b3..908eab4c46d 100644 --- a/homeassistant/components/skybell/config_flow.py +++ b/homeassistant/components/skybell/config_flow.py @@ -10,7 +10,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -18,12 +17,6 @@ from .const import DOMAIN class SkybellFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Skybell.""" - async def async_step_import(self, user_input: ConfigType) -> FlowResult: - """Import a config entry from configuration.yaml.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - return await self.async_step_user(user_input) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/skybell/const.py b/homeassistant/components/skybell/const.py index d8f7e4992d5..1d46e45dad1 100644 --- a/homeassistant/components/skybell/const.py +++ b/homeassistant/components/skybell/const.py @@ -2,9 +2,6 @@ import logging from typing import Final -CONF_ACTIVITY_NAME = "activity_name" -CONF_AVATAR_NAME = "avatar_name" -DEFAULT_CACHEDB = "./skybell_cache.pickle" DEFAULT_NAME = "SkyBell" DOMAIN: Final = "skybell" diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index bfef4bc3422..4365a9cf713 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", "requirements": ["aioskybell==22.7.0"], - "dependencies": ["ffmpeg"], + "dependencies": ["ffmpeg", "repairs"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["aioskybell"] diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index 352d29bd793..7acc30d0bd0 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -7,18 +7,14 @@ from typing import Any from aioskybell import SkybellDevice from aioskybell.helpers import const as CONST -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -95,18 +91,6 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), ) -MONITORED_CONDITIONS = SENSOR_TYPES - -# Deprecated in Home Assistant 2022.6 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_ENTITY_NAMESPACE, default=DOMAIN): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] - ), - } -) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/skybell/strings.json b/homeassistant/components/skybell/strings.json index e48a75c12bd..949223250df 100644 --- a/homeassistant/components/skybell/strings.json +++ b/homeassistant/components/skybell/strings.json @@ -17,5 +17,11 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "issues": { + "removed_yaml": { + "title": "The Skybell YAML configuration has been removed", + "description": "Configuring Skybell using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index 529be94f1ac..b3cb8c53032 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -3,17 +3,9 @@ from __future__ import annotations from typing import Any, cast -import voluptuous as vol - -from homeassistant.components.switch import ( - PLATFORM_SCHEMA, - SwitchEntity, - SwitchEntityDescription, -) +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -34,16 +26,6 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( ), ) -# Deprecated in Home Assistant 2022.6 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_ENTITY_NAMESPACE, default=DOMAIN): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(SWITCH_TYPES)] - ), - } -) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/skybell/translations/en.json b/homeassistant/components/skybell/translations/en.json index d996004e5c4..f9fa0048854 100644 --- a/homeassistant/components/skybell/translations/en.json +++ b/homeassistant/components/skybell/translations/en.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "title": "The Skybell YAML configuration has been removed", + "description": "Configuring Skybell using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } \ No newline at end of file diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index cd2b5053ac7..21ead201b54 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from aioskybell import exceptions from homeassistant.components.skybell.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -99,35 +99,3 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} - - -async def test_flow_import(hass: HomeAssistant) -> None: - """Test import step.""" - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=CONF_CONFIG_FLOW, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "user" - assert result["data"] == CONF_CONFIG_FLOW - - -async def test_flow_import_already_configured(hass: HomeAssistant) -> None: - """Test import step already configured.""" - entry = MockConfigEntry( - domain=DOMAIN, unique_id="123456789012345678901234", data=CONF_CONFIG_FLOW - ) - - entry.add_to_hass(hass) - - with _patch_skybell(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" From 61af82223f04548225d991b1ec04e3ce9ab9a526 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:58:51 +0200 Subject: [PATCH 492/903] Improve type hint in blebox light entity (#77013) * Improve type hint in blebox light entity * Adjust * Adjust supported_features * Adjust effect_list property * Improve base class --- homeassistant/components/blebox/__init__.py | 5 +++-- homeassistant/components/blebox/light.py | 11 +++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index ff907d728b7..0f4bd1c1490 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -3,6 +3,7 @@ import logging from blebox_uniapi.box import Box from blebox_uniapi.error import Error +from blebox_uniapi.feature import Feature from blebox_uniapi.session import ApiHost from homeassistant.config_entries import ConfigEntry @@ -83,7 +84,7 @@ def create_blebox_entities( class BleBoxEntity(Entity): """Implements a common class for entities representing a BleBox feature.""" - def __init__(self, feature): + def __init__(self, feature: Feature) -> None: """Initialize a BleBox entity.""" self._feature = feature self._attr_name = feature.full_name @@ -97,7 +98,7 @@ class BleBoxEntity(Entity): sw_version=product.firmware_version, ) - async def async_update(self): + async def async_update(self) -> None: """Update the entity state.""" try: await self._feature.async_update() diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 8202186d86d..c1245d52fec 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -56,11 +56,14 @@ COLOR_MODE_MAP = { class BleBoxLightEntity(BleBoxEntity, LightEntity): """Representation of BleBox lights.""" - def __init__(self, feature): + _feature: blebox_uniapi.light.Light + + def __init__(self, feature: blebox_uniapi.light.Light) -> None: """Initialize a BleBox light.""" super().__init__(feature) self._attr_supported_color_modes = {self.color_mode} - self._attr_supported_features = LightEntityFeature.EFFECT + if feature.effect_list: + self._attr_supported_features = LightEntityFeature.EFFECT @property def is_on(self) -> bool: @@ -91,7 +94,7 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): return color_mode_tmp @property - def effect_list(self) -> list[str] | None: + def effect_list(self) -> list[str]: """Return the list of supported effects.""" return self._feature.effect_list @@ -125,7 +128,7 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): return None return tuple(blebox_uniapi.light.Light.rgb_hex_to_rgb_list(rgbww_hex)) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" rgbw = kwargs.get(ATTR_RGBW_COLOR) From f966b48d8477df3af2c76bdace1d0e1571142c2d Mon Sep 17 00:00:00 2001 From: Johannes Jonker Date: Fri, 19 Aug 2022 11:01:42 +0200 Subject: [PATCH 493/903] Add newly-released Amazon Polly voices (#76934) * Add newly-released Amazon Polly voices Cf. announcement at https://aws.amazon.com/about-aws/whats-new/2022/06/amazon-polly-adds-male-neural-tts-voices-languages/ and updated voice list at https://docs.aws.amazon.com/polly/latest/dg/voicelist.html * Fix inline comment spacing --- homeassistant/components/amazon_polly/const.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/amazon_polly/const.py b/homeassistant/components/amazon_polly/const.py index 0d5e65b2a3a..3892204c47a 100644 --- a/homeassistant/components/amazon_polly/const.py +++ b/homeassistant/components/amazon_polly/const.py @@ -36,6 +36,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Aditi", # Hindi "Amy", "Aria", + "Arthur", # English, Neural "Astrid", # Swedish "Ayanda", "Bianca", # Italian @@ -47,6 +48,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Chantal", # French Canadian "Conchita", "Cristiano", + "Daniel", # German, Neural "Dora", # Icelandic "Emma", # English "Enrique", @@ -69,6 +71,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Kevin", "Kimberly", "Lea", # French + "Liam", # Canadian French, Neural "Liv", # Norwegian "Lotte", # Dutch "Lucia", # Spanish European @@ -86,6 +89,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Nicole", # English Australian "Olivia", # Female, Australian, Neural "Penelope", # Spanish US + "Pedro", # Spanish US, Neural "Raveena", # English, Indian "Ricardo", "Ruben", From a5e151691ce00cc72439997295f8460d48e61fe0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 11:02:48 +0200 Subject: [PATCH 494/903] Fix acmeda battery sensor definition (#76928) * Fix acmeda battery sensor definition * Use float | int | None --- homeassistant/components/acmeda/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index 88a0886a84e..f92d9fcf57b 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .base import AcmedaBase from .const import ACMEDA_HUB_UPDATE, DOMAIN from .helpers import async_add_acmeda_entities +from .hub import PulseHub async def async_setup_entry( @@ -19,7 +20,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Acmeda Rollers from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id] current: set[int] = set() @@ -41,15 +42,15 @@ async def async_setup_entry( class AcmedaBattery(AcmedaBase, SensorEntity): """Representation of a Acmeda cover device.""" - device_class = SensorDeviceClass.BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE @property - def name(self): + def name(self) -> str: """Return the name of roller.""" return f"{super().name} Battery" @property - def native_value(self): + def native_value(self) -> float | int | None: """Return the state of the device.""" return self.roller.battery From 90aba6c52376157b760148a136bc109eefd823c0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 19 Aug 2022 11:12:47 +0200 Subject: [PATCH 495/903] Add cv.deprecated to MQTT modern schema's too (#76884) Add cv.deprcated to modern schema too --- homeassistant/components/mqtt/climate.py | 14 +++++++------- homeassistant/components/mqtt/cover.py | 1 + homeassistant/components/mqtt/fan.py | 10 ++++++++++ .../components/mqtt/light/schema_basic.py | 11 ++++++++++- homeassistant/components/mqtt/light/schema_json.py | 2 ++ .../components/mqtt/light/schema_template.py | 6 +++++- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index f44cf6fe8fc..f39d3857ec2 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -286,13 +286,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) PLATFORM_SCHEMA_MODERN = vol.All( - _PLATFORM_SCHEMA_BASE, - valid_preset_mode_configuration, -) - -# Configuring MQTT Climate under the climate platform key is deprecated in HA Core 2022.6 -PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 @@ -304,6 +297,13 @@ PLATFORM_SCHEMA = vol.All( cv.removed(CONF_HOLD_STATE_TEMPLATE), cv.removed(CONF_HOLD_STATE_TOPIC), cv.removed(CONF_HOLD_LIST), + _PLATFORM_SCHEMA_BASE, + valid_preset_mode_configuration, +) + +# Configuring MQTT Climate under the climate platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), valid_preset_mode_configuration, warn_for_legacy_schema(climate.DOMAIN), ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index b0fbacd10fc..fd96fe524d9 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -200,6 +200,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) PLATFORM_SCHEMA_MODERN = vol.All( + cv.removed("tilt_invert_state"), _PLATFORM_SCHEMA_BASE, validate_options, ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 20c4936ab38..fab748d2bfc 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -186,6 +186,16 @@ PLATFORM_SCHEMA = vol.All( ) PLATFORM_SCHEMA_MODERN = vol.All( + # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and + # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, + # are no longer supported, support was removed in release 2021.12 + cv.removed(CONF_PAYLOAD_HIGH_SPEED), + cv.removed(CONF_PAYLOAD_LOW_SPEED), + cv.removed(CONF_PAYLOAD_MEDIUM_SPEED), + cv.removed(CONF_SPEED_COMMAND_TOPIC), + cv.removed(CONF_SPEED_LIST), + cv.removed(CONF_SPEED_STATE_TOPIC), + cv.removed(CONF_SPEED_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE, valid_speed_range_configuration, valid_preset_mode_configuration, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 05778aa7711..e2805781f45 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -224,7 +224,16 @@ DISCOVERY_SCHEMA_BASIC = vol.All( _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) -PLATFORM_SCHEMA_MODERN_BASIC = _PLATFORM_SCHEMA_BASE +PLATFORM_SCHEMA_MODERN_BASIC = vol.All( + # CONF_VALUE_TEMPLATE is no longer supported, support was removed in 2022.2 + cv.removed(CONF_VALUE_TEMPLATE), + # CONF_WHITE_VALUE_* is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE_COMMAND_TOPIC), + cv.removed(CONF_WHITE_VALUE_SCALE), + cv.removed(CONF_WHITE_VALUE_STATE_TOPIC), + cv.removed(CONF_WHITE_VALUE_TEMPLATE), + _PLATFORM_SCHEMA_BASE, +) async def async_setup_entity_basic( diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 85a0bc335cd..295b43120d4 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -167,6 +167,8 @@ DISCOVERY_SCHEMA_JSON = vol.All( ) PLATFORM_SCHEMA_MODERN_JSON = vol.All( + # CONF_WHITE_VALUE is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE), _PLATFORM_SCHEMA_BASE, valid_color_configuration, ) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 6f211e598b4..73f2786ad12 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -99,7 +99,11 @@ DISCOVERY_SCHEMA_TEMPLATE = vol.All( _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) -PLATFORM_SCHEMA_MODERN_TEMPLATE = _PLATFORM_SCHEMA_BASE +PLATFORM_SCHEMA_MODERN_TEMPLATE = vol.All( + # CONF_WHITE_VALUE_TEMPLATE is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE_TEMPLATE), + _PLATFORM_SCHEMA_BASE, +) async def async_setup_entity_template( From 324f5555ed6caa668111173ad81c50fb6f4f2e3a Mon Sep 17 00:00:00 2001 From: Dave Atherton Date: Fri, 19 Aug 2022 10:51:27 +0100 Subject: [PATCH 496/903] Change growatt server URL (#76824) Co-authored-by: Chris Straffon --- homeassistant/components/growatt_server/const.py | 6 +++++- homeassistant/components/growatt_server/sensor.py | 14 +++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/growatt_server/const.py b/homeassistant/components/growatt_server/const.py index 4fcc4887843..4e548ef2c2a 100644 --- a/homeassistant/components/growatt_server/const.py +++ b/homeassistant/components/growatt_server/const.py @@ -8,11 +8,15 @@ DEFAULT_PLANT_ID = "0" DEFAULT_NAME = "Growatt" SERVER_URLS = [ - "https://server.growatt.com/", + "https://server-api.growatt.com/", "https://server-us.growatt.com/", "http://server.smten.com/", ] +DEPRECATED_URLS = [ + "https://server.growatt.com/", +] + DEFAULT_URL = SERVER_URLS[0] DOMAIN = "growatt_server" diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index db045242987..c90bfa6f3fb 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -19,6 +19,7 @@ from .const import ( CONF_PLANT_ID, DEFAULT_PLANT_ID, DEFAULT_URL, + DEPRECATED_URLS, DOMAIN, LOGIN_INVALID_AUTH_CODE, ) @@ -62,12 +63,23 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Growatt sensor.""" - config = config_entry.data + config = {**config_entry.data} username = config[CONF_USERNAME] password = config[CONF_PASSWORD] url = config.get(CONF_URL, DEFAULT_URL) name = config[CONF_NAME] + # If the URL has been deprecated then change to the default instead + if url in DEPRECATED_URLS: + _LOGGER.info( + "URL: %s has been deprecated, migrating to the latest default: %s", + url, + DEFAULT_URL, + ) + url = DEFAULT_URL + config[CONF_URL] = url + hass.config_entries.async_update_entry(config_entry, data=config) + api = growattServer.GrowattApi() api.server_url = url From 63dcd8ec089e9490ba6caa4961d6a5af5adf6b3f Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 19 Aug 2022 12:57:30 +0300 Subject: [PATCH 497/903] Bump pydroid-ipcam to 2.0.0 (#76906) Co-authored-by: Martin Hjelmare --- .../android_ip_webcam/binary_sensor.py | 2 +- .../android_ip_webcam/config_flow.py | 19 ++++++++--- .../android_ip_webcam/coordinator.py | 8 +++-- .../android_ip_webcam/manifest.json | 2 +- .../components/android_ip_webcam/sensor.py | 32 +++++++++---------- .../components/android_ip_webcam/strings.json | 3 +- .../components/android_ip_webcam/switch.py | 8 ++--- .../android_ip_webcam/translations/en.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../android_ip_webcam/test_config_flow.py | 23 ++++++++++++- .../components/android_ip_webcam/test_init.py | 24 ++++++++++++-- 12 files changed, 91 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index a2dc25d825b..6f17616a216 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -57,4 +57,4 @@ class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return if motion is detected.""" - return self.cam.export_sensor(MOTION_ACTIVE)[0] == 1.0 + return self.cam.get_sensor_value(MOTION_ACTIVE) == 1.0 diff --git a/homeassistant/components/android_ip_webcam/config_flow.py b/homeassistant/components/android_ip_webcam/config_flow.py index 09f0fdaa3a2..c41a998ff54 100644 --- a/homeassistant/components/android_ip_webcam/config_flow.py +++ b/homeassistant/components/android_ip_webcam/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any from pydroid_ipcam import PyDroidIPCam +from pydroid_ipcam.exceptions import PyDroidIPCamException, Unauthorized import voluptuous as vol from homeassistant import config_entries @@ -33,7 +34,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect.""" websession = async_get_clientsession(hass) @@ -45,8 +46,16 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: password=data.get(CONF_PASSWORD), ssl=False, ) - await cam.update() - return cam.available + errors = {} + try: + await cam.update() + except Unauthorized: + errors[CONF_USERNAME] = "invalid_auth" + errors[CONF_PASSWORD] = "invalid_auth" + except PyDroidIPCamException: + errors["base"] = "cannot_connect" + + return errors class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -68,13 +77,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) # to be removed when YAML import is removed title = user_input.get(CONF_NAME) or user_input[CONF_HOST] - if await validate_input(self.hass, user_input): + if not (errors := await validate_input(self.hass, user_input)): return self.async_create_entry(title=title, data=user_input) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, - errors={"base": "cannot_connect"}, + errors=errors, ) async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: diff --git a/homeassistant/components/android_ip_webcam/coordinator.py b/homeassistant/components/android_ip_webcam/coordinator.py index 3940c6df7e4..1647b6890c1 100644 --- a/homeassistant/components/android_ip_webcam/coordinator.py +++ b/homeassistant/components/android_ip_webcam/coordinator.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging from pydroid_ipcam import PyDroidIPCam +from pydroid_ipcam.exceptions import PyDroidIPCamException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -37,6 +38,7 @@ class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Update Android IP Webcam entities.""" - await self.cam.update() - if not self.cam.available: - raise UpdateFailed + try: + await self.cam.update() + except PyDroidIPCamException as err: + raise UpdateFailed(err) from err diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 0023454728a..29a077443c0 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", - "requirements": ["pydroid-ipcam==1.3.1"], + "requirements": ["pydroid-ipcam==2.0.0"], "codeowners": ["@engrbm87"], "iot_class": "local_polling" } diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index d699121d6c9..43a4a0c828c 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -54,8 +54,8 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda ipcam: ipcam.export_sensor("battery_level")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("battery_level")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("battery_level"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_level"), ), AndroidIPWebcamSensorEntityDescription( key="battery_temp", @@ -63,56 +63,56 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = ( icon="mdi:thermometer", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("battery_temp"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_temp"), ), AndroidIPWebcamSensorEntityDescription( key="battery_voltage", name="Battery voltage", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("battery_voltage"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_voltage"), ), AndroidIPWebcamSensorEntityDescription( key="light", name="Light level", icon="mdi:flashlight", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("light")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("light")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("light"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("light"), ), AndroidIPWebcamSensorEntityDescription( key="motion", name="Motion", icon="mdi:run", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("motion")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("motion")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("motion"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("motion"), ), AndroidIPWebcamSensorEntityDescription( key="pressure", name="Pressure", icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("pressure")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("pressure")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("pressure"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("pressure"), ), AndroidIPWebcamSensorEntityDescription( key="proximity", name="Proximity", icon="mdi:map-marker-radius", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("proximity")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("proximity")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("proximity"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("proximity"), ), AndroidIPWebcamSensorEntityDescription( key="sound", name="Sound", icon="mdi:speaker", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("sound")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("sound")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("sound"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("sound"), ), AndroidIPWebcamSensorEntityDescription( key="video_connections", diff --git a/homeassistant/components/android_ip_webcam/strings.json b/homeassistant/components/android_ip_webcam/strings.json index a9ade78a413..6f6639cecb4 100644 --- a/homeassistant/components/android_ip_webcam/strings.json +++ b/homeassistant/components/android_ip_webcam/strings.json @@ -11,7 +11,8 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index b09b0de4be8..9d2175fe3d3 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -55,8 +55,8 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = ( name="Focus", icon="mdi:image-filter-center-focus", entity_category=EntityCategory.CONFIG, - on_func=lambda ipcam: ipcam.torch(activate=True), - off_func=lambda ipcam: ipcam.torch(activate=False), + on_func=lambda ipcam: ipcam.focus(activate=True), + off_func=lambda ipcam: ipcam.focus(activate=False), ), AndroidIPWebcamSwitchEntityDescription( key="gps_active", @@ -111,8 +111,8 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = ( name="Video recording", icon="mdi:record-rec", entity_category=EntityCategory.CONFIG, - on_func=lambda ipcam: ipcam.record(activate=True), - off_func=lambda ipcam: ipcam.record(activate=False), + on_func=lambda ipcam: ipcam.record(record=True), + off_func=lambda ipcam: ipcam.record(record=False), ), ) diff --git a/homeassistant/components/android_ip_webcam/translations/en.json b/homeassistant/components/android_ip_webcam/translations/en.json index 775263225ea..be6416341c2 100644 --- a/homeassistant/components/android_ip_webcam/translations/en.json +++ b/homeassistant/components/android_ip_webcam/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" }, "step": { "user": { diff --git a/requirements_all.txt b/requirements_all.txt index 733d82c9668..88174881477 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1476,7 +1476,7 @@ pydexcom==0.2.3 pydoods==1.0.2 # homeassistant.components.android_ip_webcam -pydroid-ipcam==1.3.1 +pydroid-ipcam==2.0.0 # homeassistant.components.ebox pyebox==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef6a03ddcd9..1291ff55669 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1025,7 +1025,7 @@ pydeconz==103 pydexcom==0.2.3 # homeassistant.components.android_ip_webcam -pydroid-ipcam==1.3.1 +pydroid-ipcam==2.0.0 # homeassistant.components.econet pyeconet==0.1.15 diff --git a/tests/components/android_ip_webcam/test_config_flow.py b/tests/components/android_ip_webcam/test_config_flow.py index 1ede523ecd2..d203ef15e63 100644 --- a/tests/components/android_ip_webcam/test_config_flow.py +++ b/tests/components/android_ip_webcam/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Android IP Webcam config flow.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import Mock, patch import aiohttp @@ -99,6 +99,27 @@ async def test_device_already_configured( assert result2["reason"] == "already_configured" +async def test_form_invalid_auth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we handle invalid auth error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientResponseError(Mock(), (), status=401), + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1", "port": 8080, "username": "user", "password": "wrong-pass"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"username": "invalid_auth", "password": "invalid_auth"} + + async def test_form_cannot_connect( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/android_ip_webcam/test_init.py b/tests/components/android_ip_webcam/test_init.py index e0c21445d71..1fee1a5c388 100644 --- a/tests/components/android_ip_webcam/test_init.py +++ b/tests/components/android_ip_webcam/test_init.py @@ -3,6 +3,7 @@ from collections.abc import Awaitable from typing import Callable +from unittest.mock import Mock import aiohttp @@ -19,6 +20,8 @@ MOCK_CONFIG_DATA = { "name": "IP Webcam", "host": "1.1.1.1", "port": 8080, + "username": "user", + "password": "pass", } @@ -50,10 +53,10 @@ async def test_successful_config_entry( assert entry.state == ConfigEntryState.LOADED -async def test_setup_failed( +async def test_setup_failed_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: - """Test integration failed due to an error.""" + """Test integration failed due to connection error.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) @@ -67,6 +70,23 @@ async def test_setup_failed( assert entry.state == ConfigEntryState.SETUP_RETRY +async def test_setup_failed_invalid_auth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test integration failed due to invalid auth.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientResponseError(Mock(), (), status=401), + ) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.SETUP_RETRY + + async def test_unload_entry(hass: HomeAssistant, aioclient_mock_fixture) -> None: """Test removing integration.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) From 039c071a80b5da5f767114740fa3356e6bfdcf9c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 12:09:58 +0200 Subject: [PATCH 498/903] Improve type hint in brottsplatskartan sensor entity (#77019) --- homeassistant/components/brottsplatskartan/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 535fa9f56ad..d76cb7c8a5f 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -87,7 +87,7 @@ class BrottsplatskartanSensor(SensorEntity): _attr_attribution = brottsplatskartan.ATTRIBUTION - def __init__(self, bpk, name): + def __init__(self, bpk: brottsplatskartan.BrottsplatsKartan, name: str) -> None: """Initialize the Brottsplatskartan sensor.""" self._brottsplatskartan = bpk self._attr_name = name @@ -103,8 +103,8 @@ class BrottsplatskartanSensor(SensorEntity): return for incident in incidents: - incident_type = incident.get("title_type") - incident_counts[incident_type] += 1 + if (incident_type := incident.get("title_type")) is not None: + incident_counts[incident_type] += 1 self._attr_extra_state_attributes = incident_counts self._attr_native_value = len(incidents) From 80c1c11b1a38c750093a321fd13138a21b8b13dd Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 19 Aug 2022 13:10:34 +0300 Subject: [PATCH 499/903] Re-write tests for `transmission` (#76607) Co-authored-by: Martin Hjelmare --- tests/components/transmission/__init__.py | 8 + .../transmission/test_config_flow.py | 459 +++++++----------- tests/components/transmission/test_init.py | 146 ++---- 3 files changed, 238 insertions(+), 375 deletions(-) diff --git a/tests/components/transmission/__init__.py b/tests/components/transmission/__init__.py index b8f8d8c847f..9da6c8304e0 100644 --- a/tests/components/transmission/__init__.py +++ b/tests/components/transmission/__init__.py @@ -1 +1,9 @@ """Tests for Transmission.""" + +MOCK_CONFIG_DATA = { + "name": "Transmission", + "host": "0.0.0.0", + "username": "user", + "password": "pass", + "port": 9091, +} diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 24df92f536e..44edc4b28a9 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,334 +1,165 @@ """Tests for Transmission config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from transmissionrpc.error import TransmissionError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import transmission -from homeassistant.components.transmission import config_flow -from homeassistant.components.transmission.const import DEFAULT_SCAN_INTERVAL -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_SCAN_INTERVAL, - CONF_USERNAME, -) +from homeassistant.components.transmission.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import MOCK_CONFIG_DATA from tests.common import MockConfigEntry -NAME = "Transmission" -HOST = "192.168.1.100" -USERNAME = "username" -PASSWORD = "password" -PORT = 9091 -SCAN_INTERVAL = 10 -MOCK_ENTRY = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, -} - - -@pytest.fixture(name="api") -def mock_transmission_api(): +@pytest.fixture(autouse=True) +def mock_api(): """Mock an api.""" - with patch("transmissionrpc.Client"): - yield + with patch("transmissionrpc.Client") as api: + yield api -@pytest.fixture(name="auth_error") -def mock_api_authentication_error(): - """Mock an api.""" - with patch( - "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") - ): - yield - - -@pytest.fixture(name="conn_error") -def mock_api_connection_error(): - """Mock an api.""" - with patch( - "transmissionrpc.Client", - side_effect=TransmissionError("111: Connection refused"), - ): - yield - - -@pytest.fixture(name="unknown_error") -def mock_api_unknown_error(): - """Mock an api.""" - with patch("transmissionrpc.Client", side_effect=TransmissionError): - yield - - -@pytest.fixture(name="transmission_setup", autouse=True) -def transmission_setup_fixture(): - """Mock transmission entry setup.""" - with patch( - "homeassistant.components.transmission.async_setup_entry", return_value=True - ): - yield - - -def init_config_flow(hass): - """Init a configuration flow.""" - flow = config_flow.TransmissionFlowHandler() - flow.hass = hass - return flow - - -async def test_flow_user_config(hass, api): - """Test user config.""" +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.transmission.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Transmission" + assert result2["data"] == MOCK_CONFIG_DATA + assert len(mock_setup_entry.mock_calls) == 1 -async def test_flow_required_fields(hass, api): - """Test with required fields only.""" - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT}, - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == NAME - assert result["data"][CONF_NAME] == NAME - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == PORT - - -async def test_flow_all_provided(hass, api): - """Test with all provided.""" - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=MOCK_ENTRY, - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == NAME - assert result["data"][CONF_NAME] == NAME - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_PORT] == PORT - - -async def test_options(hass): - """Test updating options.""" - entry = MockConfigEntry( - domain=transmission.DOMAIN, - title=CONF_NAME, - data=MOCK_ENTRY, - options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, - ) - flow = init_config_flow(hass) - options_flow = flow.async_get_options_flow(entry) - - result = await options_flow.async_step_init() - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"][CONF_SCAN_INTERVAL] == 10 - - -async def test_host_already_configured(hass, api): - """Test host is already configured.""" - entry = MockConfigEntry( - domain=transmission.DOMAIN, - data=MOCK_ENTRY, - options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, - ) +async def test_device_already_configured( + hass: HomeAssistant, +) -> None: + """Test aborting if the device is already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - mock_entry_unique_name = MOCK_ENTRY.copy() - mock_entry_unique_name[CONF_NAME] = "Transmission 1" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=mock_entry_unique_name, + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + assert result["type"] == FlowResultType.FORM - mock_entry_unique_port = MOCK_ENTRY.copy() - mock_entry_unique_port[CONF_PORT] = 9092 - mock_entry_unique_port[CONF_NAME] = "Transmission 2" - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=mock_entry_unique_port, + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() - mock_entry_unique_host = MOCK_ENTRY.copy() - mock_entry_unique_host[CONF_HOST] = "192.168.1.101" - mock_entry_unique_host[CONF_NAME] = "Transmission 3" - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=mock_entry_unique_host, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" -async def test_name_already_configured(hass, api): +async def test_name_already_configured(hass): """Test name is already configured.""" entry = MockConfigEntry( domain=transmission.DOMAIN, - data=MOCK_ENTRY, - options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + data=MOCK_CONFIG_DATA, + options={"scan_interval": 120}, ) entry.add_to_hass(hass) - mock_entry = MOCK_ENTRY.copy() - mock_entry[CONF_HOST] = "0.0.0.0" + mock_entry = MOCK_CONFIG_DATA.copy() + mock_entry["host"] = "1.1.1.1" result = await hass.config_entries.flow.async_init( transmission.DOMAIN, context={"source": config_entries.SOURCE_USER}, data=mock_entry, ) - assert result["type"] == "form" - assert result["errors"] == {CONF_NAME: "name_exists"} + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"name": "name_exists"} -async def test_error_on_wrong_credentials(hass, auth_error): - """Test with wrong credentials.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - } - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == { - CONF_USERNAME: "invalid_auth", - CONF_PASSWORD: "invalid_auth", - } - - -async def test_error_on_connection_failure(hass, conn_error): - """Test when connection to host fails.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - } - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_error_on_unknown_error(hass, unknown_error): - """Test when connection to host fails.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - } - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_reauth_success(hass, api): - """Test we can reauth.""" +async def test_options(hass: HomeAssistant) -> None: + """Test updating options.""" entry = MockConfigEntry( domain=transmission.DOMAIN, - data=MOCK_ENTRY, + data=MOCK_CONFIG_DATA, + options={"scan_interval": 120}, ) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - }, - data=MOCK_ENTRY, + with patch( + "homeassistant.components.transmission.async_setup_entry", + return_value=True, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"scan_interval": 10} ) - assert result["type"] == "form" - assert result["step_id"] == "reauth_confirm" - assert result["description_placeholders"] == {CONF_USERNAME: USERNAME} + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"]["scan_interval"] == 10 + +async def test_error_on_wrong_credentials( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test we handle invalid credentials.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_api.side_effect = TransmissionError("401: Unauthorized") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_PASSWORD: "test-password", - }, + MOCK_CONFIG_DATA, ) - - assert result2["type"] == "abort" - assert result2["reason"] == "reauth_successful" - - -async def test_reauth_failed(hass, auth_error): - """Test we can reauth.""" - entry = MockConfigEntry( - domain=transmission.DOMAIN, - data=MOCK_ENTRY, - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - }, - data=MOCK_ENTRY, - ) - - assert result["type"] == "form" - assert result["step_id"] == "reauth_confirm" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_PASSWORD: "test-wrong-password", - }, - ) - - assert result2["type"] == "form" + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == { - CONF_PASSWORD: "invalid_auth", + "username": "invalid_auth", + "password": "invalid_auth", } -async def test_reauth_failed_conn_error(hass, conn_error): +async def test_error_on_connection_failure( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_api.side_effect = TransmissionError("111: Connection refused") + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG_DATA, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_success(hass: HomeAssistant) -> None: """Test we can reauth.""" entry = MockConfigEntry( domain=transmission.DOMAIN, - data=MOCK_ENTRY, + data=MOCK_CONFIG_DATA, ) entry.add_to_hass(hass) @@ -338,18 +169,92 @@ async def test_reauth_failed_conn_error(hass, conn_error): "source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id, }, - data=MOCK_ENTRY, + data=MOCK_CONFIG_DATA, ) - assert result["type"] == "form" + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {"username": "user"} + with patch( + "homeassistant.components.transmission.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_failed(hass: HomeAssistant, mock_api: MagicMock) -> None: + """Test we can't reauth due to invalid password.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_CONFIG_DATA, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG_DATA, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {"username": "user"} + + mock_api.side_effect = TransmissionError("401: Unauthorized") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_PASSWORD: "test-wrong-password", + "password": "wrong-password", }, ) - assert result2["type"] == "form" + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"password": "invalid_auth"} + + +async def test_reauth_failed_connection_error( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test we can't reauth due to connection error.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_CONFIG_DATA, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG_DATA, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {"username": "user"} + + mock_api.side_effect = TransmissionError("111: Connection refused") + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index c3dc924c54e..60e2d67d75c 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -1,125 +1,75 @@ """Tests for Transmission init.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from transmissionrpc.error import TransmissionError -from homeassistant.components import transmission -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.setup import async_setup_component +from homeassistant.components.transmission.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, mock_coro +from . import MOCK_CONFIG_DATA -MOCK_ENTRY = MockConfigEntry( - domain=transmission.DOMAIN, - data={ - transmission.CONF_NAME: "Transmission", - transmission.CONF_HOST: "0.0.0.0", - transmission.CONF_USERNAME: "user", - transmission.CONF_PASSWORD: "pass", - transmission.CONF_PORT: 9091, - }, -) +from tests.common import MockConfigEntry -@pytest.fixture(name="api") -def mock_transmission_api(): +@pytest.fixture(autouse=True) +def mock_api(): """Mock an api.""" - with patch("transmissionrpc.Client"): - yield + with patch("transmissionrpc.Client") as api: + yield api -@pytest.fixture(name="auth_error") -def mock_api_authentication_error(): - """Mock an api.""" - with patch( - "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") - ): - yield +async def test_successful_config_entry(hass: HomeAssistant) -> None: + """Test settings up integration from config entry.""" - -@pytest.fixture(name="unknown_error") -def mock_api_unknown_error(): - """Mock an api.""" - with patch("transmissionrpc.Client", side_effect=TransmissionError): - yield - - -async def test_setup_with_no_config(hass): - """Test that we do not discover anything or try to set up a Transmission client.""" - assert await async_setup_component(hass, transmission.DOMAIN, {}) is True - assert transmission.DOMAIN not in hass.data - - -async def test_setup_with_config(hass, api): - """Test that we import the config and setup the client.""" - config = { - transmission.DOMAIN: { - transmission.CONF_NAME: "Transmission", - transmission.CONF_HOST: "0.0.0.0", - transmission.CONF_USERNAME: "user", - transmission.CONF_PASSWORD: "pass", - transmission.CONF_PORT: 9091, - }, - transmission.DOMAIN: { - transmission.CONF_NAME: "Transmission2", - transmission.CONF_HOST: "0.0.0.1", - transmission.CONF_USERNAME: "user", - transmission.CONF_PASSWORD: "pass", - transmission.CONF_PORT: 9091, - }, - } - assert await async_setup_component(hass, transmission.DOMAIN, config) is True - - -async def test_successful_config_entry(hass, api): - """Test that configured transmission is configured successfully.""" - - entry = MOCK_ENTRY + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - assert await transmission.async_setup_entry(hass, entry) is True - assert entry.options == { - transmission.CONF_SCAN_INTERVAL: transmission.DEFAULT_SCAN_INTERVAL, - transmission.CONF_LIMIT: transmission.DEFAULT_LIMIT, - transmission.CONF_ORDER: transmission.DEFAULT_ORDER, - } + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.LOADED -async def test_setup_failed(hass): - """Test transmission failed due to an error.""" +async def test_setup_failed_connection_error( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test integration failed due to connection error.""" - entry = MOCK_ENTRY + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - # test connection error raising ConfigEntryNotReady - with patch( - "transmissionrpc.Client", - side_effect=TransmissionError("111: Connection refused"), - ), pytest.raises(ConfigEntryNotReady): + mock_api.side_effect = TransmissionError("111: Connection refused") - await transmission.async_setup_entry(hass, entry) - - # test Authentication error returning false - - with patch( - "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") - ), pytest.raises(ConfigEntryAuthFailed): - - assert await transmission.async_setup_entry(hass, entry) is False + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass, api): - """Test removing transmission client.""" - entry = MOCK_ENTRY +async def test_setup_failed_auth_error( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test integration failed due to invalid credentials error.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - with patch.object( - hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) - ) as unload_entry: - assert await transmission.async_setup_entry(hass, entry) + mock_api.side_effect = TransmissionError("401: Unauthorized") - assert await transmission.async_unload_entry(hass, entry) - assert unload_entry.call_count == 2 - assert entry.entry_id not in hass.data[transmission.DOMAIN] + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test removing integration.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data[DOMAIN] From 0f792eb92eb28f19a07dbd823e62df64c28b833c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 13:02:46 +0200 Subject: [PATCH 500/903] Improve entity type hints [c] (#77023) --- homeassistant/components/caldav/calendar.py | 6 +++-- .../components/channels/media_player.py | 20 +++++++------- homeassistant/components/citybikes/sensor.py | 2 +- .../components/clementine/media_player.py | 24 ++++++++--------- .../components/cloud/binary_sensor.py | 4 +-- homeassistant/components/cmus/media_player.py | 27 ++++++++++--------- homeassistant/components/coinbase/sensor.py | 4 +-- .../components/comed_hourly_pricing/sensor.py | 2 +- .../components/command_line/switch.py | 4 +-- .../components/compensation/sensor.py | 2 +- .../components/concord232/binary_sensor.py | 2 +- .../components/coolmaster/climate.py | 13 ++++----- .../components/coronavirus/sensor.py | 2 +- homeassistant/components/cups/sensor.py | 6 ++--- .../components/currencylayer/sensor.py | 2 +- 15 files changed, 63 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 17a8d5deb2f..d510c0c08e7 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -135,11 +135,13 @@ class WebDavCalendarEntity(CalendarEntity): """Return the next upcoming event.""" return self._event - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" return await self.data.async_get_events(hass, start_date, end_date) - def update(self): + def update(self) -> None: """Update event data.""" self.data.update() self._event = self.data.event diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 36fa2fe7ba0..acb9f7ae680 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -1,6 +1,8 @@ """Support for interfacing with an instance of getchannels.com.""" from __future__ import annotations +from typing import Any + from pychannels import Channels import voluptuous as vol @@ -167,7 +169,7 @@ class ChannelsPlayer(MediaPlayerEntity): return None - def update(self): + def update(self) -> None: """Retrieve latest state.""" self.update_favorite_channels() self.update_state(self.client.status()) @@ -211,39 +213,39 @@ class ChannelsPlayer(MediaPlayerEntity): return None - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) player.""" if mute != self.muted: response = self.client.toggle_muted() self.update_state(response) - def media_stop(self): + def media_stop(self) -> None: """Send media_stop command to player.""" self.status = "stopped" response = self.client.stop() self.update_state(response) - def media_play(self): + def media_play(self) -> None: """Send media_play command to player.""" response = self.client.resume() self.update_state(response) - def media_pause(self): + def media_pause(self) -> None: """Send media_pause command to player.""" response = self.client.pause() self.update_state(response) - def media_next_track(self): + def media_next_track(self) -> None: """Seek ahead.""" response = self.client.skip_forward() self.update_state(response) - def media_previous_track(self): + def media_previous_track(self) -> None: """Seek back.""" response = self.client.skip_backward() self.update_state(response) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select a channel to tune to.""" for channel in self.favorite_channels: if channel["name"] == source: @@ -251,7 +253,7 @@ class ChannelsPlayer(MediaPlayerEntity): self.update_state(response) break - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Send the play_media command to the player.""" if media_type == MEDIA_TYPE_CHANNEL: response = self.client.play_channel(media_id) diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 418e206fc36..b9085e24f45 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -284,7 +284,7 @@ class CityBikesStation(SensorEntity): self._station_id = station_id self.entity_id = entity_id - async def async_update(self): + async def async_update(self) -> None: """Update station state.""" for station in self._network.stations: if station[ATTR_ID] == self._station_id: diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 6b0f74a9e63..06bfb654ea1 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -78,7 +78,7 @@ class ClementineDevice(MediaPlayerEntity): self._client = client self._attr_name = name - def update(self): + def update(self) -> None: """Retrieve the latest data from the Clementine Player.""" try: client = self._client @@ -115,14 +115,14 @@ class ClementineDevice(MediaPlayerEntity): self._attr_state = STATE_OFF raise - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" client = self._client sources = [s for s in client.playlists.values() if s["name"] == source] if len(sources) == 1: client.change_song(sources[0]["id"], 0) - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch media image of current playing image.""" if self._client.current_track: image = bytes(self._client.current_track["art"]) @@ -130,45 +130,45 @@ class ClementineDevice(MediaPlayerEntity): return None, None - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" newvolume = min(self._client.volume + 4, 100) self._client.set_volume(newvolume) - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" newvolume = max(self._client.volume - 4, 0) self._client.set_volume(newvolume) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" self._client.set_volume(0) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level.""" self._client.set_volume(int(100 * volume)) - def media_play_pause(self): + def media_play_pause(self) -> None: """Simulate play pause media player.""" if self.state == STATE_PLAYING: self.media_pause() else: self.media_play() - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._attr_state = STATE_PLAYING self._client.play() - def media_pause(self): + def media_pause(self) -> None: """Send media pause command to media player.""" self._attr_state = STATE_PAUSED self._client.pause() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self._client.next() - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the previous track command.""" self._client.previous() diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 5f4c715c41a..b09d282e56b 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -56,7 +56,7 @@ class CloudRemoteBinary(BinarySensorEntity): """Return True if entity is available.""" return self.cloud.remote.certificate is not None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register update dispatcher.""" async def async_state_update(data): @@ -68,7 +68,7 @@ class CloudRemoteBinary(BinarySensorEntity): self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Register update dispatcher.""" if self._unsub_dispatcher is not None: self._unsub_dispatcher() diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index d80b4f6ffb1..09fec24b543 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pycmus import exceptions, remote import voluptuous as vol @@ -115,7 +116,7 @@ class CmusDevice(MediaPlayerEntity): self._attr_name = name or auto_name self.status = {} - def update(self): + def update(self) -> None: """Get the latest data and update the state.""" try: status = self._remote.cmus.get_status_dict() @@ -150,19 +151,19 @@ class CmusDevice(MediaPlayerEntity): _LOGGER.warning("Received no status from cmus") - def turn_off(self): + def turn_off(self) -> None: """Service to send the CMUS the command to stop playing.""" self._remote.cmus.player_stop() - def turn_on(self): + def turn_on(self) -> None: """Service to send the CMUS the command to start playing.""" self._remote.cmus.player_play() - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._remote.cmus.set_volume(int(volume * 100)) - def volume_up(self): + def volume_up(self) -> None: """Set the volume up.""" left = self.status["set"].get("vol_left") right = self.status["set"].get("vol_right") @@ -174,7 +175,7 @@ class CmusDevice(MediaPlayerEntity): if current_volume <= 100: self._remote.cmus.set_volume(int(current_volume) + 5) - def volume_down(self): + def volume_down(self) -> None: """Set the volume down.""" left = self.status["set"].get("vol_left") right = self.status["set"].get("vol_right") @@ -186,7 +187,7 @@ class CmusDevice(MediaPlayerEntity): if current_volume <= 100: self._remote.cmus.set_volume(int(current_volume) - 5) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Send the play command.""" if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]: self._remote.cmus.player_play_file(media_id) @@ -198,26 +199,26 @@ class CmusDevice(MediaPlayerEntity): MEDIA_TYPE_PLAYLIST, ) - def media_pause(self): + def media_pause(self) -> None: """Send the pause command.""" self._remote.cmus.player_pause() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self._remote.cmus.player_next() - def media_previous_track(self): + def media_previous_track(self) -> None: """Send next track command.""" self._remote.cmus.player_prev() - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Send seek command.""" self._remote.cmus.seek(position) - def media_play(self): + def media_play(self) -> None: """Send the play command.""" self._remote.cmus.player_play() - def media_stop(self): + def media_stop(self) -> None: """Send the stop command.""" self._remote.cmus.stop() diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index fb9ce0fe434..d1e25dcf2a0 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -161,7 +161,7 @@ class AccountSensor(SensorEntity): ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._native_currency}", } - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" self._coinbase_data.update() for account in self._coinbase_data.accounts: @@ -233,7 +233,7 @@ class ExchangeRateSensor(SensorEntity): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" self._coinbase_data.update() self._state = round( diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 4b658cdaddd..38421813439 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -101,7 +101,7 @@ class ComedHourlyPricingSensor(SensorEntity): self._attr_name = name self.offset = offset - async def async_update(self): + async def async_update(self) -> None: """Get the ComEd Hourly Pricing data from the web service.""" try: sensor_type = self.entity_description.key diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index ff0d9b65f9d..7142f14e82d 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -172,13 +172,13 @@ class CommandSwitch(SwitchEntity): payload = self._value_template.render_with_possible_json_value(payload) self._attr_is_on = payload.lower() == "true" - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._switch(self._command_on) and not self._command_state: self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._switch(self._command_off) and not self._command_state: self._attr_is_on = False diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index a1781d454e7..58666e0f3be 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -90,7 +90,7 @@ class CompensationSensor(SensorEntity): self._unique_id = unique_id self._name = name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle added to Hass.""" self.async_on_remove( async_track_state_change_event( diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index dc5f89d84bb..305822222ac 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -133,7 +133,7 @@ class Concord232ZoneSensor(BinarySensorEntity): # True means "faulted" or "open" or "abnormal state" return bool(self._zone["state"] != "Normal") - def update(self): + def update(self) -> None: """Get updated stats from API.""" last_update = dt_util.utcnow() - self._client.last_zone_update _LOGGER.debug("Zone: %s ", self._zone) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 2f2ea58bd5b..8333e66753e 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -1,5 +1,6 @@ """CoolMasterNet platform to control of CoolMasterNet Climate Devices.""" import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode @@ -93,7 +94,7 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): return self.unique_id @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" if self._unit.temperature_unit == "celsius": return TEMP_CELSIUS @@ -134,20 +135,20 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): """Return the list of available fan modes.""" return FAN_MODES - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: _LOGGER.debug("Setting temp of %s to %s", self.unique_id, str(temp)) self._unit = await self._unit.set_thermostat(temp) self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" _LOGGER.debug("Setting fan mode of %s to %s", self.unique_id, fan_mode) self._unit = await self._unit.set_fan_speed(fan_mode) self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" _LOGGER.debug("Setting operation mode of %s to %s", self.unique_id, hvac_mode) @@ -157,13 +158,13 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): self._unit = await self._unit.set_mode(HA_STATE_TO_CM[hvac_mode]) await self.async_turn_on() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on.""" _LOGGER.debug("Turning %s on", self.unique_id) self._unit = await self._unit.turn_on() self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off.""" _LOGGER.debug("Turning %s off", self.unique_id) self._unit = await self._unit.turn_off() diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index 52ed49b759e..1a4f5b72fcd 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -53,7 +53,7 @@ class CoronavirusSensor(CoordinatorEntity, SensorEntity): self.info_type = info_type @property - def available(self): + def available(self) -> bool: """Return if sensor is available.""" return self.coordinator.last_update_success and ( self.country in self.coordinator.data or self.country == OPTION_WORLDWIDE diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 103a352f6cd..893e92f546e 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -157,7 +157,7 @@ class CupsSensor(SensorEntity): ATTR_PRINTER_URI_SUPPORTED: self._printer["printer-uri-supported"], } - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" self.data.update() self._printer = self.data.printers.get(self._name) @@ -234,7 +234,7 @@ class IPPSensor(SensorEntity): return state_attributes - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" self.data.update() self._attributes = self.data.attributes.get(self._name) @@ -309,7 +309,7 @@ class MarkerSensor(SensorEntity): ATTR_PRINTER_NAME: printer_name, } - def update(self): + def update(self) -> None: """Update the state of the sensor.""" # Data fetching is done by CupsSensor/IPPSensor self._attributes = self.data.attributes diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index 9c43b5f0bc1..85f8b876765 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -99,7 +99,7 @@ class CurrencylayerSensor(SensorEntity): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self): + def update(self) -> None: """Update current date.""" self.rest.update() if (value := self.rest.data) is not None: From d0986c765083fd7d597f03ea4679245417d8a6f8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 19 Aug 2022 13:20:41 +0200 Subject: [PATCH 501/903] Type feedreader strictly (#76707) * Type feedreader strictly * Run hassfest --- .strict-typing | 1 + .../components/feedreader/__init__.py | 56 ++++++++++++------- mypy.ini | 10 ++++ 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/.strict-typing b/.strict-typing index a215c2187ab..1e6a47f508b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -98,6 +98,7 @@ homeassistant.components.energy.* homeassistant.components.evil_genius_labs.* homeassistant.components.fan.* homeassistant.components.fastdotcom.* +homeassistant.components.feedreader.* homeassistant.components.file_upload.* homeassistant.components.filesize.* homeassistant.components.fitbit.* diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index b3f1a916012..0ee4d3c39f3 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -1,9 +1,13 @@ """Support for RSS/Atom feeds.""" +from __future__ import annotations + from datetime import datetime, timedelta from logging import getLogger from os.path import exists import pickle from threading import Lock +from time import struct_time +from typing import cast import feedparser import voluptuous as vol @@ -44,9 +48,9 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Feedreader component.""" - urls = config[DOMAIN][CONF_URLS] - scan_interval = config[DOMAIN].get(CONF_SCAN_INTERVAL) - max_entries = config[DOMAIN].get(CONF_MAX_ENTRIES) + urls: list[str] = config[DOMAIN][CONF_URLS] + scan_interval: timedelta = config[DOMAIN][CONF_SCAN_INTERVAL] + max_entries: int = config[DOMAIN][CONF_MAX_ENTRIES] data_file = hass.config.path(f"{DOMAIN}.pickle") storage = StoredData(data_file) feeds = [ @@ -58,16 +62,23 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: class FeedManager: """Abstraction over Feedparser module.""" - def __init__(self, url, scan_interval, max_entries, hass, storage): + def __init__( + self, + url: str, + scan_interval: timedelta, + max_entries: int, + hass: HomeAssistant, + storage: StoredData, + ) -> None: """Initialize the FeedManager object, poll as per scan interval.""" self._url = url self._scan_interval = scan_interval self._max_entries = max_entries - self._feed = None + self._feed: feedparser.FeedParserDict | None = None self._hass = hass self._firstrun = True self._storage = storage - self._last_entry_timestamp = None + self._last_entry_timestamp: struct_time | None = None self._last_update_successful = False self._has_published_parsed = False self._has_updated_parsed = False @@ -76,23 +87,23 @@ class FeedManager: hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: self._update()) self._init_regular_updates(hass) - def _log_no_entries(self): + def _log_no_entries(self) -> None: """Send no entries log at debug level.""" _LOGGER.debug("No new entries to be published in feed %s", self._url) - def _init_regular_updates(self, hass): + def _init_regular_updates(self, hass: HomeAssistant) -> None: """Schedule regular updates at the top of the clock.""" track_time_interval(hass, lambda now: self._update(), self._scan_interval) @property - def last_update_successful(self): + def last_update_successful(self) -> bool: """Return True if the last feed update was successful.""" return self._last_update_successful - def _update(self): + def _update(self) -> None: """Update the feed and publish new entries to the event bus.""" _LOGGER.info("Fetching new data from feed %s", self._url) - self._feed = feedparser.parse( + self._feed: feedparser.FeedParserDict = feedparser.parse( # type: ignore[no-redef] self._url, etag=None if not self._feed else self._feed.get("etag"), modified=None if not self._feed else self._feed.get("modified"), @@ -125,15 +136,16 @@ class FeedManager: self._publish_new_entries() if self._has_published_parsed or self._has_updated_parsed: self._storage.put_timestamp( - self._feed_id, self._last_entry_timestamp + self._feed_id, cast(struct_time, self._last_entry_timestamp) ) else: self._log_no_entries() self._last_update_successful = True _LOGGER.info("Fetch from feed %s completed", self._url) - def _filter_entries(self): + def _filter_entries(self) -> None: """Filter the entries provided and return the ones to keep.""" + assert self._feed is not None if len(self._feed.entries) > self._max_entries: _LOGGER.debug( "Processing only the first %s entries in feed %s", @@ -142,7 +154,7 @@ class FeedManager: ) self._feed.entries = self._feed.entries[0 : self._max_entries] - def _update_and_fire_entry(self, entry): + def _update_and_fire_entry(self, entry: feedparser.FeedParserDict) -> None: """Update last_entry_timestamp and fire entry.""" # Check if the entry has a published or updated date. if "published_parsed" in entry and entry.published_parsed: @@ -169,8 +181,9 @@ class FeedManager: entry.update({"feed_url": self._url}) self._hass.bus.fire(self._event_type, entry) - def _publish_new_entries(self): + def _publish_new_entries(self) -> None: """Publish new entries to the event bus.""" + assert self._feed is not None new_entries = False self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id) if self._last_entry_timestamp: @@ -202,15 +215,15 @@ class FeedManager: class StoredData: """Abstraction over pickle data storage.""" - def __init__(self, data_file): + def __init__(self, data_file: str) -> None: """Initialize pickle data storage.""" self._data_file = data_file self._lock = Lock() self._cache_outdated = True - self._data = {} + self._data: dict[str, struct_time] = {} self._fetch_data() - def _fetch_data(self): + def _fetch_data(self) -> None: """Fetch data stored into pickle file.""" if self._cache_outdated and exists(self._data_file): try: @@ -223,20 +236,21 @@ class StoredData: "Error loading data from pickled file %s", self._data_file ) - def get_timestamp(self, feed_id): + def get_timestamp(self, feed_id: str) -> struct_time | None: """Return stored timestamp for given feed id (usually the url).""" self._fetch_data() return self._data.get(feed_id) - def put_timestamp(self, feed_id, timestamp): + def put_timestamp(self, feed_id: str, timestamp: struct_time) -> None: """Update timestamp for given feed id (usually the url).""" self._fetch_data() with self._lock, open(self._data_file, "wb") as myfile: self._data.update({feed_id: timestamp}) _LOGGER.debug( - "Overwriting feed %s timestamp in storage file %s", + "Overwriting feed %s timestamp in storage file %s: %s", feed_id, self._data_file, + timestamp, ) try: pickle.dump(self._data, myfile) diff --git a/mypy.ini b/mypy.ini index fd609d8099b..863e673401c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -739,6 +739,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.feedreader.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.file_upload.*] check_untyped_defs = true disallow_incomplete_defs = true From d8392ef6ba320a03ad460ac31b8ca400059fbbbc Mon Sep 17 00:00:00 2001 From: Stephan Uhle Date: Fri, 19 Aug 2022 13:28:03 +0200 Subject: [PATCH 502/903] Add edl21 sensor unit mapping for Hz (#76783) Added sensor unit mapping for Hz. --- homeassistant/components/edl21/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 65603b0c8c4..730acabbc98 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + FREQUENCY_HERTZ, POWER_WATT, ) from homeassistant.core import HomeAssistant, callback @@ -252,6 +253,7 @@ SENSOR_UNIT_MAPPING = { "A": ELECTRIC_CURRENT_AMPERE, "V": ELECTRIC_POTENTIAL_VOLT, "°": DEGREE, + "Hz": FREQUENCY_HERTZ, } From 2d197fd59efafd7f9edb4ee4d192e395af9932fe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Aug 2022 15:24:53 +0200 Subject: [PATCH 503/903] Add state selector (#77024) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/selector.py | 24 ++++++++++++++++++++++++ tests/helpers/test_selector.py | 15 +++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index deacc821672..deb5cc7401c 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -741,6 +741,30 @@ class TargetSelectorConfig(TypedDict, total=False): device: SingleDeviceSelectorConfig +class StateSelectorConfig(TypedDict): + """Class to represent an state selector config.""" + + entity_id: str + + +@SELECTORS.register("state") +class StateSelector(Selector): + """Selector for an entity state.""" + + selector_type = "state" + + CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) + + def __init__(self, config: StateSelectorConfig) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + state: str = vol.Schema(str)(data) + return state + + @SELECTORS.register("target") class TargetSelector(Selector): """Selector of a target value (area ID, device ID, entity ID etc). diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index d1018299d96..70e17058923 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -294,6 +294,21 @@ def test_time_selector_schema(schema, valid_selections, invalid_selections): _test_selector("time", schema, valid_selections, invalid_selections) +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {"entity_id": "sensor.abc"}, + ("on", "armed"), + (None, True, 1), + ), + ), +) +def test_state_selector_schema(schema, valid_selections, invalid_selections): + """Test state selector.""" + _test_selector("state", schema, valid_selections, invalid_selections) + + @pytest.mark.parametrize( "schema,valid_selections,invalid_selections", ( From bf7239c25db06f1377a895244a906b43242c9963 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 16:10:45 +0200 Subject: [PATCH 504/903] Improve entity type hints [d] (#77031) --- homeassistant/components/daikin/climate.py | 17 +++---- homeassistant/components/daikin/sensor.py | 7 +-- homeassistant/components/daikin/switch.py | 39 ++++++++-------- .../components/danfoss_air/binary_sensor.py | 2 +- .../components/danfoss_air/sensor.py | 2 +- .../components/danfoss_air/switch.py | 7 +-- homeassistant/components/darksky/sensor.py | 2 +- homeassistant/components/darksky/weather.py | 4 +- homeassistant/components/delijn/sensor.py | 2 +- homeassistant/components/demo/media_player.py | 46 ++++++++++--------- homeassistant/components/demo/vacuum.py | 7 ++- homeassistant/components/demo/water_heater.py | 10 ++-- .../components/denon/media_player.py | 24 +++++----- .../components/denonavr/media_player.py | 24 +++++----- homeassistant/components/derivative/sensor.py | 2 +- .../components/deutsche_bahn/sensor.py | 2 +- .../components/devolo_home_control/climate.py | 2 +- .../components/digital_ocean/binary_sensor.py | 2 +- .../components/digital_ocean/switch.py | 7 +-- .../components/directv/media_player.py | 19 ++++---- homeassistant/components/discogs/sensor.py | 2 +- homeassistant/components/dlink/switch.py | 7 +-- .../components/dlna_dmr/media_player.py | 2 +- homeassistant/components/doorbird/camera.py | 2 +- homeassistant/components/dovado/sensor.py | 2 +- .../components/dsmr_reader/sensor.py | 2 +- .../components/dte_energy_bridge/sensor.py | 2 +- .../components/dublin_bus_transport/sensor.py | 2 +- .../components/dwd_weather_warnings/sensor.py | 4 +- homeassistant/components/dweet/sensor.py | 2 +- homeassistant/components/dynalite/switch.py | 6 ++- 31 files changed, 141 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 1b84b182ac8..271449a01cd 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -177,7 +178,7 @@ class DaikinClimate(ClimateEntity): return self._api.device.mac @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement which this thermostat uses.""" return TEMP_CELSIUS @@ -196,7 +197,7 @@ class DaikinClimate(ClimateEntity): """Return the supported step of target temperature.""" return 1 - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self._set(kwargs) @@ -232,7 +233,7 @@ class DaikinClimate(ClimateEntity): """Return the fan setting.""" return self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])[1].title() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set fan mode.""" await self._set({ATTR_FAN_MODE: fan_mode}) @@ -246,7 +247,7 @@ class DaikinClimate(ClimateEntity): """Return the fan setting.""" return self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE])[1].title() - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target temperature.""" await self._set({ATTR_SWING_MODE: swing_mode}) @@ -275,7 +276,7 @@ class DaikinClimate(ClimateEntity): return PRESET_ECO return PRESET_NONE - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" if preset_mode == PRESET_AWAY: await self._api.device.set_holiday(ATTR_STATE_ON) @@ -309,15 +310,15 @@ class DaikinClimate(ClimateEntity): ret += [PRESET_ECO, PRESET_BOOST] return ret - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn device on.""" await self._api.device.set({}) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn device off.""" await self._api.device.set( {HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]} diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 1843bdac25f..1adacd322cc 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -181,7 +182,7 @@ class DaikinSensor(SensorEntity): self._attr_name = f"{api.name} {description.name}" @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return f"{self._api.device.mac}-{self.entity_description.key}" @@ -190,11 +191,11 @@ class DaikinSensor(SensorEntity): """Return the state of the sensor.""" return self.entity_description.value_func(self._api.device) - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return self._api.device_info diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index d5601a38989..0f885e63bf7 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -1,13 +1,16 @@ """Support for Daikin AirBase zones.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as DAIKIN_DOMAIN +from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi ZONE_ICON = "mdi:home-circle" STREAMER_ICON = "mdi:air-filter" @@ -32,7 +35,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Daikin climate based on config_entry.""" - daikin_api = hass.data[DAIKIN_DOMAIN][entry.entry_id] + daikin_api: DaikinApi = hass.data[DAIKIN_DOMAIN][entry.entry_id] switches: list[DaikinZoneSwitch | DaikinStreamerSwitch] = [] if zones := daikin_api.device.zones: switches.extend( @@ -54,13 +57,13 @@ async def async_setup_entry( class DaikinZoneSwitch(SwitchEntity): """Representation of a zone.""" - def __init__(self, daikin_api, zone_id): + def __init__(self, daikin_api: DaikinApi, zone_id): """Initialize the zone.""" self._api = daikin_api self._zone_id = zone_id @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return f"{self._api.device.mac}-zone{self._zone_id}" @@ -70,29 +73,29 @@ class DaikinZoneSwitch(SwitchEntity): return ZONE_ICON @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._api.name} {self._api.device.zones[self._zone_id][0]}" @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the sensor.""" return self._api.device.zones[self._zone_id][1] == "1" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return self._api.device_info - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" await self._api.device.set_zone(self._zone_id, "1") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self._api.device.set_zone(self._zone_id, "0") @@ -100,12 +103,12 @@ class DaikinZoneSwitch(SwitchEntity): class DaikinStreamerSwitch(SwitchEntity): """Streamer state.""" - def __init__(self, daikin_api): + def __init__(self, daikin_api: DaikinApi) -> None: """Initialize streamer switch.""" self._api = daikin_api @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return f"{self._api.device.mac}-streamer" @@ -115,30 +118,30 @@ class DaikinStreamerSwitch(SwitchEntity): return STREAMER_ICON @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._api.name} streamer" @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the sensor.""" return ( DAIKIN_ATTR_STREAMER in self._api.device.represent(DAIKIN_ATTR_ADVANCED)[1] ) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return self._api.device_info - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" await self._api.device.set_streamer("on") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self._api.device.set_streamer("off") diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index b01e51e5061..3764345a7b8 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -50,7 +50,7 @@ class DanfossAirBinarySensor(BinarySensorEntity): self._type = sensor_type self._attr_device_class = device_class - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" self._data.update() diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index d85e8248e60..de736b2c599 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -119,7 +119,7 @@ class DanfossAir(SensorEntity): self._attr_device_class = device_class self._attr_state_class = state_class - def update(self): + def update(self) -> None: """Update the new state of the sensor. This is done through the DanfossAir object that does the actual diff --git a/homeassistant/components/danfoss_air/switch.py b/homeassistant/components/danfoss_air/switch.py index b2e7189550f..b1ee7dce44a 100644 --- a/homeassistant/components/danfoss_air/switch.py +++ b/homeassistant/components/danfoss_air/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pydanfossair.commands import ReadCommand, UpdateCommand @@ -75,17 +76,17 @@ class DanfossAir(SwitchEntity): """Return true if switch is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" _LOGGER.debug("Turning on switch with command %s", self._on_command) self._data.update_state(self._on_command, self._state_command) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" _LOGGER.debug("Turning off switch with command %s", self._off_command) self._data.update_state(self._off_command, self._state_command) - def update(self): + def update(self) -> None: """Update the switch's state.""" self._data.update() diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 0686f304674..84a0b0f23f2 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -825,7 +825,7 @@ class DarkSkyAlertSensor(SensorEntity): """Return the state attributes.""" return self._alerts - def update(self): + def update(self) -> None: """Get the latest data from Dark Sky and updates the states.""" # Call the API for new forecast data. Each sensor will re-trigger this # same exact call, but that's fine. We cache results for a short period diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 1bc56706007..88f3b6b2bc9 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -131,7 +131,7 @@ class DarkSkyWeather(WeatherEntity): self._ds_daily = None @property - def available(self): + def available(self) -> bool: """Return if weather data is available from Dark Sky.""" return self._ds_data is not None @@ -233,7 +233,7 @@ class DarkSkyWeather(WeatherEntity): return data - def update(self): + def update(self) -> None: """Get the latest data from Dark Sky.""" self._dark_sky.update() diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index ee58a4f21c7..f0a263d0d0f 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -92,7 +92,7 @@ class DeLijnPublicTransportSensor(SensorEntity): self.line = line self._attr_extra_state_attributes = {} - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from the De Lijn API.""" try: await self.line.get_passages() diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index f3046a6f807..e52bf8720e1 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -1,6 +1,8 @@ """Demo implementation of the media player.""" from __future__ import annotations +from typing import Any + from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, @@ -164,57 +166,57 @@ class AbstractDemoPlayer(MediaPlayerEntity): """Return the device class of the media player.""" return self._device_class - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._player_state = STATE_PLAYING self.schedule_update_ha_state() - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self._player_state = STATE_OFF self.schedule_update_ha_state() - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute the volume.""" self._volume_muted = mute self.schedule_update_ha_state() - def volume_up(self): + def volume_up(self) -> None: """Increase volume.""" self._volume_level = min(1.0, self._volume_level + 0.1) self.schedule_update_ha_state() - def volume_down(self): + def volume_down(self) -> None: """Decrease volume.""" self._volume_level = max(0.0, self._volume_level - 0.1) self.schedule_update_ha_state() - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set the volume level, range 0..1.""" self._volume_level = volume self.schedule_update_ha_state() - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._player_state = STATE_PLAYING self.schedule_update_ha_state() - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._player_state = STATE_PAUSED self.schedule_update_ha_state() - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" self._player_state = STATE_OFF self.schedule_update_ha_state() - def set_shuffle(self, shuffle): + def set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" self._shuffle = shuffle self.schedule_update_ha_state() - def select_sound_mode(self, sound_mode): + def select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" self._sound_mode = sound_mode self.schedule_update_ha_state() @@ -291,12 +293,12 @@ class DemoYoutubePlayer(AbstractDemoPlayer): if self._player_state == STATE_PLAYING: return self._progress_updated_at - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play a piece of media.""" self.youtube_id = media_id self.schedule_update_ha_state() - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._progress = self.media_position self._progress_updated_at = dt_util.utcnow() @@ -393,38 +395,38 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Flag media player features that are supported.""" return MUSIC_PLAYER_SUPPORT - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" if self._cur_track > 0: self._cur_track -= 1 self.schedule_update_ha_state() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" if self._cur_track < len(self.tracks) - 1: self._cur_track += 1 self.schedule_update_ha_state() - def clear_playlist(self): + def clear_playlist(self) -> None: """Clear players playlist.""" self.tracks = [] self._cur_track = 0 self._player_state = STATE_OFF self.schedule_update_ha_state() - def set_repeat(self, repeat): + def set_repeat(self, repeat: str) -> None: """Enable/disable repeat mode.""" self._repeat = repeat self.schedule_update_ha_state() - def join_players(self, group_members): + def join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" self._group_members = [ self.entity_id, ] + group_members self.schedule_update_ha_state() - def unjoin_player(self): + def unjoin_player(self) -> None: """Remove this player from any group.""" self._group_members = [] self.schedule_update_ha_state() @@ -505,19 +507,19 @@ class DemoTVShowPlayer(AbstractDemoPlayer): """Flag media player features that are supported.""" return NETFLIX_PLAYER_SUPPORT - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" if self._cur_episode > 1: self._cur_episode -= 1 self.schedule_update_ha_state() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" if self._cur_episode < self._episode_count: self._cur_episode += 1 self.schedule_update_ha_state() - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" self._source = source self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 58b76ba6347..10952f86785 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -240,7 +240,12 @@ class DemoVacuum(VacuumEntity): self._battery_level += 5 self.schedule_update_ha_state() - def send_command(self, command, params=None, **kwargs: Any) -> None: + def send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: """Send a command to the vacuum.""" if self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0: return diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index 70332277d90..c3cb2be4fb3 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -1,6 +1,8 @@ """Demo platform that offers a fake water heater device.""" from __future__ import annotations +from typing import Any + from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, @@ -79,22 +81,22 @@ class DemoWaterHeater(WaterHeaterEntity): "off", ] - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" self._attr_target_temperature = kwargs.get(ATTR_TEMPERATURE) self.schedule_update_ha_state() - def set_operation_mode(self, operation_mode): + def set_operation_mode(self, operation_mode: str) -> None: """Set new operation mode.""" self._attr_current_operation = operation_mode self.schedule_update_ha_state() - def turn_away_mode_on(self): + def turn_away_mode_on(self) -> None: """Turn away mode on.""" self._attr_is_away_mode_on = True self.schedule_update_ha_state() - def turn_away_mode_off(self): + def turn_away_mode_off(self) -> None: """Turn away mode off.""" self._attr_is_away_mode_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index d55adcf5db7..4533af66927 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -253,51 +253,51 @@ class DenonDevice(MediaPlayerEntity): if self._mediasource == name: return pretty_name - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self.telnet_command("PWSTANDBY") - def volume_up(self): + def volume_up(self) -> None: """Volume up media player.""" self.telnet_command("MVUP") - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self.telnet_command("MVDOWN") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.telnet_command(f"MV{round(volume * self._volume_max):02}") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" mute_status = "ON" if mute else "OFF" self.telnet_command(f"MU{mute_status})") - def media_play(self): + def media_play(self) -> None: """Play media player.""" self.telnet_command("NS9A") - def media_pause(self): + def media_pause(self) -> None: """Pause media player.""" self.telnet_command("NS9B") - def media_stop(self): + def media_stop(self) -> None: """Pause media player.""" self.telnet_command("NS9C") - def media_next_track(self): + def media_next_track(self) -> None: """Send the next track command.""" self.telnet_command("NS9D") - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the previous track command.""" self.telnet_command("NS9E") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.telnet_command("PWON") - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self.telnet_command(f"SI{self._source_list.get(source)}") diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 7c5d98ca1b3..bc3b4264d24 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -390,32 +390,32 @@ class DenonDevice(MediaPlayerEntity): return self._receiver.dynamic_eq @async_log_errors - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Play or pause the media player.""" await self._receiver.async_toggle_play_pause() @async_log_errors - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._receiver.async_play() @async_log_errors - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._receiver.async_pause() @async_log_errors - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self._receiver.async_previous_track() @async_log_errors - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._receiver.async_next_track() @async_log_errors - async def async_select_source(self, source: str): + async def async_select_source(self, source: str) -> None: """Select input source.""" # Ensure that the AVR is turned on, which is necessary for input # switch to work. @@ -423,27 +423,27 @@ class DenonDevice(MediaPlayerEntity): await self._receiver.async_set_input_func(source) @async_log_errors - async def async_select_sound_mode(self, sound_mode: str): + async def async_select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" await self._receiver.async_set_sound_mode(sound_mode) @async_log_errors - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on media player.""" await self._receiver.async_power_on() @async_log_errors - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off media player.""" await self._receiver.async_power_off() @async_log_errors - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" await self._receiver.async_volume_up() @async_log_errors - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down media player.""" await self._receiver.async_volume_down() @@ -458,7 +458,7 @@ class DenonDevice(MediaPlayerEntity): await self._receiver.async_set_volume(volume_denon) @async_log_errors - async def async_mute_volume(self, mute: bool): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" await self._receiver.async_mute(mute) diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index fb5bf7e518d..5337328bbe2 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -168,7 +168,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity): self._unit_time = UNIT_TIME[unit_time] self._time_window = time_window.total_seconds() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() if (state := await self.async_get_last_state()) is not None: diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index 07c330fd402..9638fccf2cd 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -98,7 +98,7 @@ class DeutscheBahnSensor(SensorEntity): connections["next_on"] = self.data.connections[2]["departure"] return connections - def update(self): + def update(self) -> None: """Get the latest delay from bahn.de and updates the state.""" self.data.update() self._state = self.data.connections[0].get("departure", "Unknown") diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index cde7ca33152..4b8e8fc00e6 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -88,7 +88,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit """Return the target temperature.""" return self._value - def set_hvac_mode(self, hvac_mode: str) -> None: + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Do nothing as devolo devices do not support changing the hvac mode.""" def set_temperature(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index b92e009e618..9728da99a6f 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -102,7 +102,7 @@ class DigitalOceanBinarySensor(BinarySensorEntity): ATTR_VCPUS: self.data.vcpus, } - def update(self): + def update(self) -> None: """Update state of sensor.""" self._digital_ocean.update() diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index efaa4fec6be..da955e221a3 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -94,17 +95,17 @@ class DigitalOceanSwitch(SwitchEntity): ATTR_VCPUS: self.data.vcpus, } - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Boot-up the droplet.""" if self.data.status != "active": self.data.power_on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Shutdown the droplet.""" if self.data.status == "active": self.data.power_off() - def update(self): + def update(self) -> None: """Get the latest data from the device and update the data.""" self._digital_ocean.update() diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 2fb5acf8295..affefcacd85 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from directv import DIRECTV @@ -278,7 +279,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): return dt_util.as_local(self._program.start_time) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the receiver.""" if self._is_client: raise NotImplementedError() @@ -286,7 +287,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): _LOGGER.debug("Turn on %s", self.name) await self.dtv.remote("poweron", self._address) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the receiver.""" if self._is_client: raise NotImplementedError() @@ -294,32 +295,34 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): _LOGGER.debug("Turn off %s", self.name) await self.dtv.remote("poweroff", self._address) - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" _LOGGER.debug("Play on %s", self.name) await self.dtv.remote("play", self._address) - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" _LOGGER.debug("Pause on %s", self.name) await self.dtv.remote("pause", self._address) - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" _LOGGER.debug("Stop on %s", self.name) await self.dtv.remote("stop", self._address) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send rewind command.""" _LOGGER.debug("Rewind on %s", self.name) await self.dtv.remote("rew", self._address) - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send fast forward command.""" _LOGGER.debug("Fast forward on %s", self.name) await self.dtv.remote("ffwd", self._address) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Select input source.""" if media_type != MEDIA_TYPE_CHANNEL: _LOGGER.error( diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 447eb4a754e..e207755ec24 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -157,7 +157,7 @@ class DiscogsSensor(SensorEntity): return None - def update(self): + def update(self) -> None: """Set state to the amount of records in user's collection.""" if self.entity_description.key == SENSOR_COLLECTION_TYPE: self._attr_native_value = self._discogs_data["collection_count"] diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 4d5d7b5c639..f1ca99c51f2 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import urllib from pyW215.pyW215 import SmartPlug @@ -106,15 +107,15 @@ class SmartPlugSwitch(SwitchEntity): """Return true if switch is on.""" return self.data.state == "ON" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self.data.smartplug.state = "ON" - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self.data.smartplug.state = "OFF" - def update(self): + def update(self) -> None: """Get the latest data from the smart plug and updates the states.""" self.data.update() diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 9ecf9f8ad40..156e8fdffef 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -578,7 +578,7 @@ class DlnaDmrEntity(MediaPlayerEntity): await self._device.async_stop() @catch_request_errors - async def async_media_seek(self, position: int | float) -> None: + async def async_media_seek(self, position: float) -> None: """Send seek command.""" assert self._device is not None time = timedelta(seconds=position) diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 77dc8a2d45c..fce76a65ff5 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -134,7 +134,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): ) return self._last_image - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add callback after being added to hass. Registers entity_id map for the logbook diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index 99b1bd459fe..ee23592ef2d 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -133,7 +133,7 @@ class DovadoSensor(SensorEntity): return round(float(state) / 1e6, 1) return state - def update(self): + def update(self) -> None: """Update sensor values.""" self._data.update() self._attr_native_value = self._compute_state() diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 3dac30dcf6c..603b5682f42 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -35,7 +35,7 @@ class DSMRSensor(SensorEntity): slug = slugify(description.key.replace("/", "_")) self.entity_id = f"sensor.{slug}" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" @callback diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 7f85710206d..b97a8eb83eb 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -71,7 +71,7 @@ class DteEnergyBridgeSensor(SensorEntity): self._attr_name = name - def update(self): + def update(self) -> None: """Get the energy usage data from the DTE energy bridge.""" try: response = requests.get(self._url, timeout=5) diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index e6ee4d92de9..cf65c18e91c 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -125,7 +125,7 @@ class DublinPublicTransportSensor(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data from opendata.ch and update the states.""" self.data.update() self._times = self.data.info diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 941e1ee9017..1436b416031 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -164,11 +164,11 @@ class DwdWeatherWarningsSensor(SensorEntity): return data @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self._api.api.data_valid - def update(self): + def update(self) -> None: """Get the latest data from the DWD-Weather-Warnings API.""" _LOGGER.debug( "Update requested for %s (%s) by %s", diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index fd5b64e206a..8a1b5a1bc6c 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -92,7 +92,7 @@ class DweetSensor(SensorEntity): """Return the state.""" return self._state - def update(self): + def update(self) -> None: """Get the latest data from REST API.""" self.dweet.update() diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py index c98a1ce0ec4..3e459e45847 100644 --- a/homeassistant/components/dynalite/switch.py +++ b/homeassistant/components/dynalite/switch.py @@ -1,5 +1,7 @@ """Support for the Dynalite channels and presets as switches.""" +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -27,10 +29,10 @@ class DynaliteSwitch(DynaliteBase, SwitchEntity): """Return true if switch is on.""" return self._device.is_on - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._device.async_turn_on() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.async_turn_off() From 98c9399ff0b53c2221fbd78d2a4d4301affdc960 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Aug 2022 11:27:43 -1000 Subject: [PATCH 505/903] Bump yalexs-ble to 1.6.0 (#77042) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 4bc21e17d3a..1c97f687db6 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.4.0"], + "requirements": ["yalexs-ble==1.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 88174881477..561a7752b45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2509,7 +2509,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.4.0 +yalexs-ble==1.6.0 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1291ff55669..fe7c8f4c37d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1710,7 +1710,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.4.0 +yalexs-ble==1.6.0 # homeassistant.components.august yalexs==1.2.1 From a076d3faa03f4fdd4744c26562f5471a54c800e1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 19 Aug 2022 23:27:33 +0100 Subject: [PATCH 506/903] Address late review of system bridge media source (#77032) Co-authored-by: Martin Hjelmare --- .../components/system_bridge/media_source.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/system_bridge/media_source.py b/homeassistant/components/system_bridge/media_source.py index 6190cc1c5fe..dc6bc1bbbdc 100644 --- a/homeassistant/components/system_bridge/media_source.py +++ b/homeassistant/components/system_bridge/media_source.py @@ -124,7 +124,10 @@ def _build_base_url( entry: ConfigEntry, ) -> str: """Build base url for System Bridge media.""" - return f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}/api/media/file/data?apiKey={entry.data[CONF_API_KEY]}" + return ( + f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" + f"/api/media/file/data?apiKey={entry.data[CONF_API_KEY]}" + ) def _build_root_paths( @@ -191,17 +194,19 @@ def _build_media_item( media_file: MediaFile, ) -> BrowseMediaSource: """Build individual media item.""" - ext = ( - f"~~{media_file.mime_type}" - if media_file.is_file and media_file.mime_type is not None - else "" - ) + ext = "" + if media_file.is_file and media_file.mime_type is not None: + ext = f"~~{media_file.mime_type}" + + if media_file.is_directory or media_file.mime_type is None: + media_class = MEDIA_CLASS_DIRECTORY + else: + media_class = MEDIA_CLASS_MAP[media_file.mime_type.split("/", 1)[0]] + return BrowseMediaSource( domain=DOMAIN, identifier=f"{path}/{media_file.name}{ext}", - media_class=MEDIA_CLASS_DIRECTORY - if media_file.is_directory or media_file.mime_type is None - else MEDIA_CLASS_MAP[media_file.mime_type.split("/", 1)[0]], + media_class=media_class, media_content_type=media_file.mime_type, title=media_file.name, can_play=media_file.is_file, From 21cd2f5db7e1a221b5b2b69b25e679c8f91adcbb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 20 Aug 2022 00:23:43 +0000 Subject: [PATCH 507/903] [ci skip] Translation update --- .../components/adax/translations/zh-Hant.json | 2 +- .../components/ambee/translations/es.json | 2 +- .../android_ip_webcam/translations/de.json | 3 +- .../android_ip_webcam/translations/es.json | 5 +- .../android_ip_webcam/translations/fr.json | 3 +- .../android_ip_webcam/translations/id.json | 3 +- .../android_ip_webcam/translations/it.json | 3 +- .../android_ip_webcam/translations/ja.json | 3 +- .../android_ip_webcam/translations/tr.json | 3 +- .../translations/zh-Hant.json | 3 +- .../components/awair/translations/id.json | 2 +- .../components/awair/translations/tr.json | 37 ++++++++++++-- .../components/bluetooth/translations/de.json | 11 +++- .../components/bluetooth/translations/en.json | 12 +++++ .../components/bluetooth/translations/es.json | 11 +++- .../components/bluetooth/translations/id.json | 11 +++- .../components/bluetooth/translations/it.json | 9 ++++ .../components/bluetooth/translations/ja.json | 9 ++++ .../components/bluetooth/translations/no.json | 11 +++- .../bluetooth/translations/pt-BR.json | 11 +++- .../components/bluetooth/translations/tr.json | 9 ++++ .../bluetooth/translations/zh-Hant.json | 15 ++++-- .../deutsche_bahn/translations/es.json | 2 +- .../fully_kiosk/translations/id.json | 20 ++++++++ .../fully_kiosk/translations/tr.json | 20 ++++++++ .../components/google/translations/es.json | 2 +- .../components/guardian/translations/es.json | 4 +- .../components/guardian/translations/tr.json | 13 +++++ .../translations/sensor.id.json | 2 + .../components/hue/translations/id.json | 2 +- .../components/hue/translations/tr.json | 5 +- .../lacrosse_view/translations/id.json | 3 +- .../lacrosse_view/translations/it.json | 3 +- .../lacrosse_view/translations/ja.json | 3 +- .../lacrosse_view/translations/no.json | 3 +- .../lacrosse_view/translations/tr.json | 3 +- .../lacrosse_view/translations/zh-Hant.json | 3 +- .../components/lametric/translations/id.json | 50 +++++++++++++++++++ .../components/lametric/translations/it.json | 46 +++++++++++++++++ .../components/lametric/translations/ja.json | 1 + .../components/lametric/translations/tr.json | 50 +++++++++++++++++++ .../landisgyr_heat_meter/translations/id.json | 23 +++++++++ .../landisgyr_heat_meter/translations/it.json | 23 +++++++++ .../landisgyr_heat_meter/translations/ja.json | 20 ++++++++ .../landisgyr_heat_meter/translations/no.json | 23 +++++++++ .../landisgyr_heat_meter/translations/tr.json | 23 +++++++++ .../translations/zh-Hant.json | 23 +++++++++ .../components/lyric/translations/es.json | 4 +- .../miflora/translations/zh-Hant.json | 2 +- .../mitemp_bt/translations/zh-Hant.json | 4 +- .../components/nest/translations/es.json | 2 +- .../components/pushover/translations/de.json | 34 +++++++++++++ .../components/pushover/translations/es.json | 34 +++++++++++++ .../components/pushover/translations/fr.json | 28 +++++++++++ .../components/pushover/translations/id.json | 34 +++++++++++++ .../components/pushover/translations/it.json | 34 +++++++++++++ .../components/pushover/translations/ja.json | 33 ++++++++++++ .../components/pushover/translations/no.json | 34 +++++++++++++ .../components/pushover/translations/tr.json | 34 +++++++++++++ .../pushover/translations/zh-Hant.json | 34 +++++++++++++ .../components/qingping/translations/tr.json | 22 ++++++++ .../radiotherm/translations/es.json | 2 +- .../components/schedule/translations/tr.json | 9 ++++ .../components/senz/translations/es.json | 4 +- .../simplepush/translations/es.json | 2 +- .../components/skybell/translations/de.json | 6 +++ .../components/skybell/translations/en.json | 4 +- .../components/skybell/translations/es.json | 6 +++ .../components/skybell/translations/fr.json | 5 ++ .../components/skybell/translations/id.json | 6 +++ .../components/skybell/translations/it.json | 6 +++ .../components/skybell/translations/ja.json | 6 +++ .../components/skybell/translations/tr.json | 6 +++ .../skybell/translations/zh-Hant.json | 6 +++ .../components/spotify/translations/es.json | 2 +- .../steam_online/translations/es.json | 2 +- .../components/uscis/translations/es.json | 2 +- .../components/xbox/translations/es.json | 2 +- .../xiaomi_miio/translations/select.id.json | 10 ++++ .../xiaomi_miio/translations/select.tr.json | 10 ++++ .../yalexs_ble/translations/tr.json | 31 ++++++++++++ .../components/zha/translations/de.json | 3 ++ .../components/zha/translations/en.json | 3 ++ .../components/zha/translations/es.json | 3 ++ .../components/zha/translations/fr.json | 3 ++ .../components/zha/translations/id.json | 3 ++ .../components/zha/translations/it.json | 3 ++ .../components/zha/translations/ja.json | 3 ++ .../components/zha/translations/tr.json | 3 ++ .../components/zha/translations/zh-Hant.json | 3 ++ 90 files changed, 984 insertions(+), 51 deletions(-) create mode 100644 homeassistant/components/fully_kiosk/translations/id.json create mode 100644 homeassistant/components/fully_kiosk/translations/tr.json create mode 100644 homeassistant/components/lametric/translations/id.json create mode 100644 homeassistant/components/lametric/translations/it.json create mode 100644 homeassistant/components/lametric/translations/tr.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/id.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/it.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/ja.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/no.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/tr.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json create mode 100644 homeassistant/components/pushover/translations/de.json create mode 100644 homeassistant/components/pushover/translations/es.json create mode 100644 homeassistant/components/pushover/translations/fr.json create mode 100644 homeassistant/components/pushover/translations/id.json create mode 100644 homeassistant/components/pushover/translations/it.json create mode 100644 homeassistant/components/pushover/translations/ja.json create mode 100644 homeassistant/components/pushover/translations/no.json create mode 100644 homeassistant/components/pushover/translations/tr.json create mode 100644 homeassistant/components/pushover/translations/zh-Hant.json create mode 100644 homeassistant/components/qingping/translations/tr.json create mode 100644 homeassistant/components/schedule/translations/tr.json create mode 100644 homeassistant/components/yalexs_ble/translations/tr.json diff --git a/homeassistant/components/adax/translations/zh-Hant.json b/homeassistant/components/adax/translations/zh-Hant.json index cd99affe40e..89275f8aab7 100644 --- a/homeassistant/components/adax/translations/zh-Hant.json +++ b/homeassistant/components/adax/translations/zh-Hant.json @@ -27,7 +27,7 @@ "data": { "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225" }, - "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u82bd\u52a0\u71b1\u5668" + "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u7259\u52a0\u71b1\u5668" } } } diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json index 1bf2f5391ad..fde555ad801 100644 --- a/homeassistant/components/ambee/translations/es.json +++ b/homeassistant/components/ambee/translations/es.json @@ -27,7 +27,7 @@ }, "issues": { "pending_removal": { - "description": "La integraci\u00f3n Ambee est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque Ambee elimin\u00f3 sus cuentas gratuitas (limitadas) y ya no proporciona una forma para que los usuarios regulares se registren en un plan pago. \n\nElimina la entrada de la integraci\u00f3n Ambee de tu instancia para solucionar este problema.", + "description": "La integraci\u00f3n Ambee est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nSe va a eliminar la integraci\u00f3n porque Ambee elimin\u00f3 sus cuentas gratuitas (limitadas) y ya no proporciona una forma para que los usuarios regulares se registren en un plan pago. \n\nElimina la entrada de la integraci\u00f3n Ambee de tu instancia para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n Ambee" } } diff --git a/homeassistant/components/android_ip_webcam/translations/de.json b/homeassistant/components/android_ip_webcam/translations/de.json index 3fe34f4a259..1de9d79b389 100644 --- a/homeassistant/components/android_ip_webcam/translations/de.json +++ b/homeassistant/components/android_ip_webcam/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/es.json b/homeassistant/components/android_ip_webcam/translations/es.json index d004be2aeeb..4d6b55c5238 100644 --- a/homeassistant/components/android_ip_webcam/translations/es.json +++ b/homeassistant/components/android_ip_webcam/translations/es.json @@ -4,7 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "user": { @@ -19,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Se eliminar\u00e1 la configuraci\u00f3n de la Android IP Webcam mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Android IP Webcam de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Android IP Webcam mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Android IP Webcam de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android" } } diff --git a/homeassistant/components/android_ip_webcam/translations/fr.json b/homeassistant/components/android_ip_webcam/translations/fr.json index 0e83b0feaf7..19f42be4376 100644 --- a/homeassistant/components/android_ip_webcam/translations/fr.json +++ b/homeassistant/components/android_ip_webcam/translations/fr.json @@ -4,7 +4,8 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/id.json b/homeassistant/components/android_ip_webcam/translations/id.json index 430ebe3645f..593fa61dea3 100644 --- a/homeassistant/components/android_ip_webcam/translations/id.json +++ b/homeassistant/components/android_ip_webcam/translations/id.json @@ -4,7 +4,8 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/it.json b/homeassistant/components/android_ip_webcam/translations/it.json index db0cfc79d84..35ed4267a94 100644 --- a/homeassistant/components/android_ip_webcam/translations/it.json +++ b/homeassistant/components/android_ip_webcam/translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/ja.json b/homeassistant/components/android_ip_webcam/translations/ja.json index 519edb51609..beb3f387d64 100644 --- a/homeassistant/components/android_ip_webcam/translations/ja.json +++ b/homeassistant/components/android_ip_webcam/translations/ja.json @@ -4,7 +4,8 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/tr.json b/homeassistant/components/android_ip_webcam/translations/tr.json index cac43d82490..efd14cf5b21 100644 --- a/homeassistant/components/android_ip_webcam/translations/tr.json +++ b/homeassistant/components/android_ip_webcam/translations/tr.json @@ -4,7 +4,8 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/zh-Hant.json b/homeassistant/components/android_ip_webcam/translations/zh-Hant.json index 523c5a8b0b3..a8170bd2a7d 100644 --- a/homeassistant/components/android_ip_webcam/translations/zh-Hant.json +++ b/homeassistant/components/android_ip_webcam/translations/zh-Hant.json @@ -4,7 +4,8 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { "user": { diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 39b63bef7e5..3d633070f20 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -29,7 +29,7 @@ "data": { "host": "Alamat IP" }, - "description": "API Awair Local harus diaktifkan dengan mengikuti langkah-langkah berikut: {url}" + "description": "Ikuti [petunjuk ini]( {url} ) untuk mengaktifkan API Lokal Awair. \n\n Klik kirim setelah selesai." }, "local_pick": { "data": { diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index ef34620b5f7..8d49f985eae 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -2,14 +2,41 @@ "config": { "abort": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured_account": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unreachable": "Ba\u011flanma hatas\u0131" }, "error": { "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "unreachable": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Eri\u015fim Anahtar\u0131", + "email": "E-posta" + }, + "description": "Bir Awair geli\u015ftirici eri\u015fim belirtecine \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: {url}" + }, + "discovery_confirm": { + "description": "{model} ( {device_id} ) kurulumunu yapmak istiyor musunuz?" + }, + "local": { + "data": { + "host": "IP Adresi" + }, + "description": "Awair Yerel API'sinin nas\u0131l etkinle\u015ftirilece\u011fiyle ilgili [bu talimatlar\u0131]( {url} ) uygulay\u0131n. \n\n \u0130\u015finiz bitti\u011finde g\u00f6nder'i t\u0131klay\u0131n." + }, + "local_pick": { + "data": { + "device": "Cihaz", + "host": "IP Adresi" + } + }, "reauth": { "data": { "access_token": "Eri\u015fim Anahtar\u0131", @@ -29,7 +56,11 @@ "access_token": "Eri\u015fim Anahtar\u0131", "email": "E-posta" }, - "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login" + "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Bulut \u00fczerinden ba\u011flan\u0131n", + "local": "Yerel olarak ba\u011flan (tercih edilen)" + } } } } diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index 6e65b985478..50723328c4a 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Der Dienst ist bereits konfiguriert", - "no_adapters": "Keine Bluetooth-Adapter gefunden" + "no_adapters": "Keine unkonfigurierten Bluetooth-Adapter gefunden" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "M\u00f6chtest du Bluetooth einrichten?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "W\u00e4hle einen Bluetooth-Adapter zum Einrichten aus" + }, + "single_adapter": { + "description": "M\u00f6chtest du den Bluetooth-Adapter {name} einrichten?" + }, "user": { "data": { "address": "Ger\u00e4t" diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index ac80cfb620e..5b40308cd3c 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,6 +9,9 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -25,5 +28,14 @@ "description": "Choose a device to setup" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "The Bluetooth Adapter to use for scanning" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json index 1dc8669a067..170f44e446f 100644 --- a/homeassistant/components/bluetooth/translations/es.json +++ b/homeassistant/components/bluetooth/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "no_adapters": "No se encontraron adaptadores Bluetooth" + "no_adapters": "No se encontraron adaptadores Bluetooth no configurados" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "\u00bfQuieres configurar Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adaptador" + }, + "description": "Selecciona un adaptador Bluetooth para configurar" + }, + "single_adapter": { + "description": "\u00bfQuieres configurar el adaptador Bluetooth {name}?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index 3fc2d6a7623..a0ce38665b7 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Layanan sudah dikonfigurasi", - "no_adapters": "Tidak ada adaptor Bluetooth yang ditemukan" + "no_adapters": "Tidak ada adaptor Bluetooth yang belum dikonfigurasi yang ditemukan" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Ingin menyiapkan Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adaptor" + }, + "description": "Pilih adaptor Bluetooth untuk disiapkan" + }, + "single_adapter": { + "description": "Ingin menyiapkan adaptor Bluetooth {name}?" + }, "user": { "data": { "address": "Perangkat" diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index 86809b41a7d..fd03835ebce 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Vuoi configurare il Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adattatore" + }, + "description": "Seleziona un adattatore Bluetooth da configurare" + }, + "single_adapter": { + "description": "Vuoi configurare l'adattatore Bluetooth {name}?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index b3f6b794ee9..e7588f378af 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Bluetooth\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, + "multiple_adapters": { + "data": { + "adapter": "\u30a2\u30c0\u30d7\u30bf\u30fc" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u3001Bluetooth\u30a2\u30c0\u30d7\u30bf\u30fc\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "single_adapter": { + "description": "Bluetooth\u30a2\u30c0\u30d7\u30bf\u30fc {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/bluetooth/translations/no.json b/homeassistant/components/bluetooth/translations/no.json index fbc59772d6f..8e82e77216c 100644 --- a/homeassistant/components/bluetooth/translations/no.json +++ b/homeassistant/components/bluetooth/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "no_adapters": "Finner ingen Bluetooth-adaptere" + "no_adapters": "Fant ingen ukonfigurerte Bluetooth-adaptere" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Vil du konfigurere Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "Velg en Bluetooth-adapter for \u00e5 konfigurere" + }, + "single_adapter": { + "description": "Vil du konfigurere Bluetooth-adapteren {name} ?" + }, "user": { "data": { "address": "Enhet" diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index 0a5cf354495..ec69310dc40 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", - "no_adapters": "Nenhum adaptador Bluetooth encontrado" + "no_adapters": "N\u00e3o foram encontrados adaptadores Bluetooth n\u00e3o configurados" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Deseja configurar o Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adaptador" + }, + "description": "Selecione um adaptador Bluetooth para configurar" + }, + "single_adapter": { + "description": "Deseja configurar o adaptador Bluetooth {name}?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json index a464d65dd93..e2286fcd122 100644 --- a/homeassistant/components/bluetooth/translations/tr.json +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Bluetooth'u kurmak istiyor musunuz?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapt\u00f6r" + }, + "description": "Kurulum i\u00e7in bir Bluetooth adapt\u00f6r\u00fc se\u00e7in" + }, + "single_adapter": { + "description": "{name} Bluetooth adapt\u00f6r\u00fcn\u00fc kurmak istiyor musunuz?" + }, "user": { "data": { "address": "Cihaz" diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index 34ab75775ab..8dea681a178 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_adapters": "\u627e\u4e0d\u5230\u4efb\u4f55\u85cd\u82bd\u50b3\u8f38\u5668" + "no_adapters": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u85cd\u7259\u50b3\u8f38\u5668" }, "flow_title": "{name}", "step": { @@ -10,7 +10,16 @@ "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, "enable_bluetooth": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u82bd\uff1f" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u7259\uff1f" + }, + "multiple_adapters": { + "data": { + "adapter": "\u50b3\u8f38\u5668" + }, + "description": "\u9078\u64c7\u9032\u884c\u8a2d\u5b9a\u7684\u85cd\u7259\u50b3\u8f38\u5668" + }, + "single_adapter": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u7259\u50b3\u8f38\u5668 {name}\uff1f" }, "user": { "data": { @@ -24,7 +33,7 @@ "step": { "init": { "data": { - "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u82bd\u50b3\u8f38\u5668" + "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u7259\u50b3\u8f38\u5668" } } } diff --git a/homeassistant/components/deutsche_bahn/translations/es.json b/homeassistant/components/deutsche_bahn/translations/es.json index 2572474f2bc..6d8b849c11b 100644 --- a/homeassistant/components/deutsche_bahn/translations/es.json +++ b/homeassistant/components/deutsche_bahn/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nSe va a eliminar la integraci\u00f3n porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n Deutsche Bahn" } } diff --git a/homeassistant/components/fully_kiosk/translations/id.json b/homeassistant/components/fully_kiosk/translations/id.json new file mode 100644 index 00000000000..d9be1351db4 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/tr.json b/homeassistant/components/fully_kiosk/translations/tr.json new file mode 100644 index 00000000000..5d1e2c90e1b --- /dev/null +++ b/homeassistant/components/fully_kiosk/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": { + "host": "Sunucu", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 4b36c5006c0..401a1f37b94 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -35,7 +35,7 @@ }, "issues": { "deprecated_yaml": { - "description": "La configuraci\u00f3n de Google Calendar en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.9. \n\nTs credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Google Calendar en configuration.yaml en Home Assistant 2022.9. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Google Calendar" }, "removed_track_new_yaml": { diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 8df17ed3ff3..93bccfbd03d 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -24,11 +24,11 @@ "step": { "confirm": { "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", - "title": "El servicio {deprecated_service} ser\u00e1 eliminado" + "title": "Se va a eliminar el servicio {deprecated_service}" } } }, - "title": "El servicio {deprecated_service} ser\u00e1 eliminado" + "title": "Se va a eliminar el servicio {deprecated_service}" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json index fe4dffad3aa..e5de0cb73cd 100644 --- a/homeassistant/components/guardian/translations/tr.json +++ b/homeassistant/components/guardian/translations/tr.json @@ -17,5 +17,18 @@ "description": "Yerel bir Elexa Guardian cihaz\u0131 yap\u0131land\u0131r\u0131n." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \" {alternate_service} {alternate_target} hizmetini kullanacak \u015fekilde g\u00fcncelleyin. Ard\u0131ndan, bu sorunu \u00e7\u00f6z\u00fcld\u00fc olarak i\u015faretlemek i\u00e7in a\u015fa\u011f\u0131daki G\u00d6NDER'i t\u0131klay\u0131n.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131l\u0131yor" + } + } + }, + "title": "{deprecated_service} hizmeti kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.id.json b/homeassistant/components/homekit_controller/translations/sensor.id.json index 5697598ef54..feb6a4f869b 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.id.json +++ b/homeassistant/components/homekit_controller/translations/sensor.id.json @@ -1,6 +1,8 @@ { "state": { "homekit_controller__thread_node_capabilities": { + "full": "Perangkat Akhir Lengkap", + "minimal": "Perangkat Akhir Minimal", "none": "Tidak Ada" }, "homekit_controller__thread_status": { diff --git a/homeassistant/components/hue/translations/id.json b/homeassistant/components/hue/translations/id.json index 0b81e1093df..db37c4a60e3 100644 --- a/homeassistant/components/hue/translations/id.json +++ b/homeassistant/components/hue/translations/id.json @@ -63,7 +63,7 @@ "remote_double_button_long_press": "Kedua \"{subtype}\" dilepaskan setelah ditekan lama", "remote_double_button_short_press": "Kedua \"{subtype}\" dilepas", "repeat": "\"{subtype}\" ditekan terus", - "short_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan sebentar", + "short_release": "\"{subtype}\" dilepaskan setelah ditekan sebentar", "start": "\"{subtype}\" awalnya ditekan" } }, diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index 32ab9bb1887..df6d4e247a9 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -44,6 +44,8 @@ "button_2": "\u0130kinci d\u00fc\u011fme", "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "clock_wise": "Saat y\u00f6n\u00fcnde d\u00f6n\u00fc\u015f", + "counter_clock_wise": "Saat y\u00f6n\u00fcn\u00fcn tersine d\u00f6n\u00fc\u015f", "dim_down": "K\u0131sma", "dim_up": "A\u00e7ma", "double_buttons_1_3": "Birinci ve \u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fmeler", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "Her iki \" {subtype} \" uzun bas\u0131\u015ftan sonra b\u0131rak\u0131ld\u0131", "remote_double_button_short_press": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131", "repeat": "\" {subtype} \" d\u00fc\u011fmesi bas\u0131l\u0131 tutuldu", - "short_release": "K\u0131sa bas\u0131ld\u0131ktan sonra \"{subtype}\" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131" + "short_release": "K\u0131sa bas\u0131ld\u0131ktan sonra \"{subtype}\" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", + "start": "\" {subtype} \" ba\u015flang\u0131\u00e7ta bas\u0131ld\u0131" } }, "options": { diff --git a/homeassistant/components/lacrosse_view/translations/id.json b/homeassistant/components/lacrosse_view/translations/id.json index d244ba002b8..0f9446e354d 100644 --- a/homeassistant/components/lacrosse_view/translations/id.json +++ b/homeassistant/components/lacrosse_view/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_auth": "Autentikasi tidak valid", diff --git a/homeassistant/components/lacrosse_view/translations/it.json b/homeassistant/components/lacrosse_view/translations/it.json index 9ce6c75dcbc..efa81bf75cd 100644 --- a/homeassistant/components/lacrosse_view/translations/it.json +++ b/homeassistant/components/lacrosse_view/translations/it.json @@ -1,7 +1,8 @@ { "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": { "invalid_auth": "Autenticazione non valida", diff --git a/homeassistant/components/lacrosse_view/translations/ja.json b/homeassistant/components/lacrosse_view/translations/ja.json index 6b058f78cdb..c4cca751722 100644 --- a/homeassistant/components/lacrosse_view/translations/ja.json +++ b/homeassistant/components/lacrosse_view/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", diff --git a/homeassistant/components/lacrosse_view/translations/no.json b/homeassistant/components/lacrosse_view/translations/no.json index 9cef140da52..cd512e6fb86 100644 --- a/homeassistant/components/lacrosse_view/translations/no.json +++ b/homeassistant/components/lacrosse_view/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/lacrosse_view/translations/tr.json b/homeassistant/components/lacrosse_view/translations/tr.json index cf82d698150..cff5bd2e3e8 100644 --- a/homeassistant/components/lacrosse_view/translations/tr.json +++ b/homeassistant/components/lacrosse_view/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", diff --git a/homeassistant/components/lacrosse_view/translations/zh-Hant.json b/homeassistant/components/lacrosse_view/translations/zh-Hant.json index 78235452297..a7ecf20d04e 100644 --- a/homeassistant/components/lacrosse_view/translations/zh-Hant.json +++ b/homeassistant/components/lacrosse_view/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/lametric/translations/id.json b/homeassistant/components/lametric/translations/id.json new file mode 100644 index 00000000000..69f776e636c --- /dev/null +++ b/homeassistant/components/lametric/translations/id.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "invalid_discovery_info": "Informasi penemuan yang tidak valid diterima", + "link_local_address": "Tautan alamat lokal tidak didukung", + "missing_configuration": "Integrasi LaMetric tidak dikonfigurasi. Silakan ikuti dokumentasi.", + "no_devices": "Pengguna yang diotorisasi tidak memiliki perangkat LaMetric", + "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Perangkat LaMetric bisa disiapkan di Home Assistant dengan dua cara berbeda.\n\nAnda dapat memasukkan sendiri semua informasi perangkat dan token API, atau Home Asssistant dapat mengimpornya dari akun LaMetric.com Anda.", + "menu_options": { + "manual_entry": "Masukkan secara manual", + "pick_implementation": "Impor dari LaMetric.com (direkomendasikan)" + } + }, + "manual_entry": { + "data": { + "api_key": "Kunci API", + "host": "Host" + }, + "data_description": { + "api_key": "Anda dapat menemukan kunci API ini di [halaman perangkat di akun pengembang LaMetric Anda](https://developer.lametric.com/user/devices).", + "host": "Alamat IP atau nama host LaMetric TIME di jaringan Anda." + } + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "user_cloud_select_device": { + "data": { + "device": "Pilih perangkat LaMetric untuk ditambahkan" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "Integrasi LaMetric telah dimodernisasi: kini integrasinya dikonfigurasi dan disiapkan melalui antarmuka pengguna dan komunikasi menjadi lokal.\n\nSayangnya, tidak ada jalur migrasi otomatis yang mungkin dan oleh sebab itu Anda harus mengatur ulang integrasi LaMetric dengan Home Assistant. Baca dokumentasi integrasi LaMetric Home Assistant tentang cara persiapannya.\n\nHapus konfigurasi YAML LaMetric yang lama dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Migrasi manual diperlukan untuk LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/it.json b/homeassistant/components/lametric/translations/it.json new file mode 100644 index 00000000000..61159f16def --- /dev/null +++ b/homeassistant/components/lametric/translations/it.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "invalid_discovery_info": "Informazioni di rilevamento non valide ricevute", + "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", + "missing_configuration": "L'integrazione LaMetric non \u00e8 configurata. Segui la documentazione.", + "no_devices": "L'utente autorizzato non dispone di dispositivi LaMetric", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Un dispositivo LaMetric pu\u00f2 essere configurato in Home Assistant in due modi diversi. \n\nPuoi inserire tu stesso tutte le informazioni sul dispositivo e i token API oppure Home Assistant pu\u00f2 importarli dal tuo account LaMetric.com.", + "menu_options": { + "manual_entry": "Inserisci manualmente", + "pick_implementation": "Importa da LaMetric.com (consigliato)" + } + }, + "manual_entry": { + "data": { + "api_key": "Chiave API", + "host": "Host" + } + }, + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + }, + "user_cloud_select_device": { + "data": { + "device": "Seleziona il dispositivo LaMetric da aggiungere." + } + } + } + }, + "issues": { + "manual_migration": { + "description": "L'integrazione LaMetric \u00e8 stata modernizzata: viene ora configurata e inizializzata tramite l'interfaccia utente e le comunicazioni sono ora locali. \n\nSfortunatamente, la migrazione automatica non \u00e8 disponibile, quindi \u00e8 necessario configurare nuovamente LaMetric con Home Assistant. Consulta la documentazione di Home Assistant sull'integrazione LaMetric per sapere come configurarlo. \n\nRimuovere la vecchia configurazione YAML di LaMetric dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "Migrazione manuale richiesta per LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ja.json b/homeassistant/components/lametric/translations/ja.json index 242a43bddb7..4f6768ca80b 100644 --- a/homeassistant/components/lametric/translations/ja.json +++ b/homeassistant/components/lametric/translations/ja.json @@ -15,6 +15,7 @@ }, "step": { "choice_enter_manual_or_fetch_cloud": { + "description": "LaMetric\u30c7\u30d0\u30a4\u30b9\u306f\u3001Home Assistant\u30672\u3064\u306e\u7570\u306a\u308b\u65b9\u6cd5\u3067\u8a2d\u5b9a\u3067\u304d\u307e\u3059\u3002\n\n\u3059\u3079\u3066\u306e\u30c7\u30d0\u30a4\u30b9\u60c5\u5831\u3068API\u30c8\u30fc\u30af\u30f3\u3092\u81ea\u5206\u3067\u5165\u529b\u3059\u308b\u3053\u3068\u3084\u3001LaMetric.com\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u304b\u3089Home Asssistant\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002", "menu_options": { "manual_entry": "\u624b\u52d5\u3067\u5165\u529b", "pick_implementation": "LaMetric.com\u304b\u3089\u306e\u30a4\u30f3\u30dd\u30fc\u30c8((\u63a8\u5968)" diff --git a/homeassistant/components/lametric/translations/tr.json b/homeassistant/components/lametric/translations/tr.json new file mode 100644 index 00000000000..1801d6aac08 --- /dev/null +++ b/homeassistant/components/lametric/translations/tr.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "invalid_discovery_info": "Ge\u00e7ersiz ke\u015fif bilgisi al\u0131nd\u0131", + "link_local_address": "Ba\u011flant\u0131 yerel adresleri desteklenmiyor", + "missing_configuration": "LaMetric entegrasyonu yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_devices": "Yetkili kullan\u0131c\u0131n\u0131n LaMetric cihaz\u0131 yok", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Bir LaMetric cihaz\u0131, Home Assistant'ta iki farkl\u0131 \u015fekilde kurulabilir. \n\n T\u00fcm cihaz bilgilerini ve API belirte\u00e7lerini kendiniz girebilirsiniz veya Home Assistant bunlar\u0131 LaMetric.com hesab\u0131n\u0131zdan i\u00e7e aktarabilir.", + "menu_options": { + "manual_entry": "Manuel olarak giriniz", + "pick_implementation": "LaMetric.com'dan i\u00e7e aktar (\u00f6nerilir)" + } + }, + "manual_entry": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Sunucu" + }, + "data_description": { + "api_key": "Bu API anahtar\u0131n\u0131 [LaMetric geli\u015ftirici hesab\u0131n\u0131zdaki cihazlar sayfas\u0131nda] (https://developer.lametric.com/user/devices) bulabilirsiniz.", + "host": "A\u011f\u0131n\u0131zdaki LaMetric TIME'\u0131n\u0131z\u0131n IP adresi veya ana bilgisayar ad\u0131." + } + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "user_cloud_select_device": { + "data": { + "device": "Eklenecek LaMetric cihaz\u0131n\u0131 se\u00e7in" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "LaMetric entegrasyonu modernle\u015ftirildi: Art\u0131k kullan\u0131c\u0131 aray\u00fcz\u00fc \u00fczerinden yap\u0131land\u0131r\u0131ld\u0131 ve kuruldu ve ileti\u015fimler art\u0131k yerel. \n\n Maalesef otomatik ge\u00e7i\u015f yolu m\u00fcmk\u00fcn de\u011fildir ve bu nedenle LaMetric'inizi Home Assistant ile yeniden kurman\u0131z\u0131 gerektirir. L\u00fctfen nas\u0131l kurulaca\u011f\u0131na ili\u015fkin Home Assistant LaMetric entegrasyon belgelerine bak\u0131n. \n\n Bu sorunu d\u00fczeltmek i\u00e7in eski LaMetric YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "LaMetric i\u00e7in manuel ge\u00e7i\u015f gerekli" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/id.json b/homeassistant/components/landisgyr_heat_meter/translations/id.json new file mode 100644 index 00000000000..97bb43eb4ba --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Jalur Perangkat USB" + } + }, + "user": { + "data": { + "device": "Pilih perangkat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/it.json b/homeassistant/components/landisgyr_heat_meter/translations/it.json new file mode 100644 index 00000000000..1c320671a40 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Percorso del dispositivo USB" + } + }, + "user": { + "data": { + "device": "Seleziona il dispositivo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ja.json b/homeassistant/components/landisgyr_heat_meter/translations/ja.json new file mode 100644 index 00000000000..e05d6d2dffa --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + } + }, + "user": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/no.json b/homeassistant/components/landisgyr_heat_meter/translations/no.json new file mode 100644 index 00000000000..7c3a6e348f3 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB enhetsbane" + } + }, + "user": { + "data": { + "device": "Velg enhet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/tr.json b/homeassistant/components/landisgyr_heat_meter/translations/tr.json new file mode 100644 index 00000000000..1ff9d1c85d0 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB Cihaz Yolu" + } + }, + "user": { + "data": { + "device": "Cihaz se\u00e7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json b/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json new file mode 100644 index 00000000000..718576d8a26 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB \u88dd\u7f6e\u8def\u5f91" + } + }, + "user": { + "data": { + "device": "\u9078\u64c7\u88dd\u7f6e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index aaddd58b433..e4bd36ad952 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -20,8 +20,8 @@ }, "issues": { "removed_yaml": { - "description": "Se elimin\u00f3 la configuraci\u00f3n de Honeywell Lyric mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se elimin\u00f3 la configuraci\u00f3n YAML de Honeywell Lyric" + "description": "Se ha eliminado la configuraci\u00f3n de Honeywell Lyric mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Honeywell Lyric" } } } \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/zh-Hant.json b/homeassistant/components/miflora/translations/zh-Hant.json index e6af26efcb9..b90bc751176 100644 --- a/homeassistant/components/miflora/translations/zh-Hant.json +++ b/homeassistant/components/miflora/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u82b1\u82b1\u8349\u8349 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u7259\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u82b1\u82b1\u8349\u8349 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" } } diff --git a/homeassistant/components/mitemp_bt/translations/zh-Hant.json b/homeassistant/components/mitemp_bt/translations/zh-Hant.json index 05799bbd712..a6e4805f0eb 100644 --- a/homeassistant/components/mitemp_bt/translations/zh-Hant.json +++ b/homeassistant/components/mitemp_bt/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "issues": { "replaced": { - "description": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" + "description": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u7259\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u7259\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u7c73\u5bb6\u85cd\u7259\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u7c73\u5bb6\u85cd\u7259\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u7259\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 535482b74da..7c7c8444479 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -99,7 +99,7 @@ }, "issues": { "deprecated_yaml": { - "description": "La configuraci\u00f3n de Nest en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.10. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Nest en configuration.yaml en Home Assistant 2022.10. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Nest" }, "removed_app_auth": { diff --git a/homeassistant/components/pushover/translations/de.json b/homeassistant/components/pushover/translations/de.json new file mode 100644 index 00000000000..9a5aa0cb571 --- /dev/null +++ b/homeassistant/components/pushover/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "invalid_user_key": "Ung\u00fcltiger Benutzerschl\u00fcssel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "name": "Name", + "user_key": "Benutzerschl\u00fcssel" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Das Konfigurieren von Pushover mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Pushover-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Pushover-YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/es.json b/homeassistant/components/pushover/translations/es.json new file mode 100644 index 00000000000..c36644e575e --- /dev/null +++ b/homeassistant/components/pushover/translations/es.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida", + "invalid_user_key": "Clave de usuario no v\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "title": "Volver a autenticar la integraci\u00f3n" + }, + "user": { + "data": { + "api_key": "Clave API", + "name": "Nombre", + "user_key": "Clave de usuario" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Pushover mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Pushover de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Pushover" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/fr.json b/homeassistant/components/pushover/translations/fr.json new file mode 100644 index 00000000000..a8d7a213d64 --- /dev/null +++ b/homeassistant/components/pushover/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 d'API non valide", + "invalid_user_key": "Cl\u00e9 utilisateur non valide" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "name": "Nom", + "user_key": "Cl\u00e9 utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/id.json b/homeassistant/components/pushover/translations/id.json new file mode 100644 index 00000000000..077ee9a450b --- /dev/null +++ b/homeassistant/components/pushover/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "invalid_user_key": "Kunci pengguna tidak valid" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "api_key": "Kunci API", + "name": "Nama", + "user_key": "Kunci pengguna" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Pushover lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Pushover dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Pushover dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/it.json b/homeassistant/components/pushover/translations/it.json new file mode 100644 index 00000000000..c72f62b0c21 --- /dev/null +++ b/homeassistant/components/pushover/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "invalid_user_key": "Chiave utente non valida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "api_key": "Chiave API", + "name": "Nome", + "user_key": "Chiave utente" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Pushover tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Pushover dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Pushover sar\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/ja.json b/homeassistant/components/pushover/translations/ja.json new file mode 100644 index 00000000000..344e21952dc --- /dev/null +++ b/homeassistant/components/pushover/translations/ja.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_user_key": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u30ad\u30fc" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + }, + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "name": "\u540d\u524d", + "user_key": "\u30e6\u30fc\u30b6\u30fc\u30ad\u30fc" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Pushover\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Pushover\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "title": "Pushover YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/no.json b/homeassistant/components/pushover/translations/no.json new file mode 100644 index 00000000000..32f733eedcb --- /dev/null +++ b/homeassistant/components/pushover/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "invalid_user_key": "Ugyldig brukern\u00f8kkel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "name": "Navn", + "user_key": "Brukern\u00f8kkel" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Pushover med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Pushover YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Pushover YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/tr.json b/homeassistant/components/pushover/translations/tr.json new file mode 100644 index 00000000000..d91471736b1 --- /dev/null +++ b/homeassistant/components/pushover/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet 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_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "invalid_user_key": "Ge\u00e7ersiz kullan\u0131c\u0131 anahtar\u0131" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "name": "Ad", + "user_key": "Kullan\u0131c\u0131 anahtar\u0131" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak Pushover'\u0131 yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Pushover YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Pushover YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/zh-Hant.json b/homeassistant/components/pushover/translations/zh-Hant.json new file mode 100644 index 00000000000..bf359028bde --- /dev/null +++ b/homeassistant/components/pushover/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "invalid_user_key": "\u4f7f\u7528\u8005\u91d1\u9470\u7121\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "name": "\u540d\u7a31", + "user_key": "\u4f7f\u7528\u8005\u91d1\u9470" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Pushover \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Pushover YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Pushover YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/tr.json b/homeassistant/components/qingping/translations/tr.json new file mode 100644 index 00000000000..f0ddbc274c9 --- /dev/null +++ b/homeassistant/components/qingping/translations/tr.json @@ -0,0 +1,22 @@ +{ + "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", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "not_supported": "Cihaz desteklenmiyor" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/es.json b/homeassistant/components/radiotherm/translations/es.json index 165068f38fa..fab444ede2a 100644 --- a/homeassistant/components/radiotherm/translations/es.json +++ b/homeassistant/components/radiotherm/translations/es.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "La configuraci\u00f3n de la plataforma clim\u00e1tica Radio Thermostat mediante YAML se eliminar\u00e1 en Home Assistant 2022.9. \n\nTu configuraci\u00f3n existente se ha importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de la plataforma clim\u00e1tica Radio Thermostat mediante YAML en Home Assistant 2022.9. \n\nTu configuraci\u00f3n existente se ha importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Radio Thermostat" } }, diff --git a/homeassistant/components/schedule/translations/tr.json b/homeassistant/components/schedule/translations/tr.json new file mode 100644 index 00000000000..6c59c8507dc --- /dev/null +++ b/homeassistant/components/schedule/translations/tr.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + } + }, + "title": "Zamanlama" +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/es.json b/homeassistant/components/senz/translations/es.json index 406d6b64017..671bd012f77 100644 --- a/homeassistant/components/senz/translations/es.json +++ b/homeassistant/components/senz/translations/es.json @@ -19,8 +19,8 @@ }, "issues": { "removed_yaml": { - "description": "Se elimin\u00f3 la configuraci\u00f3n de nVent RAYCHEM SENZ mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se elimin\u00f3 la configuraci\u00f3n YAML de nVent RAYCHEM SENZ" + "description": "Se ha eliminado la configuraci\u00f3n de nVent RAYCHEM SENZ mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de nVent RAYCHEM SENZ" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/es.json b/homeassistant/components/simplepush/translations/es.json index acf2da9e5d2..e1bb97650ff 100644 --- a/homeassistant/components/simplepush/translations/es.json +++ b/homeassistant/components/simplepush/translations/es.json @@ -24,7 +24,7 @@ "title": "Se va a eliminar la configuraci\u00f3n YAML de Simplepush" }, "removed_yaml": { - "description": "Se ha eliminado la configuraci\u00f3n de Simplepush mediante YAML.\n\nTu configuraci\u00f3n YAML existente no es utilizada por Home Assistant.\n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se ha eliminado la configuraci\u00f3n de Simplepush usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se ha eliminado la configuraci\u00f3n YAML de Simplepush" } } diff --git a/homeassistant/components/skybell/translations/de.json b/homeassistant/components/skybell/translations/de.json index 65a21e4b8f5..2705eab7086 100644 --- a/homeassistant/components/skybell/translations/de.json +++ b/homeassistant/components/skybell/translations/de.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Skybell mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Skybell YAML-Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/en.json b/homeassistant/components/skybell/translations/en.json index f9fa0048854..767f6bfe64e 100644 --- a/homeassistant/components/skybell/translations/en.json +++ b/homeassistant/components/skybell/translations/en.json @@ -20,8 +20,8 @@ }, "issues": { "removed_yaml": { - "title": "The Skybell YAML configuration has been removed", - "description": "Configuring Skybell using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "Configuring Skybell using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Skybell YAML configuration has been removed" } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/es.json b/homeassistant/components/skybell/translations/es.json index 68633c72180..18cb57a194b 100644 --- a/homeassistant/components/skybell/translations/es.json +++ b/homeassistant/components/skybell/translations/es.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Se ha eliminado la configuraci\u00f3n de Skybell usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Skybell" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/fr.json b/homeassistant/components/skybell/translations/fr.json index 457e7c48157..ecb6a0d773d 100644 --- a/homeassistant/components/skybell/translations/fr.json +++ b/homeassistant/components/skybell/translations/fr.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "removed_yaml": { + "title": "La configuration YAML pour Skybell a \u00e9t\u00e9 supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/id.json b/homeassistant/components/skybell/translations/id.json index d59082eb80b..8e7aa4a5a87 100644 --- a/homeassistant/components/skybell/translations/id.json +++ b/homeassistant/components/skybell/translations/id.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Skybell lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Skybell telah dihapus" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/it.json b/homeassistant/components/skybell/translations/it.json index 39d4856dbe4..d9e798e534c 100644 --- a/homeassistant/components/skybell/translations/it.json +++ b/homeassistant/components/skybell/translations/it.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Skybell tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Skybell \u00e8 stata rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/ja.json b/homeassistant/components/skybell/translations/ja.json index 9e2d562206f..0cac70de54a 100644 --- a/homeassistant/components/skybell/translations/ja.json +++ b/homeassistant/components/skybell/translations/ja.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Skybell\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "title": "Skybell YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/tr.json b/homeassistant/components/skybell/translations/tr.json index 68bd9029559..23e8cb87551 100644 --- a/homeassistant/components/skybell/translations/tr.json +++ b/homeassistant/components/skybell/translations/tr.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "YAML kullanarak Skybell'i yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Skybell YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/zh-Hant.json b/homeassistant/components/skybell/translations/zh-Hant.json index 1e614212c45..faae37d9b31 100644 --- a/homeassistant/components/skybell/translations/zh-Hant.json +++ b/homeassistant/components/skybell/translations/zh-Hant.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Skybell \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Skybell YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index e2e3cf927a7..f9267f93c08 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -21,7 +21,7 @@ }, "issues": { "removed_yaml": { - "description": "Se elimin\u00f3 la configuraci\u00f3n de Spotify usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se ha eliminado la configuraci\u00f3n de Spotify usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se ha eliminado la configuraci\u00f3n YAML de Spotify" } }, diff --git a/homeassistant/components/steam_online/translations/es.json b/homeassistant/components/steam_online/translations/es.json index e558ead9ff5..dfb1cdbd50e 100644 --- a/homeassistant/components/steam_online/translations/es.json +++ b/homeassistant/components/steam_online/translations/es.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "Se elimin\u00f3 la configuraci\u00f3n de Steam usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se ha eliminado la configuraci\u00f3n de Steam usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se ha eliminado la configuraci\u00f3n YAML de Steam" } }, diff --git a/homeassistant/components/uscis/translations/es.json b/homeassistant/components/uscis/translations/es.json index f18acfff461..cb8689e1b8b 100644 --- a/homeassistant/components/uscis/translations/es.json +++ b/homeassistant/components/uscis/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nSe va a eliminar la integraci\u00f3n porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n USCIS" } } diff --git a/homeassistant/components/xbox/translations/es.json b/homeassistant/components/xbox/translations/es.json index f1b1945c832..eaf4275884b 100644 --- a/homeassistant/components/xbox/translations/es.json +++ b/homeassistant/components/xbox/translations/es.json @@ -16,7 +16,7 @@ }, "issues": { "deprecated_yaml": { - "description": "La configuraci\u00f3n de Xbox en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.9. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Xbox en configuration.yaml en Home Assistant 2022.9. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Xbox" } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.id.json b/homeassistant/components/xiaomi_miio/translations/select.id.json index 178bc06301c..149e2715e49 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.id.json +++ b/homeassistant/components/xiaomi_miio/translations/select.id.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Maju", + "left": "Kiri", + "right": "Kanan" + }, "xiaomi_miio__led_brightness": { "bright": "Terang", "dim": "Redup", "off": "Mati" + }, + "xiaomi_miio__ptc_level": { + "high": "Tinggi", + "low": "Rendah", + "medium": "Sedang" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.tr.json b/homeassistant/components/xiaomi_miio/translations/select.tr.json index 7767a54fe2d..9ffb8dbd212 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.tr.json +++ b/homeassistant/components/xiaomi_miio/translations/select.tr.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u0130leri", + "left": "Sol", + "right": "Sa\u011f" + }, "xiaomi_miio__led_brightness": { "bright": "Ayd\u0131nl\u0131k", "dim": "Dim", "off": "Kapal\u0131" + }, + "xiaomi_miio__ptc_level": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta" } } } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/tr.json b/homeassistant/components/yalexs_ble/translations/tr.json new file mode 100644 index 00000000000..15711050225 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/tr.json @@ -0,0 +1,31 @@ +{ + "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", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "no_unconfigured_devices": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f cihaz bulunamad\u0131." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_key_format": "\u00c7evrimd\u0131\u015f\u0131 anahtar 32 baytl\u0131k bir onalt\u0131l\u0131k dize olmal\u0131d\u0131r.", + "invalid_key_index": "\u00c7evrimd\u0131\u015f\u0131 anahtar yuvas\u0131, 0 ile 255 aras\u0131nda bir tam say\u0131 olmal\u0131d\u0131r.", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "{address} adresiyle Bluetooth \u00fczerinden {name} kurmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Bluetooth adresi", + "key": "\u00c7evrimd\u0131\u015f\u0131 Anahtar (32 baytl\u0131k onalt\u0131l\u0131k dize)", + "slot": "\u00c7evrimd\u0131\u015f\u0131 Anahtar Yuvas\u0131 (0 ile 255 aras\u0131nda tam say\u0131)" + }, + "description": "\u00c7evrimd\u0131\u015f\u0131 anahtar\u0131n nas\u0131l bulunaca\u011f\u0131na ili\u015fkin belgelere bak\u0131n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 7786a3b7bf3..5ac32a1df1f 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -13,6 +13,9 @@ "confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "confirm_hardware": { + "description": "M\u00f6chtest du {name} einrichten?" + }, "pick_radio": { "data": { "radio_type": "Funktyp" diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 757ab338ec6..46e897167b0 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -13,6 +13,9 @@ "confirm": { "description": "Do you want to setup {name}?" }, + "confirm_hardware": { + "description": "Do you want to setup {name}?" + }, "pick_radio": { "data": { "radio_type": "Radio Type" diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 6dcc3c84e42..a3e5f0b63ef 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -13,6 +13,9 @@ "confirm": { "description": "\u00bfQuieres configurar {name} ?" }, + "confirm_hardware": { + "description": "\u00bfQuieres configurar {name}?" + }, "pick_radio": { "data": { "radio_type": "Tipo de Radio" diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index f69e4fb36ff..87027c7ca62 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -13,6 +13,9 @@ "confirm": { "description": "Voulez-vous configurer {name}\u00a0?" }, + "confirm_hardware": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, "pick_radio": { "data": { "radio_type": "Type de radio" diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index ef91a4227c8..f9947809a6d 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -13,6 +13,9 @@ "confirm": { "description": "Ingin menyiapkan {name}?" }, + "confirm_hardware": { + "description": "Ingin menyiapkan {name}?" + }, "pick_radio": { "data": { "radio_type": "Jenis Radio" diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index a71e96ed308..9ceb94cbd00 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -13,6 +13,9 @@ "confirm": { "description": "Vuoi configurare {name}?" }, + "confirm_hardware": { + "description": "Vuoi configurare {name}?" + }, "pick_radio": { "data": { "radio_type": "Tipo di radio" diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 25d5a5bd9f6..6cfd70056b6 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -13,6 +13,9 @@ "confirm": { "description": "{name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" }, + "confirm_hardware": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "pick_radio": { "data": { "radio_type": "\u7121\u7dda\u30bf\u30a4\u30d7" diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index 391b9315b48..82eb9b11b68 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -13,6 +13,9 @@ "confirm": { "description": "{name} kurulumunu yapmak istiyor musunuz?" }, + "confirm_hardware": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, "pick_radio": { "data": { "radio_type": "Radyo Tipi" diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 546a2f77c31..a6c69311181 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -13,6 +13,9 @@ "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "confirm_hardware": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, "pick_radio": { "data": { "radio_type": "\u7121\u7dda\u96fb\u985e\u5225" From 3a3f41f3df932368791d3ee3f5fbae5fb3b38bfe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 07:52:55 +0200 Subject: [PATCH 508/903] Improve entity type hints [e] (#77041) --- homeassistant/components/ebox/sensor.py | 2 +- homeassistant/components/ebusd/sensor.py | 2 +- .../components/ecoal_boiler/sensor.py | 2 +- .../components/ecoal_boiler/switch.py | 8 ++++-- .../components/ecobee/binary_sensor.py | 4 +-- homeassistant/components/ecobee/climate.py | 19 +++++++------ homeassistant/components/ecobee/sensor.py | 6 ++-- homeassistant/components/ecobee/weather.py | 2 +- homeassistant/components/econet/climate.py | 16 ++++++----- .../components/econet/water_heater.py | 11 ++++---- homeassistant/components/ecovacs/vacuum.py | 22 +++++++++------ homeassistant/components/edimax/switch.py | 8 ++++-- homeassistant/components/edl21/sensor.py | 4 +-- .../components/egardia/binary_sensor.py | 2 +- homeassistant/components/eliqonline/sensor.py | 2 +- homeassistant/components/elv/switch.py | 7 +++-- homeassistant/components/emby/media_player.py | 14 +++++----- homeassistant/components/emoncms/sensor.py | 2 +- .../components/enigma2/media_player.py | 28 +++++++++---------- homeassistant/components/enocean/sensor.py | 2 +- homeassistant/components/enocean/switch.py | 6 ++-- .../components/envisalink/binary_sensor.py | 2 +- homeassistant/components/envisalink/sensor.py | 2 +- homeassistant/components/envisalink/switch.py | 7 +++-- homeassistant/components/ephember/climate.py | 11 ++++---- .../components/epson/media_player.py | 22 +++++++-------- .../components/epsonworkforce/sensor.py | 8 ++++-- .../components/eq3btsmart/climate.py | 9 +++--- homeassistant/components/etherscan/sensor.py | 2 +- homeassistant/components/eufy/switch.py | 8 ++++-- homeassistant/components/everlights/light.py | 5 ++-- .../components/evohome/water_heater.py | 4 +-- 32 files changed, 138 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index ef6c0f4b323..3e1a2fa2413 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -184,7 +184,7 @@ class EBoxSensor(SensorEntity): self._attr_name = f"{name} {description.name}" self.ebox_data = ebox_data - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from EBox and update the state.""" await self.ebox_data.async_update() if self.entity_description.key in self.ebox_data.data: diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 1acbe47c2a1..923f94f705d 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -111,7 +111,7 @@ class EbusdSensor(SensorEntity): return self._unit_of_measurement @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" try: self.data.update(self._name, self._type) diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py index d85bd9edf6c..5c8bf926fce 100644 --- a/homeassistant/components/ecoal_boiler/sensor.py +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -39,7 +39,7 @@ class EcoalTempSensor(SensorEntity): self._attr_name = name self._status_attr = status_attr - def update(self): + def update(self) -> None: """Fetch new state data for the sensor. This is the only method that should fetch new data for Home Assistant. diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py index b137cf5832d..6922a35f5de 100644 --- a/homeassistant/components/ecoal_boiler/switch.py +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -1,6 +1,8 @@ """Allows to configuration ecoal (esterownik.pl) pumps as switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -45,7 +47,7 @@ class EcoalSwitch(SwitchEntity): # status. self._contr_set_fun = getattr(self._ecoal_contr, f"set_{state_attr}") - def update(self): + def update(self) -> None: """Fetch new state data for the sensor. This is the only method that should fetch new data for Home Assistant. @@ -60,12 +62,12 @@ class EcoalSwitch(SwitchEntity): """ self._ecoal_contr.status = None - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._contr_set_fun(1) self.invalidate_ecoal_cache() - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._contr_set_fun(0) self.invalidate_ecoal_cache() diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 1f8f94e93df..2266d70e0ad 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -91,7 +91,7 @@ class EcobeeBinarySensor(BinarySensorEntity): return None @property - def available(self): + def available(self) -> bool: """Return true if device is available.""" thermostat = self.data.ecobee.get_thermostat(self.index) return thermostat["runtime"]["connected"] @@ -106,7 +106,7 @@ class EcobeeBinarySensor(BinarySensorEntity): """Return the class of this sensor, from DEVICE_CLASSES.""" return BinarySensorDeviceClass.OCCUPANCY - async def async_update(self): + async def async_update(self) -> None: """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 3673728c7fa..d256d241a4f 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import collections +from typing import Any import voluptuous as vol @@ -330,7 +331,7 @@ class Thermostat(ClimateEntity): self._fan_modes = [FAN_AUTO, FAN_ON] self.update_without_throttle = False - async def async_update(self): + async def async_update(self) -> None: """Get the latest state from the thermostat.""" if self.update_without_throttle: await self.data.update(no_throttle=True) @@ -342,12 +343,12 @@ class Thermostat(ClimateEntity): self._last_active_hvac_mode = self.hvac_mode @property - def available(self): + def available(self) -> bool: """Return if device is available.""" return self.thermostat["runtime"]["connected"] @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" if self.has_humidifier_control: return SUPPORT_FLAGS | ClimateEntityFeature.TARGET_HUMIDITY @@ -563,7 +564,7 @@ class Thermostat(ClimateEntity): if self.is_aux_heat: _LOGGER.warning("# Changing aux heat is not supported") - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Activate a preset.""" if preset_mode == self.preset_mode: return @@ -653,7 +654,7 @@ class Thermostat(ClimateEntity): self.update_without_throttle = True - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode: str) -> None: """Set the fan mode. Valid values are "on" or "auto".""" if fan_mode.lower() not in (FAN_ON, FAN_AUTO): error = "Invalid fan_mode value: Valid values are 'on' or 'auto'" @@ -689,7 +690,7 @@ class Thermostat(ClimateEntity): cool_temp = temp + delta self.set_auto_temp_hold(heat_temp, cool_temp) - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) @@ -704,7 +705,7 @@ class Thermostat(ClimateEntity): else: _LOGGER.error("Missing valid arguments for set_temperature in %s", kwargs) - def set_humidity(self, humidity): + def set_humidity(self, humidity: int) -> None: """Set the humidity level.""" if humidity not in range(0, 101): raise ValueError( @@ -714,7 +715,7 @@ class Thermostat(ClimateEntity): self.data.ecobee.set_humidity(self.thermostat_index, int(humidity)) self.update_without_throttle = True - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" ecobee_value = next( (k for k, v in ECOBEE_HVAC_TO_HASS.items() if v == hvac_mode), None @@ -821,7 +822,7 @@ class Thermostat(ClimateEntity): ) self.data.ecobee.delete_vacation(self.thermostat_index, vacation_name) - def turn_on(self): + def turn_on(self) -> None: """Set the thermostat to the last active HVAC mode.""" _LOGGER.debug( "Turning on ecobee thermostat %s in %s mode", diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 38671189132..a7d5639ae2c 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -105,6 +105,8 @@ async def async_setup_entry( class EcobeeSensor(SensorEntity): """Representation of an Ecobee sensor.""" + entity_description: EcobeeSensorEntityDescription + def __init__( self, data, @@ -163,7 +165,7 @@ class EcobeeSensor(SensorEntity): return None @property - def available(self): + def available(self) -> bool: """Return true if device is available.""" thermostat = self.data.ecobee.get_thermostat(self.index) return thermostat["runtime"]["connected"] @@ -183,7 +185,7 @@ class EcobeeSensor(SensorEntity): return self._state - async def async_update(self): + async def async_update(self) -> None: """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index aca4dcdf2f5..69f02d26294 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -188,7 +188,7 @@ class EcobeeWeather(WeatherEntity): return forecasts return None - async def async_update(self): + async def async_update(self) -> None: """Get the latest weather data.""" await self.data.update() thermostat = self.data.ecobee.get_thermostat(self._index) diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index 16dd4e043dc..9fba4883644 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -1,4 +1,6 @@ """Support for Rheem EcoNet thermostats.""" +from typing import Any + from pyeconet.equipment import EquipmentType from pyeconet.equipment.thermostat import ThermostatFanMode, ThermostatOperationMode @@ -79,7 +81,7 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): self.op_list.append(ha_mode) @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" if self._econet.supports_humidifier: return SUPPORT_FLAGS_THERMOSTAT | ClimateEntityFeature.TARGET_HUMIDITY @@ -125,7 +127,7 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): return self._econet.cool_set_point return None - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) @@ -161,14 +163,14 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): return _current_op - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" hvac_mode_to_set = HA_STATE_TO_ECONET.get(hvac_mode) if hvac_mode_to_set is None: raise ValueError(f"{hvac_mode} is not a valid mode.") self._econet.set_mode(hvac_mode_to_set) - def set_humidity(self, humidity: int): + def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" self._econet.set_dehumidifier_set_point(humidity) @@ -201,15 +203,15 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): fan_list.append(ECONET_FAN_STATE_TO_HA[mode]) return fan_list - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode: str) -> None: """Set the fan mode.""" self._econet.set_fan_mode(HA_FAN_STATE_TO_ECONET[fan_mode]) - def turn_aux_heat_on(self): + def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" self._econet.set_mode(ThermostatOperationMode.EMERGENCY_HEAT) - def turn_aux_heat_off(self): + def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" self._econet.set_mode(ThermostatOperationMode.HEATING) diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 79b821c6cba..50f080217b4 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -1,5 +1,6 @@ """Support for Rheem EcoNet water heaters.""" import logging +from typing import Any from pyeconet.equipment import EquipmentType from pyeconet.equipment.water_heater import WaterHeaterOperationMode @@ -118,14 +119,14 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): ) return WaterHeaterEntityFeature.TARGET_TEMPERATURE - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None: self.water_heater.set_set_point(target_temp) else: _LOGGER.error("A target temperature must be provided") - def set_operation_mode(self, operation_mode): + def set_operation_mode(self, operation_mode: str) -> None: """Set operation mode.""" op_mode_to_set = HA_STATE_TO_ECONET.get(operation_mode) if op_mode_to_set is not None: @@ -156,17 +157,17 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): """ return self._poll - async def async_update(self): + async def async_update(self) -> None: """Get the latest energy usage.""" await self.water_heater.get_energy_usage() await self.water_heater.get_water_usage() self.async_write_ha_state() self._poll = False - def turn_away_mode_on(self): + def turn_away_mode_on(self) -> None: """Turn away mode on.""" self.water_heater.set_away_mode(True) - def turn_away_mode_off(self): + def turn_away_mode_off(self) -> None: """Turn away mode off.""" self.water_heater.set_away_mode(False) diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 8557658d128..c380a760557 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import sucks @@ -107,7 +108,7 @@ class EcovacsVacuum(VacuumEntity): """Return the status of the vacuum cleaner.""" return self.device.vacuum_status - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" self.device.run(sucks.Charge()) @@ -132,37 +133,42 @@ class EcovacsVacuum(VacuumEntity): """Return the fan speed of the vacuum cleaner.""" return self.device.fan_speed - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the vacuum on and start cleaning.""" self.device.run(sucks.Clean()) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the vacuum off stopping the cleaning and returning home.""" self.return_to_base() - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" self.device.run(sucks.Stop()) - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" self.device.run(sucks.Spot()) - def locate(self, **kwargs): + def locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" self.device.run(sucks.PlaySound()) - def set_fan_speed(self, fan_speed, **kwargs): + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" if self.is_on: self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) - def send_command(self, command, params=None, **kwargs): + def send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: """Send a command to a vacuum cleaner.""" self.device.run(sucks.VacBotCommand(command, params)) diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 6f780f8da61..34f6a500917 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -1,6 +1,8 @@ """Support for Edimax switches.""" from __future__ import annotations +from typing import Any + from pyedimax.smartplug import SmartPlug import voluptuous as vol @@ -67,15 +69,15 @@ class SmartPlugSwitch(SwitchEntity): """Return true if switch is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self.smartplug.state = "ON" - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self.smartplug.state = "OFF" - def update(self): + def update(self) -> None: """Update edimax switch.""" if not self._info: self._info = self.smartplug.info diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 730acabbc98..fe3e52548c5 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -386,7 +386,7 @@ class EDL21Entity(SensorEntity): self._async_remove_dispatcher = None self.entity_description = entity_description - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" @callback @@ -411,7 +411,7 @@ class EDL21Entity(SensorEntity): self.hass, SIGNAL_EDL21_TELEGRAM, handle_telegram ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" if self._async_remove_dispatcher: self._async_remove_dispatcher() diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py index 7a207abfa22..021111e53b3 100644 --- a/homeassistant/components/egardia/binary_sensor.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -58,7 +58,7 @@ class EgardiaBinarySensor(BinarySensorEntity): self._device_class = device_class self._egardia_system = egardia_system - def update(self): + def update(self) -> None: """Update the status.""" egardia_input = self._egardia_system.getsensorstate(self._id) self._state = STATE_ON if egardia_input else STATE_OFF diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index ba4d32fbbd8..9b81ebad78a 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -97,7 +97,7 @@ class EliqSensor(SensorEntity): """Return the state of the device.""" return self._state - async def async_update(self): + async def async_update(self) -> None: """Get the latest data.""" try: response = await self._api.get_data_now(channelid=self._channel_id) diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index 8a7da161da0..d7e35f3e04c 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import pypca from serial import SerialException @@ -72,15 +73,15 @@ class SmartPlugSwitch(SwitchEntity): """Return true if switch is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._pca.turn_on(self._device_id) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._pca.turn_off(self._device_id) - def update(self): + def update(self) -> None: """Update the PCA switch's state.""" try: self._state = self._pca.get_state(self._device_id) diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 0278028c458..d2dcfa2c629 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -153,7 +153,7 @@ class EmbyDevice(MediaPlayerEntity): self.media_status_last_position = None self.media_status_received = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callback.""" self.emby.add_update_callback(self.async_update_callback, self.device_id) @@ -311,26 +311,26 @@ class EmbyDevice(MediaPlayerEntity): return SUPPORT_EMBY return 0 - async def async_media_play(self): + async def async_media_play(self) -> None: """Play media.""" await self.device.media_play() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause the media player.""" await self.device.media_pause() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Stop the media player.""" await self.device.media_stop() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self.device.media_next() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send next track command.""" await self.device.media_previous() - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" await self.device.media_seek(position) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 0aab21458f4..e4148d1dea5 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -225,7 +225,7 @@ class EmonCmsSensor(SensorEntity): ATTR_LASTUPDATETIMESTR: template.timestamp_local(float(self._elem["time"])), } - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" self._data.update() diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index c240d882f8c..aab3514b8e0 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -140,15 +140,15 @@ class Enigma2Device(MediaPlayerEntity): return STATE_OFF if self.e2_box.in_standby else STATE_ON @property - def available(self): + def available(self) -> bool: """Return True if the device is available.""" return not self.e2_box.is_offline - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self.e2_box.turn_off() - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.e2_box.turn_on() @@ -187,15 +187,15 @@ class Enigma2Device(MediaPlayerEntity): """Picon url for the channel.""" return self.e2_box.picon_url - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.e2_box.set_volume(int(volume * 100)) - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" self.e2_box.set_volume(int(self.e2_box.volume * 100) + 5) - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self.e2_box.set_volume(int(self.e2_box.volume * 100) - 5) @@ -204,27 +204,27 @@ class Enigma2Device(MediaPlayerEntity): """Volume level of the media player (0..1).""" return self.e2_box.volume - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" self.e2_box.set_stop() - def media_play(self): + def media_play(self) -> None: """Play media.""" self.e2_box.toggle_play_pause() - def media_pause(self): + def media_pause(self) -> None: """Pause the media player.""" self.e2_box.toggle_play_pause() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self.e2_box.set_channel_up() - def media_previous_track(self): + def media_previous_track(self) -> None: """Send next track command.""" self.e2_box.set_channel_down() - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute or unmute.""" self.e2_box.mute_volume() @@ -238,11 +238,11 @@ class Enigma2Device(MediaPlayerEntity): """List of available input sources.""" return self.e2_box.source_list - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self.e2_box.select_source(self.e2_box.sources[source]) - def update(self): + def update(self) -> None: """Update state of the media_player.""" self.e2_box.update() diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 06ea50d4cdb..84237852e80 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -162,7 +162,7 @@ class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity): self._attr_name = f"{description.name} {dev_name}" self._attr_unique_id = description.unique_id(dev_id) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" # If not None, we got an initial value. await super().async_added_to_hass() diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index 5edd2bb6155..28727bfb767 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -1,6 +1,8 @@ """Support for EnOcean switches.""" from __future__ import annotations +from typing import Any + from enocean.utils import combine_hex import voluptuous as vol @@ -94,7 +96,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): """Return the device name.""" return self.dev_name - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" optional = [0x03] optional.extend(self.dev_id) @@ -106,7 +108,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): ) self._on_state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" optional = [0x03] optional.extend(self.dev_id) diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py index d82f90aa4f4..f08989c32be 100644 --- a/homeassistant/components/envisalink/binary_sensor.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -62,7 +62,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity): _LOGGER.debug("Setting up zone: %s", zone_name) super().__init__(zone_name, info, controller) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index d8f31d7c4dd..72a64931070 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -59,7 +59,7 @@ class EnvisalinkSensor(EnvisalinkDevice, SensorEntity): _LOGGER.debug("Setting up sensor for partition: %s", partition_name) super().__init__(f"{partition_name} Keypad", info, controller) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/envisalink/switch.py b/homeassistant/components/envisalink/switch.py index 6f5179a8649..0bedc41e55e 100644 --- a/homeassistant/components/envisalink/switch.py +++ b/homeassistant/components/envisalink/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant, callback @@ -58,7 +59,7 @@ class EnvisalinkSwitch(EnvisalinkDevice, SwitchEntity): super().__init__(zone_name, info, controller) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( @@ -71,11 +72,11 @@ class EnvisalinkSwitch(EnvisalinkDevice, SwitchEntity): """Return the boolean response if the zone is bypassed.""" return self._info["bypassed"] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Send the bypass keypress sequence to toggle the zone bypass.""" self._controller.toggle_zone_bypass(self._zone_number) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Send the bypass keypress sequence to toggle the zone bypass.""" self._controller.toggle_zone_bypass(self._zone_number) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 6bc95818329..7c9d9c04318 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from pyephember.pyephember import ( EphEmber, @@ -141,7 +142,7 @@ class EphEmberThermostat(ClimateEntity): """Return the supported operations.""" return OPERATION_LIST - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the operation mode.""" mode = self.map_mode_hass_eph(hvac_mode) if mode is not None: @@ -155,17 +156,17 @@ class EphEmberThermostat(ClimateEntity): return zone_is_boost_active(self._zone) - def turn_aux_heat_on(self): + def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" self._ember.activate_boost_by_name( self._zone_name, zone_target_temperature(self._zone) ) - def turn_aux_heat_off(self): + def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" self._ember.deactivate_boost_by_name(self._zone_name) - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -198,7 +199,7 @@ class EphEmberThermostat(ClimateEntity): return 35.0 - def update(self): + def update(self) -> None: """Get the latest data.""" self._zone = self._ember.get_zone(self._zone_name) diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 98152efb3b2..f978c145b4f 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -114,7 +114,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): ) return True - async def async_update(self): + async def async_update(self) -> None: """Update state of device.""" power_state = await self._projector.get_power() _LOGGER.debug("Projector status: %s", power_state) @@ -175,13 +175,13 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): """Return if projector is available.""" return self._available - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on epson.""" if self._state == STATE_OFF: await self._projector.send_command(TURN_ON) self._state = STATE_ON - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off epson.""" if self._state == STATE_ON: await self._projector.send_command(TURN_OFF) @@ -206,36 +206,36 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): """Set color mode in Epson.""" await self._projector.send_command(CMODE_LIST_SET[cmode]) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" selected_source = INV_SOURCES[source] await self._projector.send_command(selected_source) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) sound.""" await self._projector.send_command(MUTE) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Increase volume.""" await self._projector.send_command(VOL_UP) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Decrease volume.""" await self._projector.send_command(VOL_DOWN) - async def async_media_play(self): + async def async_media_play(self) -> None: """Play media via Epson.""" await self._projector.send_command(PLAY) - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause media via Epson.""" await self._projector.send_command(PAUSE) - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Skip to next.""" await self._projector.send_command(FAST) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Skip to previous.""" await self._projector.send_command(BACK) diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index d19371c6104..3b31082f333 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -94,7 +94,9 @@ def setup_platform( class EpsonPrinterCartridge(SensorEntity): """Representation of a cartridge sensor.""" - def __init__(self, api, description: SensorEntityDescription): + def __init__( + self, api: EpsonPrinterAPI, description: SensorEntityDescription + ) -> None: """Initialize a cartridge sensor.""" self._api = api self.entity_description = description @@ -105,10 +107,10 @@ class EpsonPrinterCartridge(SensorEntity): return self._api.getSensorValue(self.entity_description.key) @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self._api.available - def update(self): + def update(self) -> None: """Get the latest data from the Epson printer.""" self._api.update() diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 412bb8eddeb..de75f04f91e 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol @@ -140,7 +141,7 @@ class EQ3BTSmartThermostat(ClimateEntity): """Return the temperature we try to reach.""" return self._thermostat.target_temperature - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -158,7 +159,7 @@ class EQ3BTSmartThermostat(ClimateEntity): """Return the list of available operation modes.""" return list(HA_TO_EQ_HVAC) - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set operation mode.""" self._thermostat.mode = HA_TO_EQ_HVAC[hvac_mode] @@ -206,13 +207,13 @@ class EQ3BTSmartThermostat(ClimateEntity): """Return the MAC address of the thermostat.""" return format_mac(self._mac) - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode == PRESET_NONE: self.set_hvac_mode(HVACMode.HEAT) self._thermostat.mode = HA_TO_EQ_PRESET[preset_mode] - def update(self): + def update(self) -> None: """Update the data from the thermostat.""" try: diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 68c7307bba2..9f0c6f7eca9 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -83,7 +83,7 @@ class EtherscanSensor(SensorEntity): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" if self._token_address: diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index a7506daa552..a252f43a8ca 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,6 +1,8 @@ """Support for Eufy switches.""" from __future__ import annotations +from typing import Any + import lakeside from homeassistant.components.switch import SwitchEntity @@ -35,7 +37,7 @@ class EufySwitch(SwitchEntity): self._switch = lakeside.switch(self._address, self._code, self._type) self._switch.connect() - def update(self): + def update(self) -> None: """Synchronise state from the switch.""" self._switch.update() self._state = self._switch.power @@ -55,7 +57,7 @@ class EufySwitch(SwitchEntity): """Return true if device is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified switch on.""" try: self._switch.set_state(True) @@ -63,7 +65,7 @@ class EufySwitch(SwitchEntity): self._switch.connect() self._switch.set_state(power=True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified switch off.""" try: self._switch.set_state(False) diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index ab195d81530..13016ba6fe0 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import pyeverlights import voluptuous as vol @@ -155,11 +156,11 @@ class EverLightsLight(LightEntity): self._brightness = brightness self._effect = effect - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._api.clear_pattern(self._channel) - async def async_update(self): + async def async_update(self) -> None: """Synchronize state with control box.""" try: self._status = await self._api.get_status() diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index ff54cfbe4a6..86bbfc7d017 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -110,11 +110,11 @@ class EvoDHW(EvoChild, WaterHeaterEntity): self._evo_device.set_dhw_off(until=until) ) - async def async_turn_away_mode_on(self): + async def async_turn_away_mode_on(self) -> None: """Turn away mode on.""" await self._evo_broker.call_client_api(self._evo_device.set_dhw_off()) - async def async_turn_away_mode_off(self): + async def async_turn_away_mode_off(self) -> None: """Turn away mode off.""" await self._evo_broker.call_client_api(self._evo_device.set_dhw_auto()) From 0795d28ed5d3b981fdabc884ac2d6e1bdd10ae98 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 20 Aug 2022 08:03:43 +0200 Subject: [PATCH 509/903] Remove name option from config_flow for P1 Monitor (#77046) --- homeassistant/components/p1_monitor/config_flow.py | 10 ++++------ homeassistant/components/p1_monitor/strings.json | 6 ++++-- .../components/p1_monitor/translations/en.json | 6 ++++-- tests/components/p1_monitor/test_config_flow.py | 8 ++++---- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/p1_monitor/config_flow.py b/homeassistant/components/p1_monitor/config_flow.py index 9e9d695f5e9..00b035aba7f 100644 --- a/homeassistant/components/p1_monitor/config_flow.py +++ b/homeassistant/components/p1_monitor/config_flow.py @@ -7,9 +7,10 @@ from p1monitor import P1Monitor, P1MonitorError import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import TextSelector from .const import DOMAIN @@ -37,7 +38,7 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" else: return self.async_create_entry( - title=user_input[CONF_NAME], + title="P1 Monitor", data={ CONF_HOST: user_input[CONF_HOST], }, @@ -47,10 +48,7 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { - vol.Optional( - CONF_NAME, default=self.hass.config.location_name - ): str, - vol.Required(CONF_HOST): str, + vol.Required(CONF_HOST): TextSelector(), } ), errors=errors, diff --git a/homeassistant/components/p1_monitor/strings.json b/homeassistant/components/p1_monitor/strings.json index b088bf7adce..0c745554e9d 100644 --- a/homeassistant/components/p1_monitor/strings.json +++ b/homeassistant/components/p1_monitor/strings.json @@ -4,8 +4,10 @@ "user": { "description": "Set up P1 Monitor to integrate with Home Assistant.", "data": { - "host": "[%key:common::config_flow::data::host%]", - "name": "[%key:common::config_flow::data::name%]" + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The IP address or hostname of your P1 Monitor installation." } } }, diff --git a/homeassistant/components/p1_monitor/translations/en.json b/homeassistant/components/p1_monitor/translations/en.json index 4bd61c19bdc..394b6c0767b 100644 --- a/homeassistant/components/p1_monitor/translations/en.json +++ b/homeassistant/components/p1_monitor/translations/en.json @@ -6,8 +6,10 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Name" + "host": "Host" + }, + "data_description": { + "host": "The IP address or hostname of your P1 Monitor installation." }, "description": "Set up P1 Monitor to integrate with Home Assistant." } diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index 13541e782b4..d7d0608e5b3 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -5,7 +5,7 @@ from p1monitor import P1MonitorError from homeassistant.components.p1_monitor.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -27,11 +27,11 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_NAME: "Name", CONF_HOST: "example.com"}, + user_input={CONF_HOST: "example.com"}, ) assert result2.get("type") == FlowResultType.CREATE_ENTRY - assert result2.get("title") == "Name" + assert result2.get("title") == "P1 Monitor" assert result2.get("data") == {CONF_HOST: "example.com"} assert len(mock_setup_entry.mock_calls) == 1 @@ -47,7 +47,7 @@ async def test_api_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={CONF_NAME: "Name", CONF_HOST: "example.com"}, + data={CONF_HOST: "example.com"}, ) assert result.get("type") == FlowResultType.FORM From 5cb79696d0218b66c391281e3e8c0bf91f74f756 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 20 Aug 2022 08:04:17 +0200 Subject: [PATCH 510/903] Use data description for Pure Energie integration (#77047) --- homeassistant/components/pure_energie/config_flow.py | 3 ++- homeassistant/components/pure_energie/strings.json | 3 +++ homeassistant/components/pure_energie/translations/en.json | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/pure_energie/config_flow.py b/homeassistant/components/pure_energie/config_flow.py index 2b1e20d645e..9e6c510c8b6 100644 --- a/homeassistant/components/pure_energie/config_flow.py +++ b/homeassistant/components/pure_energie/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import TextSelector from .const import DOMAIN @@ -50,7 +51,7 @@ class PureEnergieFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST): str, + vol.Required(CONF_HOST): TextSelector(), } ), errors=errors or {}, diff --git a/homeassistant/components/pure_energie/strings.json b/homeassistant/components/pure_energie/strings.json index 4f65d2d8be1..a76b4a001e6 100644 --- a/homeassistant/components/pure_energie/strings.json +++ b/homeassistant/components/pure_energie/strings.json @@ -5,6 +5,9 @@ "user": { "data": { "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The IP address or hostname of your Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/en.json b/homeassistant/components/pure_energie/translations/en.json index 6773cf51478..0c4ebcc6e6e 100644 --- a/homeassistant/components/pure_energie/translations/en.json +++ b/homeassistant/components/pure_energie/translations/en.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "The IP address or hostname of your Pure Energie Meter." } }, "zeroconf_confirm": { From 828bf63ac2abdad569273550e00c62b4af5ecd82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Aug 2022 20:14:47 -1000 Subject: [PATCH 511/903] Bump pySwitchbot to 0.18.12 (#77040) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index e70f467ae74..b4d7c69b315 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.10"], + "requirements": ["PySwitchbot==0.18.12"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 561a7752b45..0d64b560978 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.10 +PySwitchbot==0.18.12 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe7c8f4c37d..85293c0fec4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.10 +PySwitchbot==0.18.12 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 52fbd50d3c41882200f36ade0621e9022d8b405f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Aug 2022 20:15:25 -1000 Subject: [PATCH 512/903] Bump yalexs_ble to 1.6.2 (#77056) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 1c97f687db6..b4e2f78906e 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.6.0"], + "requirements": ["yalexs-ble==1.6.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 0d64b560978..0925c02459c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2509,7 +2509,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.0 +yalexs-ble==1.6.2 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85293c0fec4..15d02dc048f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1710,7 +1710,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.0 +yalexs-ble==1.6.2 # homeassistant.components.august yalexs==1.2.1 From fea0ec4d4dff867ca8e3fe5ba0888f91d6d81239 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:33:27 +0200 Subject: [PATCH 513/903] Improve type hints in vacuum entities (#76561) --- homeassistant/components/ecovacs/vacuum.py | 10 ++++---- homeassistant/components/sharkiq/vacuum.py | 26 +++++++++++++-------- homeassistant/components/template/vacuum.py | 15 ++++++------ 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index c380a760557..ca1a153b5ae 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -94,17 +94,17 @@ class EcovacsVacuum(VacuumEntity): return self.device.vacuum.get("did") @property - def is_on(self): + def is_on(self) -> bool: """Return true if vacuum is currently cleaning.""" return self.device.is_cleaning @property - def is_charging(self): + def is_charging(self) -> bool: """Return true if vacuum is currently charging.""" return self.device.is_charging @property - def status(self): + def status(self) -> str | None: """Return the status of the vacuum cleaner.""" return self.device.vacuum_status @@ -173,9 +173,9 @@ class EcovacsVacuum(VacuumEntity): self.device.run(sucks.VacBotCommand(command, params)) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device-specific state attributes of this vacuum.""" - data = {} + data: dict[str, Any] = {} data[ATTR_ERROR] = self._error for key, val in self.device.components.items(): diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index a34c23012cf..48e82477809 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Iterable +from typing import Any from sharkiq import OperatingModes, PowerModes, Properties, SharkIqVacuum @@ -89,11 +90,16 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum self._attr_unique_id = sharkiq.serial_number self._serial_number = sharkiq.serial_number - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Clean a spot. Not yet implemented.""" raise NotImplementedError() - def send_command(self, command, params=None, **kwargs): + def send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: """Send a command to the vacuum. Not yet implemented.""" raise NotImplementedError() @@ -146,7 +152,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME) @property - def state(self): + def state(self) -> str | None: """ Get the current vacuum state. @@ -169,27 +175,27 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum """Get the current battery level.""" return self.sharkiq.get_property_value(Properties.BATTERY_CAPACITY) - async def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs: Any) -> None: """Have the device return to base.""" await self.sharkiq.async_set_operating_mode(OperatingModes.RETURN) await self.coordinator.async_refresh() - async def async_pause(self): + async def async_pause(self) -> None: """Pause the cleaning task.""" await self.sharkiq.async_set_operating_mode(OperatingModes.PAUSE) await self.coordinator.async_refresh() - async def async_start(self): + async def async_start(self) -> None: """Start the device.""" await self.sharkiq.async_set_operating_mode(OperatingModes.START) await self.coordinator.async_refresh() - async def async_stop(self, **kwargs): + async def async_stop(self, **kwargs: Any) -> None: """Stop the device.""" await self.sharkiq.async_set_operating_mode(OperatingModes.STOP) await self.coordinator.async_refresh() - async def async_locate(self, **kwargs): + async def async_locate(self, **kwargs: Any) -> None: """Cause the device to generate a loud chirp.""" await self.sharkiq.async_find_device() @@ -203,7 +209,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum fan_speed = k return fan_speed - async def async_set_fan_speed(self, fan_speed: str, **kwargs): + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the fan speed.""" await self.sharkiq.async_set_property_value( Properties.POWER_MODE, FAN_SPEEDS_MAP.get(fan_speed.capitalize()) @@ -227,7 +233,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.get_property_value(Properties.LOW_LIGHT_MISSION) @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Return a dictionary of device state attributes specific to sharkiq.""" data = { ATTR_ERROR_CODE: self.error_code, diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 0a74ee5c5fc..f95c2660164 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -204,46 +205,46 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): """Return the status of the vacuum cleaner.""" return self._state - async def async_start(self): + async def async_start(self) -> None: """Start or resume the cleaning task.""" await self.async_run_script(self._start_script, context=self._context) - async def async_pause(self): + async def async_pause(self) -> None: """Pause the cleaning task.""" if self._pause_script is None: return await self.async_run_script(self._pause_script, context=self._context) - async def async_stop(self, **kwargs): + async def async_stop(self, **kwargs: Any) -> None: """Stop the cleaning task.""" if self._stop_script is None: return await self.async_run_script(self._stop_script, context=self._context) - async def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" if self._return_to_base_script is None: return await self.async_run_script(self._return_to_base_script, context=self._context) - async def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" if self._clean_spot_script is None: return await self.async_run_script(self._clean_spot_script, context=self._context) - async def async_locate(self, **kwargs): + async def async_locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" if self._locate_script is None: return await self.async_run_script(self._locate_script, context=self._context) - async def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" if self._set_fan_speed_script is None: return From 09ab07921a7ee0f4c58e41a1b6f7dec2bf42c5be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:34:47 +0200 Subject: [PATCH 514/903] Improve type hint in compensation sensor entity (#77027) --- .../components/compensation/sensor.py | 86 +++++++------------ 1 file changed, 32 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 58666e0f3be..16226974120 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -2,6 +2,9 @@ from __future__ import annotations import logging +from typing import Any + +import numpy as np from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( @@ -12,7 +15,7 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -42,11 +45,11 @@ async def async_setup_platform( if discovery_info is None: return - compensation = discovery_info[CONF_COMPENSATION] - conf = hass.data[DATA_COMPENSATION][compensation] + compensation: str = discovery_info[CONF_COMPENSATION] + conf: dict[str, Any] = hass.data[DATA_COMPENSATION][compensation] - source = conf[CONF_SOURCE] - attribute = conf.get(CONF_ATTRIBUTE) + source: str = conf[CONF_SOURCE] + attribute: str | None = conf.get(CONF_ATTRIBUTE) name = f"{DEFAULT_NAME} {source}" if attribute is not None: name = f"{name} {attribute}" @@ -69,26 +72,27 @@ async def async_setup_platform( class CompensationSensor(SensorEntity): """Representation of a Compensation sensor.""" + _attr_should_poll = False + def __init__( self, - unique_id, - name, - source, - attribute, - precision, - polynomial, - unit_of_measurement, - ): + unique_id: str | None, + name: str, + source: str, + attribute: str | None, + precision: int, + polynomial: np.poly1d, + unit_of_measurement: str | None, + ) -> None: """Initialize the Compensation sensor.""" self._source_entity_id = source self._precision = precision self._source_attribute = attribute - self._unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._poly = polynomial self._coefficients = polynomial.coefficients.tolist() - self._state = None - self._unique_id = unique_id - self._name = name + self._attr_unique_id = unique_id + self._attr_name = name async def async_added_to_hass(self) -> None: """Handle added to Hass.""" @@ -101,27 +105,7 @@ class CompensationSensor(SensorEntity): ) @property - def unique_id(self): - """Return the unique id of this sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" ret = { ATTR_SOURCE: self._source_entity_id, @@ -131,33 +115,27 @@ class CompensationSensor(SensorEntity): ret[ATTR_SOURCE_ATTRIBUTE] = self._source_attribute return ret - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - @callback - def _async_compensation_sensor_state_listener(self, event): + def _async_compensation_sensor_state_listener(self, event: Event) -> None: """Handle sensor state changes.""" + new_state: State | None if (new_state := event.data.get("new_state")) is None: return - if self._unit_of_measurement is None and self._source_attribute is None: - self._unit_of_measurement = new_state.attributes.get( + if self.native_unit_of_measurement is None and self._source_attribute is None: + self._attr_native_unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT ) + if self._source_attribute: + value = new_state.attributes.get(self._source_attribute) + else: + value = None if new_state.state == STATE_UNKNOWN else new_state.state try: - if self._source_attribute: - value = float(new_state.attributes.get(self._source_attribute)) - else: - value = ( - None if new_state.state == STATE_UNKNOWN else float(new_state.state) - ) - self._state = round(self._poly(value), self._precision) + self._attr_native_value = round(self._poly(float(value)), self._precision) except (ValueError, TypeError): - self._state = None + self._attr_native_value = None if self._source_attribute: _LOGGER.warning( "%s attribute %s is not numerical", From b88e71762de349faea2469823d4741bb6937b825 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:37:47 +0200 Subject: [PATCH 515/903] Improve type hint in cups sensor entity (#77030) --- homeassistant/components/cups/sensor.py | 97 +++++++++---------------- 1 file changed, 35 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 893e92f546e..c642eb9112e 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import importlib import logging +from typing import Any import voluptuous as vol @@ -62,10 +63,10 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the CUPS sensor.""" - host = config[CONF_HOST] - port = config[CONF_PORT] - printers = config[CONF_PRINTERS] - is_cups = config[CONF_IS_CUPS_SERVER] + host: str = config[CONF_HOST] + port: int = config[CONF_PORT] + printers: list[str] = config[CONF_PRINTERS] + is_cups: bool = config[CONF_IS_CUPS_SERVER] if is_cups: data = CupsData(host, port, None) @@ -73,6 +74,7 @@ def setup_platform( if data.available is False: _LOGGER.error("Unable to connect to CUPS server: %s:%s", host, port) raise PlatformNotReady() + assert data.printers is not None dev: list[SensorEntity] = [] for printer in printers: @@ -108,17 +110,14 @@ def setup_platform( class CupsSensor(SensorEntity): """Representation of a CUPS sensor.""" - def __init__(self, data, printer): + _attr_icon = ICON_PRINTER + + def __init__(self, data: CupsData, printer_name: str) -> None: """Initialize the CUPS sensor.""" self.data = data - self._name = printer - self._printer = None - self._available = False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name + self._attr_name = printer_name + self._printer: dict[str, Any] | None = None + self._attr_available = False @property def native_value(self): @@ -129,16 +128,6 @@ class CupsSensor(SensorEntity): key = self._printer["printer-state"] return PRINTER_STATES.get(key, key) - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON_PRINTER - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -160,8 +149,10 @@ class CupsSensor(SensorEntity): def update(self) -> None: """Get the latest data and updates the states.""" self.data.update() - self._printer = self.data.printers.get(self._name) - self._available = self.data.available + assert self.name is not None + assert self.data.printers is not None + self._printer = self.data.printers.get(self.name) + self._attr_available = self.data.available class IPPSensor(SensorEntity): @@ -170,28 +161,20 @@ class IPPSensor(SensorEntity): This sensor represents the status of the printer. """ - def __init__(self, data, name): + _attr_icon = ICON_PRINTER + + def __init__(self, data: CupsData, printer_name: str) -> None: """Initialize the sensor.""" self.data = data - self._name = name + self._printer_name = printer_name self._attributes = None - self._available = False + self._attr_available = False @property def name(self): """Return the name of the sensor.""" return self._attributes["printer-make-and-model"] - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON_PRINTER - - @property - def available(self): - """Return True if entity is available.""" - return self._available - @property def native_value(self): """Return the state of the sensor.""" @@ -237,8 +220,8 @@ class IPPSensor(SensorEntity): def update(self) -> None: """Fetch new state data for the sensor.""" self.data.update() - self._attributes = self.data.attributes.get(self._name) - self._available = self.data.available + self._attributes = self.data.attributes.get(self._printer_name) + self._attr_available = self.data.available class MarkerSensor(SensorEntity): @@ -247,24 +230,17 @@ class MarkerSensor(SensorEntity): This sensor represents the percentage of ink or toner. """ - def __init__(self, data, printer, name, is_cups): + _attr_icon = ICON_MARKER + _attr_native_unit_of_measurement = PERCENTAGE + + def __init__(self, data: CupsData, printer: str, name: str, is_cups: bool) -> None: """Initialize the sensor.""" self.data = data - self._name = name + self._attr_name = name self._printer = printer self._index = data.attributes[printer]["marker-names"].index(name) self._is_cups = is_cups - self._attributes = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON_MARKER + self._attributes: dict[str, Any] | None = None @property def native_value(self): @@ -274,11 +250,6 @@ class MarkerSensor(SensorEntity): return self._attributes[self._printer]["marker-levels"][self._index] - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return PERCENTAGE - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -318,17 +289,17 @@ class MarkerSensor(SensorEntity): class CupsData: """Get the latest data from CUPS and update the state.""" - def __init__(self, host, port, ipp_printers): + def __init__(self, host: str, port: int, ipp_printers: list[str] | None) -> None: """Initialize the data object.""" self._host = host self._port = port self._ipp_printers = ipp_printers self.is_cups = ipp_printers is None - self.printers = None - self.attributes = {} + self.printers: dict[str, dict[str, Any]] | None = None + self.attributes: dict[str, Any] = {} self.available = False - def update(self): + def update(self) -> None: """Get the latest data from CUPS.""" cups = importlib.import_module("cups") @@ -336,9 +307,11 @@ class CupsData: conn = cups.Connection(host=self._host, port=self._port) if self.is_cups: self.printers = conn.getPrinters() + assert self.printers is not None for printer in self.printers: self.attributes[printer] = conn.getPrinterAttributes(name=printer) else: + assert self._ipp_printers is not None for ipp_printer in self._ipp_printers: self.attributes[ipp_printer] = conn.getPrinterAttributes( uri=f"ipp://{self._host}:{self._port}/{ipp_printer}" From f329428c7f373e158f6ebb1295df4846ad1a5c02 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:38:59 +0200 Subject: [PATCH 516/903] Remove unused variable from directv media player (#77034) --- homeassistant/components/directv/media_player.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index affefcacd85..7d1434e9909 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -99,14 +99,13 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): self._last_update = None self._paused = None self._program = None - self._state = None - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" - self._state = await self.dtv.state(self._address) - self._attr_available = self._state.available - self._is_standby = self._state.standby - self._program = self._state.program + state = await self.dtv.state(self._address) + self._attr_available = state.available + self._is_standby = state.standby + self._program = state.program if self._is_standby: self._attr_assumed_state = False @@ -118,7 +117,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): self._paused = self._last_position == self._program.position self._is_recorded = self._program.recorded self._last_position = self._program.position - self._last_update = self._state.at + self._last_update = state.at self._attr_assumed_state = self._is_recorded @property From 1edb68f8baf7b49b11d04c2b3f63aebf2d8a645a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:39:53 +0200 Subject: [PATCH 517/903] Improve type hint in darksky sensor entity (#77035) --- homeassistant/components/darksky/sensor.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 84a0b0f23f2..db79b82de53 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -663,8 +663,7 @@ class DarkSkySensor(SensorEntity): self.forecast_data = forecast_data self.forecast_day = forecast_day self.forecast_hour = forecast_hour - self._icon = None - self._unit_of_measurement = None + self._icon: str | None = None if forecast_day is not None: self._attr_name = f"{name} {description.name} {forecast_day}d" @@ -673,18 +672,13 @@ class DarkSkySensor(SensorEntity): else: self._attr_name = f"{name} {description.name}" - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - @property def unit_system(self): """Return the unit system of this entity.""" return self.forecast_data.unit_system @property - def entity_picture(self): + def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" if self._icon is None or "summary" not in self.entity_description.key: return None @@ -694,13 +688,15 @@ class DarkSkySensor(SensorEntity): return None - def update_unit_of_measurement(self): + def update_unit_of_measurement(self) -> None: """Update units based on unit system.""" unit_key = MAP_UNIT_SYSTEM.get(self.unit_system, "si_unit") - self._unit_of_measurement = getattr(self.entity_description, unit_key) + self._attr_native_unit_of_measurement = getattr( + self.entity_description, unit_key + ) @property - def icon(self): + def icon(self) -> str | None: """Icon to use in the frontend, if any.""" if ( "summary" in self.entity_description.key @@ -710,7 +706,7 @@ class DarkSkySensor(SensorEntity): return self.entity_description.icon - def update(self): + def update(self) -> None: """Get the latest data from Dark Sky and updates the states.""" # Call the API for new forecast data. Each sensor will re-trigger this # same exact call, but that's fine. We cache results for a short period From c9fe1f44b89a38566fdcc22a60db03fc3aba9a17 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:40:51 +0200 Subject: [PATCH 518/903] Improve type hint in denon media player entity (#77036) --- homeassistant/components/denon/media_player.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 4533af66927..2dd7a29e17e 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -84,7 +84,7 @@ def setup_platform( """Set up the Denon platform.""" denon = DenonDevice(config[CONF_NAME], config[CONF_HOST]) - if denon.update(): + if denon.do_update(): add_entities([denon]) @@ -161,8 +161,12 @@ class DenonDevice(MediaPlayerEntity): telnet.read_very_eager() # skip response telnet.close() - def update(self): + def update(self) -> None: """Get the latest details from the device.""" + self.do_update() + + def do_update(self) -> bool: + """Get the latest details from the device, as boolean.""" try: telnet = telnetlib.Telnet(self._host) except OSError: From 2c2e0cd4a0720485044f2feda0d95b8fd0da512d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:41:41 +0200 Subject: [PATCH 519/903] Improve type hint in daikin climate entity (#77037) --- homeassistant/components/daikin/climate.py | 28 +++++++--------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 271449a01cd..d7bfb8cf7a0 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -27,7 +27,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as DAIKIN_DOMAIN +from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi from .const import ( ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, @@ -115,14 +115,17 @@ def format_target_temperature(target_temperature): class DaikinClimate(ClimateEntity): """Representation of a Daikin HVAC.""" - def __init__(self, api): + def __init__(self, api: DaikinApi) -> None: """Initialize the climate device.""" self._api = api + self._attr_hvac_modes = list(HA_STATE_TO_DAIKIN) + self._attr_fan_modes = self._api.device.fan_rate + self._attr_swing_modes = self._api.device.swing_modes self._list = { - ATTR_HVAC_MODE: list(HA_STATE_TO_DAIKIN), - ATTR_FAN_MODE: self._api.device.fan_rate, - ATTR_SWING_MODE: self._api.device.swing_modes, + ATTR_HVAC_MODE: self._attr_hvac_modes, + ATTR_FAN_MODE: self._attr_fan_modes, + ATTR_SWING_MODE: self._attr_swing_modes, } self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE @@ -219,11 +222,6 @@ class DaikinClimate(ClimateEntity): daikin_mode = self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE])[1] return DAIKIN_TO_HA_STATE.get(daikin_mode, HVACMode.HEAT_COOL) - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available operation modes.""" - return self._list.get(ATTR_HVAC_MODE) - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set HVAC mode.""" await self._set({ATTR_HVAC_MODE: hvac_mode}) @@ -237,11 +235,6 @@ class DaikinClimate(ClimateEntity): """Set fan mode.""" await self._set({ATTR_FAN_MODE: fan_mode}) - @property - def fan_modes(self): - """List of available fan modes.""" - return self._list.get(ATTR_FAN_MODE) - @property def swing_mode(self): """Return the fan setting.""" @@ -251,11 +244,6 @@ class DaikinClimate(ClimateEntity): """Set new target temperature.""" await self._set({ATTR_SWING_MODE: swing_mode}) - @property - def swing_modes(self): - """List of available swing modes.""" - return self._list.get(ATTR_SWING_MODE) - @property def preset_mode(self): """Return the preset_mode.""" From 9ac01b8c9be494cfcabc7b2a2a87a86cd288824b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 11:27:01 +0200 Subject: [PATCH 520/903] Improve type hint in derivative sensor entity (#77038) --- homeassistant/components/derivative/sensor.py | 59 ++++++------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 5337328bbe2..8e1934dcecf 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -1,7 +1,7 @@ """Numeric derivative of data coming from a source sensor over time.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta from decimal import Decimal, DecimalException import logging @@ -20,7 +20,7 @@ from homeassistant.const import ( TIME_MINUTES, TIME_SECONDS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -133,6 +133,9 @@ async def async_setup_platform( class DerivativeSensor(RestoreEntity, SensorEntity): """Representation of an derivative sensor.""" + _attr_icon = ICON + _attr_should_poll = False + def __init__( self, *, @@ -150,19 +153,19 @@ class DerivativeSensor(RestoreEntity, SensorEntity): self._sensor_source_id = source_entity self._round_digits = round_digits self._state = 0 - self._state_list = ( - [] - ) # List of tuples with (timestamp_start, timestamp_end, derivative) + # List of tuples with (timestamp_start, timestamp_end, derivative) + self._state_list: list[tuple[datetime, datetime, Decimal]] = [] - self._name = name if name is not None else f"{source_entity} derivative" + self._attr_name = name if name is not None else f"{source_entity} derivative" + self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity} if unit_of_measurement is None: final_unit_prefix = "" if unit_prefix is None else unit_prefix self._unit_template = f"{final_unit_prefix}{{}}/{unit_time}" # we postpone the definition of unit_of_measurement to later - self._unit_of_measurement = None + self._attr_native_unit_of_measurement = None else: - self._unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] @@ -178,20 +181,21 @@ class DerivativeSensor(RestoreEntity, SensorEntity): _LOGGER.warning("Could not restore last state: %s", err) @callback - def calc_derivative(event): + def calc_derivative(event: Event) -> None: """Handle the sensor state changes.""" - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + old_state: State | None + new_state: State | None if ( - old_state is None + (old_state := event.data.get("old_state")) is None or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) + or (new_state := event.data.get("new_state")) is None or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): return - if self._unit_of_measurement is None: + if self.native_unit_of_measurement is None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - self._unit_of_measurement = self._unit_template.format( + self._attr_native_unit_of_measurement = self._unit_template.format( "" if unit is None else unit ) @@ -242,7 +246,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity): if elapsed_time > self._time_window: derivative = new_derivative else: - derivative = 0 + derivative = Decimal(0) for (start, end, value) in self._state_list: weight = calculate_weight(start, end, new_state.last_updated) derivative = derivative + (value * Decimal(weight)) @@ -256,32 +260,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity): ) ) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def native_value(self): """Return the state of the sensor.""" return round(self._state, self._round_digits) - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return {ATTR_SOURCE_ID: self._sensor_source_id} - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON From 49957c752b9eebb596833ff30f9b3fac4f527c21 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 20 Aug 2022 19:06:35 +0200 Subject: [PATCH 521/903] Add coordinator and number platform to LaMetric (#76766) --- .coveragerc | 3 + homeassistant/components/lametric/__init__.py | 41 ++++----- homeassistant/components/lametric/const.py | 5 ++ .../components/lametric/coordinator.py | 38 ++++++++ homeassistant/components/lametric/entity.py | 29 ++++++ .../components/lametric/manifest.json | 2 +- homeassistant/components/lametric/number.py | 90 +++++++++++++++++++ 7 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/lametric/coordinator.py create mode 100644 homeassistant/components/lametric/entity.py create mode 100644 homeassistant/components/lametric/number.py diff --git a/.coveragerc b/.coveragerc index 49cfbb3acc7..14773947be1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -640,7 +640,10 @@ omit = homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lametric/__init__.py + homeassistant/components/lametric/coordinator.py + homeassistant/components/lametric/entity.py homeassistant/components/lametric/notify.py + homeassistant/components/lametric/number.py homeassistant/components/lannouncer/notify.py homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index 2f89d88d79d..04f02a7f8f9 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,25 +1,17 @@ """Support for LaMetric time.""" -from demetriek import LaMetricConnectionError, LaMetricDevice import voluptuous as vol +from homeassistant.components import notify as hass_notify from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_HOST, - CONF_NAME, - Platform, -) +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DOMAIN, PLATFORMS +from .coordinator import LaMetricDataUpdateCoordinator CONFIG_SCHEMA = vol.Schema( vol.All( @@ -56,18 +48,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LaMetric from a config entry.""" - lametric = LaMetricDevice( - host=entry.data[CONF_HOST], - api_key=entry.data[CONF_API_KEY], - session=async_get_clientsession(hass), - ) + coordinator = LaMetricDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() - try: - device = await lametric.device() - except LaMetricConnectionError as ex: - raise ConfigEntryNotReady("Cannot connect to LaMetric device") from ex - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = lametric + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. @@ -76,8 +61,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, Platform.NOTIFY, DOMAIN, - {CONF_NAME: device.name, "entry_id": entry.entry_id}, + {CONF_NAME: coordinator.data.name, "entry_id": entry.entry_id}, hass.data[DOMAIN]["hass_config"], ) ) return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload LaMetric config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + await hass_notify.async_reload(hass, DOMAIN) + return unload_ok diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py index d357f678d9d..0fe4b3f21d8 100644 --- a/homeassistant/components/lametric/const.py +++ b/homeassistant/components/lametric/const.py @@ -1,11 +1,16 @@ """Constants for the LaMetric integration.""" +from datetime import timedelta import logging from typing import Final +from homeassistant.const import Platform + DOMAIN: Final = "lametric" +PLATFORMS = [Platform.NUMBER] LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(seconds=30) CONF_CYCLES: Final = "cycles" CONF_ICON_TYPE: Final = "icon_type" diff --git a/homeassistant/components/lametric/coordinator.py b/homeassistant/components/lametric/coordinator.py new file mode 100644 index 00000000000..0a5e99e5668 --- /dev/null +++ b/homeassistant/components/lametric/coordinator.py @@ -0,0 +1,38 @@ +"""DataUpdateCoordinator for the LaMatric integration.""" +from __future__ import annotations + +from demetriek import Device, LaMetricDevice, LaMetricError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + + +class LaMetricDataUpdateCoordinator(DataUpdateCoordinator[Device]): + """The LaMetric Data Update Coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the LaMatric coordinator.""" + self.config_entry = entry + self.lametric = LaMetricDevice( + host=entry.data[CONF_HOST], + api_key=entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), + ) + + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + + async def _async_update_data(self) -> Device: + """Fetch device information of the LaMetric device.""" + try: + return await self.lametric.device() + except LaMetricError as ex: + raise UpdateFailed( + "Could not fetch device information from LaMetric device" + ) from ex diff --git a/homeassistant/components/lametric/entity.py b/homeassistant/components/lametric/entity.py new file mode 100644 index 00000000000..1e31b2968af --- /dev/null +++ b/homeassistant/components/lametric/entity.py @@ -0,0 +1,29 @@ +"""Base entity for the LaMetric integration.""" +from __future__ import annotations + +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator + + +class LaMetricEntity(CoordinatorEntity[LaMetricDataUpdateCoordinator], Entity): + """Defines a LaMetric entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: LaMetricDataUpdateCoordinator) -> None: + """Initialize the LaMetric entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + connections={ + (CONNECTION_NETWORK_MAC, format_mac(coordinator.data.wifi.mac)) + }, + identifiers={(DOMAIN, coordinator.data.serial_number)}, + manufacturer="LaMetric Inc.", + model=coordinator.data.model, + name=coordinator.data.name, + sw_version=coordinator.data.os_version, + ) diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 1a40f962156..578906483fa 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/lametric", "requirements": ["demetriek==0.2.2"], "codeowners": ["@robbiet480", "@frenck"], - "iot_class": "local_push", + "iot_class": "local_polling", "dependencies": ["application_credentials", "repairs"], "loggers": ["demetriek"], "config_flow": true, diff --git a/homeassistant/components/lametric/number.py b/homeassistant/components/lametric/number.py new file mode 100644 index 00000000000..c788eb3255e --- /dev/null +++ b/homeassistant/components/lametric/number.py @@ -0,0 +1,90 @@ +"""Support for LaMetric numbers.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from demetriek import Device, LaMetricDevice + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator +from .entity import LaMetricEntity + + +@dataclass +class LaMetricEntityDescriptionMixin: + """Mixin values for LaMetric entities.""" + + value_fn: Callable[[Device], int | None] + set_value_fn: Callable[[LaMetricDevice, float], Awaitable[Any]] + + +@dataclass +class LaMetricNumberEntityDescription( + NumberEntityDescription, LaMetricEntityDescriptionMixin +): + """Class describing LaMetric number entities.""" + + +NUMBERS = [ + LaMetricNumberEntityDescription( + key="volume", + name="Volume", + icon="mdi:volume-high", + entity_category=EntityCategory.CONFIG, + native_step=1, + native_min_value=0, + native_max_value=100, + value_fn=lambda device: device.audio.volume, + set_value_fn=lambda api, volume: api.audio(volume=int(volume)), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LaMetric number based on a config entry.""" + coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LaMetricNumberEntity( + coordinator=coordinator, + description=description, + ) + for description in NUMBERS + ) + + +class LaMetricNumberEntity(LaMetricEntity, NumberEntity): + """Representation of a LaMetric number.""" + + entity_description: LaMetricNumberEntityDescription + + def __init__( + self, + coordinator: LaMetricDataUpdateCoordinator, + description: LaMetricNumberEntityDescription, + ) -> None: + """Initiate LaMetric Number.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}" + + @property + def native_value(self) -> int | None: + """Return the number value.""" + return self.entity_description.value_fn(self.coordinator.data) + + async def async_set_native_value(self, value: float) -> None: + """Change to new number value.""" + await self.entity_description.set_value_fn(self.coordinator.lametric, value) + await self.coordinator.async_request_refresh() From 18246bb8c88577bbeb99d83930079d2a5c908cb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 07:22:42 -1000 Subject: [PATCH 522/903] Improve bluetooth logging when there are multiple adapters (#77007) --- homeassistant/components/bluetooth/__init__.py | 10 +++++++--- homeassistant/components/bluetooth/scanner.py | 9 +++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index e659cec60a0..83c1247e3e2 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -269,13 +269,17 @@ async def async_setup_entry( assert address is not None adapter = await manager.async_get_adapter_from_address(address) if adapter is None: - raise ConfigEntryNotReady(f"Bluetooth adapter with address {address} not found") + raise ConfigEntryNotReady( + f"Bluetooth adapter {adapter} with address {address} not found" + ) try: bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter) except RuntimeError as err: - raise ConfigEntryNotReady from err - scanner = HaScanner(hass, bleak_scanner, adapter) + raise ConfigEntryNotReady( + f"{adapter_human_name(adapter, address)}: {err}" + ) from err + scanner = HaScanner(hass, bleak_scanner, adapter, address) entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) await scanner.async_start() entry.async_on_unload(manager.async_register_scanner(scanner)) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index c3d45dbde95..6faada73e02 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -33,6 +33,7 @@ from .const import ( START_TIMEOUT, ) from .models import BluetoothScanningMode +from .util import adapter_human_name OriginalBleakScanner = bleak.BleakScanner MONOTONIC_TIME = time.monotonic @@ -76,7 +77,11 @@ class HaScanner: """ def __init__( - self, hass: HomeAssistant, scanner: bleak.BleakScanner, adapter: str | None + self, + hass: HomeAssistant, + scanner: bleak.BleakScanner, + adapter: str, + address: str, ) -> None: """Init bluetooth discovery.""" self.hass = hass @@ -89,7 +94,7 @@ class HaScanner: self._callbacks: list[ Callable[[BLEDevice, AdvertisementData, float, str], None] ] = [] - self.name = self.adapter or "default" + self.name = adapter_human_name(adapter, address) self.source = self.adapter or SOURCE_LOCAL @property From 453307e01ad8c865ce30767463f5b5ab7acef263 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 20 Aug 2022 19:30:38 +0200 Subject: [PATCH 523/903] Add attribute support to state selector (#77071) --- homeassistant/helpers/selector.py | 10 ++++++++-- tests/helpers/test_selector.py | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index deb5cc7401c..9655f93ace5 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -741,10 +741,11 @@ class TargetSelectorConfig(TypedDict, total=False): device: SingleDeviceSelectorConfig -class StateSelectorConfig(TypedDict): +class StateSelectorConfig(TypedDict, total=False): """Class to represent an state selector config.""" entity_id: str + attribute: str @SELECTORS.register("state") @@ -753,7 +754,12 @@ class StateSelector(Selector): selector_type = "state" - CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) + CONFIG_SCHEMA = vol.Schema( + { + vol.Required("entity_id"): cv.entity_id, + vol.Optional("attribute"): str, + } + ) def __init__(self, config: StateSelectorConfig) -> None: """Instantiate a selector.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 70e17058923..2d870a85f6f 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -302,6 +302,11 @@ def test_time_selector_schema(schema, valid_selections, invalid_selections): ("on", "armed"), (None, True, 1), ), + ( + {"entity_id": "sensor.abc", "attribute": "device_class"}, + ("temperature", "humidity"), + (None,), + ), ), ) def test_state_selector_schema(schema, valid_selections, invalid_selections): From 87be71ce6a111d5dc0ea6bf56ed79ea99dcbb4ef Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 20 Aug 2022 20:18:27 +0200 Subject: [PATCH 524/903] Update pyotgw to 2.0.3 (#77073) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 02b1604ea11..97767ab8383 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.2"], + "requirements": ["pyotgw==2.0.3"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 0925c02459c..b9820f94872 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1734,7 +1734,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.2 +pyotgw==2.0.3 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15d02dc048f..5cb5b40a2c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1211,7 +1211,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.2 +pyotgw==2.0.3 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 8b1713a691bd0c90824261be785f1998ad89f66f Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Sat, 20 Aug 2022 16:56:19 -0400 Subject: [PATCH 525/903] Add support for non-serialized devices (light, switch, cover, fan in RA3 Zones) (#75323) Co-authored-by: J. Nick Koston --- CODEOWNERS | 4 +- .../components/lutron_caseta/__init__.py | 7 + .../components/lutron_caseta/manifest.json | 2 +- tests/components/lutron_caseta/__init__.py | 205 +++++++++++++++++- .../lutron_caseta/test_config_flow.py | 12 +- tests/components/lutron_caseta/test_cover.py | 19 ++ .../lutron_caseta/test_diagnostics.py | 62 +++++- tests/components/lutron_caseta/test_fan.py | 19 ++ tests/components/lutron_caseta/test_light.py | 27 +++ tests/components/lutron_caseta/test_switch.py | 18 ++ 10 files changed, 355 insertions(+), 20 deletions(-) create mode 100644 tests/components/lutron_caseta/test_cover.py create mode 100644 tests/components/lutron_caseta/test_fan.py create mode 100644 tests/components/lutron_caseta/test_light.py create mode 100644 tests/components/lutron_caseta/test_switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 9a0c092eceb..2513e290230 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -630,8 +630,8 @@ build.json @home-assistant/supervisor /tests/components/luftdaten/ @fabaff @frenck /homeassistant/components/lupusec/ @majuss /homeassistant/components/lutron/ @JonGilmore -/homeassistant/components/lutron_caseta/ @swails @bdraco -/tests/components/lutron_caseta/ @swails @bdraco +/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues +/tests/components/lutron_caseta/ @swails @bdraco @danaues /homeassistant/components/lyric/ @timmo001 /tests/components/lyric/ @timmo001 /homeassistant/components/mastodon/ @fabaff diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 5653504c98a..bcbaedeb8d1 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -387,6 +387,13 @@ class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): self._device = self._smartbridge.get_device_by_id(self.device_id) _LOGGER.debug(self._device) + @property + def unique_id(self): + """Return a unique identifier if serial number is None.""" + if self.serial is None: + return f"{self._bridge_unique_id}_{self.device_id}" + return super().unique_id + def _id_to_identifier(lutron_id: str) -> tuple[str, str]: """Convert a lutron caseta identifier to a device identifier.""" diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 206d8b51233..c80d0deb794 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -8,7 +8,7 @@ "homekit": { "models": ["Smart Bridge"] }, - "codeowners": ["@swails", "@bdraco"], + "codeowners": ["@swails", "@bdraco", "@danaues"], "iot_class": "local_push", "loggers": ["pylutron_caseta"] } diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index ace4066ae3b..91ddfe26fb5 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -1,6 +1,103 @@ """Tests for the Lutron Caseta integration.""" +from unittest.mock import patch + +from homeassistant.components.lutron_caseta import DOMAIN +from homeassistant.components.lutron_caseta.const import ( + CONF_CA_CERTS, + CONF_CERTFILE, + CONF_KEYFILE, +) +from homeassistant.const import CONF_HOST + +from tests.common import MockConfigEntry + +ENTRY_MOCK_DATA = { + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", +} + +_LEAP_DEVICE_TYPES = { + "light": [ + "WallDimmer", + "PlugInDimmer", + "InLineDimmer", + "SunnataDimmer", + "TempInWallPaddleDimmer", + "WallDimmerWithPreset", + "Dimmed", + ], + "switch": [ + "WallSwitch", + "OutdoorPlugInSwitch", + "PlugInSwitch", + "InLineSwitch", + "PowPakSwitch", + "SunnataSwitch", + "TempInWallPaddleSwitch", + "Switched", + ], + "fan": [ + "CasetaFanSpeedController", + "MaestroFanSpeedController", + "FanSpeed", + ], + "cover": [ + "SerenaHoneycombShade", + "SerenaRollerShade", + "TriathlonHoneycombShade", + "TriathlonRollerShade", + "QsWirelessShade", + "QsWirelessHorizontalSheerBlind", + "QsWirelessWoodBlind", + "RightDrawDrape", + "Shade", + "SerenaTiltOnlyWoodBlind", + ], + "sensor": [ + "Pico1Button", + "Pico2Button", + "Pico2ButtonRaiseLower", + "Pico3Button", + "Pico3ButtonRaiseLower", + "Pico4Button", + "Pico4ButtonScene", + "Pico4ButtonZone", + "Pico4Button2Group", + "FourGroupRemote", + "SeeTouchTabletopKeypad", + "SunnataKeypad", + "SunnataKeypad_2Button", + "SunnataKeypad_3ButtonRaiseLower", + "SunnataKeypad_4Button", + "SeeTouchHybridKeypad", + "SeeTouchInternational", + "SeeTouchKeypad", + "HomeownerKeypad", + "GrafikTHybridKeypad", + "AlisseKeypad", + "PalladiomKeypad", + ], +} + + +async def async_setup_integration(hass, mock_bridge) -> MockConfigEntry: + """Set up a mock bridge.""" + mock_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA) + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.lutron_caseta.Smartbridge.create_tls" + ) as create_tls: + create_tls.return_value = mock_bridge(can_connect=True) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + return mock_entry + + class MockBridge: """Mock Lutron bridge that emulates configured connected status.""" @@ -12,26 +109,120 @@ class MockBridge: self.areas = {} self.occupancy_groups = {} self.scenes = self.get_scenes() - self.devices = self.get_devices() + self.devices = self.load_devices() async def connect(self): """Connect the mock bridge.""" if self.can_connect: self.is_currently_connected = True + def add_subscriber(self, device_id: str, callback_): + """Mock a listener to be notified of state changes.""" + def is_connected(self): """Return whether the mock bridge is connected.""" return self.is_currently_connected - def get_devices(self): - """Return devices on the bridge.""" + def load_devices(self): + """Load mock devices into self.devices.""" return { - "1": {"serial": 1234, "name": "bridge", "model": "model", "type": "type"} + "1": {"serial": 1234, "name": "bridge", "model": "model", "type": "type"}, + "801": { + "device_id": "801", + "current_state": 100, + "fan_speed": None, + "zone": "801", + "name": "Basement Bedroom_Main Lights", + "button_groups": None, + "type": "Dimmed", + "model": None, + "serial": None, + "tilt": None, + }, + "802": { + "device_id": "802", + "current_state": 100, + "fan_speed": None, + "zone": "802", + "name": "Basement Bedroom_Left Shade", + "button_groups": None, + "type": "SerenaRollerShade", + "model": None, + "serial": None, + "tilt": None, + }, + "803": { + "device_id": "803", + "current_state": 100, + "fan_speed": None, + "zone": "803", + "name": "Basement Bathroom_Exhaust Fan", + "button_groups": None, + "type": "Switched", + "model": None, + "serial": None, + "tilt": None, + }, + "804": { + "device_id": "804", + "current_state": 100, + "fan_speed": None, + "zone": "804", + "name": "Master Bedroom_Ceiling Fan", + "button_groups": None, + "type": "FanSpeed", + "model": None, + "serial": None, + "tilt": None, + }, + "901": { + "device_id": "901", + "current_state": 100, + "fan_speed": None, + "zone": "901", + "name": "Kitchen_Main Lights", + "button_groups": None, + "type": "WallDimmer", + "model": None, + "serial": 5442321, + "tilt": None, + }, } - def get_devices_by_domain(self, domain): - """Return devices on the bridge.""" - return {} + def get_devices(self) -> dict[str, dict]: + """Will return all known devices connected to the Smart Bridge.""" + return self.devices + + def get_devices_by_domain(self, domain: str) -> list[dict]: + """ + Return a list of devices for the given domain. + + :param domain: one of 'light', 'switch', 'cover', 'fan' or 'sensor' + :returns list of zero or more of the devices + """ + types = _LEAP_DEVICE_TYPES.get(domain, None) + + # return immediately if not a supported domain + if types is None: + return [] + + return self.get_devices_by_types(types) + + def get_devices_by_type(self, type_: str) -> list[dict]: + """ + Will return all devices of a given device type. + + :param type_: LEAP device type, e.g. WallSwitch + """ + return [device for device in self.devices.values() if device["type"] == type_] + + def get_devices_by_types(self, types: list[str]) -> list[dict]: + """ + Will return all devices for a list of given device types. + + :param types: list of LEAP device types such as WallSwitch, WallDimmer + """ + return [device for device in self.devices.values() if device["type"] in types] def get_scenes(self): """Return scenes on the bridge.""" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index b5e8271d351..d1997051e26 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -20,7 +20,7 @@ from homeassistant.components.lutron_caseta.const import ( ) from homeassistant.const import CONF_HOST -from . import MockBridge +from . import ENTRY_MOCK_DATA, MockBridge from tests.common import MockConfigEntry @@ -151,13 +151,7 @@ async def test_bridge_invalid_ssl_error(hass): async def test_duplicate_bridge_import(hass): """Test that creating a bridge entry with a duplicate host errors.""" - entry_mock_data = { - CONF_HOST: "1.1.1.1", - CONF_KEYFILE: "", - CONF_CERTFILE: "", - CONF_CA_CERTS: "", - } - mock_entry = MockConfigEntry(domain=DOMAIN, data=entry_mock_data) + mock_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA) mock_entry.add_to_hass(hass) with patch( @@ -168,7 +162,7 @@ async def test_duplicate_bridge_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=entry_mock_data, + data=ENTRY_MOCK_DATA, ) assert result["type"] == data_entry_flow.FlowResultType.ABORT diff --git a/tests/components/lutron_caseta/test_cover.py b/tests/components/lutron_caseta/test_cover.py new file mode 100644 index 00000000000..ef5fc2a5228 --- /dev/null +++ b/tests/components/lutron_caseta/test_cover.py @@ -0,0 +1,19 @@ +"""Tests for the Lutron Caseta integration.""" + + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MockBridge, async_setup_integration + + +async def test_cover_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + await async_setup_integration(hass, MockBridge) + + cover_entity_id = "cover.basement_bedroom_left_shade" + + entity_registry = er.async_get(hass) + + # Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID + assert entity_registry.async_get(cover_entity_id).unique_id == "000004d2_802" diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py index 89fcb65df9d..42fc1dac5c1 100644 --- a/tests/components/lutron_caseta/test_diagnostics.py +++ b/tests/components/lutron_caseta/test_diagnostics.py @@ -48,7 +48,67 @@ async def test_diagnostics(hass, hass_client) -> None: "name": "bridge", "serial": 1234, "type": "type", - } + }, + "801": { + "device_id": "801", + "current_state": 100, + "fan_speed": None, + "zone": "801", + "name": "Basement Bedroom_Main Lights", + "button_groups": None, + "type": "Dimmed", + "model": None, + "serial": None, + "tilt": None, + }, + "802": { + "device_id": "802", + "current_state": 100, + "fan_speed": None, + "zone": "802", + "name": "Basement Bedroom_Left Shade", + "button_groups": None, + "type": "SerenaRollerShade", + "model": None, + "serial": None, + "tilt": None, + }, + "803": { + "device_id": "803", + "current_state": 100, + "fan_speed": None, + "zone": "803", + "name": "Basement Bathroom_Exhaust Fan", + "button_groups": None, + "type": "Switched", + "model": None, + "serial": None, + "tilt": None, + }, + "804": { + "device_id": "804", + "current_state": 100, + "fan_speed": None, + "zone": "804", + "name": "Master Bedroom_Ceiling Fan", + "button_groups": None, + "type": "FanSpeed", + "model": None, + "serial": None, + "tilt": None, + }, + "901": { + "device_id": "901", + "current_state": 100, + "fan_speed": None, + "zone": "901", + "name": "Kitchen_Main Lights", + "button_groups": None, + "type": "WallDimmer", + "model": None, + "serial": 5442321, + "tilt": None, + }, }, "occupancy_groups": {}, "scenes": {}, diff --git a/tests/components/lutron_caseta/test_fan.py b/tests/components/lutron_caseta/test_fan.py new file mode 100644 index 00000000000..f9c86cc9c58 --- /dev/null +++ b/tests/components/lutron_caseta/test_fan.py @@ -0,0 +1,19 @@ +"""Tests for the Lutron Caseta integration.""" + + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MockBridge, async_setup_integration + + +async def test_fan_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + await async_setup_integration(hass, MockBridge) + + fan_entity_id = "fan.master_bedroom_ceiling_fan" + + entity_registry = er.async_get(hass) + + # Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID + assert entity_registry.async_get(fan_entity_id).unique_id == "000004d2_804" diff --git a/tests/components/lutron_caseta/test_light.py b/tests/components/lutron_caseta/test_light.py new file mode 100644 index 00000000000..6449ce04832 --- /dev/null +++ b/tests/components/lutron_caseta/test_light.py @@ -0,0 +1,27 @@ +"""Tests for the Lutron Caseta integration.""" + + +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MockBridge, async_setup_integration + + +async def test_light_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + await async_setup_integration(hass, MockBridge) + + ra3_entity_id = "light.basement_bedroom_main_lights" + caseta_entity_id = "light.kitchen_main_lights" + + entity_registry = er.async_get(hass) + + # Assert that RA3 lights will have the bridge serial hash and the zone id as the uniqueID + assert entity_registry.async_get(ra3_entity_id).unique_id == "000004d2_801" + + # Assert that Caseta lights will have the serial number as the uniqueID + assert entity_registry.async_get(caseta_entity_id).unique_id == "5442321" + + state = hass.states.get(ra3_entity_id) + assert state.state == STATE_ON diff --git a/tests/components/lutron_caseta/test_switch.py b/tests/components/lutron_caseta/test_switch.py new file mode 100644 index 00000000000..842aca94423 --- /dev/null +++ b/tests/components/lutron_caseta/test_switch.py @@ -0,0 +1,18 @@ +"""Tests for the Lutron Caseta integration.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MockBridge, async_setup_integration + + +async def test_switch_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + await async_setup_integration(hass, MockBridge) + + switch_entity_id = "switch.basement_bathroom_exhaust_fan" + + entity_registry = er.async_get(hass) + + # Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID + assert entity_registry.async_get(switch_entity_id).unique_id == "000004d2_803" From eb0828efdb672c4861125fa4c02933a3943ea911 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 20 Aug 2022 21:58:59 +0100 Subject: [PATCH 526/903] Dont rely on config flow to monitor homekit_controller c# changes (#76861) --- .../components/homekit_controller/__init__.py | 4 +- .../homekit_controller/config_flow.py | 18 ------- .../homekit_controller/connection.py | 24 +-------- .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/utils.py | 4 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/common.py | 50 ++++++++----------- .../specific_devices/test_ecobee3.py | 7 ++- .../homekit_controller/test_config_flow.py | 4 +- .../homekit_controller/test_init.py | 2 + 11 files changed, 36 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 3a5ba42848c..9b431b09c9e 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS -from .storage import EntityMapStorage, async_get_entity_storage +from .storage import EntityMapStorage from .utils import async_get_controller _LOGGER = logging.getLogger(__name__) @@ -50,8 +50,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up for Homekit devices.""" - await async_get_entity_storage(hass) - await async_get_controller(hass) hass.data[KNOWN_DEVICES] = {} diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 2ccdd557a5b..62144077a94 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -24,7 +24,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr -from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES from .storage import async_get_entity_storage from .utils import async_get_controller @@ -253,17 +252,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): category = Categories(int(properties.get("ci", 0))) paired = not status_flags & 0x01 - # The configuration number increases every time the characteristic map - # needs updating. Some devices use a slightly off-spec name so handle - # both cases. - try: - config_num = int(properties["c#"]) - except KeyError: - _LOGGER.warning( - "HomeKit device %s: c# not exposed, in violation of spec", hkid - ) - config_num = None - # Set unique-id and error out if it's already configured existing_entry = await self.async_set_unique_id( normalized_hkid, raise_on_progress=False @@ -280,12 +268,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( existing_entry, data={**existing_entry.data, **updated_ip_port} ) - conn: HKDevice = self.hass.data[KNOWN_DEVICES][hkid] - if config_num and conn.config_num != config_num: - _LOGGER.debug( - "HomeKit info %s: c# incremented, refreshing entities", hkid - ) - conn.async_notify_config_changed(config_num) return self.async_abort(reason="already_configured") _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index b2f3b3ae8e0..b4aaab5acf0 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -30,7 +30,6 @@ from .const import ( CHARACTERISTIC_PLATFORMS, CONTROLLER, DOMAIN, - ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH, IDENTIFIER_ACCESSORY_ID, IDENTIFIER_LEGACY_ACCESSORY_ID, @@ -38,7 +37,6 @@ from .const import ( IDENTIFIER_SERIAL_NUMBER, ) from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry -from .storage import EntityMapStorage RETRY_INTERVAL = 60 # seconds MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 @@ -182,14 +180,10 @@ class HKDevice: async def async_setup(self) -> None: """Prepare to use a paired HomeKit device in Home Assistant.""" - entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] pairing = self.pairing transport = pairing.transport entry = self.config_entry - if cache := entity_storage.get_map(self.unique_id): - pairing.restore_accessories_state(cache["accessories"], cache["config_num"]) - # We need to force an update here to make sure we have # the latest values since the async_update we do in # async_process_entity_map will no values to poll yet @@ -203,7 +197,7 @@ class HKDevice: try: await self.pairing.async_populate_accessories_state(force_update=True) except AccessoryNotFoundError: - if transport != Transport.BLE or not cache: + if transport != Transport.BLE or not pairing.accessories: # BLE devices may sleep and we can't force a connection raise @@ -217,9 +211,6 @@ class HKDevice: await self.async_process_entity_map() - if not cache: - # If its missing from the cache, make sure we save it - self.async_save_entity_map() # If everything is up to date, we can create the entities # since we know the data is not stale. await self.async_add_new_entities() @@ -438,31 +429,18 @@ class HKDevice: self.config_entry, self.platforms ) - def async_notify_config_changed(self, config_num: int) -> None: - """Notify the pairing of a config change.""" - self.pairing.notify_config_changed(config_num) - def process_config_changed(self, config_num: int) -> None: """Handle a config change notification from the pairing.""" self.hass.async_create_task(self.async_update_new_accessories_state()) async def async_update_new_accessories_state(self) -> None: """Process a change in the pairings accessories state.""" - self.async_save_entity_map() await self.async_process_entity_map() if self.watchable_characteristics: await self.pairing.subscribe(self.watchable_characteristics) await self.async_update() await self.async_add_new_entities() - @callback - def async_save_entity_map(self) -> None: - """Save the entity map.""" - entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] - entity_storage.async_create_or_update_map( - self.unique_id, self.config_num, self.entity_map.serialize() - ) - def add_accessory_factory(self, add_entities_cb) -> None: """Add a callback to run when discovering new entities for accessories.""" self.accessory_factories.append(add_entities_cb) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3f8d7828236..143627fe7f0 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.3.0"], + "requirements": ["aiohomekit==1.4.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 6e272067b54..b43f1ee05f7 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -8,6 +8,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from .const import CONTROLLER +from .storage import async_get_entity_storage def folded_name(name: str) -> str: @@ -22,6 +23,8 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) + char_cache = await async_get_entity_storage(hass) + # In theory another call to async_get_controller could have run while we were # trying to get the zeroconf instance. So we check again to make sure we # don't leak a Controller instance here. @@ -33,6 +36,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: controller = Controller( async_zeroconf_instance=async_zeroconf_instance, bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type] + char_cache=char_cache, ) hass.data[CONTROLLER] = controller diff --git a/requirements_all.txt b/requirements_all.txt index b9820f94872..7901c1594a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.3.0 +aiohomekit==1.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5cb5b40a2c4..e6b9004cecf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.3.0 +aiohomekit==1.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 18367d28f63..fd543d55ffb 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -11,10 +11,9 @@ from unittest import mock from aiohomekit.model import Accessories, AccessoriesState, Accessory from aiohomekit.testing import FakeController, FakePairing +from aiohomekit.zeroconf import HomeKitService -from homeassistant.components import zeroconf from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import ( CONTROLLER, DOMAIN, @@ -22,6 +21,7 @@ from homeassistant.components.homekit_controller.const import ( IDENTIFIER_ACCESSORY_ID, IDENTIFIER_SERIAL_NUMBER, ) +from homeassistant.components.homekit_controller.utils import async_get_controller from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -175,12 +175,11 @@ async def setup_platform(hass): config = {"discovery": {}} with mock.patch( - "homeassistant.components.homekit_controller.utils.Controller" - ) as controller: - fake_controller = controller.return_value = FakeController() + "homeassistant.components.homekit_controller.utils.Controller", FakeController + ): await async_setup_component(hass, DOMAIN, config) - return fake_controller + return await async_get_controller(hass) async def setup_test_accessories(hass, accessories): @@ -228,31 +227,24 @@ async def device_config_changed(hass, accessories): pairing._accessories_state = AccessoriesState( accessories_obj, pairing.config_num + 1 ) - - discovery_info = zeroconf.ZeroconfServiceInfo( - host="127.0.0.1", - addresses=["127.0.0.1"], - hostname="mock_hostname", - name="TestDevice._hap._tcp.local.", - port=8080, - properties={ - "md": "TestDevice", - "id": "00:00:00:00:00:00", - "c#": "2", - "sf": "0", - }, - type="mock_type", + pairing._async_description_update( + HomeKitService( + name="TestDevice.local", + id="00:00:00:00:00:00", + model="", + config_num=2, + state_num=3, + feature_flags=0, + status_flags=0, + category=1, + protocol_version="1.0", + type="_hap._tcp.local.", + address="127.0.0.1", + addresses=["127.0.0.1"], + port=8080, + ) ) - # Config Flow will abort and notify us if the discovery event is of - # interest - in this case c# has incremented - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass - flow.context = {} - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - # Wait for services to reconfigure await hass.async_block_till_done() await hass.async_block_till_done() diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 3c47195b442..4da2f572626 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -6,7 +6,7 @@ https://github.com/home-assistant/core/issues/15336 from unittest import mock -from aiohomekit import AccessoryDisconnectedError +from aiohomekit import AccessoryNotFoundError from aiohomekit.testing import FakePairing from homeassistant.components.climate.const import ( @@ -184,9 +184,8 @@ async def test_ecobee3_setup_connection_failure(hass): # Test that the connection fails during initial setup. # No entities should be created. - list_accessories = "list_accessories_and_characteristics" - with mock.patch.object(FakePairing, list_accessories) as laac: - laac.side_effect = AccessoryDisconnectedError("Connection failed") + with mock.patch.object(FakePairing, "async_populate_accessories_state") as laac: + laac.side_effect = AccessoryNotFoundError("Connection failed") # If there is no cached entity map and the accessory connection is # failing then we have to fail the config entry setup. diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 3f545be8931..5e2c8249560 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for homekit_controller config flow.""" import asyncio import unittest.mock -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch import aiohomekit from aiohomekit.exceptions import AuthenticationError @@ -524,7 +524,6 @@ async def test_discovery_already_configured_update_csharp(hass, controller): entry.add_to_hass(hass) connection_mock = AsyncMock() - connection_mock.async_notify_config_changed = MagicMock() hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} device = setup_mock_accessory(controller) @@ -547,7 +546,6 @@ async def test_discovery_already_configured_update_csharp(hass, controller): assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryPort"] == discovery_info.port - assert connection_mock.async_notify_config_changed.call_count == 1 @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 37d41fcf372..a91700f699c 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -119,6 +119,7 @@ async def test_offline_device_raises(hass, controller): nonlocal is_connected if not is_connected: raise AccessoryNotFoundError("any") + await super().async_populate_accessories_state(*args, **kwargs) async def get_characteristics(self, chars, *args, **kwargs): nonlocal is_connected @@ -173,6 +174,7 @@ async def test_ble_device_only_checks_is_available(hass, controller): nonlocal is_available if not is_available: raise AccessoryNotFoundError("any") + await super().async_populate_accessories_state(*args, **kwargs) async def get_characteristics(self, chars, *args, **kwargs): nonlocal is_available From ced8278e3222501dde7d769ea4b57aae75f62438 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 11:58:14 -1000 Subject: [PATCH 527/903] Auto recover when the Bluetooth adapter stops responding (#77043) --- .../components/bluetooth/__init__.py | 7 +- homeassistant/components/bluetooth/const.py | 29 ++- homeassistant/components/bluetooth/manager.py | 2 +- .../components/bluetooth/manifest.json | 6 +- homeassistant/components/bluetooth/scanner.py | 208 +++++++++++------ homeassistant/components/bluetooth/util.py | 10 + homeassistant/package_constraints.txt | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/bluetooth/test_scanner.py | 209 ++++++++++++++++++ 10 files changed, 406 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 83c1247e3e2..f3b476a15ad 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -39,7 +39,7 @@ from .models import ( HaBleakScannerWrapper, ProcessAdvertisementCallback, ) -from .scanner import HaScanner, create_bleak_scanner +from .scanner import HaScanner, ScannerStartError, create_bleak_scanner from .util import adapter_human_name, adapter_unique_name, async_default_adapter if TYPE_CHECKING: @@ -281,7 +281,10 @@ async def async_setup_entry( ) from err scanner = HaScanner(hass, bleak_scanner, adapter, address) entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) - await scanner.async_start() + try: + await scanner.async_start() + except ScannerStartError as err: + raise ConfigEntryNotReady from err entry.async_on_unload(manager.async_register_scanner(scanner)) await async_update_device(entry, manager, adapter, address) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 0cd02bcbb8d..d6f7b515532 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -13,13 +13,12 @@ WINDOWS_DEFAULT_BLUETOOTH_ADAPTER = "bluetooth" MACOS_DEFAULT_BLUETOOTH_ADAPTER = "Core Bluetooth" UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" -DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} - DEFAULT_ADAPTER_BY_PLATFORM = { "Windows": WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, "Darwin": MACOS_DEFAULT_BLUETOOTH_ADAPTER, } + # Some operating systems hide the adapter address for privacy reasons (ex MacOS) DEFAULT_ADDRESS: Final = "00:00:00:00:00:00" @@ -28,9 +27,29 @@ SOURCE_LOCAL: Final = "local" DATA_MANAGER: Final = "bluetooth_manager" UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 -START_TIMEOUT = 12 -SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 -SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) + +START_TIMEOUT = 15 + +MAX_DBUS_SETUP_SECONDS = 5 + +# Anything after 30s is considered stale, we have buffer +# for start timeouts and execution time +STALE_ADVERTISEMENT_SECONDS: Final = 30 + START_TIMEOUT + MAX_DBUS_SETUP_SECONDS + + +# We must recover before we hit the 180s mark +# where the device is removed from the stack +# or the devices will go unavailable. Since +# we only check every 30s, we need this number +# to be +# 180s Time when device is removed from stack +# - 30s check interval +# - 20s scanner restart time * 2 +# +SCANNER_WATCHDOG_TIMEOUT: Final = 110 +# How often to check if the scanner has reached +# the SCANNER_WATCHDOG_TIMEOUT without seeing anything +SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30) class AdapterDetails(TypedDict, total=False): diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 0b588e71681..4b826efd6bd 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -23,6 +23,7 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( ADAPTER_ADDRESS, SOURCE_LOCAL, + STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, AdapterDetails, ) @@ -46,7 +47,6 @@ FILTER_UUIDS: Final = "UUIDs" RSSI_SWITCH_THRESHOLD = 6 -STALE_ADVERTISEMENT_SECONDS = 180 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index ff99bd3d97d..21755723772 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,11 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.2.0"], + "requirements": [ + "bleak==0.15.1", + "bluetooth-adapters==0.2.0", + "bluetooth-auto-recovery==0.2.1" + ], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 6faada73e02..ad6341910dd 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Callable from datetime import datetime import logging +import platform import time import async_timeout @@ -21,19 +22,18 @@ from homeassistant.core import ( HomeAssistant, callback as hass_callback, ) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.package import is_docker_env from .const import ( - DEFAULT_ADAPTERS, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, START_TIMEOUT, ) from .models import BluetoothScanningMode -from .util import adapter_human_name +from .util import adapter_human_name, async_reset_adapter OriginalBleakScanner = bleak.BleakScanner MONOTONIC_TIME = time.monotonic @@ -44,6 +44,12 @@ _LOGGER = logging.getLogger(__name__) MONOTONIC_TIME = time.monotonic +NEED_RESET_ERRORS = [ + "org.bluez.Error.Failed", + "org.bluez.Error.InProgress", + "org.bluez.Error.NotReady", +] +START_ATTEMPTS = 2 SCANNING_MODE_TO_BLEAK = { BluetoothScanningMode.ACTIVE: "active", @@ -51,12 +57,17 @@ SCANNING_MODE_TO_BLEAK = { } +class ScannerStartError(HomeAssistantError): + """Error to indicate that the scanner failed to start.""" + + def create_bleak_scanner( scanning_mode: BluetoothScanningMode, adapter: str | None ) -> bleak.BleakScanner: """Create a Bleak scanner.""" scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} - if adapter and adapter not in DEFAULT_ADAPTERS: + # Only Linux supports multiple adapters + if adapter and platform.system() == "Linux": scanner_kwargs["adapter"] = adapter _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) try: @@ -66,7 +77,7 @@ def create_bleak_scanner( class HaScanner: - """Operate a BleakScanner. + """Operate and automatically recover a BleakScanner. Multiple BleakScanner can be used at the same time if there are multiple adapters. This is only useful @@ -91,6 +102,7 @@ class HaScanner: self._cancel_stop: CALLBACK_TYPE | None = None self._cancel_watchdog: CALLBACK_TYPE | None = None self._last_detection = 0.0 + self._start_time = 0.0 self._callbacks: list[ Callable[[BLEDevice, AdvertisementData, float, str], None] ] = [] @@ -129,9 +141,19 @@ class HaScanner: Currently this is used to feed the callbacks into the central manager. """ - self._last_detection = MONOTONIC_TIME() + callback_time = MONOTONIC_TIME() + if ( + advertisement_data.local_name + or advertisement_data.manufacturer_data + or advertisement_data.service_data + or advertisement_data.service_uuids + ): + # Don't count empty advertisements + # as the adapter is in a failure + # state if all the data is empty. + self._last_detection = callback_time for callback in self._callbacks: - callback(ble_device, advertisement_data, self._last_detection, self.source) + callback(ble_device, advertisement_data, callback_time, self.source) async def async_start(self) -> None: """Start bluetooth scanner.""" @@ -142,55 +164,85 @@ class HaScanner: async def _async_start(self) -> None: """Start bluetooth scanner under the lock.""" - try: - async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() # type: ignore[no-untyped-call] - except InvalidMessageError as ex: + for attempt in range(START_ATTEMPTS): _LOGGER.debug( - "%s: Invalid DBus message received: %s", self.name, ex, exc_info=True - ) - raise ConfigEntryNotReady( - f"{self.name}: Invalid DBus message received: {ex}; try restarting `dbus`" - ) from ex - except BrokenPipeError as ex: - _LOGGER.debug( - "%s: DBus connection broken: %s", self.name, ex, exc_info=True - ) - if is_docker_env(): - raise ConfigEntryNotReady( - f"{self.name}: DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" - ) from ex - raise ConfigEntryNotReady( - f"{self.name}: DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" - ) from ex - except FileNotFoundError as ex: - _LOGGER.debug( - "%s: FileNotFoundError while starting bluetooth: %s", + "%s: Starting bluetooth discovery attempt: (%s/%s)", self.name, - ex, - exc_info=True, + attempt + 1, + START_ATTEMPTS, ) - if is_docker_env(): - raise ConfigEntryNotReady( - f"{self.name}: DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + try: + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + _LOGGER.debug( + "%s: Invalid DBus message received: %s", + self.name, + ex, + exc_info=True, + ) + raise ScannerStartError( + f"{self.name}: Invalid DBus message received: {ex}; " + "try restarting `dbus`" ) from ex - raise ConfigEntryNotReady( - f"{self.name}: DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" - ) from ex - except asyncio.TimeoutError as ex: - raise ConfigEntryNotReady( - f"{self.name}: Timed out starting Bluetooth after {START_TIMEOUT} seconds" - ) from ex - except BleakError as ex: - _LOGGER.debug( - "%s: BleakError while starting bluetooth: %s", - self.name, - ex, - exc_info=True, - ) - raise ConfigEntryNotReady( - f"{self.name}: Failed to start Bluetooth: {ex}" - ) from ex + except BrokenPipeError as ex: + _LOGGER.debug( + "%s: DBus connection broken: %s", self.name, ex, exc_info=True + ) + if is_docker_env(): + raise ScannerStartError( + f"{self.name}: DBus connection broken: {ex}; try restarting " + "`bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ScannerStartError( + f"{self.name}: DBus connection broken: {ex}; try restarting " + "`bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + _LOGGER.debug( + "%s: FileNotFoundError while starting bluetooth: %s", + self.name, + ex, + exc_info=True, + ) + if is_docker_env(): + raise ScannerStartError( + f"{self.name}: DBus service not found; docker config may " + "be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ScannerStartError( + f"{self.name}: DBus service not found; make sure the DBus socket " + f"is available to Home Assistant: {ex}" + ) from ex + except asyncio.TimeoutError as ex: + if attempt == 0: + await self._async_reset_adapter() + continue + raise ScannerStartError( + f"{self.name}: Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex + except BleakError as ex: + if attempt == 0: + error_str = str(ex) + if any( + needs_reset_error in error_str + for needs_reset_error in NEED_RESET_ERRORS + ): + await self._async_reset_adapter() + continue + _LOGGER.debug( + "%s: BleakError while starting bluetooth: %s", + self.name, + ex, + exc_info=True, + ) + raise ScannerStartError( + f"{self.name}: Failed to start Bluetooth: {ex}" + ) from ex + + # Everything is fine, break out of the loop + break + self._async_setup_scanner_watchdog() self._cancel_stop = self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping @@ -199,48 +251,78 @@ class HaScanner: @hass_callback def _async_setup_scanner_watchdog(self) -> None: """If Dbus gets restarted or updated, we need to restart the scanner.""" - self._last_detection = MONOTONIC_TIME() - self._cancel_watchdog = async_track_time_interval( - self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL - ) + self._start_time = self._last_detection = MONOTONIC_TIME() + if not self._cancel_watchdog: + self._cancel_watchdog = async_track_time_interval( + self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL + ) async def _async_scanner_watchdog(self, now: datetime) -> None: """Check if the scanner is running.""" time_since_last_detection = MONOTONIC_TIME() - self._last_detection + _LOGGER.debug( + "%s: Scanner watchdog time_since_last_detection: %s", + self.name, + time_since_last_detection, + ) if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: return _LOGGER.info( - "%s: Bluetooth scanner has gone quiet for %s, restarting", + "%s: Bluetooth scanner has gone quiet for %ss, restarting", self.name, - SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, ) async with self._start_stop_lock: - await self._async_stop() - await self._async_start() + # Stop the scanner but not the watchdog + # since we want to try again later if it's still quiet + await self._async_stop_scanner() + if self._start_time == self._last_detection or ( + time_since_last_detection + ) > (SCANNER_WATCHDOG_TIMEOUT + SCANNER_WATCHDOG_INTERVAL.total_seconds()): + await self._async_reset_adapter() + try: + await self._async_start() + except ScannerStartError as ex: + _LOGGER.error( + "%s: Failed to restart Bluetooth scanner: %s", + self.name, + ex, + exc_info=True, + ) async def _async_hass_stopping(self, event: Event) -> None: """Stop the Bluetooth integration at shutdown.""" self._cancel_stop = None await self.async_stop() + async def _async_reset_adapter(self) -> None: + """Reset the adapter.""" + _LOGGER.warning("%s: adapter stopped responding; executing reset", self.name) + result = await async_reset_adapter(self.adapter) + _LOGGER.info("%s: adapter reset result: %s", self.name, result) + async def async_stop(self) -> None: """Stop bluetooth scanner.""" async with self._start_stop_lock: await self._async_stop() async def _async_stop(self) -> None: - """Stop bluetooth discovery under the lock.""" - _LOGGER.debug("Stopping bluetooth discovery") + """Cancel watchdog and bluetooth discovery under the lock.""" if self._cancel_watchdog: self._cancel_watchdog() self._cancel_watchdog = None + await self._async_stop_scanner() + + async def _async_stop_scanner(self) -> None: + """Stop bluetooth discovery under the lock.""" if self._cancel_stop: self._cancel_stop() self._cancel_stop = None + _LOGGER.debug("%s: Stopping bluetooth discovery", self.name) try: await self.scanner.stop() # type: ignore[no-untyped-call] except BleakError as ex: # This is not fatal, and they may want to reload # the config entry to restart the scanner if they # change the bluetooth dongle. - _LOGGER.error("Error stopping scanner: %s", ex) + _LOGGER.error("%s: Error stopping scanner: %s", self.name, ex) diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 3133b2f210d..450c1812483 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -3,6 +3,8 @@ from __future__ import annotations import platform +from bluetooth_auto_recovery import recover_adapter + from homeassistant.core import callback from .const import ( @@ -65,3 +67,11 @@ def adapter_human_name(adapter: str, address: str) -> str: def adapter_unique_name(adapter: str, address: str) -> str: """Return a unique name for the adapter.""" return adapter if address == DEFAULT_ADDRESS else address + + +async def async_reset_adapter(adapter: str | None) -> bool | None: + """Reset the adapter.""" + if adapter and adapter.startswith("hci"): + adapter_id = int(adapter[3:]) + return await recover_adapter(adapter_id) + return False diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b6e5dbc4119..12febc7c2e3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,6 +12,7 @@ awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.1 bluetooth-adapters==0.2.0 +bluetooth-auto-recovery==0.2.1 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 7901c1594a2..50064f5af89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -426,6 +426,9 @@ blockchain==1.4.4 # homeassistant.components.bluetooth bluetooth-adapters==0.2.0 +# homeassistant.components.bluetooth +bluetooth-auto-recovery==0.2.1 + # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6b9004cecf..81bc764f519 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,6 +337,9 @@ blinkpy==0.19.0 # homeassistant.components.bluetooth bluetooth-adapters==0.2.0 +# homeassistant.components.bluetooth +bluetooth-auto-recovery==0.2.1 + # homeassistant.components.bond bond-async==0.1.22 diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index bde1dbd1696..fc2e74144e7 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -8,12 +8,14 @@ from bleak.backends.scanner import ( BLEDevice, ) from dbus_next import InvalidMessageError +import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth.const import ( SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, ) +from homeassistant.components.bluetooth.scanner import NEED_RESET_ERRORS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.util import dt as dt_util @@ -140,6 +142,27 @@ async def test_invalid_dbus_message(hass, caplog, one_adapter): assert "dbus" in caplog.text +@pytest.mark.parametrize("error", NEED_RESET_ERRORS) +async def test_adapter_needs_reset_at_start(hass, caplog, one_adapter, error): + """Test we cycle the adapter when it needs a restart.""" + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=[BleakError(error), None], + ), patch( + "homeassistant.components.bluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter: + await async_setup_with_one_adapter(hass) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_recover_adapter.mock_calls) == 1 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + async def test_recovery_from_dbus_restart(hass, one_adapter): """Test we can recover when DBus gets restarted out from under us.""" @@ -223,3 +246,189 @@ async def test_recovery_from_dbus_restart(hass, one_adapter): await hass.async_block_till_done() assert called_start == 2 + + +async def test_adapter_recovery(hass, one_adapter): + """Test we can recover when the adapter stops responding.""" + + called_start = 0 + called_stop = 0 + _callback = None + mock_discovered = [] + + class MockBleakScanner: + async def start(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_start + called_start += 1 + + async def stop(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_stop + called_stop += 1 + + @property + def discovered_devices(self): + """Mock discovered_devices.""" + nonlocal mock_discovered + return mock_discovered + + def register_detection_callback(self, callback: AdvertisementDataCallback): + """Mock Register Detection Callback.""" + nonlocal _callback + _callback = callback + + scanner = MockBleakScanner() + start_time_monotonic = 1000 + + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic, + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ): + await async_setup_with_one_adapter(hass) + + assert called_start == 1 + + scanner = _get_manager() + mock_discovered = [MagicMock()] + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # We hit the timer with no detections, so we reset the adapter and restart the scanner + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), patch( + "homeassistant.components.bluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter: + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 2 + + +async def test_adapter_scanner_fails_to_start_first_time(hass, one_adapter): + """Test we can recover when the adapter stops responding and the first recovery fails.""" + + called_start = 0 + called_stop = 0 + _callback = None + mock_discovered = [] + + class MockBleakScanner: + async def start(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_start + called_start += 1 + if called_start == 1: + return # Start ok the first time + if called_start < 4: + raise BleakError("Failed to start") + + async def stop(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_stop + called_stop += 1 + + @property + def discovered_devices(self): + """Mock discovered_devices.""" + nonlocal mock_discovered + return mock_discovered + + def register_detection_callback(self, callback: AdvertisementDataCallback): + """Mock Register Detection Callback.""" + nonlocal _callback + _callback = callback + + scanner = MockBleakScanner() + start_time_monotonic = 1000 + + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic, + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ): + await async_setup_with_one_adapter(hass) + + assert called_start == 1 + + scanner = _get_manager() + mock_discovered = [MagicMock()] + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # We hit the timer with no detections, so we reset the adapter and restart the scanner + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), patch( + "homeassistant.components.bluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter: + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 3 + + # We hit the timer again the previous start call failed, make sure + # we try again + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), patch( + "homeassistant.components.bluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter: + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 4 From 0bd49731347cd8ce8e037ff54327b19e4e96bd3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 13:41:09 -1000 Subject: [PATCH 528/903] Bump bluetooth-auto-recovery to 0.2.2 (#77082) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 21755723772..29c534322f2 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.15.1", "bluetooth-adapters==0.2.0", - "bluetooth-auto-recovery==0.2.1" + "bluetooth-auto-recovery==0.2.2" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 12febc7c2e3..412a1841394 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.1 bluetooth-adapters==0.2.0 -bluetooth-auto-recovery==0.2.1 +bluetooth-auto-recovery==0.2.2 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 50064f5af89..89ac7e7d77b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ blockchain==1.4.4 bluetooth-adapters==0.2.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.2.1 +bluetooth-auto-recovery==0.2.2 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81bc764f519..9504c1bd1ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ blinkpy==0.19.0 bluetooth-adapters==0.2.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.2.1 +bluetooth-auto-recovery==0.2.2 # homeassistant.components.bond bond-async==0.1.22 From 2d0b11f18e8aad8187e2065977947656bc79d90d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 13:41:25 -1000 Subject: [PATCH 529/903] Add a new constant for multiple bluetooth watchdog failure hits (#77081) --- homeassistant/components/bluetooth/scanner.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index ad6341910dd..96a0fe572c6 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -56,6 +56,15 @@ SCANNING_MODE_TO_BLEAK = { BluetoothScanningMode.PASSIVE: "passive", } +# The minimum number of seconds to know +# the adapter has not had advertisements +# and we already tried to restart the scanner +# without success when the first time the watch +# dog hit the failure path. +SCANNER_WATCHDOG_MULTIPLE = ( + SCANNER_WATCHDOG_TIMEOUT + SCANNER_WATCHDOG_INTERVAL.total_seconds() +) + class ScannerStartError(HomeAssistantError): """Error to indicate that the scanner failed to start.""" @@ -276,9 +285,13 @@ class HaScanner: # Stop the scanner but not the watchdog # since we want to try again later if it's still quiet await self._async_stop_scanner() - if self._start_time == self._last_detection or ( - time_since_last_detection - ) > (SCANNER_WATCHDOG_TIMEOUT + SCANNER_WATCHDOG_INTERVAL.total_seconds()): + # If there have not been any valid advertisements, + # or the watchdog has hit the failure path multiple times, + # do the reset. + if ( + self._start_time == self._last_detection + or time_since_last_detection > SCANNER_WATCHDOG_MULTIPLE + ): await self._async_reset_adapter() try: await self._async_start() From 296e52d91862fb15e9537d03f05ca9920b0ec146 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 21 Aug 2022 00:24:25 +0000 Subject: [PATCH 530/903] [ci skip] Translation update --- .../components/agent_dvr/translations/he.json | 2 +- .../android_ip_webcam/translations/pt-BR.json | 9 ++--- .../anthemav/translations/pt-BR.json | 6 ++-- .../components/awair/translations/pt-BR.json | 12 +++---- .../components/bluetooth/translations/fr.json | 6 ++++ .../components/bluetooth/translations/ru.json | 9 +++++ .../components/cloud/translations/cs.json | 1 + .../derivative/translations/cs.json | 9 +++++ .../components/escea/translations/pt-BR.json | 2 +- .../components/filesize/translations/cs.json | 7 ++++ .../flunearyou/translations/ja.json | 1 + .../fully_kiosk/translations/pt-BR.json | 6 ++-- .../components/google/translations/pt-BR.json | 4 +-- .../govee_ble/translations/pt-BR.json | 4 +-- .../inkbird/translations/pt-BR.json | 4 +-- .../components/isy994/translations/cs.json | 5 +++ .../components/jellyfin/translations/cs.json | 1 + .../justnimbus/translations/pt-BR.json | 4 +-- .../lacrosse_view/translations/pt-BR.json | 4 +-- .../lacrosse_view/translations/ru.json | 3 +- .../lametric/translations/pt-BR.json | 10 +++--- .../landisgyr_heat_meter/translations/ja.json | 3 ++ .../translations/pt-BR.json | 6 ++-- .../landisgyr_heat_meter/translations/ru.json | 23 +++++++++++++ .../lg_soundbar/translations/pt-BR.json | 6 ++-- .../life360/translations/pt-BR.json | 6 ++-- .../components/lifx/translations/pt-BR.json | 6 ++-- .../components/moat/translations/pt-BR.json | 4 +-- .../components/nest/translations/pt-BR.json | 2 +- .../nextdns/translations/pt-BR.json | 4 +-- .../components/nina/translations/pt-BR.json | 2 +- .../openexchangerates/translations/pt-BR.json | 10 +++--- .../opentherm_gw/translations/pt-BR.json | 2 +- .../p1_monitor/translations/de.json | 3 ++ .../p1_monitor/translations/en.json | 3 +- .../p1_monitor/translations/es.json | 3 ++ .../p1_monitor/translations/fr.json | 3 ++ .../p1_monitor/translations/ja.json | 3 ++ .../p1_monitor/translations/pt-BR.json | 3 ++ .../p1_monitor/translations/zh-Hant.json | 3 ++ .../components/point/translations/pt-BR.json | 2 +- .../pure_energie/translations/de.json | 3 ++ .../pure_energie/translations/es.json | 3 ++ .../pure_energie/translations/fr.json | 3 ++ .../pure_energie/translations/ja.json | 3 ++ .../pure_energie/translations/pt-BR.json | 3 ++ .../pure_energie/translations/zh-Hant.json | 3 ++ .../components/pushover/translations/ja.json | 1 + .../pushover/translations/pt-BR.json | 34 +++++++++++++++++++ .../components/pushover/translations/ru.json | 20 +++++++++++ .../qingping/translations/pt-BR.json | 4 +-- .../qnap_qsw/translations/pt-BR.json | 2 +- .../rhasspy/translations/pt-BR.json | 2 +- .../components/scrape/translations/cs.json | 11 ++++++ .../sensorpush/translations/pt-BR.json | 4 +-- .../simplepush/translations/pt-BR.json | 2 +- .../skybell/translations/pt-BR.json | 6 ++++ .../soundtouch/translations/pt-BR.json | 6 ++-- .../transmission/translations/pt-BR.json | 4 +-- .../tuya/translations/select.cs.json | 7 ++++ .../components/unifi/translations/cs.json | 18 ++++++---- .../components/uptime/translations/cs.json | 3 +- .../components/wallbox/translations/cs.json | 7 ++++ .../xiaomi_ble/translations/pt-BR.json | 4 +-- .../yalexs_ble/translations/pt-BR.json | 6 ++-- .../components/zha/translations/pt-BR.json | 3 ++ 66 files changed, 283 insertions(+), 85 deletions(-) create mode 100644 homeassistant/components/filesize/translations/cs.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/ru.json create mode 100644 homeassistant/components/pushover/translations/pt-BR.json create mode 100644 homeassistant/components/pushover/translations/ru.json create mode 100644 homeassistant/components/scrape/translations/cs.json create mode 100644 homeassistant/components/tuya/translations/select.cs.json diff --git a/homeassistant/components/agent_dvr/translations/he.json b/homeassistant/components/agent_dvr/translations/he.json index d37b99a2f45..6105472b0d5 100644 --- a/homeassistant/components/agent_dvr/translations/he.json +++ b/homeassistant/components/agent_dvr/translations/he.json @@ -11,7 +11,7 @@ "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", - "port": "\u05e4\u05d5\u05e8\u05d8" + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/android_ip_webcam/translations/pt-BR.json b/homeassistant/components/android_ip_webcam/translations/pt-BR.json index 95b5ffa7166..6ddbbaf40db 100644 --- a/homeassistant/components/android_ip_webcam/translations/pt-BR.json +++ b/homeassistant/components/android_ip_webcam/translations/pt-BR.json @@ -1,18 +1,19 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "password": "Senha", "port": "Porta", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json index 5a6038bb480..dbfe4b35801 100644 --- a/homeassistant/components/anthemav/translations/pt-BR.json +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "cannot_receive_deviceinfo": "Falha ao recuperar o endere\u00e7o MAC. Verifique se o dispositivo est\u00e1 ligado" }, "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "port": "Porta" } } diff --git a/homeassistant/components/awair/translations/pt-BR.json b/homeassistant/components/awair/translations/pt-BR.json index 20d2c104b60..109f123f9e2 100644 --- a/homeassistant/components/awair/translations/pt-BR.json +++ b/homeassistant/components/awair/translations/pt-BR.json @@ -2,23 +2,23 @@ "config": { "abort": { "already_configured": "A conta j\u00e1 foi configurada", - "already_configured_account": "A conta j\u00e1 est\u00e1 configurada", - "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured_account": "A conta j\u00e1 foi configurada", + "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado", "no_devices_found": "Nenhum dispositivo encontrado na rede", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "unreachable": "Falhou ao conectar" + "unreachable": "Falha ao conectar" }, "error": { "invalid_access_token": "Token de acesso inv\u00e1lido", "unknown": "Erro inesperado", - "unreachable": "Falhou ao conectar" + "unreachable": "Falha ao conectar" }, "flow_title": "{model} ({device_id})", "step": { "cloud": { "data": { "access_token": "Token de acesso", - "email": "E-mail" + "email": "Email" }, "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: {url}" }, @@ -47,7 +47,7 @@ "reauth_confirm": { "data": { "access_token": "Token de acesso", - "email": "E-mail" + "email": "Email" }, "description": "Insira novamente seu token de acesso de desenvolvedor Awair." }, diff --git a/homeassistant/components/bluetooth/translations/fr.json b/homeassistant/components/bluetooth/translations/fr.json index 80a0ac6ea3f..da430eedb0e 100644 --- a/homeassistant/components/bluetooth/translations/fr.json +++ b/homeassistant/components/bluetooth/translations/fr.json @@ -11,6 +11,12 @@ "enable_bluetooth": { "description": "Voulez-vous configurer le Bluetooth\u00a0?" }, + "multiple_adapters": { + "data": { + "adapter": "Adaptateur" + }, + "description": "S\u00e9lectionner un adaptateur Bluetooth \u00e0 configurer" + }, "user": { "data": { "address": "Appareil" diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json index 802470d7c29..17108371409 100644 --- a/homeassistant/components/bluetooth/translations/ru.json +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "single_adapter": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Bluetooth-\u0430\u0434\u0430\u043f\u0442\u0435\u0440 {name}?" + }, "user": { "data": { "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/cloud/translations/cs.json b/homeassistant/components/cloud/translations/cs.json index e6cf308e370..ac8af2ac812 100644 --- a/homeassistant/components/cloud/translations/cs.json +++ b/homeassistant/components/cloud/translations/cs.json @@ -10,6 +10,7 @@ "relayer_connected": "Relayer p\u0159ipojen", "remote_connected": "Vzd\u00e1len\u00e1 spr\u00e1va p\u0159ipojena", "remote_enabled": "Vzd\u00e1len\u00e1 spr\u00e1va povolena", + "remote_server": "Vzd\u00e1len\u00fd server", "subscription_expiration": "Platnost p\u0159edplatn\u00e9ho" } } diff --git a/homeassistant/components/derivative/translations/cs.json b/homeassistant/components/derivative/translations/cs.json index ee8ee0e6d86..087490dc949 100644 --- a/homeassistant/components/derivative/translations/cs.json +++ b/homeassistant/components/derivative/translations/cs.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "time_window": "\u010casov\u00e9 okno" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/escea/translations/pt-BR.json b/homeassistant/components/escea/translations/pt-BR.json index 5bcd3a25634..17aa2f1d1f7 100644 --- a/homeassistant/components/escea/translations/pt-BR.json +++ b/homeassistant/components/escea/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo encontrado na rede", - "single_instance_allowed": "J\u00e1 foi configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/filesize/translations/cs.json b/homeassistant/components/filesize/translations/cs.json new file mode 100644 index 00000000000..1c38c1deb5d --- /dev/null +++ b/homeassistant/components/filesize/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "not_valid": "Cesta nen\u00ed platn\u00e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ja.json b/homeassistant/components/flunearyou/translations/ja.json index 116800656a6..06cf83e27be 100644 --- a/homeassistant/components/flunearyou/translations/ja.json +++ b/homeassistant/components/flunearyou/translations/ja.json @@ -22,6 +22,7 @@ "fix_flow": { "step": { "confirm": { + "description": "Flu Near You\u3068\u306e\u7d71\u5408\u306b\u5fc5\u8981\u306a\u5916\u90e8\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u3063\u305f\u305f\u3081\u3001\u7d71\u5408\u306f\u6a5f\u80fd\u3057\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\n\nSUBMIT\u3092\u62bc\u3057\u3066\u3001Flu Near You\u3092Home Assistant\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u304b\u3089\u524a\u9664\u3057\u307e\u3059\u3002", "title": "\u8fd1\u304f\u306eFlu Near You\u3092\u524a\u9664" } } diff --git a/homeassistant/components/fully_kiosk/translations/pt-BR.json b/homeassistant/components/fully_kiosk/translations/pt-BR.json index 172933953e8..2649409ede7 100644 --- a/homeassistant/components/fully_kiosk/translations/pt-BR.json +++ b/homeassistant/components/fully_kiosk/translations/pt-BR.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada" + "already_configured": "A conta j\u00e1 foi configurada" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "password": "Senha" } } diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index 0b115c46423..709737dbe2d 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -6,13 +6,13 @@ "abort": { "already_configured": "A conta j\u00e1 foi configurada", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", - "cannot_connect": "Falha ao se conectar", + "cannot_connect": "Falha ao conectar", "code_expired": "O c\u00f3digo de autentica\u00e7\u00e3o expirou ou a configura\u00e7\u00e3o da credencial \u00e9 inv\u00e1lida. Tente novamente.", "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "oauth_error": "Dados de token recebidos inv\u00e1lidos.", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" + "timeout_connect": "Tempo limite para estabelecer conex\u00e3o atingido" }, "create_entry": { "default": "Autenticado com sucesso" diff --git a/homeassistant/components/govee_ble/translations/pt-BR.json b/homeassistant/components/govee_ble/translations/pt-BR.json index 2067d7f9312..3f93e65c087 100644 --- a/homeassistant/components/govee_ble/translations/pt-BR.json +++ b/homeassistant/components/govee_ble/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "flow_title": "{name}", diff --git a/homeassistant/components/inkbird/translations/pt-BR.json b/homeassistant/components/inkbird/translations/pt-BR.json index 2067d7f9312..3f93e65c087 100644 --- a/homeassistant/components/inkbird/translations/pt-BR.json +++ b/homeassistant/components/inkbird/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "flow_title": "{name}", diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json index ac6773f09e1..d165050bf77 100644 --- a/homeassistant/components/isy994/translations/cs.json +++ b/homeassistant/components/isy994/translations/cs.json @@ -11,6 +11,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "host": "URL", diff --git a/homeassistant/components/jellyfin/translations/cs.json b/homeassistant/components/jellyfin/translations/cs.json index c9a6f8f2462..5d03904568e 100644 --- a/homeassistant/components/jellyfin/translations/cs.json +++ b/homeassistant/components/jellyfin/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } diff --git a/homeassistant/components/justnimbus/translations/pt-BR.json b/homeassistant/components/justnimbus/translations/pt-BR.json index c6fec98d719..7774e424ac9 100644 --- a/homeassistant/components/justnimbus/translations/pt-BR.json +++ b/homeassistant/components/justnimbus/translations/pt-BR.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/lacrosse_view/translations/pt-BR.json b/homeassistant/components/lacrosse_view/translations/pt-BR.json index 89f951e857b..b977e29baea 100644 --- a/homeassistant/components/lacrosse_view/translations/pt-BR.json +++ b/homeassistant/components/lacrosse_view/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { @@ -13,7 +13,7 @@ "user": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/lacrosse_view/translations/ru.json b/homeassistant/components/lacrosse_view/translations/ru.json index 931b4c32274..c2730f9f07f 100644 --- a/homeassistant/components/lacrosse_view/translations/ru.json +++ b/homeassistant/components/lacrosse_view/translations/ru.json @@ -1,7 +1,8 @@ { "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": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", diff --git a/homeassistant/components/lametric/translations/pt-BR.json b/homeassistant/components/lametric/translations/pt-BR.json index 4153b06e94d..7349baeb8dc 100644 --- a/homeassistant/components/lametric/translations/pt-BR.json +++ b/homeassistant/components/lametric/translations/pt-BR.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "invalid_discovery_info": "Informa\u00e7\u00f5es de descoberta inv\u00e1lidas recebidas", "link_local_address": "Endere\u00e7os locais de links n\u00e3o s\u00e3o suportados", "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.", "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric", - "no_url_available": "Nenhuma URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre este erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})" + "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "step": { @@ -23,8 +23,8 @@ }, "manual_entry": { "data": { - "api_key": "Chave API", - "host": "Host" + "api_key": "Chave da API", + "host": "Nome do host" }, "data_description": { "api_key": "Voc\u00ea pode encontrar essa chave de API em [p\u00e1gina de dispositivos em sua conta de desenvolvedor LaMetric](https://developer.lametric.com/user/devices).", diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ja.json b/homeassistant/components/landisgyr_heat_meter/translations/ja.json index e05d6d2dffa..b9ac41b8244 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/ja.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json index 1ac8f7b38bf..97cced694cf 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "step": { "setup_serial_manual_path": { "data": { - "device": "Caminho do dispositivo USB" + "device": "Caminho do Dispositivo USB" } }, "user": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ru.json b/homeassistant/components/landisgyr_heat_meter/translations/ru.json new file mode 100644 index 00000000000..b4977ecdc39 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/ru.json @@ -0,0 +1,23 @@ +{ + "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": { + "setup_serial_manual_path": { + "data": { + "device": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + }, + "user": { + "data": { + "device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/pt-BR.json b/homeassistant/components/lg_soundbar/translations/pt-BR.json index 8a2b69e069d..60e047a8acf 100644 --- a/homeassistant/components/lg_soundbar/translations/pt-BR.json +++ b/homeassistant/components/lg_soundbar/translations/pt-BR.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." }, "error": { - "cannot_connect": "Falhou ao conectar" + "cannot_connect": "Falha ao conectar" }, "step": { "user": { "data": { - "host": "Host" + "host": "Nome do host" } } } diff --git a/homeassistant/components/life360/translations/pt-BR.json b/homeassistant/components/life360/translations/pt-BR.json index 13349bcef67..25e917f2578 100644 --- a/homeassistant/components/life360/translations/pt-BR.json +++ b/homeassistant/components/life360/translations/pt-BR.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_configured": "A conta j\u00e1 foi configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "reauth_successful": "Integra\u00e7\u00e3o Reautenticar", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "unknown": "Erro inesperado" }, "create_entry": { @@ -11,7 +11,7 @@ }, "error": { "already_configured": "A conta j\u00e1 foi configurada", - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", "unknown": "Erro inesperado" diff --git a/homeassistant/components/lifx/translations/pt-BR.json b/homeassistant/components/lifx/translations/pt-BR.json index ee340af857e..616f3f03cc8 100644 --- a/homeassistant/components/lifx/translations/pt-BR.json +++ b/homeassistant/components/lifx/translations/pt-BR.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { - "cannot_connect": "Falhou ao conectar" + "cannot_connect": "Falha ao conectar" }, "flow_title": "{label} ( {host} ) {serial}", "step": { @@ -24,7 +24,7 @@ }, "user": { "data": { - "host": "Host" + "host": "Nome do host" }, "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para localizar dispositivos." } diff --git a/homeassistant/components/moat/translations/pt-BR.json b/homeassistant/components/moat/translations/pt-BR.json index 2067d7f9312..3f93e65c087 100644 --- a/homeassistant/components/moat/translations/pt-BR.json +++ b/homeassistant/components/moat/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "flow_title": "{name}", diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index 973a3cf3b69..9f5f9b9eff7 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -4,7 +4,7 @@ }, "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada", + "already_configured": "A conta j\u00e1 foi configurada", "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", diff --git a/homeassistant/components/nextdns/translations/pt-BR.json b/homeassistant/components/nextdns/translations/pt-BR.json index 90d7cf3f31e..4eebf2c5900 100644 --- a/homeassistant/components/nextdns/translations/pt-BR.json +++ b/homeassistant/components/nextdns/translations/pt-BR.json @@ -4,7 +4,7 @@ "already_configured": "Este perfil NextDNS j\u00e1 est\u00e1 configurado." }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_api_key": "Chave de API inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -16,7 +16,7 @@ }, "user": { "data": { - "api_key": "Chave de API" + "api_key": "Chave da API" } } } diff --git a/homeassistant/components/nina/translations/pt-BR.json b/homeassistant/components/nina/translations/pt-BR.json index 3da9c79f05f..774f51d044a 100644 --- a/homeassistant/components/nina/translations/pt-BR.json +++ b/homeassistant/components/nina/translations/pt-BR.json @@ -26,7 +26,7 @@ }, "options": { "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "no_selection": "Selecione pelo menos uma cidade/munic\u00edpio", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/openexchangerates/translations/pt-BR.json b/homeassistant/components/openexchangerates/translations/pt-BR.json index b7a8bc33614..7f6edf42577 100644 --- a/homeassistant/components/openexchangerates/translations/pt-BR.json +++ b/homeassistant/components/openexchangerates/translations/pt-BR.json @@ -2,20 +2,20 @@ "config": { "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" + "timeout_connect": "Tempo limite para estabelecer conex\u00e3o atingido" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "timeout_connect": "Tempo limite para estabelecer conex\u00e3o atingido", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "api_key": "Chave API", + "api_key": "Chave da API", "base": "Moeda base" }, "data_description": { diff --git a/homeassistant/components/opentherm_gw/translations/pt-BR.json b/homeassistant/components/opentherm_gw/translations/pt-BR.json index 3d2649aad08..a6ffea67c44 100644 --- a/homeassistant/components/opentherm_gw/translations/pt-BR.json +++ b/homeassistant/components/opentherm_gw/translations/pt-BR.json @@ -4,7 +4,7 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha ao conectar", "id_exists": "ID do gateway j\u00e1 existe", - "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" + "timeout_connect": "Tempo limite para estabelecer conex\u00e3o atingido" }, "step": { "init": { diff --git a/homeassistant/components/p1_monitor/translations/de.json b/homeassistant/components/p1_monitor/translations/de.json index 8ac00192251..8740c9dccbb 100644 --- a/homeassistant/components/p1_monitor/translations/de.json +++ b/homeassistant/components/p1_monitor/translations/de.json @@ -9,6 +9,9 @@ "host": "Host", "name": "Name" }, + "data_description": { + "host": "Die IP-Adresse oder der Hostname deiner P1 Monitor-Installation." + }, "description": "Richte den P1-Monitor zur Integration mit Home Assistant ein." } } diff --git a/homeassistant/components/p1_monitor/translations/en.json b/homeassistant/components/p1_monitor/translations/en.json index 394b6c0767b..4347e2d89d2 100644 --- a/homeassistant/components/p1_monitor/translations/en.json +++ b/homeassistant/components/p1_monitor/translations/en.json @@ -6,7 +6,8 @@ "step": { "user": { "data": { - "host": "Host" + "host": "Host", + "name": "Name" }, "data_description": { "host": "The IP address or hostname of your P1 Monitor installation." diff --git a/homeassistant/components/p1_monitor/translations/es.json b/homeassistant/components/p1_monitor/translations/es.json index 28dcb186505..3893952a1ec 100644 --- a/homeassistant/components/p1_monitor/translations/es.json +++ b/homeassistant/components/p1_monitor/translations/es.json @@ -9,6 +9,9 @@ "host": "Host", "name": "Nombre" }, + "data_description": { + "host": "La direcci\u00f3n IP o el nombre de host de tu instalaci\u00f3n de P1 Monitor." + }, "description": "Configura P1 Monitor para integrarlo con Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/fr.json b/homeassistant/components/p1_monitor/translations/fr.json index 34699234e0e..5da5fe439cd 100644 --- a/homeassistant/components/p1_monitor/translations/fr.json +++ b/homeassistant/components/p1_monitor/translations/fr.json @@ -9,6 +9,9 @@ "host": "H\u00f4te", "name": "Nom" }, + "data_description": { + "host": "L'adresse IP ou le nom d'h\u00f4te de votre installation P1 Monitor." + }, "description": "Configurez P1 Monitor pour l'int\u00e9grer \u00e0 Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/ja.json b/homeassistant/components/p1_monitor/translations/ja.json index 29bfd50332f..66e79ebdc0b 100644 --- a/homeassistant/components/p1_monitor/translations/ja.json +++ b/homeassistant/components/p1_monitor/translations/ja.json @@ -9,6 +9,9 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, + "data_description": { + "host": "P1 Monitor\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u3001IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30db\u30b9\u30c8\u540d\u3002" + }, "description": "P1 Monitor\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/p1_monitor/translations/pt-BR.json b/homeassistant/components/p1_monitor/translations/pt-BR.json index dda88d419e6..97dfd9a0ec3 100644 --- a/homeassistant/components/p1_monitor/translations/pt-BR.json +++ b/homeassistant/components/p1_monitor/translations/pt-BR.json @@ -9,6 +9,9 @@ "host": "Nome do host", "name": "Nome" }, + "data_description": { + "host": "O endere\u00e7o IP ou o nome do host da instala\u00e7\u00e3o do P1 Monitor." + }, "description": "Configure o P1 Monitor para integrar com o Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/zh-Hant.json b/homeassistant/components/p1_monitor/translations/zh-Hant.json index a62ff38bbda..27e0200f1b2 100644 --- a/homeassistant/components/p1_monitor/translations/zh-Hant.json +++ b/homeassistant/components/p1_monitor/translations/zh-Hant.json @@ -9,6 +9,9 @@ "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, + "data_description": { + "host": "P1 Monitor \u5b89\u88dd IP \u4f4d\u5740\u6216\u4e3b\u6a5f\u540d\u7a31\u3002" + }, "description": "\u8a2d\u5b9a P1 Monitor \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" } } diff --git a/homeassistant/components/point/translations/pt-BR.json b/homeassistant/components/point/translations/pt-BR.json index a940c67daf9..7ef07947019 100644 --- a/homeassistant/components/point/translations/pt-BR.json +++ b/homeassistant/components/point/translations/pt-BR.json @@ -4,7 +4,7 @@ "already_setup": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", - "no_flows": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.\nVoc\u00ea precisa configurar o Point antes de ser capaz de autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es](https://www.home-assistant.io/components/point/).", + "no_flows": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { diff --git a/homeassistant/components/pure_energie/translations/de.json b/homeassistant/components/pure_energie/translations/de.json index 6aafb35d5f9..a3632b93bd5 100644 --- a/homeassistant/components/pure_energie/translations/de.json +++ b/homeassistant/components/pure_energie/translations/de.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "Die IP-Adresse oder der Hostname deines Pure Energie Meters." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/es.json b/homeassistant/components/pure_energie/translations/es.json index 58d02574211..6bbff02d6a5 100644 --- a/homeassistant/components/pure_energie/translations/es.json +++ b/homeassistant/components/pure_energie/translations/es.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "La direcci\u00f3n IP o el nombre de host de tu Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/fr.json b/homeassistant/components/pure_energie/translations/fr.json index e121c7f655c..98069d82e1f 100644 --- a/homeassistant/components/pure_energie/translations/fr.json +++ b/homeassistant/components/pure_energie/translations/fr.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "H\u00f4te" + }, + "data_description": { + "host": "L'adresse IP ou le nom d'h\u00f4te de votre compteur Pure Energie." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/ja.json b/homeassistant/components/pure_energie/translations/ja.json index bb7b3fe9f13..6558ed09594 100644 --- a/homeassistant/components/pure_energie/translations/ja.json +++ b/homeassistant/components/pure_energie/translations/ja.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8" + }, + "data_description": { + "host": "Pure Energie Meter\u306e\u3001IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30db\u30b9\u30c8\u540d\u3002" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/pt-BR.json b/homeassistant/components/pure_energie/translations/pt-BR.json index b43c1c285ba..a2663ccc317 100644 --- a/homeassistant/components/pure_energie/translations/pt-BR.json +++ b/homeassistant/components/pure_energie/translations/pt-BR.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Nome do host" + }, + "data_description": { + "host": "O endere\u00e7o IP ou nome de host do seu Medidor Pure Energie." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/zh-Hant.json b/homeassistant/components/pure_energie/translations/zh-Hant.json index 56235b16952..c758ee0cf55 100644 --- a/homeassistant/components/pure_energie/translations/zh-Hant.json +++ b/homeassistant/components/pure_energie/translations/zh-Hant.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" + }, + "data_description": { + "host": "Pure Energie Meter IP \u4f4d\u5740\u6216\u4e3b\u6a5f\u540d\u7a31\u3002" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/ja.json b/homeassistant/components/pushover/translations/ja.json index 344e21952dc..ab1170be1b0 100644 --- a/homeassistant/components/pushover/translations/ja.json +++ b/homeassistant/components/pushover/translations/ja.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "invalid_user_key": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u30ad\u30fc" }, "step": { diff --git a/homeassistant/components/pushover/translations/pt-BR.json b/homeassistant/components/pushover/translations/pt-BR.json new file mode 100644 index 00000000000..60f2cb1b3b2 --- /dev/null +++ b/homeassistant/components/pushover/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "invalid_user_key": "Chave de usu\u00e1rio inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + }, + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "api_key": "Chave da API", + "name": "Nome", + "user_key": "Chave do usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Pushover usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Pushover YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o do Pushover YAML est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/ru.json b/homeassistant/components/pushover/translations/ru.json new file mode 100644 index 00000000000..ae879b1fd27 --- /dev/null +++ b/homeassistant/components/pushover/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "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_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "invalid_user_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f." + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/pt-BR.json b/homeassistant/components/qingping/translations/pt-BR.json index 0da7639fa2a..5b654163201 100644 --- a/homeassistant/components/qingping/translations/pt-BR.json +++ b/homeassistant/components/qingping/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "not_supported": "Dispositivo n\u00e3o suportado" }, diff --git a/homeassistant/components/qnap_qsw/translations/pt-BR.json b/homeassistant/components/qnap_qsw/translations/pt-BR.json index fe2016bdce5..e5a2b0fec96 100644 --- a/homeassistant/components/qnap_qsw/translations/pt-BR.json +++ b/homeassistant/components/qnap_qsw/translations/pt-BR.json @@ -12,7 +12,7 @@ "discovered_connection": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } }, "user": { diff --git a/homeassistant/components/rhasspy/translations/pt-BR.json b/homeassistant/components/rhasspy/translations/pt-BR.json index 0e62dceb57d..c17f7891bd0 100644 --- a/homeassistant/components/rhasspy/translations/pt-BR.json +++ b/homeassistant/components/rhasspy/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "J\u00e1 foi configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "user": { diff --git a/homeassistant/components/scrape/translations/cs.json b/homeassistant/components/scrape/translations/cs.json new file mode 100644 index 00000000000..8669b5b1330 --- /dev/null +++ b/homeassistant/components/scrape/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "headers": "Hlavi\u010dky" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/pt-BR.json b/homeassistant/components/sensorpush/translations/pt-BR.json index 2067d7f9312..3f93e65c087 100644 --- a/homeassistant/components/sensorpush/translations/pt-BR.json +++ b/homeassistant/components/sensorpush/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "flow_title": "{name}", diff --git a/homeassistant/components/simplepush/translations/pt-BR.json b/homeassistant/components/simplepush/translations/pt-BR.json index 90d21e1c44c..993c8d7214b 100644 --- a/homeassistant/components/simplepush/translations/pt-BR.json +++ b/homeassistant/components/simplepush/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "cannot_connect": "Falha ao conectar" diff --git a/homeassistant/components/skybell/translations/pt-BR.json b/homeassistant/components/skybell/translations/pt-BR.json index 9fed8c0da02..6ebac606d58 100644 --- a/homeassistant/components/skybell/translations/pt-BR.json +++ b/homeassistant/components/skybell/translations/pt-BR.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Skybell usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Skybell foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt-BR.json b/homeassistant/components/soundtouch/translations/pt-BR.json index 7446cbc5a06..146e5352614 100644 --- a/homeassistant/components/soundtouch/translations/pt-BR.json +++ b/homeassistant/components/soundtouch/translations/pt-BR.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar" + "cannot_connect": "Falha ao conectar" }, "step": { "user": { "data": { - "host": "Host" + "host": "Nome do host" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/transmission/translations/pt-BR.json b/homeassistant/components/transmission/translations/pt-BR.json index 781d21e2900..5579b64e2d9 100644 --- a/homeassistant/components/transmission/translations/pt-BR.json +++ b/homeassistant/components/transmission/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "reauth_successful": "Re-autenticado com sucesso" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", @@ -15,7 +15,7 @@ "password": "Senha" }, "description": "A senha para o usu\u00e1rio {username} est\u00e1 inv\u00e1lida.", - "title": "Re-autenticar integra\u00e7\u00e3o" + "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/tuya/translations/select.cs.json b/homeassistant/components/tuya/translations/select.cs.json new file mode 100644 index 00000000000..c1b2e3b1516 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.cs.json @@ -0,0 +1,7 @@ +{ + "state": { + "tuya__humidifier_spray_mode": { + "humidity": "Vlhkost" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json index 0281dfbb750..981bb926d3a 100644 --- a/homeassistant/components/unifi/translations/cs.json +++ b/homeassistant/components/unifi/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ovlada\u010d je ji\u017e nastaven", + "already_configured": "Um\u00edst\u011bn\u00ed Unifi Network je ji\u017e nastaveno", + "configuration_updated": "Nastaven\u00ed aktualizov\u00e1no", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { @@ -9,7 +10,7 @@ "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})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { @@ -20,11 +21,14 @@ "username": "U\u017eivatelsk\u00e9 jm\u00e9no", "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" }, - "title": "Nastaven\u00ed UniFi ovlada\u010de" + "title": "Nastaven\u00ed UniFi Network" } } }, "options": { + "abort": { + "integration_not_setup": "Integrace UniFi nen\u00ed nastavena" + }, "step": { "client_control": { "data": { @@ -33,7 +37,7 @@ "poe_clients": "Povolit u klient\u016f ovl\u00e1d\u00e1n\u00ed POE" }, "description": "Nakonfigurujte ovl\u00e1dac\u00ed prvky klienta\n\nVytvo\u0159te vyp\u00edna\u010de pro s\u00e9riov\u00e1 \u010d\u00edsla, pro kter\u00e1 chcete \u0159\u00eddit p\u0159\u00edstup k s\u00edti.", - "title": "Mo\u017enosti UniFi 2/3" + "title": "Mo\u017enosti UniFi Network 2/3" }, "device_tracker": { "data": { @@ -44,7 +48,7 @@ "track_wired_clients": "V\u010detn\u011b klient\u016f p\u0159ipojen\u00fdch kabelem" }, "description": "Konfigurace sledov\u00e1n\u00ed za\u0159\u00edzen\u00ed", - "title": "Mo\u017enosti UniFi 1/3" + "title": "Mo\u017enosti UniFi Network 1/3" }, "simple_options": { "data": { @@ -52,7 +56,7 @@ "track_clients": "Sledov\u00e1n\u00ed p\u0159ipojen\u00fdch za\u0159\u00edzen\u00ed", "track_devices": "Sledov\u00e1n\u00ed s\u00ed\u0165ov\u00fdch za\u0159\u00edzen\u00ed (za\u0159\u00edzen\u00ed Ubiquiti)" }, - "description": "Konfigurace integrace UniFi" + "description": "Nastaven\u00ed integrace UniFi Network" }, "statistics_sensors": { "data": { @@ -60,7 +64,7 @@ "allow_uptime_sensors": "Vytvo\u0159it senzory doby provozuschopnosti pro s\u00ed\u0165ov\u00e9 klienty" }, "description": "Konfigurovat statistick\u00e9 senzory", - "title": "Mo\u017enosti UniFi 3/3" + "title": "Mo\u017enosti UniFi Network 3/3" } } } diff --git a/homeassistant/components/uptime/translations/cs.json b/homeassistant/components/uptime/translations/cs.json index ce19e127348..882fc5f4598 100644 --- a/homeassistant/components/uptime/translations/cs.json +++ b/homeassistant/components/uptime/translations/cs.json @@ -8,5 +8,6 @@ "description": "Chcete za\u010d\u00edt nastavovat?" } } - } + }, + "title": "Uptime" } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/cs.json b/homeassistant/components/wallbox/translations/cs.json index 72df4a96818..5bc59de6051 100644 --- a/homeassistant/components/wallbox/translations/cs.json +++ b/homeassistant/components/wallbox/translations/cs.json @@ -6,6 +6,13 @@ "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json index 4702ca14bcc..a21c3e1dd9c 100644 --- a/homeassistant/components/xiaomi_ble/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", "expected_24_characters": "Espera-se uma bindkey hexadecimal de 24 caracteres.", "expected_32_characters": "Esperado um bindkey hexadecimal de 32 caracteres.", diff --git a/homeassistant/components/yalexs_ble/translations/pt-BR.json b/homeassistant/components/yalexs_ble/translations/pt-BR.json index 19467e26e36..c53e0d24567 100644 --- a/homeassistant/components/yalexs_ble/translations/pt-BR.json +++ b/homeassistant/components/yalexs_ble/translations/pt-BR.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado." }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_key_format": "A chave offline deve ser uma string hexadecimal de 32 bytes.", "invalid_key_index": "O slot de chave offline deve ser um n\u00famero inteiro entre 0 e 255.", diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 69b8ced6970..35936dbe60b 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -13,6 +13,9 @@ "confirm": { "description": "Voc\u00ea deseja configurar {name}?" }, + "confirm_hardware": { + "description": "Deseja configurar {name}?" + }, "pick_radio": { "data": { "radio_type": "Tipo de hub zigbee" From 9edb25887cb8888166fbd784f58b9a8fe75b0a96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 15:08:35 -1000 Subject: [PATCH 531/903] Bump yalexs_ble to 1.6.4 (#77080) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index b4e2f78906e..de0034c755f 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.6.2"], + "requirements": ["yalexs-ble==1.6.4"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 89ac7e7d77b..452f0f47ef2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2512,7 +2512,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.2 +yalexs-ble==1.6.4 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9504c1bd1ae..78d547f75ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1713,7 +1713,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.2 +yalexs-ble==1.6.4 # homeassistant.components.august yalexs==1.2.1 From 2689eddbe84adfb40cba25cdce72cdc9ec39afba Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 21 Aug 2022 09:47:04 +0200 Subject: [PATCH 532/903] =?UTF-8?q?Make=20sure=20we=20always=20connect=20t?= =?UTF-8?q?o=20last=20known=20bluetooth=20device=20in=20fj=C3=A4r=C3=A5sku?= =?UTF-8?q?pan=20(#77088)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make sure we always connect to last known device --- .../components/fjaraskupan/__init__.py | 15 ++++++++- homeassistant/components/fjaraskupan/fan.py | 31 ++++++++----------- homeassistant/components/fjaraskupan/light.py | 24 +++++++------- .../components/fjaraskupan/number.py | 14 +++------ 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 28032b3f997..75086c631a1 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -1,7 +1,8 @@ """The Fjäråskupan integration.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import AsyncIterator, Callable +from contextlib import asynccontextmanager from dataclasses import dataclass from datetime import timedelta import logging @@ -14,6 +15,7 @@ from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfoBleak, async_address_present, + async_ble_device_from_address, async_rediscover_address, async_register_callback, ) @@ -84,9 +86,20 @@ class Coordinator(DataUpdateCoordinator[State]): def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None: """Handle a new announcement of data.""" + self.device.device = service_info.device self.device.detection_callback(service_info.device, service_info.advertisement) self.async_set_updated_data(self.device.state) + @asynccontextmanager + async def async_connect_and_update(self) -> AsyncIterator[Device]: + """Provide an up to date device for use during connections.""" + if ble_device := async_ble_device_from_address(self.hass, self.device.address): + self.device.device = ble_device + async with self.device: + yield self.device + + self.async_set_updated_data(self.device.state) + @dataclass class EntryState: diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index 3642438d5d6..e0c18158088 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -8,7 +8,6 @@ from fjaraskupan import ( COMMAND_AFTERCOOKINGTIMERMANUAL, COMMAND_AFTERCOOKINGTIMEROFF, COMMAND_STOP_FAN, - Device, State, ) @@ -58,7 +57,7 @@ async def async_setup_entry( """Set up sensors dynamically through discovery.""" def _constructor(coordinator: Coordinator): - return [Fan(coordinator, coordinator.device, coordinator.device_info)] + return [Fan(coordinator, coordinator.device_info)] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) @@ -72,14 +71,12 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): def __init__( self, coordinator: Coordinator, - device: Device, device_info: DeviceInfo, ) -> None: """Init fan entity.""" super().__init__(coordinator) - self._device = device self._default_on_speed = 25 - self._attr_unique_id = device.address + self._attr_unique_id = coordinator.device.address self._attr_device_info = device_info self._percentage = 0 self._preset_mode = PRESET_MODE_NORMAL @@ -90,8 +87,8 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): new_speed = percentage_to_ordered_list_item( ORDERED_NAMED_FAN_SPEEDS, percentage ) - await self._device.send_fan_speed(int(new_speed)) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_fan_speed(int(new_speed)) async def async_turn_on( self, @@ -111,34 +108,32 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): ORDERED_NAMED_FAN_SPEEDS, percentage ) - async with self._device: + async with self.coordinator.async_connect_and_update() as device: if preset_mode != self._preset_mode: if command := PRESET_TO_COMMAND.get(preset_mode): - await self._device.send_command(command) + await device.send_command(command) else: raise UnsupportedPreset(f"The preset {preset_mode} is unsupported") if preset_mode == PRESET_MODE_NORMAL: - await self._device.send_fan_speed(int(new_speed)) + await device.send_fan_speed(int(new_speed)) elif preset_mode == PRESET_MODE_AFTER_COOKING_MANUAL: - await self._device.send_after_cooking(int(new_speed)) + await device.send_after_cooking(int(new_speed)) elif preset_mode == PRESET_MODE_AFTER_COOKING_AUTO: - await self._device.send_after_cooking(0) - - self.coordinator.async_set_updated_data(self._device.state) + await device.send_after_cooking(0) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if command := PRESET_TO_COMMAND.get(preset_mode): - await self._device.send_command(command) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_command(command) else: raise UnsupportedPreset(f"The preset {preset_mode} is unsupported") async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - await self._device.send_command(COMMAND_STOP_FAN) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_command(COMMAND_STOP_FAN) @property def speed_count(self) -> int: diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index c42943710de..b6028e017d4 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from fjaraskupan import COMMAND_LIGHT_ON_OFF, Device +from fjaraskupan import COMMAND_LIGHT_ON_OFF from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -23,7 +23,7 @@ async def async_setup_entry( """Set up tuya sensors dynamically through tuya discovery.""" def _constructor(coordinator: Coordinator) -> list[Entity]: - return [Light(coordinator, coordinator.device, coordinator.device_info)] + return [Light(coordinator, coordinator.device_info)] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) @@ -36,31 +36,29 @@ class Light(CoordinatorEntity[Coordinator], LightEntity): def __init__( self, coordinator: Coordinator, - device: Device, device_info: DeviceInfo, ) -> None: """Init light entity.""" super().__init__(coordinator) - self._device = device self._attr_color_mode = ColorMode.BRIGHTNESS self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} - self._attr_unique_id = device.address + self._attr_unique_id = coordinator.device.address self._attr_device_info = device_info async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - await self._device.send_dim(int(kwargs[ATTR_BRIGHTNESS] * (100.0 / 255.0))) - else: - if not self.is_on: - await self._device.send_command(COMMAND_LIGHT_ON_OFF) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + if ATTR_BRIGHTNESS in kwargs: + await device.send_dim(int(kwargs[ATTR_BRIGHTNESS] * (100.0 / 255.0))) + else: + if not self.is_on: + await device.send_command(COMMAND_LIGHT_ON_OFF) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if self.is_on: - await self._device.send_command(COMMAND_LIGHT_ON_OFF) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_command(COMMAND_LIGHT_ON_OFF) @property def is_on(self) -> bool: diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index fb793d2328e..d70f9b6a423 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -1,8 +1,6 @@ """Support for sensors.""" from __future__ import annotations -from fjaraskupan import Device - from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MINUTES @@ -23,9 +21,7 @@ async def async_setup_entry( def _constructor(coordinator: Coordinator) -> list[Entity]: return [ - PeriodicVentingTime( - coordinator, coordinator.device, coordinator.device_info - ), + PeriodicVentingTime(coordinator, coordinator.device_info), ] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) @@ -45,13 +41,11 @@ class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): def __init__( self, coordinator: Coordinator, - device: Device, device_info: DeviceInfo, ) -> None: """Init number entities.""" super().__init__(coordinator) - self._device = device - self._attr_unique_id = f"{device.address}-periodic-venting" + self._attr_unique_id = f"{coordinator.device.address}-periodic-venting" self._attr_device_info = device_info self._attr_name = "Periodic venting" @@ -64,5 +58,5 @@ class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set new value.""" - await self._device.send_periodic_venting(int(value)) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_periodic_venting(int(value)) From 4dad24bc51d74836613945e1ad752ce34e053ea2 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 21 Aug 2022 09:48:23 +0200 Subject: [PATCH 533/903] Don't check for periodic ventilation in fan control (#77089) Don't check for periodic ventilation --- homeassistant/components/fjaraskupan/fan.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index e0c18158088..c037966f0ef 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -30,12 +30,10 @@ ORDERED_NAMED_FAN_SPEEDS = ["1", "2", "3", "4", "5", "6", "7", "8"] PRESET_MODE_NORMAL = "normal" PRESET_MODE_AFTER_COOKING_MANUAL = "after_cooking_manual" PRESET_MODE_AFTER_COOKING_AUTO = "after_cooking_auto" -PRESET_MODE_PERIODIC_VENTILATION = "periodic_ventilation" PRESET_MODES = [ PRESET_MODE_NORMAL, PRESET_MODE_AFTER_COOKING_AUTO, PRESET_MODE_AFTER_COOKING_MANUAL, - PRESET_MODE_PERIODIC_VENTILATION, ] PRESET_TO_COMMAND = { @@ -178,8 +176,6 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): self._preset_mode = PRESET_MODE_AFTER_COOKING_MANUAL else: self._preset_mode = PRESET_MODE_AFTER_COOKING_AUTO - elif data.periodic_venting_on: - self._preset_mode = PRESET_MODE_PERIODIC_VENTILATION else: self._preset_mode = PRESET_MODE_NORMAL From 5d8f5708f4b22dedac1882c97a5011adc8c3b7d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Aug 2022 06:51:58 -1000 Subject: [PATCH 534/903] Bump qingping-ble to 0.3.0 (#77094) --- homeassistant/components/qingping/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 20adbaf15f1..8152793d805 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qingping", "bluetooth": [{ "local_name": "Qingping*" }], - "requirements": ["qingping-ble==0.2.4"], + "requirements": ["qingping-ble==0.3.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 452f0f47ef2..de4c96218d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2070,7 +2070,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.2.4 +qingping-ble==0.3.0 # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78d547f75ca..abd7c4d08f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1418,7 +1418,7 @@ pyws66i==1.1 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.2.4 +qingping-ble==0.3.0 # homeassistant.components.rachio rachiopy==1.0.3 From ac56b3306a8314950f5efdb4d851042c7225d977 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Sun, 21 Aug 2022 19:54:37 +0300 Subject: [PATCH 535/903] Fix covers moving state in HomeKit (#77101) Co-authored-by: J. Nick Koston --- homeassistant/components/homekit/type_covers.py | 17 ++++++++++++----- tests/components/homekit/test_type_covers.py | 6 +++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 5879609644c..6db9b081bf1 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -29,7 +29,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import callback +from homeassistant.core import State, callback from homeassistant.helpers.event import async_track_state_change_event from .accessories import TYPES, HomeAccessory @@ -79,6 +79,8 @@ DOOR_TARGET_HASS_TO_HK = { STATE_CLOSING: HK_DOOR_CLOSED, } +MOVING_STATES = {STATE_OPENING, STATE_CLOSING} + _LOGGER = logging.getLogger(__name__) @@ -301,13 +303,16 @@ class OpeningDevice(OpeningDeviceBase, HomeAccessory): self.async_call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value) @callback - def async_update_state(self, new_state): + def async_update_state(self, new_state: State) -> None: """Update cover position and tilt after state changed.""" current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, (float, int)): current_position = int(current_position) self.char_current_position.set_value(current_position) - self.char_target_position.set_value(current_position) + # Writing target_position on a moving cover + # will break the moving state in HK. + if new_state.state not in MOVING_STATES: + self.char_target_position.set_value(current_position) position_state = _hass_state_to_position_start(new_state.state) self.char_position_state.set_value(position_state) @@ -390,14 +395,16 @@ class WindowCoveringBasic(OpeningDeviceBase, HomeAccessory): self.char_target_position.set_value(position) @callback - def async_update_state(self, new_state): + def async_update_state(self, new_state: State) -> None: """Update cover position after state changed.""" position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0} hk_position = position_mapping.get(new_state.state) if hk_position is not None: + is_moving = new_state.state in MOVING_STATES + if self.char_current_position.value != hk_position: self.char_current_position.set_value(hk_position) - if self.char_target_position.value != hk_position: + if self.char_target_position.value != hk_position and not is_moving: self.char_target_position.set_value(hk_position) position_state = _hass_state_to_position_start(new_state.state) if self.char_position_state.value != position_state: diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 44c9365fc04..8f26967c160 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -166,7 +166,7 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): ) await hass.async_block_till_done() assert acc.char_current_position.value == 60 - assert acc.char_target_position.value == 60 + assert acc.char_target_position.value == 0 assert acc.char_position_state.value == 1 hass.states.async_set( @@ -176,7 +176,7 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): ) await hass.async_block_till_done() assert acc.char_current_position.value == 70 - assert acc.char_target_position.value == 70 + assert acc.char_target_position.value == 0 assert acc.char_position_state.value == 1 hass.states.async_set( @@ -186,7 +186,7 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): ) await hass.async_block_till_done() assert acc.char_current_position.value == 50 - assert acc.char_target_position.value == 50 + assert acc.char_target_position.value == 0 assert acc.char_position_state.value == 0 hass.states.async_set( From 23ef3bf9ac5badeb6a4535a33deb82c9156044fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Gyenge?= Date: Sun, 21 Aug 2022 19:31:34 +0200 Subject: [PATCH 536/903] Add UV switch to Pet Waterer in Tuya integration (#76718) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/switch.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 37834f0f273..3ec24f4daab 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -73,6 +73,12 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { icon="mdi:water-sync", entity_category=EntityCategory.CONFIG, ), + SwitchEntityDescription( + key=DPCode.UV, + name="UV Sterilization", + icon="mdi:lightbulb", + entity_category=EntityCategory.CONFIG, + ), ), # Light # https://developer.tuya.com/en/docs/iot/f?id=K9i5ql3v98hn3 From 2f652901b647b5e67e0851fc6913dbe477aa3f92 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 21 Aug 2022 19:31:49 +0200 Subject: [PATCH 537/903] Add long term statistics for tellduslive (#75789) Co-authored-by: Franck Nijhof --- homeassistant/components/tellduslive/sensor.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 42513a60eef..00949fc41b8 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -6,6 +6,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -43,24 +44,28 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_HUMIDITY: SensorEntityDescription( key=SENSOR_TYPE_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_RAINRATE: SensorEntityDescription( key=SENSOR_TYPE_RAINRATE, name="Rain rate", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, icon="mdi:water", + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_RAINTOTAL: SensorEntityDescription( key=SENSOR_TYPE_RAINTOTAL, name="Rain total", native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:water", + state_class=SensorStateClass.TOTAL_INCREASING, ), SENSOR_TYPE_WINDDIRECTION: SensorEntityDescription( key=SENSOR_TYPE_WINDDIRECTION, @@ -70,38 +75,45 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key=SENSOR_TYPE_WINDAVERAGE, name="Wind average", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_WINDGUST: SensorEntityDescription( key=SENSOR_TYPE_WINDGUST, name="Wind gust", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_UV: SensorEntityDescription( key=SENSOR_TYPE_UV, name="UV", native_unit_of_measurement=UV_INDEX, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_WATT: SensorEntityDescription( key=SENSOR_TYPE_WATT, name="Power", native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_LUMINANCE: SensorEntityDescription( key=SENSOR_TYPE_LUMINANCE, name="Luminance", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_DEW_POINT: SensorEntityDescription( key=SENSOR_TYPE_DEW_POINT, name="Dew Point", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_BAROMETRIC_PRESSURE: SensorEntityDescription( key=SENSOR_TYPE_BAROMETRIC_PRESSURE, name="Barometric Pressure", native_unit_of_measurement="kPa", + state_class=SensorStateClass.MEASUREMENT, ), } From ed7ceb026854203430cf442f2dfcdb64796d79a3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 21 Aug 2022 19:34:37 +0200 Subject: [PATCH 538/903] Bump NextDNS backend library (#77105) --- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index eb5d71173bf..ee52aaaee75 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -3,7 +3,7 @@ "name": "NextDNS", "documentation": "https://www.home-assistant.io/integrations/nextdns", "codeowners": ["@bieniu"], - "requirements": ["nextdns==1.1.0"], + "requirements": ["nextdns==1.1.1"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["nextdns"] diff --git a/requirements_all.txt b/requirements_all.txt index de4c96218d9..9bf6ff21ff6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1109,7 +1109,7 @@ nextcloudmonitor==1.1.0 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.1.0 +nextdns==1.1.1 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abd7c4d08f4..11b41b41b87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -793,7 +793,7 @@ nexia==2.0.2 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.1.0 +nextdns==1.1.1 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 From 90ef87f4a6970e11e8d065300c4379ce10176c18 Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Sun, 21 Aug 2022 20:16:38 +0200 Subject: [PATCH 539/903] Add default polling for landis gyr heat meter (#77078) --- homeassistant/components/landisgyr_heat_meter/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index b5a9a3fba79..4321f53bed6 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -1,6 +1,7 @@ """The Landis+Gyr Heat Meter integration.""" from __future__ import annotations +from datetime import timedelta import logging from ultraheat_api import HeatMeterService, UltraheatReader @@ -38,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER, name="ultraheat_gateway", update_method=async_update_data, - update_interval=None, + update_interval=timedelta(days=1), ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator From 86e17865f9545b985ab4acd36e1ffe0cb5ea51eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Aug 2022 08:36:27 -1000 Subject: [PATCH 540/903] Bump pySwitchbot to 0.18.14 (#77090) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b4d7c69b315..5631134cdf6 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.12"], + "requirements": ["PySwitchbot==0.18.14"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9bf6ff21ff6..bcabcdb7297 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.12 +PySwitchbot==0.18.14 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 11b41b41b87..65e33123f38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.12 +PySwitchbot==0.18.14 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From f3e432c9c7735daddd2946773cc6572567821a5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Aug 2022 12:03:14 -1000 Subject: [PATCH 541/903] Reduce bluetooth logging noise when an adapter is recovered (#77109) --- homeassistant/components/bluetooth/scanner.py | 38 ++++++++++-- tests/components/bluetooth/test_scanner.py | 59 +++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 96a0fe572c6..730249d70a0 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -44,12 +44,19 @@ _LOGGER = logging.getLogger(__name__) MONOTONIC_TIME = time.monotonic +# If the adapter is in a stuck state the following errors are raised: NEED_RESET_ERRORS = [ "org.bluez.Error.Failed", "org.bluez.Error.InProgress", "org.bluez.Error.NotReady", ] -START_ATTEMPTS = 2 + +# When the adapter is still initializing, the scanner will raise an exception +# with org.freedesktop.DBus.Error.UnknownObject +WAIT_FOR_ADAPTER_TO_INIT_ERRORS = ["org.freedesktop.DBus.Error.UnknownObject"] +ADAPTER_INIT_TIME = 1.5 + +START_ATTEMPTS = 3 SCANNING_MODE_TO_BLEAK = { BluetoothScanningMode.ACTIVE: "active", @@ -231,17 +238,35 @@ class HaScanner: f"{self.name}: Timed out starting Bluetooth after {START_TIMEOUT} seconds" ) from ex except BleakError as ex: + error_str = str(ex) if attempt == 0: - error_str = str(ex) if any( needs_reset_error in error_str for needs_reset_error in NEED_RESET_ERRORS ): await self._async_reset_adapter() continue + if attempt != START_ATTEMPTS - 1: + # If we are not out of retry attempts, and the + # adapter is still initializing, wait a bit and try again. + if any( + wait_error in error_str + for wait_error in WAIT_FOR_ADAPTER_TO_INIT_ERRORS + ): + _LOGGER.debug( + "%s: Waiting for adapter to initialize; attempt (%s/%s)", + self.name, + attempt + 1, + START_ATTEMPTS, + ) + await asyncio.sleep(ADAPTER_INIT_TIME) + continue + _LOGGER.debug( - "%s: BleakError while starting bluetooth: %s", + "%s: BleakError while starting bluetooth; attempt: (%s/%s): %s", self.name, + attempt + 1, + START_ATTEMPTS, ex, exc_info=True, ) @@ -310,9 +335,12 @@ class HaScanner: async def _async_reset_adapter(self) -> None: """Reset the adapter.""" - _LOGGER.warning("%s: adapter stopped responding; executing reset", self.name) + # There is currently nothing the user can do to fix this + # so we log at debug level. If we later come up with a repair + # strategy, we will change this to raise a repair issue as well. + _LOGGER.debug("%s: adapter stopped responding; executing reset", self.name) result = await async_reset_adapter(self.adapter) - _LOGGER.info("%s: adapter reset result: %s", self.name, result) + _LOGGER.debug("%s: adapter reset result: %s", self.name, result) async def async_stop(self) -> None: """Stop bluetooth scanner.""" diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index fc2e74144e7..26e949ad2e3 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -432,3 +432,62 @@ async def test_adapter_scanner_fails_to_start_first_time(hass, one_adapter): assert len(mock_recover_adapter.mock_calls) == 1 assert called_start == 4 + + +async def test_adapter_fails_to_start_and_takes_a_bit_to_init( + hass, one_adapter, caplog +): + """Test we can recover the adapter at startup and we wait for Dbus to init.""" + + called_start = 0 + called_stop = 0 + _callback = None + mock_discovered = [] + + class MockBleakScanner: + async def start(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_start + called_start += 1 + if called_start == 1: + raise BleakError("org.bluez.Error.InProgress") + if called_start == 2: + raise BleakError("org.freedesktop.DBus.Error.UnknownObject") + + async def stop(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_stop + called_stop += 1 + + @property + def discovered_devices(self): + """Mock discovered_devices.""" + nonlocal mock_discovered + return mock_discovered + + def register_detection_callback(self, callback: AdvertisementDataCallback): + """Mock Register Detection Callback.""" + nonlocal _callback + _callback = callback + + scanner = MockBleakScanner() + start_time_monotonic = 1000 + + with patch( + "homeassistant.components.bluetooth.scanner.ADAPTER_INIT_TIME", + 0, + ), patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic, + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ), patch( + "homeassistant.components.bluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter: + await async_setup_with_one_adapter(hass) + + assert called_start == 3 + + assert len(mock_recover_adapter.mock_calls) == 1 + assert "Waiting for adapter to initialize" in caplog.text From eef7bdb44b2d848083cba40d148fd5177fcf5fbc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 22 Aug 2022 00:27:12 +0000 Subject: [PATCH 542/903] [ci skip] Translation update --- .../components/androidtv/translations/pt.json | 12 ++++++++++++ .../components/binary_sensor/translations/pt.json | 2 ++ .../components/co2signal/translations/pt.json | 1 + .../components/derivative/translations/pt.json | 3 +++ .../environment_canada/translations/pt.json | 3 +++ .../components/esphome/translations/pt.json | 3 +++ .../components/flux_led/translations/pt.json | 3 ++- homeassistant/components/isy994/translations/pt.json | 3 ++- .../components/kaleidescape/translations/pt.json | 3 +++ .../components/lacrosse_view/translations/pt.json | 7 +++++++ .../components/onewire/translations/pt.json | 7 +++++++ .../components/p1_monitor/translations/id.json | 3 +++ .../components/pure_energie/translations/id.json | 3 +++ .../components/system_bridge/translations/pt.json | 3 ++- .../components/tankerkoenig/translations/pt.json | 3 +++ .../components/tractive/translations/sensor.pt.json | 7 +++++++ .../components/vlc_telnet/translations/pt.json | 1 + .../components/watttime/translations/pt.json | 10 ++++++++++ .../yamaha_musiccast/translations/select.pt.json | 6 ++++++ .../components/yeelight/translations/pt.json | 3 +++ 20 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/lacrosse_view/translations/pt.json create mode 100644 homeassistant/components/tractive/translations/sensor.pt.json diff --git a/homeassistant/components/androidtv/translations/pt.json b/homeassistant/components/androidtv/translations/pt.json index 09a78c773cc..0d9b37a78f0 100644 --- a/homeassistant/components/androidtv/translations/pt.json +++ b/homeassistant/components/androidtv/translations/pt.json @@ -4,5 +4,17 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." } + }, + "options": { + "error": { + "invalid_det_rules": "Regras de detec\u00e7\u00e3o de estado inv\u00e1lidas" + }, + "step": { + "init": { + "data": { + "screencap": "Use a captura de tela para a arte do \u00e1lbum" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index 9eba64372d4..cfaf2e36bd3 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -2,6 +2,7 @@ "device_automation": { "condition_type": { "is_bat_low": "a bateria {entity_name} est\u00e1 baixa", + "is_co": "{entity_name} est\u00e1 detectando mon\u00f3xido de carbono", "is_cold": "{entity_name} est\u00e1 frio", "is_connected": "{entity_name} est\u00e1 ligado", "is_gas": "{entity_name} est\u00e1 a detectar g\u00e1s", @@ -90,6 +91,7 @@ } }, "device_class": { + "heat": "aquecer", "moisture": "humidade", "problem": "problema" }, diff --git a/homeassistant/components/co2signal/translations/pt.json b/homeassistant/components/co2signal/translations/pt.json index 6d105e40d36..6af5ca912fe 100644 --- a/homeassistant/components/co2signal/translations/pt.json +++ b/homeassistant/components/co2signal/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "error": { + "api_ratelimit": "Limite de taxa da API excedido", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { diff --git a/homeassistant/components/derivative/translations/pt.json b/homeassistant/components/derivative/translations/pt.json index 6801ab6b6d4..0b3ad11d873 100644 --- a/homeassistant/components/derivative/translations/pt.json +++ b/homeassistant/components/derivative/translations/pt.json @@ -4,6 +4,9 @@ "user": { "data": { "round": "Precis\u00e3o" + }, + "data_description": { + "time_window": "Se definido, o valor do sensor \u00e9 uma m\u00e9dia m\u00f3vel ponderada no tempo das derivadas dentro desta janela." } } } diff --git a/homeassistant/components/environment_canada/translations/pt.json b/homeassistant/components/environment_canada/translations/pt.json index c7081cd694a..8eb252e9a11 100644 --- a/homeassistant/components/environment_canada/translations/pt.json +++ b/homeassistant/components/environment_canada/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "error_response": "Resposta do Environment Canada com erro" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index da3216186ea..636749dd130 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -22,6 +22,9 @@ "description": "Deseja adicionar um n\u00f3 ESPHome `{name}` ao Home Assistant?", "title": "N\u00f3 ESPHome descoberto" }, + "encryption_key": { + "description": "Insira a chave de criptografia que voc\u00ea definiu em sua configura\u00e7\u00e3o para {name}." + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/flux_led/translations/pt.json b/homeassistant/components/flux_led/translations/pt.json index 7f2f103180a..5e78131c687 100644 --- a/homeassistant/components/flux_led/translations/pt.json +++ b/homeassistant/components/flux_led/translations/pt.json @@ -8,7 +8,8 @@ "user": { "data": { "host": "Servidor" - } + }, + "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para localizar dispositivos." } } } diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json index aa4c5f614e6..6d8ee09a26c 100644 --- a/homeassistant/components/isy994/translations/pt.json +++ b/homeassistant/components/isy994/translations/pt.json @@ -13,7 +13,8 @@ "reauth_confirm": { "data": { "password": "Palavra-passe" - } + }, + "description": "As credenciais para {host} n\u00e3o s\u00e3o mais v\u00e1lidas." }, "user": { "data": { diff --git a/homeassistant/components/kaleidescape/translations/pt.json b/homeassistant/components/kaleidescape/translations/pt.json index ce7cbc3f548..dee723be258 100644 --- a/homeassistant/components/kaleidescape/translations/pt.json +++ b/homeassistant/components/kaleidescape/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unsupported": "Dispositivo n\u00e3o compat\u00edvel" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/lacrosse_view/translations/pt.json b/homeassistant/components/lacrosse_view/translations/pt.json new file mode 100644 index 00000000000..7af622c544e --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "no_locations": "Nenhum local encontrado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pt.json b/homeassistant/components/onewire/translations/pt.json index fa5aa3de317..e4a7c7c095a 100644 --- a/homeassistant/components/onewire/translations/pt.json +++ b/homeassistant/components/onewire/translations/pt.json @@ -14,5 +14,12 @@ } } } + }, + "options": { + "step": { + "configure_device": { + "title": "Precis\u00e3o do Sensor OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/id.json b/homeassistant/components/p1_monitor/translations/id.json index 2deaf4c09e0..52bb67d00a6 100644 --- a/homeassistant/components/p1_monitor/translations/id.json +++ b/homeassistant/components/p1_monitor/translations/id.json @@ -9,6 +9,9 @@ "host": "Host", "name": "Nama" }, + "data_description": { + "host": "Alamat IP atau nama host instalasi P1 Monitor Anda." + }, "description": "Siapkan Monitor P1 untuk diintegrasikan dengan Home Assistant." } } diff --git a/homeassistant/components/pure_energie/translations/id.json b/homeassistant/components/pure_energie/translations/id.json index 9557e4bc08f..ef0e2b69785 100644 --- a/homeassistant/components/pure_energie/translations/id.json +++ b/homeassistant/components/pure_energie/translations/id.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "Alamat IP atau nama host Pure Energi Meter Anda." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/system_bridge/translations/pt.json b/homeassistant/components/system_bridge/translations/pt.json index 8f319572c97..cf026d3cb05 100644 --- a/homeassistant/components/system_bridge/translations/pt.json +++ b/homeassistant/components/system_bridge/translations/pt.json @@ -5,6 +5,7 @@ }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" - } + }, + "flow_title": "{name}" } } \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/pt.json b/homeassistant/components/tankerkoenig/translations/pt.json index 7af02efc468..1dc6ead753c 100644 --- a/homeassistant/components/tankerkoenig/translations/pt.json +++ b/homeassistant/components/tankerkoenig/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, + "error": { + "no_stations": "N\u00e3o foi poss\u00edvel encontrar nenhuma esta\u00e7\u00e3o ao alcance." + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/tractive/translations/sensor.pt.json b/homeassistant/components/tractive/translations/sensor.pt.json new file mode 100644 index 00000000000..a04a35ccc1c --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "tractive__tracker_state": { + "system_startup": "Inicializa\u00e7\u00e3o do sistema" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/pt.json b/homeassistant/components/vlc_telnet/translations/pt.json index 55ccd56b497..5e8878a0a55 100644 --- a/homeassistant/components/vlc_telnet/translations/pt.json +++ b/homeassistant/components/vlc_telnet/translations/pt.json @@ -8,6 +8,7 @@ "error": { "unknown": "Erro inesperado" }, + "flow_title": "{host}", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/watttime/translations/pt.json b/homeassistant/components/watttime/translations/pt.json index 859d8de1627..c4b4c354300 100644 --- a/homeassistant/components/watttime/translations/pt.json +++ b/homeassistant/components/watttime/translations/pt.json @@ -4,11 +4,21 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "coordinates": { + "description": "Insira a latitude e longitude para monitorar:" + }, "user": { "data": { "username": "Nome de Utilizador" } } } + }, + "options": { + "step": { + "init": { + "title": "Configurar WattTime" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.pt.json b/homeassistant/components/yamaha_musiccast/translations/select.pt.json index 059993c8829..e538327157b 100644 --- a/homeassistant/components/yamaha_musiccast/translations/select.pt.json +++ b/homeassistant/components/yamaha_musiccast/translations/select.pt.json @@ -17,6 +17,12 @@ "yamaha_musiccast__zone_link_audio_quality": { "compressed": "Comprimido", "uncompressed": "Incomprimido" + }, + "yamaha_musiccast__zone_link_control": { + "stability": "Estabilidade" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "dolby_pl": "Dolby ProLogic" } } } \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/pt.json b/homeassistant/components/yeelight/translations/pt.json index b03d5b8fc6b..923a9a3f4d1 100644 --- a/homeassistant/components/yeelight/translations/pt.json +++ b/homeassistant/components/yeelight/translations/pt.json @@ -8,6 +8,9 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "discovery_confirm": { + "description": "Deseja configurar {model} ({host})?" + }, "pick_device": { "data": { "device": "Dispositivo" From cba289386256f51cbe1040241804a71638e9ee79 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 22 Aug 2022 07:08:57 +0200 Subject: [PATCH 543/903] Set quality scale to platinum in the NextDNS integration (#77099) * Set quality scale to platinum * Catch exceptions on when service calls * Add tests --- .../components/nextdns/manifest.json | 3 +- homeassistant/components/nextdns/switch.py | 36 ++++++++++++------- tests/components/nextdns/test_switch.py | 30 +++++++++++++++- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index ee52aaaee75..04c2e3575f1 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -6,5 +6,6 @@ "requirements": ["nextdns==1.1.1"], "config_flow": true, "iot_class": "cloud_polling", - "loggers": ["nextdns"] + "loggers": ["nextdns"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index a353106723b..c0a0973a24c 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -1,15 +1,19 @@ """Support for the NextDNS service.""" from __future__ import annotations +import asyncio from collections.abc import Callable from dataclasses import dataclass from typing import Any, Generic -from nextdns import Settings +from aiohttp import ClientError +from aiohttp.client_exceptions import ClientConnectorError +from nextdns import ApiError, Settings from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -564,20 +568,28 @@ class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchE async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" - result = await self.coordinator.nextdns.set_setting( - self.coordinator.profile_id, self.entity_description.key, True - ) - - if result: - self._attr_is_on = True - self.async_write_ha_state() + await self.async_set_setting(True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" - result = await self.coordinator.nextdns.set_setting( - self.coordinator.profile_id, self.entity_description.key, False - ) + await self.async_set_setting(False) + + async def async_set_setting(self, new_state: bool) -> None: + """Set the new state.""" + try: + result = await self.coordinator.nextdns.set_setting( + self.coordinator.profile_id, self.entity_description.key, new_state + ) + except ( + ApiError, + ClientConnectorError, + asyncio.TimeoutError, + ClientError, + ) as err: + raise HomeAssistantError( + f"NextDNS API returned an error calling set_setting for {self.entity_id}: {err}" + ) from err if result: - self._attr_is_on = False + self._attr_is_on = new_state self.async_write_ha_state() diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index cc873fcc4b7..72d504be574 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -1,8 +1,12 @@ """Test switch of NextDNS integration.""" +import asyncio from datetime import timedelta -from unittest.mock import patch +from unittest.mock import Mock, patch +from aiohttp import ClientError +from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError +import pytest from homeassistant.components.nextdns.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -15,6 +19,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -977,3 +982,26 @@ async def test_availability(hass: HomeAssistant) -> None: assert state assert state.state != STATE_UNAVAILABLE assert state.state == STATE_ON + + +@pytest.mark.parametrize( + "exc", + [ + ApiError(Mock()), + asyncio.TimeoutError, + ClientConnectorError(Mock(), Mock()), + ClientError, + ], +) +async def test_switch_failure(hass: HomeAssistant, exc: Exception) -> None: + """Tests that the turn on/off service throws HomeAssistantError.""" + await init_integration(hass) + + with patch("homeassistant.components.nextdns.NextDns.set_setting", side_effect=exc): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.fake_profile_block_page"}, + blocking=True, + ) From 8b46174667a711d1d371749b9caf1e9895a50450 Mon Sep 17 00:00:00 2001 From: sophof Date: Mon, 22 Aug 2022 09:02:05 +0200 Subject: [PATCH 544/903] Add NZBGet speed limit sensor (#77104) * Added sensor for download rate limit * Added test for speed limit --- homeassistant/components/nzbget/sensor.py | 8 ++++++++ tests/components/nzbget/__init__.py | 1 + tests/components/nzbget/test_sensor.py | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index a1097389020..941c528f544 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -74,6 +74,11 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Uptime", device_class=SensorDeviceClass.TIMESTAMP, ), + SensorEntityDescription( + key="DownloadLimit", + name="Speed Limit", + native_unit_of_measurement=DATA_RATE_MEGABYTES_PER_SECOND, + ), ) @@ -127,6 +132,9 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): elif "DownloadRate" in sensor_type and value > 0: # Convert download rate from Bytes/s to MBytes/s self._native_value = round(value / 2**20, 2) + elif "DownloadLimit" in sensor_type and value > 0: + # Convert download rate from Bytes/s to MBytes/s + self._native_value = round(value / 2**20, 2) elif "UpTimeSec" in sensor_type and value > 0: uptime = utcnow().replace(microsecond=0) - timedelta(seconds=value) if not isinstance(self._attr_native_value, datetime) or abs( diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py index 331b45e3de8..4446ac0cd55 100644 --- a/tests/components/nzbget/__init__.py +++ b/tests/components/nzbget/__init__.py @@ -49,6 +49,7 @@ MOCK_STATUS = { "PostPaused": False, "RemainingSizeMB": 512, "UpTimeSec": 600, + "DownloadLimit": 1000000, } MOCK_HISTORY = [ diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index 290414ab0ff..9cedf29c82e 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -40,6 +40,12 @@ async def test_sensors(hass, nzbget_api) -> None: "post_processing_paused": ("PostPaused", "False", None, None), "queue_size": ("RemainingSizeMB", "512", DATA_MEGABYTES, None), "uptime": ("UpTimeSec", uptime.isoformat(), None, SensorDeviceClass.TIMESTAMP), + "speed_limit": ( + "DownloadLimit", + "0.95", + DATA_RATE_MEGABYTES_PER_SECOND, + None, + ), } for (sensor_id, data) in sensors.items(): From e03eb238e208c572ab0e5366d9a70881b053a9b7 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 22 Aug 2022 09:03:32 +0200 Subject: [PATCH 545/903] Protect against an exception in Shelly climate platform (#77102) Check if state in HVACMode --- homeassistant/components/shelly/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 453d67f39a7..a3f42c4a928 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -188,7 +188,10 @@ class BlockSleepingClimate( def hvac_mode(self) -> HVACMode: """HVAC current mode.""" if self.device_block is None: - return HVACMode(self.last_state.state) if self.last_state else HVACMode.OFF + if self.last_state and self.last_state.state in list(HVACMode): + return HVACMode(self.last_state.state) + return HVACMode.OFF + if self.device_block.mode is None or self._check_is_off(): return HVACMode.OFF From ed60611b07e38e7009c6cc266c14625a751e7b32 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 09:13:14 +0200 Subject: [PATCH 546/903] Improve type hint in cast media_player entity (#77025) * Improve type hint in cast media_player entity * Update docstring --- homeassistant/components/cast/media_player.py | 95 +++++++++++-------- 1 file changed, 54 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index da32dfd6ae7..75d3de06856 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -7,6 +7,7 @@ from contextlib import suppress from datetime import datetime import json import logging +from typing import Any import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -52,7 +53,8 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -232,9 +234,9 @@ class CastDevice: self._status_listener = CastStatusListener( self, chromecast, self.mz_mgr, self._mz_only ) - self._chromecast.start() + chromecast.start() - async def _async_disconnect(self): + async def _async_disconnect(self) -> None: """Disconnect Chromecast object if it is set.""" if self._chromecast is not None: _LOGGER.debug( @@ -246,7 +248,7 @@ class CastDevice: self._invalidate() - def _invalidate(self): + def _invalidate(self) -> None: """Invalidate some attributes.""" self._chromecast = None self.mz_mgr = None @@ -254,7 +256,7 @@ class CastDevice: self._status_listener.invalidate() self._status_listener = None - async def _async_cast_discovered(self, discover: ChromecastInfo): + async def _async_cast_discovered(self, discover: ChromecastInfo) -> None: """Handle discovery of new Chromecast.""" if self._cast_info.uuid != discover.uuid: # Discovered is not our device. @@ -263,13 +265,19 @@ class CastDevice: _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) self._cast_info = discover - async def _async_cast_removed(self, discover: ChromecastInfo): + async def _async_cast_removed(self, discover: ChromecastInfo) -> None: """Handle removal of Chromecast.""" - async def _async_stop(self, event): + async def _async_stop(self, event: Event) -> None: """Disconnect socket on Home Assistant stop.""" await self._async_disconnect() + def _get_chromecast(self) -> pychromecast.Chromecast: + """Ensure chromecast is available, to facilitate type checking.""" + if self._chromecast is None: + raise HomeAssistantError("Chromecast is not available.") + return self._chromecast + class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Representation of a Cast device on the network.""" @@ -292,7 +300,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): self._attr_available = False self._hass_cast_controller: HomeAssistantController | None = None - self._cast_view_remove_handler = None + self._cast_view_remove_handler: CALLBACK_TYPE | None = None self._attr_unique_id = str(cast_info.uuid) self._attr_device_info = DeviceInfo( identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, @@ -307,7 +315,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): ]: self._attr_device_class = MediaPlayerDeviceClass.SPEAKER - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Create chromecast object when added to hass.""" self._async_setup(self.entity_id) @@ -491,62 +499,63 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return media_controller - def turn_on(self): + def turn_on(self) -> None: """Turn on the cast device.""" - if not self._chromecast.is_idle: + chromecast = self._get_chromecast() + if not chromecast.is_idle: # Already turned on return - if self._chromecast.app_id is not None: + if chromecast.app_id is not None: # Quit the previous app before starting splash screen or media player - self._chromecast.quit_app() + chromecast.quit_app() # The only way we can turn the Chromecast is on is by launching an app - if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: + if chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"} - quick_play(self._chromecast, "default_media_receiver", app_data) + quick_play(chromecast, "default_media_receiver", app_data) else: - self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) + chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) - def turn_off(self): + def turn_off(self) -> None: """Turn off the cast device.""" - self._chromecast.quit_app() + self._get_chromecast().quit_app() - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute the volume.""" - self._chromecast.set_volume_muted(mute) + self._get_chromecast().set_volume_muted(mute) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - self._chromecast.set_volume(volume) + self._get_chromecast().set_volume(volume) - def media_play(self): + def media_play(self) -> None: """Send play command.""" media_controller = self._media_controller() media_controller.play() - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" media_controller = self._media_controller() media_controller.pause() - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" media_controller = self._media_controller() media_controller.stop() - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" media_controller = self._media_controller() media_controller.queue_prev() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" media_controller = self._media_controller() media_controller.queue_next() - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Seek the media to a specific location.""" media_controller = self._media_controller() media_controller.seek(position) @@ -589,11 +598,14 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): children=sorted(children, key=lambda c: c.title), ) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" content_filter = None - if self._chromecast.cast_type in ( + chromecast = self._get_chromecast() + if chromecast.cast_type in ( pychromecast.const.CAST_TYPE_AUDIO, pychromecast.const.CAST_TYPE_GROUP, ): @@ -612,7 +624,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): self.hass, media_content_type, media_content_id, - self._chromecast.cast_type, + chromecast.cast_type, ) if browse_media: return browse_media @@ -621,8 +633,11 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): self.hass, media_content_id, content_filter=content_filter ) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" + chromecast = self._get_chromecast() # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( @@ -648,9 +663,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if "app_id" in app_data: app_id = app_data.pop("app_id") _LOGGER.info("Starting Cast app by ID %s", app_id) - await self.hass.async_add_executor_job( - self._chromecast.start_app, app_id - ) + await self.hass.async_add_executor_job(chromecast.start_app, app_id) if app_data: _LOGGER.warning( "Extra keys %s were ignored. Please use app_name to cast media", @@ -661,7 +674,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): app_name = app_data.pop("app_name") try: await self.hass.async_add_executor_job( - quick_play, self._chromecast, app_name, app_data + quick_play, chromecast, app_name, app_data ) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) @@ -670,7 +683,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): # Try the cast platforms for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): result = await platform.async_play_media( - self.hass, self.entity_id, self._chromecast, media_type, media_id + self.hass, self.entity_id, chromecast, media_type, media_id ) if result: return @@ -735,7 +748,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): app_data, ) await self.hass.async_add_executor_job( - quick_play, self._chromecast, "default_media_receiver", app_data + quick_play, chromecast, "default_media_receiver", app_data ) def _media_status(self): @@ -761,7 +774,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return (media_status, media_status_received) @property - def state(self): + def state(self) -> str | None: """Return the state of the player.""" # The lovelace app loops media to prevent timing out, don't show that if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: @@ -785,7 +798,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return None @property - def media_content_id(self): + def media_content_id(self) -> str | None: """Content ID of current playing media.""" # The lovelace app loops media to prevent timing out, don't show that if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: @@ -794,7 +807,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return media_status.content_id if media_status else None @property - def media_content_type(self): + def media_content_type(self) -> str | None: """Content type of current playing media.""" # The lovelace app loops media to prevent timing out, don't show that if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: From 1940d9a377c78f0991cf35e887b35b1c22381efa Mon Sep 17 00:00:00 2001 From: Khole Date: Mon, 22 Aug 2022 08:20:12 +0100 Subject: [PATCH 547/903] Hive Add ability to trigger the alarm (#76985) * Add ability to trigger the alarm * Add mapping for sos state when triggered manually * Update homeassistant/components/hive/alarm_control_panel.py Co-authored-by: Erik Montnemery * Update homeassistant/components/hive/alarm_control_panel.py Co-authored-by: Erik Montnemery * Fix linter issues Co-authored-by: Erik Montnemery --- homeassistant/components/hive/alarm_control_panel.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py index 48b59e351be..5f0b3d8f03c 100644 --- a/homeassistant/components/hive/alarm_control_panel.py +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -27,6 +27,7 @@ HIVETOHA = { "home": STATE_ALARM_DISARMED, "asleep": STATE_ALARM_ARMED_NIGHT, "away": STATE_ALARM_ARMED_AWAY, + "sos": STATE_ALARM_TRIGGERED, } @@ -49,6 +50,7 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity): _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_NIGHT | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.TRIGGER ) async def async_alarm_disarm(self, code: str | None = None) -> None: @@ -63,6 +65,10 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity): """Send arm away command.""" await self.hive.alarm.setMode(self.device, "away") + async def async_alarm_trigger(self, code=None) -> None: + """Send alarm trigger command.""" + await self.hive.alarm.setMode(self.device, "sos") + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) From 8cd04750fcd68c64d4f6e4d2ff4dcdd98a90511e Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Mon, 22 Aug 2022 01:22:43 -0600 Subject: [PATCH 548/903] Support send SMS using GSM alphabet (#76834) * Fix #76283 Fix #76283 * Update notify.py * Load SMS via discovery * Put back send as ANSI --- homeassistant/components/sms/const.py | 1 + homeassistant/components/sms/notify.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py index 858e53d9808..841c4bd8f89 100644 --- a/homeassistant/components/sms/const.py +++ b/homeassistant/components/sms/const.py @@ -13,6 +13,7 @@ NETWORK_COORDINATOR = "network_coordinator" GATEWAY = "gateway" DEFAULT_SCAN_INTERVAL = 30 CONF_BAUD_SPEED = "baud_speed" +CONF_UNICODE = "unicode" DEFAULT_BAUD_SPEED = "0" DEFAULT_BAUD_SPEEDS = [ {"value": DEFAULT_BAUD_SPEED, "label": "Auto"}, diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index 21b48946f55..d82e4951c00 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -8,7 +8,7 @@ from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationSer from homeassistant.const import CONF_NAME, CONF_RECIPIENT, CONF_TARGET import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, GATEWAY, SMS_GATEWAY +from .const import CONF_UNICODE, DOMAIN, GATEWAY, SMS_GATEWAY _LOGGER = logging.getLogger(__name__) @@ -47,9 +47,10 @@ class SMSNotificationService(BaseNotificationService): gateway = self.hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] targets = kwargs.get(CONF_TARGET, [self.number]) + is_unicode = kwargs.get(CONF_UNICODE, True) smsinfo = { "Class": -1, - "Unicode": True, + "Unicode": is_unicode, "Entries": [{"ID": "ConcatenatedTextLong", "Buffer": message}], } try: From 361f82f5faa0f7ccc29bd8412c2e0e702786a3fb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 11:34:20 +0200 Subject: [PATCH 549/903] Improve type hints in epson media player (#77129) --- .../components/epson/media_player.py | 116 ++++++------------ 1 file changed, 37 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index f978c145b4f..57bb0165f6a 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from epson_projector import Projector from epson_projector.const import ( BACK, BUSY, @@ -52,13 +53,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Epson projector from a config entry.""" - entry_id = config_entry.entry_id - unique_id = config_entry.unique_id - projector = hass.data[DOMAIN][entry_id] + projector: Projector = hass.data[DOMAIN][config_entry.entry_id] projector_entity = EpsonProjectorMediaPlayer( projector=projector, name=config_entry.title, - unique_id=unique_id, + unique_id=config_entry.unique_id, entry=config_entry, ) async_add_entities([projector_entity], True) @@ -83,23 +82,30 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): | MediaPlayerEntityFeature.PREVIOUS_TRACK ) - def __init__(self, projector, name, unique_id, entry): + def __init__( + self, projector: Projector, name: str, unique_id: str | None, entry: ConfigEntry + ) -> None: """Initialize entity to control Epson projector.""" self._projector = projector self._entry = entry - self._name = name - self._available = False + self._attr_name = name + self._attr_available = False self._cmode = None - self._source_list = list(DEFAULT_SOURCES.values()) - self._source = None - self._volume = None - self._state = None - self._unique_id = unique_id + self._attr_source_list = list(DEFAULT_SOURCES.values()) + self._attr_unique_id = unique_id + if unique_id: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="Epson", + model="Epson", + name="Epson projector", + via_device=(DOMAIN, unique_id), + ) - async def set_unique_id(self): + async def set_unique_id(self) -> bool: """Set unique id for projector config entry.""" _LOGGER.debug("Setting unique_id for projector") - if self._unique_id: + if self.unique_id: return False if uid := await self._projector.get_serial_number(): self.hass.config_entries.async_update_entry(self._entry, unique_id=uid) @@ -113,96 +119,48 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): self.hass.config_entries.async_reload(self._entry.entry_id) ) return True + return False async def async_update(self) -> None: """Update state of device.""" power_state = await self._projector.get_power() _LOGGER.debug("Projector status: %s", power_state) if not power_state or power_state == EPSON_STATE_UNAVAILABLE: - self._available = False + self._attr_available = False return - self._available = True + self._attr_available = True if power_state == EPSON_CODES[POWER]: - self._state = STATE_ON + self._attr_state = STATE_ON if await self.set_unique_id(): return - self._source_list = list(DEFAULT_SOURCES.values()) + self._attr_source_list = list(DEFAULT_SOURCES.values()) cmode = await self._projector.get_property(CMODE) self._cmode = CMODE_LIST.get(cmode, self._cmode) source = await self._projector.get_property(SOURCE) - self._source = SOURCE_LIST.get(source, self._source) - volume = await self._projector.get_property(VOLUME) - if volume: + self._attr_source = SOURCE_LIST.get(source, self._attr_source) + if volume := await self._projector.get_property(VOLUME): try: - self._volume = float(volume) + self._attr_volume_level = float(volume) except ValueError: - self._volume = None + self._attr_volume_level = None elif power_state == BUSY: - self._state = STATE_ON + self._attr_state = STATE_ON else: - self._state = STATE_OFF - - @property - def device_info(self) -> DeviceInfo | None: - """Get attributes about the device.""" - if not self._unique_id: - return None - return DeviceInfo( - identifiers={(DOMAIN, self._unique_id)}, - manufacturer="Epson", - model="Epson", - name="Epson projector", - via_device=(DOMAIN, self._unique_id), - ) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return unique ID.""" - return self._unique_id - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def available(self): - """Return if projector is available.""" - return self._available + self._attr_state = STATE_OFF async def async_turn_on(self) -> None: """Turn on epson.""" - if self._state == STATE_OFF: + if self.state == STATE_OFF: await self._projector.send_command(TURN_ON) - self._state = STATE_ON + self._attr_state = STATE_ON async def async_turn_off(self) -> None: """Turn off epson.""" - if self._state == STATE_ON: + if self.state == STATE_ON: await self._projector.send_command(TURN_OFF) - self._state = STATE_OFF + self._attr_state = STATE_OFF - @property - def source_list(self): - """List of available input sources.""" - return self._source_list - - @property - def source(self): - """Get current input sources.""" - return self._source - - @property - def volume_level(self): - """Return the volume level of the media player (0..1).""" - return self._volume - - async def select_cmode(self, cmode): + async def select_cmode(self, cmode: str) -> None: """Set color mode in Epson.""" await self._projector.send_command(CMODE_LIST_SET[cmode]) @@ -240,7 +198,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): await self._projector.send_command(BACK) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return device specific state attributes.""" if self._cmode is None: return {} From 5cb91d7cefe75523a568a265ca76be36347fc9d1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 11:36:59 +0200 Subject: [PATCH 550/903] Improve type hint in eddystone sensor entity (#77135) --- .../eddystone_temperature/sensor.py | 54 +++++++------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 4178f577021..d0bf4a87cc2 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -24,7 +24,7 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -59,10 +59,10 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Validate configuration, create devices and start monitoring thread.""" - bt_device_id = config.get("bt_device_id") + bt_device_id: int = config[CONF_BT_DEVICE_ID] - beacons = config[CONF_BEACONS] - devices = [] + beacons: dict[str, dict[str, str]] = config[CONF_BEACONS] + devices: list[EddystoneTemp] = [] for dev_name, properties in beacons.items(): namespace = get_from_conf(properties, CONF_NAMESPACE, 20) @@ -78,12 +78,12 @@ def setup_platform( if devices: mon = Monitor(hass, devices, bt_device_id) - def monitor_stop(_service_or_event): + def monitor_stop(event: Event) -> None: """Stop the monitor thread.""" _LOGGER.info("Stopping scanner for Eddystone beacons") mon.stop() - def monitor_start(_service_or_event): + def monitor_start(event: Event) -> None: """Start the monitor thread.""" _LOGGER.info("Starting scanner for Eddystone beacons") mon.start() @@ -96,9 +96,9 @@ def setup_platform( _LOGGER.warning("No devices were added") -def get_from_conf(config, config_key, length): +def get_from_conf(config: dict[str, str], config_key: str, length: int) -> str | None: """Retrieve value from config and validate length.""" - string = config.get(config_key) + string = config[config_key] if len(string) != length: _LOGGER.error( "Error in configuration parameter %s: Must be exactly %d " @@ -113,44 +113,30 @@ def get_from_conf(config, config_key, length): class EddystoneTemp(SensorEntity): """Representation of a temperature sensor.""" - def __init__(self, name, namespace, instance): + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = TEMP_CELSIUS + _attr_should_poll = False + + def __init__(self, name: str, namespace: str, instance: str) -> None: """Initialize a sensor.""" - self._name = name + self._attr_name = name self.namespace = namespace self.instance = instance self.bt_addr = None self.temperature = STATE_UNKNOWN - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def native_value(self): """Return the state of the device.""" return self.temperature - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return SensorDeviceClass.TEMPERATURE - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return TEMP_CELSIUS - - @property - def should_poll(self): - """Return the polling state.""" - return False - class Monitor: """Continuously scan for BLE advertisements.""" - def __init__(self, hass, devices, bt_device_id): + def __init__( + self, hass: HomeAssistant, devices: list[EddystoneTemp], bt_device_id: int + ) -> None: """Construct interface object.""" self.hass = hass @@ -174,7 +160,7 @@ class Monitor: ) self.scanning = False - def start(self): + def start(self) -> None: """Continuously scan for BLE advertisements.""" if not self.scanning: self.scanner.start() @@ -182,7 +168,7 @@ class Monitor: else: _LOGGER.debug("start() called, but scanner is already running") - def process_packet(self, namespace, instance, temperature): + def process_packet(self, namespace, instance, temperature) -> None: """Assign temperature to device.""" _LOGGER.debug( "Received temperature for <%s,%s>: %d", namespace, instance, temperature @@ -197,7 +183,7 @@ class Monitor: dev.temperature = temperature dev.schedule_update_ha_state() - def stop(self): + def stop(self) -> None: """Signal runner to stop and join thread.""" if self.scanning: _LOGGER.debug("Stopping") From 5a0e4fa5eedff0cda29f196c5a05f0fd2cf84f75 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 22 Aug 2022 12:55:30 +0200 Subject: [PATCH 551/903] Add hide attribute support to attribute selector (#77072) Co-authored-by: Erik Montnemery --- homeassistant/helpers/selector.py | 12 ++++++++++-- tests/helpers/test_selector.py | 5 +++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 9655f93ace5..93abd6ca4e4 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -206,10 +206,11 @@ class AreaSelector(Selector): return [vol.Schema(str)(val) for val in data] -class AttributeSelectorConfig(TypedDict): +class AttributeSelectorConfig(TypedDict, total=False): """Class to represent an attribute selector config.""" entity_id: str + hide_attributes: list[str] @SELECTORS.register("attribute") @@ -218,7 +219,14 @@ class AttributeSelector(Selector): selector_type = "attribute" - CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) + CONFIG_SCHEMA = vol.Schema( + { + vol.Required("entity_id"): cv.entity_id, + # hide_attributes is used to hide attributes in the frontend. + # A hidden attribute can still be provided manually. + vol.Optional("hide_attributes"): [str], + } + ) def __init__(self, config: AttributeSelectorConfig) -> None: """Instantiate a selector.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 2d870a85f6f..c809eaea8bd 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -458,6 +458,11 @@ def test_select_selector_schema_error(schema): ("friendly_name", "device_class"), (None,), ), + ( + {"entity_id": "sensor.abc", "hide_attributes": ["friendly_name"]}, + ("device_class", "state_class"), + (None,), + ), ), ) def test_attribute_selector_schema(schema, valid_selections, invalid_selections): From 9467b7d01886c7e8dd19f7074c7ed6422f49f3bc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 13:27:02 +0200 Subject: [PATCH 552/903] Improve type hint in eq3btsmart climate entity (#77131) --- .../components/eq3btsmart/climate.py | 55 ++++--------------- 1 file changed, 12 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index de75f04f91e..e469512123b 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -100,37 +100,26 @@ def setup_platform( class EQ3BTSmartThermostat(ClimateEntity): """Representation of an eQ-3 Bluetooth Smart thermostat.""" + _attr_hvac_modes = list(HA_TO_EQ_HVAC) + _attr_precision = PRECISION_HALVES + _attr_preset_modes = list(HA_TO_EQ_PRESET) _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) + _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, _mac, _name): + def __init__(self, mac: str, name: str) -> None: """Initialize the thermostat.""" # We want to avoid name clash with this module. - self._name = _name - self._mac = _mac - self._thermostat = eq3.Thermostat(_mac) + self._attr_name = name + self._attr_unique_id = format_mac(mac) + self._thermostat = eq3.Thermostat(mac) @property def available(self) -> bool: """Return if thermostat is available.""" return self._thermostat.mode >= 0 - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement that is used.""" - return TEMP_CELSIUS - - @property - def precision(self): - """Return eq3bt's precision 0.5.""" - return PRECISION_HALVES - @property def current_temperature(self): """Can not report temperature, so return target_temperature.""" @@ -148,17 +137,12 @@ class EQ3BTSmartThermostat(ClimateEntity): self._thermostat.target_temperature = temperature @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode: """Return the current operation mode.""" if self._thermostat.mode < 0: return HVACMode.OFF return EQ_TO_HA_HVAC[self._thermostat.mode] - @property - def hvac_modes(self): - """Return the list of available operation modes.""" - return list(HA_TO_EQ_HVAC) - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set operation mode.""" self._thermostat.mode = HA_TO_EQ_HVAC[hvac_mode] @@ -174,9 +158,9 @@ class EQ3BTSmartThermostat(ClimateEntity): return self._thermostat.max_temp @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" - dev_specific = { + return { ATTR_STATE_AWAY_END: self._thermostat.away_end, ATTR_STATE_LOCKED: self._thermostat.locked, ATTR_STATE_LOW_BAT: self._thermostat.low_battery, @@ -184,29 +168,14 @@ class EQ3BTSmartThermostat(ClimateEntity): ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open, } - return dev_specific - @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp. Requires ClimateEntityFeature.PRESET_MODE. """ return EQ_TO_HA_PRESET.get(self._thermostat.mode) - @property - def preset_modes(self): - """Return a list of available preset modes. - - Requires ClimateEntityFeature.PRESET_MODE. - """ - return list(HA_TO_EQ_PRESET) - - @property - def unique_id(self) -> str: - """Return the MAC address of the thermostat.""" - return format_mac(self._mac) - def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode == PRESET_NONE: From 6693cfd036dd4d2f638dd617c6023a6e82767588 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 13:27:36 +0200 Subject: [PATCH 553/903] Improve type hint in ecobee climate entity (#77133) --- homeassistant/components/ecobee/climate.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index d256d241a4f..6a96b5418b6 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -303,6 +303,9 @@ async def async_setup_entry( class Thermostat(ClimateEntity): """A thermostat class for Ecobee.""" + _attr_precision = PRECISION_TENTHS + _attr_temperature_unit = TEMP_FAHRENHEIT + def __init__(self, data, thermostat_index, thermostat): """Initialize the thermostat.""" self.data = data @@ -381,16 +384,6 @@ class Thermostat(ClimateEntity): name=self.name, ) - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def precision(self) -> float: - """Return the precision of the system.""" - return PRECISION_TENTHS - @property def current_temperature(self) -> float: """Return the current temperature.""" From d7685f869a682dbd1adec31676e84c2afb0cc485 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 13:30:28 +0200 Subject: [PATCH 554/903] Improve type hint in emby media-player entity (#77136) --- homeassistant/components/emby/media_player.py | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index d2dcfa2c629..014f9e2ac1d 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -141,6 +141,8 @@ async def async_setup_platform( class EmbyDevice(MediaPlayerEntity): """Representation of an Emby device.""" + _attr_should_poll = False + def __init__(self, emby, device_id): """Initialize the Emby device.""" _LOGGER.debug("New Emby Device initialized with ID: %s", device_id) @@ -148,11 +150,11 @@ class EmbyDevice(MediaPlayerEntity): self.device_id = device_id self.device = self.emby.devices[self.device_id] - self._available = True - self.media_status_last_position = None self.media_status_received = None + self._attr_unique_id = device_id + async def async_added_to_hass(self) -> None: """Register callback.""" self.emby.add_update_callback(self.async_update_callback, self.device_id) @@ -172,19 +174,9 @@ class EmbyDevice(MediaPlayerEntity): self.async_write_ha_state() - @property - def available(self): - """Return True if entity is available.""" - return self._available - - def set_available(self, value): + def set_available(self, value: bool) -> None: """Set available property.""" - self._available = value - - @property - def unique_id(self): - """Return the id of this emby client.""" - return self.device_id + self._attr_available = value @property def supports_remote_control(self): @@ -196,11 +188,6 @@ class EmbyDevice(MediaPlayerEntity): """Return the name of the device.""" return f"Emby {self.device.name}" or DEVICE_DEFAULT_NAME - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - @property def state(self): """Return the state of the device.""" From b108ddbfd32fe6e3c5ff4475e9c93ed9de47ccc4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 13:32:06 +0200 Subject: [PATCH 555/903] Improve type hint in ephember climate entity (#77138) --- homeassistant/components/ephember/climate.py | 44 ++++++-------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 7c9d9c04318..f308e116ed6 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -80,6 +80,9 @@ def setup_platform( class EphEmberThermostat(ClimateEntity): """Representation of a EphEmber thermostat.""" + _attr_hvac_modes = OPERATION_LIST + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, ember, zone): """Initialize the thermostat.""" self._ember = ember @@ -87,23 +90,15 @@ class EphEmberThermostat(ClimateEntity): self._zone = zone self._hot_water = zone_is_hot_water(zone) - @property - def supported_features(self): - """Return the list of supported features.""" + self._attr_name = self._zone_name + + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.AUX_HEAT + ) + self._attr_target_temperature_step = 0.5 if self._hot_water: - return ClimateEntityFeature.AUX_HEAT - - return ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.AUX_HEAT - - @property - def name(self): - """Return the name of the thermostat, if any.""" - return self._zone_name - - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS + self._attr_supported_features = ClimateEntityFeature.AUX_HEAT + self._attr_target_temperature_step = None @property def current_temperature(self): @@ -116,15 +111,7 @@ class EphEmberThermostat(ClimateEntity): return zone_target_temperature(self._zone) @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - if self._hot_water: - return None - - return 0.5 - - @property - def hvac_action(self): + def hvac_action(self) -> HVACAction: """Return current HVAC action.""" if zone_is_active(self._zone): return HVACAction.HEATING @@ -132,16 +119,11 @@ class EphEmberThermostat(ClimateEntity): return HVACAction.IDLE @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode: """Return current operation ie. heat, cool, idle.""" mode = zone_mode(self._zone) return self.map_mode_eph_hass(mode) - @property - def hvac_modes(self): - """Return the supported operations.""" - return OPERATION_LIST - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the operation mode.""" mode = self.map_mode_hass_eph(hvac_mode) From 58b9785485af4b49097707edb7fbcc00c72a3df0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 13:36:33 +0200 Subject: [PATCH 556/903] Improve entity type hints [f] (#77143) --- homeassistant/components/fail2ban/sensor.py | 2 +- homeassistant/components/ffmpeg/camera.py | 5 +- homeassistant/components/fibaro/climate.py | 9 +-- homeassistant/components/fibaro/light.py | 7 ++- homeassistant/components/fibaro/sensor.py | 2 +- homeassistant/components/fido/sensor.py | 2 +- homeassistant/components/file/sensor.py | 2 +- homeassistant/components/filter/sensor.py | 2 +- .../components/fireservicerota/switch.py | 6 +- homeassistant/components/firmata/switch.py | 5 +- homeassistant/components/fixer/sensor.py | 2 +- homeassistant/components/flexit/climate.py | 2 +- .../components/flick_electric/sensor.py | 5 +- homeassistant/components/flo/switch.py | 8 ++- homeassistant/components/flume/sensor.py | 2 +- homeassistant/components/flux/switch.py | 7 ++- homeassistant/components/folder/sensor.py | 2 +- homeassistant/components/foobot/sensor.py | 2 +- .../components/forked_daapd/media_player.py | 55 ++++++++++--------- homeassistant/components/foscam/camera.py | 6 +- .../components/freebox/device_tracker.py | 4 +- homeassistant/components/freebox/sensor.py | 2 +- homeassistant/components/freebox/switch.py | 7 ++- homeassistant/components/fritzbox/climate.py | 2 +- .../frontier_silicon/media_player.py | 24 ++++---- 25 files changed, 93 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index f74dc7690ca..22b4bfe6ea1 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -84,7 +84,7 @@ class BanSensor(SensorEntity): """Return the most recently banned IP Address.""" return self.last_ban - def update(self): + def update(self) -> None: """Update the list of banned ips.""" self.log_parser.read_log(self.jail) diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index a15dbe654b0..fb2519fb071 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -1,6 +1,7 @@ """Support for Cameras with FFmpeg as decoder.""" from __future__ import annotations +from aiohttp import web from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG import voluptuous as vol @@ -66,7 +67,9 @@ class FFmpegCamera(Camera): extra_cmd=self._extra_arguments, ) - async def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream( + self, request: web.Request + ) -> web.StreamResponse: """Generate an HTTP MJPEG stream from the camera.""" stream = CameraMjpeg(self._manager.binary) diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index c685945fa45..a2d322528db 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity from homeassistant.components.climate.const import ( @@ -199,7 +200,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): if mode in OPMODES_PRESET: self._preset_support.append(OPMODES_PRESET[mode]) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" _LOGGER.debug( "Climate %s\n" @@ -241,7 +242,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): mode = int(self._fan_mode_device.fibaro_device.properties.mode) return FANMODES[mode] - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if not self._fan_mode_device: return @@ -270,7 +271,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): return [HVACMode.AUTO] # Default to this return self._hvac_support - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target operation mode.""" if not self._op_mode_device: return @@ -346,7 +347,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): return float(device.properties.targetLevel) return None - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" temperature = kwargs.get(ATTR_TEMPERATURE) target = self._target_temp_device diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 300b7c4c5f8..0913a18b405 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from contextlib import suppress from functools import partial +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -107,7 +108,7 @@ class FibaroLight(FibaroDevice, LightEntity): super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" async with self._update_lock: await self.hass.async_add_executor_job(partial(self._turn_on, **kwargs)) @@ -134,7 +135,7 @@ class FibaroLight(FibaroDevice, LightEntity): # The simplest case is left for last. No dimming, just switch on self.call_turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" async with self._update_lock: await self.hass.async_add_executor_job(partial(self._turn_off, **kwargs)) @@ -167,7 +168,7 @@ class FibaroLight(FibaroDevice, LightEntity): return False - async def async_update(self): + async def async_update(self) -> None: """Update the state.""" async with self._update_lock: await self.hass.async_add_executor_job(self._update) diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 88d6113ebb9..797fc6d8d44 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -142,7 +142,7 @@ class FibaroSensor(FibaroDevice, SensorEntity): fibaro_device.properties.unit, fibaro_device.properties.unit ) - def update(self): + def update(self) -> None: """Update the state.""" with suppress(KeyError, ValueError): self._attr_native_value = float(self.fibaro_device.properties.value) diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 2d0cd4ab53a..3eeb4dd0dc8 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -224,7 +224,7 @@ class FidoSensor(SensorEntity): """Return the state attributes of the sensor.""" return {"number": self._number} - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Fido and update the state.""" await self.fido_data.async_update() if (sensor_type := self.entity_description.key) == "balance": diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 8c0966f30bd..82eb5880b79 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -75,7 +75,7 @@ class FileSensor(SensorEntity): self._attr_native_unit_of_measurement = unit_of_measurement self._val_tpl = value_template - def update(self): + def update(self) -> None: """Get the latest entry from a file and updates the state.""" try: with FileReadBackwards(self._file_path, encoding="utf-8") as file_data: diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 46a3c0ec963..fead10c71a3 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -277,7 +277,7 @@ class SensorFilter(SensorEntity): if update_ha: self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if "recorder" in self.hass.config.components: diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index 48ec1a77c54..583125873d0 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -69,7 +69,7 @@ class ResponseSwitch(SwitchEntity): return False @property - def available(self): + def available(self) -> bool: """Return if switch is available.""" return self._client.on_duty @@ -99,11 +99,11 @@ class ResponseSwitch(SwitchEntity): return attr - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Send Acknowledge response status.""" await self.async_set_response(True) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send Reject response status.""" await self.async_set_response(False) diff --git a/homeassistant/components/firmata/switch.py b/homeassistant/components/firmata/switch.py index 5c3f07bafe1..10037bb6a7a 100644 --- a/homeassistant/components/firmata/switch.py +++ b/homeassistant/components/firmata/switch.py @@ -1,5 +1,6 @@ """Support for Firmata switch output.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -57,12 +58,12 @@ class FirmataSwitch(FirmataPinEntity, SwitchEntity): """Return true if switch is on.""" return self._api.is_on - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" await self._api.turn_on() self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" await self._api.turn_off() self.async_write_ha_state() diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index c05bd5a756f..234f03812fe 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -99,7 +99,7 @@ class ExchangeRateSensor(SensorEntity): """Return the icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" self.data.update() self._state = round(self.data.rate["rates"][self._target], 3) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 1c9f752c15b..3c5aca2928c 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -210,7 +210,7 @@ class Flexit(ClimateEntity): else: _LOGGER.error("Modbus error setting target temperature to Flexit") - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" if await self._async_write_int16_to_register( 17, self.fan_modes.index(fan_mode) diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index 94638695b23..215c9a88f6b 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -1,6 +1,7 @@ """Support for Flick Electric Pricing data.""" from datetime import timedelta import logging +from typing import Any import async_timeout from pyflick import FlickAPI, FlickPrice @@ -45,7 +46,7 @@ class FlickPricingSensor(SensorEntity): """Entity object for Flick Electric sensor.""" self._api: FlickAPI = api self._price: FlickPrice = None - self._attributes = { + self._attributes: dict[str, Any] = { ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_FRIENDLY_NAME: FRIENDLY_NAME, } @@ -65,7 +66,7 @@ class FlickPricingSensor(SensorEntity): """Return the state attributes.""" return self._attributes - async def async_update(self): + async def async_update(self) -> None: """Get the Flick Pricing data from the web service.""" if self._price and self._price.end_at >= utcnow(): return # Power price data is still valid diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index 884b76fc64e..84c37bb4987 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -1,6 +1,8 @@ """Switch representing the shutoff valve for the Flo by Moen integration.""" from __future__ import annotations +from typing import Any + from aioflo.location import SLEEP_MINUTE_OPTIONS, SYSTEM_MODE_HOME, SYSTEM_REVERT_MODES import voluptuous as vol @@ -83,13 +85,13 @@ class FloSwitch(FloEntity, SwitchEntity): return "mdi:valve-open" return "mdi:valve-closed" - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Open the valve.""" await self._device.api_client.device.open_valve(self._device.id) self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Close the valve.""" await self._device.api_client.device.close_valve(self._device.id) self._state = False @@ -101,7 +103,7 @@ class FloSwitch(FloEntity, SwitchEntity): self._state = self._device.last_known_valve_state == "open" self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self.async_on_remove(self._device.async_add_listener(self.async_update_state)) diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index f6d666d3eae..bc98750cc9f 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -124,7 +124,7 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): return _format_state_value(self._flume_device.values[sensor_key]) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Request an update when added.""" await super().async_added_to_hass() # We do not ask for an update with async_add_entities() diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 2d175702047..baa2d658d23 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -7,6 +7,7 @@ from __future__ import annotations import datetime import logging +from typing import Any import voluptuous as vol @@ -220,13 +221,13 @@ class FluxSwitch(SwitchEntity, RestoreEntity): """Return true if switch is on.""" return self.unsub_tracker is not None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" last_state = await self.async_get_last_state() if last_state and last_state.state == STATE_ON: await self.async_turn_on() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on flux.""" if self.is_on: return @@ -242,7 +243,7 @@ class FluxSwitch(SwitchEntity, RestoreEntity): self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off flux.""" if self.is_on: self.unsub_tracker() diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index bfad45318e6..13295d069a3 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -76,7 +76,7 @@ class Folder(SensorEntity): self._unit_of_measurement = DATA_MEGABYTES self._file_list = None - def update(self): + def update(self) -> None: """Update the sensor.""" files_list = get_files_list(self._folder_path, self._filter_term) self._file_list = files_list diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index c65b7420368..e1d3c8de8d2 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -158,7 +158,7 @@ class FoobotSensor(SensorEntity): data = None return data - async def async_update(self): + async def async_update(self) -> None: """Get the latest data.""" await self.foobot_data.async_update() diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 25695dceeb5..81a7a65cf36 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -2,6 +2,7 @@ import asyncio from collections import defaultdict import logging +from typing import Any from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI @@ -138,7 +139,7 @@ class ForkedDaapdZone(MediaPlayerEntity): self._available = True self._entry_id = entry_id - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Use lifecycle hooks.""" self.async_on_remove( async_dispatcher_connect( @@ -168,7 +169,7 @@ class ForkedDaapdZone(MediaPlayerEntity): """Entity pushes its state to HA.""" return False - async def async_toggle(self): + async def async_toggle(self) -> None: """Toggle the power on the zone.""" if self.state == STATE_OFF: await self.async_turn_on() @@ -180,21 +181,21 @@ class ForkedDaapdZone(MediaPlayerEntity): """Return whether the zone is available.""" return self._available - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Enable the output.""" await self._api.change_output(self._output_id, selected=True) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Disable the output.""" await self._api.change_output(self._output_id, selected=False) @property - def name(self): + def name(self) -> str: """Return the name of the zone.""" return f"{FD_NAME} output ({self._output['name']})" @property - def state(self): + def state(self) -> str: """State of the zone.""" if self._output["selected"]: return STATE_ON @@ -206,11 +207,11 @@ class ForkedDaapdZone(MediaPlayerEntity): return self._output["volume"] / 100 @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool: """Boolean if volume is currently muted.""" return self._output["volume"] == 0 - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" if mute: if self.volume_level == 0: @@ -221,7 +222,7 @@ class ForkedDaapdZone(MediaPlayerEntity): target_volume = self._last_volume # restore volume level await self.async_set_volume_level(volume=target_volume) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume - input range [0,1].""" await self._api.set_volume(volume=volume * 100, output_id=self._output_id) @@ -270,7 +271,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._source = SOURCE_NAME_DEFAULT self._max_playlists = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Use lifecycle hooks.""" self.async_on_remove( async_dispatcher_connect( @@ -421,7 +422,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): """Return whether the master is available.""" return self._available - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Restore the last on outputs state.""" # restore state await self._api.set_volume(volume=self._last_volume * 100) @@ -441,14 +442,14 @@ class ForkedDaapdMaster(MediaPlayerEntity): [output["id"] for output in self._outputs] ) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Pause player and store outputs state.""" await self.async_media_pause() self._last_outputs = self._outputs if any(output["selected"] for output in self._outputs): await self._api.set_enabled_outputs([]) - async def async_toggle(self): + async def async_toggle(self) -> None: """Toggle the power on the device. Default media player component method counts idle as off. @@ -460,7 +461,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): await self.async_turn_off() @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{FD_NAME} server" @@ -564,7 +565,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): """List of available input sources.""" return [*self._sources_uris] - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" if mute: if self.volume_level == 0: @@ -575,32 +576,32 @@ class ForkedDaapdMaster(MediaPlayerEntity): target_volume = self._last_volume # restore volume level await self._api.set_volume(volume=target_volume * 100) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume - input range [0,1].""" await self._api.set_volume(volume=volume * 100) - async def async_media_play(self): + async def async_media_play(self) -> None: """Start playback.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_play") else: await self._api.start_playback() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause playback.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_pause") else: await self._api.pause_playback() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Stop playback.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_stop") else: await self._api.stop_playback() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Skip to previous track.""" if self._use_pipe_control(): await self._pipe_call( @@ -609,22 +610,22 @@ class ForkedDaapdMaster(MediaPlayerEntity): else: await self._api.previous_track() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Skip to next track.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_next_track") else: await self._api.next_track() - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Seek to position.""" await self._api.seek(position_ms=position * 1000) - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear playlist.""" await self._api.clear_queue() - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" await self._api.shuffle(shuffle) @@ -662,7 +663,9 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._pause_requested = False self._paused_event.clear() - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play a URI.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC @@ -738,7 +741,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): else: _LOGGER.debug("Media type '%s' not supported", media_type) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Change source. Source name reflects whether in default mode or pipe mode. diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index d3eaf0d03d3..cf2740ab9cc 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -109,7 +109,7 @@ class HassFoscamCamera(Camera): if self._rtsp_port: self._attr_supported_features = CameraEntityFeature.STREAM - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity addition to hass.""" # Get motion detection status ret, response = await self.hass.async_add_executor_job( @@ -159,7 +159,7 @@ class HassFoscamCamera(Camera): """Camera Motion Detection Status.""" return self._motion_status - def enable_motion_detection(self): + def enable_motion_detection(self) -> None: """Enable motion detection in camera.""" try: ret = self._foscam_session.enable_motion_detection() @@ -179,7 +179,7 @@ class HassFoscamCamera(Camera): self._name, ) - def disable_motion_detection(self): + def disable_motion_detection(self) -> None: """Disable motion detection.""" try: ret = self._foscam_session.disable_motion_detection() diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 1fd7a35e975..8b5ce4fd6e8 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -93,7 +93,7 @@ class FreeboxDevice(ScannerEntity): return self._name @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the network.""" return self._active @@ -123,7 +123,7 @@ class FreeboxDevice(ScannerEntity): self.async_update_state() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" self.async_update_state() self.async_on_remove( diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 450456b9146..e2c42928e80 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -100,7 +100,7 @@ class FreeboxSensor(SensorEntity): self.async_update_state() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" self.async_update_state() self.async_on_remove( diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index ee59c097f93..ab5a1160b71 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from freebox_api.exceptions import InsufficientPermissionsError @@ -65,15 +66,15 @@ class FreeboxWifiSwitch(SwitchEntity): "Home Assistant does not have permissions to modify the Freebox settings. Please refer to documentation" ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._async_set_state(True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._async_set_state(False) - async def async_update(self): + async def async_update(self) -> None: """Get the state and update it.""" datas = await self._router.wifi.get_global_config() active = datas["enabled"] diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 01e6e8ccae6..5d96cb95c15 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -122,7 +122,7 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): """Return the list of available operation modes.""" return OPERATION_LIST - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" if hvac_mode == HVACMode.OFF: await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index b16374c0bc0..9a51847d760 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -207,59 +207,59 @@ class AFSAPIDevice(MediaPlayerEntity): # Management actions # power control - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the device.""" await self.fs_device.set_power(True) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the device.""" await self.fs_device.set_power(False) - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self.fs_device.play() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self.fs_device.pause() - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Send play/pause command.""" if self._attr_state == STATE_PLAYING: await self.fs_device.pause() else: await self.fs_device.play() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send play/pause command.""" await self.fs_device.pause() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command (results in rewind).""" await self.fs_device.rewind() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command (results in fast-forward).""" await self.fs_device.forward() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" await self.fs_device.set_mute(mute) # volume - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Send volume up command.""" volume = await self.fs_device.get_volume() volume = int(volume or 0) + 1 await self.fs_device.set_volume(min(volume, self._max_volume)) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Send volume down command.""" volume = await self.fs_device.get_volume() volume = int(volume or 0) - 1 await self.fs_device.set_volume(max(volume, 0)) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume command.""" if self._max_volume: # Can't do anything sensible if not set volume = int(volume * self._max_volume) From 61ff1b786bf6d6341b7c40895fa478058532223f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Aug 2022 15:58:01 +0200 Subject: [PATCH 557/903] Add a context variable holding a HomeAssistant reference (#76303) * Add a context variable holding a HomeAssistant reference * Move variable setup and update test * Refactor * Revert "Refactor" This reverts commit 346d005ee67b9e27e05363d04a7f48eaf416a16b. * Set context variable when creating HomeAssistant object * Update docstring * Update docstring Co-authored-by: jbouwh --- homeassistant/core.py | 21 +++++++++++++++++++++ tests/test_bootstrap.py | 2 ++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/core.py b/homeassistant/core.py index fcd41ddc856..01c75fb707e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -15,6 +15,7 @@ from collections.abc import ( Iterable, Mapping, ) +from contextvars import ContextVar import datetime import enum import functools @@ -138,6 +139,8 @@ MAX_EXPECTED_ENTITY_IDS = 16384 _LOGGER = logging.getLogger(__name__) +_cv_hass: ContextVar[HomeAssistant] = ContextVar("current_entry") + @functools.lru_cache(MAX_EXPECTED_ENTITY_IDS) def split_entity_id(entity_id: str) -> tuple[str, str]: @@ -175,6 +178,18 @@ def is_callback(func: Callable[..., Any]) -> bool: return getattr(func, "_hass_callback", False) is True +@callback +def async_get_hass() -> HomeAssistant: + """Return the HomeAssistant instance. + + Raises LookupError if no HomeAssistant instance is available. + + This should be used where it's very cumbersome or downright impossible to pass + hass to the code which needs it. + """ + return _cv_hass.get() + + @enum.unique class HassJobType(enum.Enum): """Represent a job type.""" @@ -242,6 +257,12 @@ class HomeAssistant: http: HomeAssistantHTTP = None # type: ignore[assignment] config_entries: ConfigEntries = None # type: ignore[assignment] + def __new__(cls) -> HomeAssistant: + """Set the _cv_hass context variable.""" + hass = super().__new__(cls) + _cv_hass.set(hass) + return hass + def __init__(self) -> None: """Initialize new Home Assistant object.""" self.loop = asyncio.get_running_loop() diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 06f800af7f3..56c15f49337 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -501,6 +501,8 @@ async def test_setup_hass( assert len(mock_ensure_config_exists.mock_calls) == 1 assert len(mock_process_ha_config_upgrade.mock_calls) == 1 + assert hass == core.async_get_hass() + async def test_setup_hass_takes_longer_than_log_slow_startup( mock_enable_logging, From 3938015c93e4e9d3d7b3978117c35175643ec7f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Aug 2022 08:02:26 -1000 Subject: [PATCH 558/903] Add support for scanners that do not provide connectable devices (#77132) --- .../components/bluetooth/__init__.py | 84 ++++-- .../bluetooth/active_update_coordinator.py | 3 +- homeassistant/components/bluetooth/manager.py | 276 ++++++++++------- homeassistant/components/bluetooth/match.py | 67 +++-- homeassistant/components/bluetooth/models.py | 41 ++- .../bluetooth/passive_update_coordinator.py | 3 +- .../bluetooth/passive_update_processor.py | 3 +- homeassistant/components/bluetooth/scanner.py | 27 +- .../bluetooth/update_coordinator.py | 10 +- .../bluetooth_le_tracker/device_tracker.py | 24 +- .../components/govee_ble/config_flow.py | 2 +- .../components/govee_ble/manifest.json | 27 +- .../components/inkbird/config_flow.py | 2 +- .../components/inkbird/manifest.json | 10 +- homeassistant/components/moat/config_flow.py | 2 +- homeassistant/components/moat/manifest.json | 2 +- .../components/qingping/config_flow.py | 2 +- .../components/qingping/manifest.json | 2 +- .../components/sensorpush/config_flow.py | 2 +- .../components/sensorpush/manifest.json | 3 +- .../components/switchbot/__init__.py | 17 +- .../components/switchbot/config_flow.py | 47 ++- homeassistant/components/switchbot/const.py | 15 +- .../components/switchbot/coordinator.py | 7 +- .../components/switchbot/manifest.json | 6 +- .../components/xiaomi_ble/__init__.py | 21 +- .../components/xiaomi_ble/config_flow.py | 2 +- .../components/xiaomi_ble/manifest.json | 1 + homeassistant/generated/bluetooth.py | 63 ++-- homeassistant/loader.py | 1 + script/hassfest/bluetooth.py | 8 +- script/hassfest/manifest.py | 1 + tests/components/bluetooth/__init__.py | 73 ++++- tests/components/bluetooth/test_init.py | 277 ++++++++++++++---- tests/components/bluetooth/test_manager.py | 17 +- .../test_passive_update_coordinator.py | 16 +- .../test_passive_update_processor.py | 11 +- .../test_device_tracker.py | 10 + tests/components/fjaraskupan/__init__.py | 16 +- tests/components/switchbot/__init__.py | 32 ++ .../components/switchbot/test_config_flow.py | 25 +- tests/components/xiaomi_ble/__init__.py | 18 +- tests/components/xiaomi_ble/test_sensor.py | 189 +++++++++++- tests/components/yalexs_ble/__init__.py | 8 + 44 files changed, 1088 insertions(+), 385 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index f3b476a15ad..f71ee5aa34c 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -2,15 +2,15 @@ from __future__ import annotations from asyncio import Future -from collections.abc import Callable +from collections.abc import Callable, Iterable import platform -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import async_timeout from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback as hass_callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.loader import async_get_bluetooth @@ -31,6 +31,7 @@ from .const import ( from .manager import BluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher from .models import ( + BaseHaScanner, BluetoothCallback, BluetoothChange, BluetoothScanningMode, @@ -56,6 +57,7 @@ __all__ = [ "async_rediscover_address", "async_register_callback", "async_track_unavailable", + "BaseHaScanner", "BluetoothServiceInfo", "BluetoothServiceInfoBleak", "BluetoothScanningMode", @@ -64,6 +66,11 @@ __all__ = [ ] +def _get_manager(hass: HomeAssistant) -> BluetoothManager: + """Get the bluetooth manager.""" + return cast(BluetoothManager, hass.data[DATA_MANAGER]) + + @hass_callback def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: """Return a HaBleakScannerWrapper. @@ -76,37 +83,32 @@ def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: @hass_callback def async_discovered_service_info( - hass: HomeAssistant, -) -> list[BluetoothServiceInfoBleak]: + hass: HomeAssistant, connectable: bool = True +) -> Iterable[BluetoothServiceInfoBleak]: """Return the discovered devices list.""" if DATA_MANAGER not in hass.data: return [] - manager: BluetoothManager = hass.data[DATA_MANAGER] - return manager.async_discovered_service_info() + return _get_manager(hass).async_discovered_service_info(connectable) @hass_callback def async_ble_device_from_address( - hass: HomeAssistant, - address: str, + hass: HomeAssistant, address: str, connectable: bool = True ) -> BLEDevice | None: """Return BLEDevice for an address if its present.""" if DATA_MANAGER not in hass.data: return None - manager: BluetoothManager = hass.data[DATA_MANAGER] - return manager.async_ble_device_from_address(address) + return _get_manager(hass).async_ble_device_from_address(address, connectable) @hass_callback def async_address_present( - hass: HomeAssistant, - address: str, + hass: HomeAssistant, address: str, connectable: bool = True ) -> bool: """Check if an address is present in the bluetooth device list.""" if DATA_MANAGER not in hass.data: return False - manager: BluetoothManager = hass.data[DATA_MANAGER] - return manager.async_address_present(address) + return _get_manager(hass).async_address_present(address, connectable) @hass_callback @@ -125,8 +127,7 @@ def async_register_callback( Returns a callback that can be used to cancel the registration. """ - manager: BluetoothManager = hass.data[DATA_MANAGER] - return manager.async_register_callback(callback, match_dict) + return _get_manager(hass).async_register_callback(callback, match_dict) async def async_process_advertisements( @@ -146,7 +147,9 @@ async def async_process_advertisements( if not done.done() and callback(service_info): done.set_result(service_info) - unload = async_register_callback(hass, _async_discovered_device, match_dict, mode) + unload = _get_manager(hass).async_register_callback( + _async_discovered_device, match_dict + ) try: async with async_timeout.timeout(timeout): @@ -160,26 +163,47 @@ def async_track_unavailable( hass: HomeAssistant, callback: Callable[[str], None], address: str, + connectable: bool = True, ) -> Callable[[], None]: """Register to receive a callback when an address is unavailable. Returns a callback that can be used to cancel the registration. """ - manager: BluetoothManager = hass.data[DATA_MANAGER] - return manager.async_track_unavailable(callback, address) + return _get_manager(hass).async_track_unavailable(callback, address, connectable) @hass_callback def async_rediscover_address(hass: HomeAssistant, address: str) -> None: """Trigger discovery of devices which have already been seen.""" - manager: BluetoothManager = hass.data[DATA_MANAGER] - manager.async_rediscover_address(address) + _get_manager(hass).async_rediscover_address(address) + + +@hass_callback +def async_register_scanner( + hass: HomeAssistant, scanner: BaseHaScanner, connectable: bool +) -> CALLBACK_TYPE: + """Register a BleakScanner.""" + return _get_manager(hass).async_register_scanner(scanner, connectable) + + +@hass_callback +def async_get_advertisement_callback( + hass: HomeAssistant, +) -> Callable[[BluetoothServiceInfoBleak], None]: + """Get the advertisement callback.""" + return _get_manager(hass).scanner_adv_received + + +async def async_get_adapter_from_address( + hass: HomeAssistant, address: str +) -> str | None: + """Get an adapter by the address.""" + return await _get_manager(hass).async_get_adapter_from_address(address) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) - manager = BluetoothManager(hass, integration_matcher) manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) @@ -236,10 +260,9 @@ async def async_discover_adapters( async def async_update_device( + hass: HomeAssistant, entry: config_entries.ConfigEntry, - manager: BluetoothManager, adapter: str, - address: str, ) -> None: """Update device registry entry. @@ -248,6 +271,7 @@ async def async_update_device( update the device with the new location so they can figure out where the adapter is. """ + manager: BluetoothManager = hass.data[DATA_MANAGER] adapters = await manager.async_get_bluetooth_adapters() details = adapters[adapter] registry = dr.async_get(manager.hass) @@ -264,10 +288,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Set up a config entry for a bluetooth scanner.""" - manager: BluetoothManager = hass.data[DATA_MANAGER] address = entry.unique_id assert address is not None - adapter = await manager.async_get_adapter_from_address(address) + adapter = await async_get_adapter_from_address(hass, address) if adapter is None: raise ConfigEntryNotReady( f"Bluetooth adapter {adapter} with address {address} not found" @@ -280,13 +303,14 @@ async def async_setup_entry( f"{adapter_human_name(adapter, address)}: {err}" ) from err scanner = HaScanner(hass, bleak_scanner, adapter, address) - entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) + info_callback = async_get_advertisement_callback(hass) + entry.async_on_unload(scanner.async_register_callback(info_callback)) try: await scanner.async_start() except ScannerStartError as err: raise ConfigEntryNotReady from err - entry.async_on_unload(manager.async_register_scanner(scanner)) - await async_update_device(entry, manager, adapter, address) + entry.async_on_unload(async_register_scanner(hass, scanner, True)) + await async_update_device(hass, entry, adapter) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner return True diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index 1ebd26f8203..e73414fe79f 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -61,9 +61,10 @@ class ActiveBluetoothProcessorCoordinator( ] | None = None, poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, + connectable: bool = True, ) -> None: """Initialize the processor.""" - super().__init__(hass, logger, address, mode, update_method) + super().__init__(hass, logger, address, mode, update_method, connectable) self._needs_poll_method = needs_poll_method self._poll_method = poll_method diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 4b826efd6bd..2fff99c830c 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable, Iterable -from dataclasses import dataclass from datetime import datetime, timedelta import itertools import logging @@ -22,18 +21,23 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( ADAPTER_ADDRESS, - SOURCE_LOCAL, STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, AdapterDetails, ) from .match import ( ADDRESS, + CONNECTABLE, BluetoothCallbackMatcher, IntegrationMatcher, ble_device_matches, ) -from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak +from .models import ( + BaseHaScanner, + BluetoothCallback, + BluetoothChange, + BluetoothServiceInfoBleak, +) from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .util import async_get_bluetooth_adapters @@ -41,7 +45,6 @@ if TYPE_CHECKING: from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData - from .scanner import HaScanner FILTER_UUIDS: Final = "UUIDs" @@ -51,43 +54,39 @@ RSSI_SWITCH_THRESHOLD = 6 _LOGGER = logging.getLogger(__name__) -@dataclass -class AdvertisementHistory: - """Bluetooth advertisement history.""" - - ble_device: BLEDevice - advertisement_data: AdvertisementData - time: float - source: str - - -def _prefer_previous_adv(old: AdvertisementHistory, new: AdvertisementHistory) -> bool: +def _prefer_previous_adv( + old: BluetoothServiceInfoBleak, new: BluetoothServiceInfoBleak +) -> bool: """Prefer previous advertisement if it is better.""" if new.time - old.time > STALE_ADVERTISEMENT_SECONDS: # If the old advertisement is stale, any new advertisement is preferred if new.source != old.source: _LOGGER.debug( - "%s (%s): Switching from %s to %s (time_elapsed:%s > stale_seconds:%s)", - new.advertisement_data.local_name, - new.ble_device.address, + "%s (%s): Switching from %s[%s] to %s[%s] (time elapsed:%s > stale seconds:%s)", + new.advertisement.local_name, + new.device.address, old.source, + old.connectable, new.source, + new.connectable, new.time - old.time, STALE_ADVERTISEMENT_SECONDS, ) return False - if new.ble_device.rssi - RSSI_SWITCH_THRESHOLD > old.ble_device.rssi: + if new.device.rssi - RSSI_SWITCH_THRESHOLD > old.device.rssi: # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred if new.source != old.source: _LOGGER.debug( - "%s (%s): Switching from %s to %s (new_rssi:%s - threadshold:%s > old_rssi:%s)", - new.advertisement_data.local_name, - new.ble_device.address, + "%s (%s): Switching from %s[%s] to %s[%s] (new rssi:%s - threshold:%s > old rssi:%s)", + new.advertisement.local_name, + new.device.address, old.source, + old.connectable, new.source, - new.ble_device.rssi, + new.connectable, + new.device.rssi, RSSI_SWITCH_THRESHOLD, - old.ble_device.rssi, + old.device.rssi, ) return False # If the source is the different, the old one is preferred because its @@ -128,16 +127,24 @@ class BluetoothManager: """Init bluetooth manager.""" self.hass = hass self._integration_matcher = integration_matcher - self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None + self._cancel_unavailable_tracking: list[CALLBACK_TYPE] = [] self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} + self._connectable_unavailable_callbacks: dict[ + str, list[Callable[[str], None]] + ] = {} self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] + self._connectable_callbacks: list[ + tuple[BluetoothCallback, BluetoothCallbackMatcher | None] + ] = [] self._bleak_callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] - self.history: dict[str, AdvertisementHistory] = {} - self._scanners: list[HaScanner] = [] + self._history: dict[str, BluetoothServiceInfoBleak] = {} + self._connectable_history: dict[str, BluetoothServiceInfoBleak] = {} + self._scanners: list[BaseHaScanner] = [] + self._connectable_scanners: list[BaseHaScanner] = [] self._adapters: dict[str, AdapterDetails] = {} def _find_adapter_by_address(self, address: str) -> str | None: @@ -146,9 +153,11 @@ class BluetoothManager: return adapter return None - async def async_get_bluetooth_adapters(self) -> dict[str, AdapterDetails]: + async def async_get_bluetooth_adapters( + self, cached: bool = True + ) -> dict[str, AdapterDetails]: """Get bluetooth adapters.""" - if not self._adapters: + if not cached or not self._adapters: self._adapters = await async_get_bluetooth_adapters() return self._adapters @@ -170,37 +179,51 @@ class BluetoothManager: """Stop the Bluetooth integration at shutdown.""" _LOGGER.debug("Stopping bluetooth manager") if self._cancel_unavailable_tracking: - self._cancel_unavailable_tracking() - self._cancel_unavailable_tracking = None + for cancel in self._cancel_unavailable_tracking: + cancel() + self._cancel_unavailable_tracking.clear() uninstall_multiple_bleak_catcher() @hass_callback - def async_all_discovered_devices(self) -> Iterable[BLEDevice]: + def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]: """Return all of discovered devices from all the scanners including duplicates.""" return itertools.chain.from_iterable( - scanner.discovered_devices for scanner in self._scanners + scanner.discovered_devices + for scanner in self._get_scanners_by_type(connectable) ) @hass_callback - def async_discovered_devices(self) -> list[BLEDevice]: + def async_discovered_devices(self, connectable: bool) -> list[BLEDevice]: """Return all of combined best path to discovered from all the scanners.""" - return [history.ble_device for history in self.history.values()] + return [ + history.device + for history in self._get_history_by_type(connectable).values() + ] @hass_callback def async_setup_unavailable_tracking(self) -> None: """Set up the unavailable tracking.""" + self._async_setup_unavailable_tracking(True) + self._async_setup_unavailable_tracking(False) + + @hass_callback + def _async_setup_unavailable_tracking(self, connectable: bool) -> None: + """Set up the unavailable tracking.""" + unavailable_callbacks = self._get_unavailable_callbacks_by_type(connectable) + history = self._get_history_by_type(connectable) @hass_callback def _async_check_unavailable(now: datetime) -> None: """Watch for unavailable devices.""" - history_set = set(self.history) + history_set = set(history) active_addresses = { - device.address for device in self.async_all_discovered_devices() + device.address + for device in self.async_all_discovered_devices(connectable) } disappeared = history_set.difference(active_addresses) for address in disappeared: - del self.history[address] - if not (callbacks := self._unavailable_callbacks.get(address)): + del history[address] + if not (callbacks := unavailable_callbacks.get(address)): continue for callback in callbacks: try: @@ -208,20 +231,16 @@ class BluetoothManager: except Exception: # pylint: disable=broad-except _LOGGER.exception("Error in unavailable callback") - self._cancel_unavailable_tracking = async_track_time_interval( - self.hass, - _async_check_unavailable, - timedelta(seconds=UNAVAILABLE_TRACK_SECONDS), + self._cancel_unavailable_tracking.append( + async_track_time_interval( + self.hass, + _async_check_unavailable, + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS), + ) ) @hass_callback - def scanner_adv_received( - self, - device: BLEDevice, - advertisement_data: AdvertisementData, - monotonic_time: float, - source: str, - ) -> None: + def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None: """Handle a new advertisement from any scanner. Callbacks from all the scanners arrive here. @@ -233,42 +252,46 @@ class BluetoothManager: than the source from the history or the timestamp in the history is older than 180s """ - new_history = AdvertisementHistory( - device, advertisement_data, monotonic_time, source - ) - if (old_history := self.history.get(device.address)) and _prefer_previous_adv( - old_history, new_history - ): + device = service_info.device + connectable = service_info.connectable + address = device.address + all_history = self._get_history_by_type(connectable) + old_service_info = all_history.get(address) + if old_service_info and _prefer_previous_adv(old_service_info, service_info): return - self.history[device.address] = new_history + self._history[address] = service_info + advertisement_data = service_info.advertisement + source = service_info.source - for callback_filters in self._bleak_callbacks: - _dispatch_bleak_callback(*callback_filters, device, advertisement_data) + if connectable: + self._connectable_history[address] = service_info + # Bleak callbacks must get a connectable device - matched_domains = self._integration_matcher.match_domains( - device, advertisement_data - ) + for callback_filters in self._bleak_callbacks: + _dispatch_bleak_callback(*callback_filters, device, advertisement_data) + + matched_domains = self._integration_matcher.match_domains(service_info) _LOGGER.debug( - "%s: %s %s match: %s", + "%s: %s %s connectable: %s match: %s", source, - device.address, + address, advertisement_data, + connectable, matched_domains, ) - if not matched_domains and not self._callbacks: + if ( + not matched_domains + and not self._callbacks + and not self._connectable_callbacks + ): return - service_info: BluetoothServiceInfoBleak | None = None - for callback, matcher in self._callbacks: - if matcher is None or ble_device_matches( - matcher, device, advertisement_data - ): - if service_info is None: - service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, source - ) + for connectable_callback in (True, False): + for callback, matcher in self._get_callbacks_by_type(connectable_callback): + if matcher and not ble_device_matches(matcher, service_info): + continue try: callback(service_info, BluetoothChange.ADVERTISEMENT) except Exception: # pylint: disable=broad-except @@ -276,10 +299,6 @@ class BluetoothManager: if not matched_domains: return - if service_info is None: - service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, source - ) for domain in matched_domains: discovery_flow.async_create_flow( self.hass, @@ -290,16 +309,17 @@ class BluetoothManager: @hass_callback def async_track_unavailable( - self, callback: Callable[[str], None], address: str + self, callback: Callable[[str], None], address: str, connectable: bool ) -> Callable[[], None]: """Register a callback.""" - self._unavailable_callbacks.setdefault(address, []).append(callback) + unavailable_callbacks = self._get_unavailable_callbacks_by_type(connectable) + unavailable_callbacks.setdefault(address, []).append(callback) @hass_callback def _async_remove_callback() -> None: - self._unavailable_callbacks[address].remove(callback) - if not self._unavailable_callbacks[address]: - del self._unavailable_callbacks[address] + unavailable_callbacks[address].remove(callback) + if not unavailable_callbacks[address]: + del unavailable_callbacks[address] return _async_remove_callback @@ -307,70 +327,102 @@ class BluetoothManager: def async_register_callback( self, callback: BluetoothCallback, - matcher: BluetoothCallbackMatcher | None = None, + matcher: BluetoothCallbackMatcher | None, ) -> Callable[[], None]: """Register a callback.""" + if not matcher: + matcher = BluetoothCallbackMatcher(connectable=True) + if CONNECTABLE not in matcher: + matcher[CONNECTABLE] = True + connectable = matcher[CONNECTABLE] + callback_entry = (callback, matcher) - self._callbacks.append(callback_entry) + callbacks = self._get_callbacks_by_type(connectable) + callbacks.append(callback_entry) @hass_callback def _async_remove_callback() -> None: - self._callbacks.remove(callback_entry) + callbacks.remove(callback_entry) # If we have history for the subscriber, we can trigger the callback # immediately with the last packet so the subscriber can see the # device. + all_history = self._get_history_by_type(connectable) if ( - matcher - and (address := matcher.get(ADDRESS)) - and (history := self.history.get(address)) + (address := matcher.get(ADDRESS)) + and (service_info := all_history.get(address)) + and ble_device_matches(matcher, service_info) ): try: - callback( - BluetoothServiceInfoBleak.from_advertisement( - history.ble_device, history.advertisement_data, SOURCE_LOCAL - ), - BluetoothChange.ADVERTISEMENT, - ) + callback(service_info, BluetoothChange.ADVERTISEMENT) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error in bluetooth callback") return _async_remove_callback @hass_callback - def async_ble_device_from_address(self, address: str) -> BLEDevice | None: + def async_ble_device_from_address( + self, address: str, connectable: bool + ) -> BLEDevice | None: """Return the BLEDevice if present.""" - if history := self.history.get(address): - return history.ble_device + all_history = self._get_history_by_type(connectable) + if history := all_history.get(address): + return history.device return None @hass_callback - def async_address_present(self, address: str) -> bool: + def async_address_present(self, address: str, connectable: bool) -> bool: """Return if the address is present.""" - return address in self.history + return address in self._get_history_by_type(connectable) @hass_callback - def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: + def async_discovered_service_info( + self, connectable: bool + ) -> Iterable[BluetoothServiceInfoBleak]: """Return if the address is present.""" - return [ - BluetoothServiceInfoBleak.from_advertisement( - history.ble_device, history.advertisement_data, SOURCE_LOCAL - ) - for history in self.history.values() - ] + return self._get_history_by_type(connectable).values() @hass_callback def async_rediscover_address(self, address: str) -> None: """Trigger discovery of devices which have already been seen.""" self._integration_matcher.async_clear_address(address) - def async_register_scanner(self, scanner: HaScanner) -> CALLBACK_TYPE: + def _get_scanners_by_type(self, connectable: bool) -> list[BaseHaScanner]: + """Return the scanners by type.""" + return self._connectable_scanners if connectable else self._scanners + + def _get_unavailable_callbacks_by_type( + self, connectable: bool + ) -> dict[str, list[Callable[[str], None]]]: + """Return the unavailable callbacks by type.""" + return ( + self._connectable_unavailable_callbacks + if connectable + else self._unavailable_callbacks + ) + + def _get_history_by_type( + self, connectable: bool + ) -> dict[str, BluetoothServiceInfoBleak]: + """Return the history by type.""" + return self._connectable_history if connectable else self._history + + def _get_callbacks_by_type( + self, connectable: bool + ) -> list[tuple[BluetoothCallback, BluetoothCallbackMatcher | None]]: + """Return the callbacks by type.""" + return self._connectable_callbacks if connectable else self._callbacks + + def async_register_scanner( + self, scanner: BaseHaScanner, connectable: bool + ) -> CALLBACK_TYPE: """Register a new scanner.""" + scanners = self._get_scanners_by_type(connectable) def _unregister_scanner() -> None: - self._scanners.remove(scanner) + scanners.remove(scanner) - self._scanners.append(scanner) + scanners.append(scanner) return _unregister_scanner @hass_callback @@ -388,9 +440,9 @@ class BluetoothManager: # Replay the history since otherwise we miss devices # that were already discovered before the callback was registered # or we are in passive mode - for history in self.history.values(): + for history in self._connectable_history.values(): _dispatch_bleak_callback( - callback, filters, history.ble_device, history.advertisement_data + callback, filters, history.device, history.advertisement ) return _remove_callback diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 49f9e49db54..08b3716c50a 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -9,10 +9,11 @@ from lru import LRU # pylint: disable=no-name-in-module from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional +from .models import BluetoothServiceInfoBleak + if TYPE_CHECKING: from collections.abc import MutableMapping - from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData @@ -20,6 +21,7 @@ MAX_REMEMBER_ADDRESSES: Final = 2048 ADDRESS: Final = "address" +CONNECTABLE: Final = "connectable" LOCAL_NAME: Final = "local_name" SERVICE_UUID: Final = "service_uuid" SERVICE_DATA_UUID: Final = "service_data_uuid" @@ -50,14 +52,14 @@ class IntegrationMatchHistory: def seen_all_fields( - previous_match: IntegrationMatchHistory, adv_data: AdvertisementData + previous_match: IntegrationMatchHistory, advertisement_data: AdvertisementData ) -> bool: """Return if we have seen all fields.""" - if not previous_match.manufacturer_data and adv_data.manufacturer_data: + if not previous_match.manufacturer_data and advertisement_data.manufacturer_data: return False - if not previous_match.service_data and adv_data.service_data: + if not previous_match.service_data and advertisement_data.service_data: return False - if not previous_match.service_uuids and adv_data.service_uuids: + if not previous_match.service_uuids and advertisement_data.service_uuids: return False return True @@ -73,74 +75,93 @@ class IntegrationMatcher: self._matched: MutableMapping[str, IntegrationMatchHistory] = LRU( MAX_REMEMBER_ADDRESSES ) + self._matched_connectable: MutableMapping[str, IntegrationMatchHistory] = LRU( + MAX_REMEMBER_ADDRESSES + ) def async_clear_address(self, address: str) -> None: """Clear the history matches for a set of domains.""" self._matched.pop(address, None) + self._matched_connectable.pop(address, None) - def match_domains(self, device: BLEDevice, adv_data: AdvertisementData) -> set[str]: + def _get_matched_by_type( + self, connectable: bool + ) -> MutableMapping[str, IntegrationMatchHistory]: + """Return the matches by type.""" + return self._matched_connectable if connectable else self._matched + + def match_domains(self, service_info: BluetoothServiceInfoBleak) -> set[str]: """Return the domains that are matched.""" + device = service_info.device + advertisement_data = service_info.advertisement + matched = self._get_matched_by_type(service_info.connectable) matched_domains: set[str] = set() - if (previous_match := self._matched.get(device.address)) and seen_all_fields( - previous_match, adv_data + if (previous_match := matched.get(device.address)) and seen_all_fields( + previous_match, advertisement_data ): # We have seen all fields so we can skip the rest of the matchers return matched_domains matched_domains = { matcher["domain"] for matcher in self._integration_matchers - if ble_device_matches(matcher, device, adv_data) + if ble_device_matches(matcher, service_info) } if not matched_domains: return matched_domains if previous_match: - previous_match.manufacturer_data |= bool(adv_data.manufacturer_data) - previous_match.service_data |= bool(adv_data.service_data) - previous_match.service_uuids |= bool(adv_data.service_uuids) + previous_match.manufacturer_data |= bool( + advertisement_data.manufacturer_data + ) + previous_match.service_data |= bool(advertisement_data.service_data) + previous_match.service_uuids |= bool(advertisement_data.service_uuids) else: - self._matched[device.address] = IntegrationMatchHistory( - manufacturer_data=bool(adv_data.manufacturer_data), - service_data=bool(adv_data.service_data), - service_uuids=bool(adv_data.service_uuids), + matched[device.address] = IntegrationMatchHistory( + manufacturer_data=bool(advertisement_data.manufacturer_data), + service_data=bool(advertisement_data.service_data), + service_uuids=bool(advertisement_data.service_uuids), ) return matched_domains def ble_device_matches( matcher: BluetoothCallbackMatcher | BluetoothMatcher, - device: BLEDevice, - adv_data: AdvertisementData, + service_info: BluetoothServiceInfoBleak, ) -> bool: """Check if a ble device and advertisement_data matches the matcher.""" + device = service_info.device if (address := matcher.get(ADDRESS)) is not None and device.address != address: return False + if matcher.get(CONNECTABLE, True) and not service_info.connectable: + return False + + advertisement_data = service_info.advertisement if (local_name := matcher.get(LOCAL_NAME)) is not None and not fnmatch.fnmatch( - adv_data.local_name or device.name or device.address, + advertisement_data.local_name or device.name or device.address, local_name, ): return False if ( service_uuid := matcher.get(SERVICE_UUID) - ) is not None and service_uuid not in adv_data.service_uuids: + ) is not None and service_uuid not in advertisement_data.service_uuids: return False if ( service_data_uuid := matcher.get(SERVICE_DATA_UUID) - ) is not None and service_data_uuid not in adv_data.service_data: + ) is not None and service_data_uuid not in advertisement_data.service_data: return False if ( manfacturer_id := matcher.get(MANUFACTURER_ID) - ) is not None and manfacturer_id not in adv_data.manufacturer_data: + ) is not None and manfacturer_id not in advertisement_data.manufacturer_data: return False if (manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START)) is not None: manufacturer_data_start_bytes = bytearray(manufacturer_data_start) if not any( manufacturer_data.startswith(manufacturer_data_start_bytes) - for manufacturer_data in adv_data.manufacturer_data.values() + for manufacturer_data in advertisement_data.manufacturer_data.values() ): return False diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 7857b02f121..285e991ff81 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -1,6 +1,7 @@ """Models for bluetooth.""" from __future__ import annotations +from abc import abstractmethod import asyncio from collections.abc import Callable import contextlib @@ -45,23 +46,8 @@ class BluetoothServiceInfoBleak(BluetoothServiceInfo): device: BLEDevice advertisement: AdvertisementData - - @classmethod - def from_advertisement( - cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str - ) -> BluetoothServiceInfoBleak: - """Create a BluetoothServiceInfoBleak from an advertisement.""" - return cls( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=device.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - source=source, - device=device, - advertisement=advertisement_data, - ) + connectable: bool + time: float class BluetoothScanningMode(Enum): @@ -76,6 +62,15 @@ BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] +class BaseHaScanner: + """Base class for Ha Scanners.""" + + @property + @abstractmethod + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + + class HaBleakScannerWrapper(BaseBleakScanner): """A wrapper that uses the single instance.""" @@ -89,7 +84,7 @@ class HaBleakScannerWrapper(BaseBleakScanner): """Initialize the BleakScanner.""" self._detection_cancel: CALLBACK_TYPE | None = None self._mapped_filters: dict[str, set[str]] = {} - self._adv_data_callback: AdvertisementDataCallback | None = None + self._advertisement_data_callback: AdvertisementDataCallback | None = None remapped_kwargs = { "detection_callback": detection_callback, "service_uuids": service_uuids or [], @@ -136,7 +131,7 @@ class HaBleakScannerWrapper(BaseBleakScanner): def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" assert MANAGER is not None - return list(MANAGER.async_discovered_devices()) + return list(MANAGER.async_discovered_devices(True)) def register_detection_callback( self, callback: AdvertisementDataCallback | None @@ -146,15 +141,15 @@ class HaBleakScannerWrapper(BaseBleakScanner): This method takes the callback and registers it with the long running scanner. """ - self._adv_data_callback = callback + self._advertisement_data_callback = callback self._setup_detection_callback() def _setup_detection_callback(self) -> None: """Set up the detection callback.""" - if self._adv_data_callback is None: + if self._advertisement_data_callback is None: return self._cancel_callback() - super().register_detection_callback(self._adv_data_callback) + super().register_detection_callback(self._advertisement_data_callback) assert MANAGER is not None assert self._callback is not None self._detection_cancel = MANAGER.async_register_bleak_callback( @@ -193,7 +188,7 @@ class HaBleakClientWrapper(BleakClient): error_if_core=False, ) assert MANAGER is not None - ble_device = MANAGER.async_ble_device_from_address(address_or_ble_device) + ble_device = MANAGER.async_ble_device_from_address(address_or_ble_device, True) if ble_device is None: raise BleakError(f"No device found for address {address_or_ble_device}") super().__init__(ble_device, *args, **kwargs) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 5c6b5b79509..296e49e2fa0 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -28,9 +28,10 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): logger: logging.Logger, address: str, mode: BluetoothScanningMode, + connectable: bool = False, ) -> None: """Initialize PassiveBluetoothDataUpdateCoordinator.""" - super().__init__(hass, logger, address, mode) + super().__init__(hass, logger, address, mode, connectable) self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} @callback diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index bb9e82a7dbe..5ea2f3f0742 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -72,9 +72,10 @@ class PassiveBluetoothProcessorCoordinator( address: str, mode: BluetoothScanningMode, update_method: Callable[[BluetoothServiceInfoBleak], _T], + connectable: bool = False, ) -> None: """Initialize the coordinator.""" - super().__init__(hass, logger, address, mode) + super().__init__(hass, logger, address, mode, connectable) self._processors: list[PassiveBluetoothDataProcessor] = [] self._update_method = update_method self.last_update_success = True diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 730249d70a0..8805b0adaf2 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -32,7 +32,7 @@ from .const import ( SOURCE_LOCAL, START_TIMEOUT, ) -from .models import BluetoothScanningMode +from .models import BaseHaScanner, BluetoothScanningMode, BluetoothServiceInfoBleak from .util import adapter_human_name, async_reset_adapter OriginalBleakScanner = bleak.BleakScanner @@ -92,7 +92,7 @@ def create_bleak_scanner( raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex -class HaScanner: +class HaScanner(BaseHaScanner): """Operate and automatically recover a BleakScanner. Multiple BleakScanner can be used at the same time @@ -119,9 +119,7 @@ class HaScanner: self._cancel_watchdog: CALLBACK_TYPE | None = None self._last_detection = 0.0 self._start_time = 0.0 - self._callbacks: list[ - Callable[[BLEDevice, AdvertisementData, float, str], None] - ] = [] + self._callbacks: list[Callable[[BluetoothServiceInfoBleak], None]] = [] self.name = adapter_human_name(adapter, address) self.source = self.adapter or SOURCE_LOCAL @@ -132,7 +130,7 @@ class HaScanner: @hass_callback def async_register_callback( - self, callback: Callable[[BLEDevice, AdvertisementData, float, str], None] + self, callback: Callable[[BluetoothServiceInfoBleak], None] ) -> CALLBACK_TYPE: """Register a callback. @@ -149,7 +147,7 @@ class HaScanner: @hass_callback def _async_detection_callback( self, - ble_device: BLEDevice, + device: BLEDevice, advertisement_data: AdvertisementData, ) -> None: """Call the callback when an advertisement is received. @@ -168,8 +166,21 @@ class HaScanner: # as the adapter is in a failure # state if all the data is empty. self._last_detection = callback_time + service_info = BluetoothServiceInfoBleak( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=self.source, + device=device, + advertisement=advertisement_data, + connectable=True, + time=callback_time, + ) for callback in self._callbacks: - callback(ble_device, advertisement_data, callback_time, self.source) + callback(service_info) async def async_start(self) -> None: """Start bluetooth scanner.""" diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index d0f38ce32c6..9348095f2b1 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -28,12 +28,14 @@ class BasePassiveBluetoothCoordinator: logger: logging.Logger, address: str, mode: BluetoothScanningMode, + connectable: bool, ) -> None: """Initialize the coordinator.""" self.hass = hass self.logger = logger self.name: str | None = None self.address = address + self.connectable = connectable self._cancel_track_unavailable: CALLBACK_TYPE | None = None self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None self._present = False @@ -62,13 +64,13 @@ class BasePassiveBluetoothCoordinator: self._cancel_bluetooth_advertisements = async_register_callback( self.hass, self._async_handle_bluetooth_event, - BluetoothCallbackMatcher(address=self.address), + BluetoothCallbackMatcher( + address=self.address, connectable=self.connectable + ), self.mode, ) self._cancel_track_unavailable = async_track_unavailable( - self.hass, - self._async_handle_unavailable, - self.address, + self.hass, self._async_handle_unavailable, self.address, self.connectable ) @callback diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 7e2c484e1a6..85908cc1d55 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -10,6 +10,7 @@ from bleak import BleakClient, BleakError import voluptuous as vol from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, ) @@ -139,8 +140,20 @@ async def async_setup_scanner( # noqa: C901 ) -> None: """Lookup Bluetooth LE devices and update status.""" battery = None + # We need one we can connect to since the tracker will + # accept devices from non-connectable sources + if service_info.connectable: + device = service_info.device + elif connectable_device := bluetooth.async_ble_device_from_address( + hass, service_info.device.address, True + ): + device = connectable_device + else: + # The device can be seen by a passive tracker but we + # don't have a route to make a connection + return try: - async with BleakClient(service_info.device) as client: + async with BleakClient(device) as client: bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) battery = ord(bat_char) except asyncio.TimeoutError: @@ -192,12 +205,17 @@ async def async_setup_scanner( # noqa: C901 # interval so they do not get set to not_home when # there have been no callbacks because the RSSI or # other properties have not changed. - for service_info in bluetooth.async_discovered_service_info(hass): + for service_info in bluetooth.async_discovered_service_info(hass, False): _async_update_ble(service_info, bluetooth.BluetoothChange.ADVERTISEMENT) cancels = [ bluetooth.async_register_callback( - hass, _async_update_ble, None, bluetooth.BluetoothScanningMode.ACTIVE + hass, + _async_update_ble, + BluetoothCallbackMatcher( + connectable=False + ), # We will take data from any source + bluetooth.BluetoothScanningMode.ACTIVE, ), async_track_time_interval(hass, _async_refresh_ble, interval), ] diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index 47d73f2779a..fc6fe7b310d 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -73,7 +73,7 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): ) current_addresses = self._async_current_ids() - for discovery_info in async_discovered_service_info(self.hass): + for discovery_info in async_discovered_service_info(self.hass, False): address = discovery_info.address if address in current_addresses or address in self._discovered_devices: continue diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index d31abe48cae..de76435f5d4 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -4,36 +4,43 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/govee_ble", "bluetooth": [ - { "local_name": "Govee*" }, - { "local_name": "GVH5*" }, - { "local_name": "B5178*" }, + { "local_name": "Govee*", "connectable": false }, + { "local_name": "GVH5*", "connectable": false }, + { "local_name": "B5178*", "connectable": false }, { "manufacturer_id": 6966, - "service_uuid": "00008451-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008451-0000-1000-8000-00805f9b34fb", + "connectable": false }, { "manufacturer_id": 26589, - "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": false }, { "manufacturer_id": 18994, - "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", + "connectable": false }, { "manufacturer_id": 818, - "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", + "connectable": false }, { "manufacturer_id": 59970, - "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": false }, { "manufacturer_id": 14474, - "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": false }, { "manufacturer_id": 10032, - "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb", + "connectable": false } ], "requirements": ["govee-ble==0.16.0"], diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py index 524471bbcc7..c63ad7e09d8 100644 --- a/homeassistant/components/inkbird/config_flow.py +++ b/homeassistant/components/inkbird/config_flow.py @@ -73,7 +73,7 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): ) current_addresses = self._async_current_ids() - for discovery_info in async_discovered_service_info(self.hass): + for discovery_info in async_discovered_service_info(self.hass, False): address = discovery_info.address if address in current_addresses or address in self._discovered_devices: continue diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index f65177ab6e2..97234de9d6d 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/inkbird", "bluetooth": [ - { "local_name": "sps" }, - { "local_name": "Inkbird*" }, - { "local_name": "iBBQ*" }, - { "local_name": "xBBQ*" }, - { "local_name": "tps" } + { "local_name": "sps", "connectable": false }, + { "local_name": "Inkbird*", "connectable": false }, + { "local_name": "iBBQ*", "connectable": false }, + { "local_name": "xBBQ*", "connectable": false }, + { "local_name": "tps", "connectable": false } ], "requirements": ["inkbird-ble==0.5.5"], "dependencies": ["bluetooth"], diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py index 0e1b4f89568..4e522a81c73 100644 --- a/homeassistant/components/moat/config_flow.py +++ b/homeassistant/components/moat/config_flow.py @@ -73,7 +73,7 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): ) current_addresses = self._async_current_ids() - for discovery_info in async_discovered_service_info(self.hass): + for discovery_info in async_discovered_service_info(self.hass, False): address = discovery_info.address if address in current_addresses or address in self._discovered_devices: continue diff --git a/homeassistant/components/moat/manifest.json b/homeassistant/components/moat/manifest.json index 49e6985d1c1..f8612cc992f 100644 --- a/homeassistant/components/moat/manifest.json +++ b/homeassistant/components/moat/manifest.json @@ -3,7 +3,7 @@ "name": "Moat", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/moat", - "bluetooth": [{ "local_name": "Moat_S*" }], + "bluetooth": [{ "local_name": "Moat_S*", "connectable": false }], "requirements": ["moat-ble==0.1.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], diff --git a/homeassistant/components/qingping/config_flow.py b/homeassistant/components/qingping/config_flow.py index c4ebc4c4273..5b7837a9694 100644 --- a/homeassistant/components/qingping/config_flow.py +++ b/homeassistant/components/qingping/config_flow.py @@ -100,7 +100,7 @@ class QingpingConfigFlow(ConfigFlow, domain=DOMAIN): ) current_addresses = self._async_current_ids() - for discovery_info in async_discovered_service_info(self.hass): + for discovery_info in async_discovered_service_info(self.hass, False): address = discovery_info.address if address in current_addresses or address in self._discovered_devices: continue diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 8152793d805..212011b834e 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -3,7 +3,7 @@ "name": "Qingping", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qingping", - "bluetooth": [{ "local_name": "Qingping*" }], + "bluetooth": [{ "local_name": "Qingping*", "connectable": false }], "requirements": ["qingping-ble==0.3.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py index 63edd59a5b7..9913b7f7b09 100644 --- a/homeassistant/components/sensorpush/config_flow.py +++ b/homeassistant/components/sensorpush/config_flow.py @@ -73,7 +73,7 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): ) current_addresses = self._async_current_ids() - for discovery_info in async_discovered_service_info(self.hass): + for discovery_info in async_discovered_service_info(self.hass, False): address = discovery_info.address if address in current_addresses or address in self._discovered_devices: continue diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index 906b5c22f6b..d1a370aa9d7 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -5,7 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/sensorpush", "bluetooth": [ { - "local_name": "SensorPush*" + "local_name": "SensorPush*", + "connectable": false } ], "requirements": ["sensorpush-ble==1.5.2"], diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 4d9fd2af7b6..3f63a507e52 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -18,7 +18,13 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr -from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SupportedModels +from .const import ( + CONF_RETRY_COUNT, + CONNECTABLE_SUPPORTED_MODEL_TYPES, + DEFAULT_RETRY_COUNT, + DOMAIN, + SupportedModels, +) from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { @@ -40,6 +46,7 @@ CLASS_BY_DEVICE = { SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini, } + _LOGGER = logging.getLogger(__name__) @@ -65,8 +72,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) sensor_type: str = entry.data[CONF_SENSOR_TYPE] + # connectable means we can make connections to the device + connectable = sensor_type in CONNECTABLE_SUPPORTED_MODEL_TYPES.values() address: str = entry.data[CONF_ADDRESS] - ble_device = bluetooth.async_ble_device_from_address(hass, address.upper()) + ble_device = bluetooth.async_ble_device_from_address( + hass, address.upper(), connectable + ) if not ble_device: raise ConfigEntryNotReady( f"Could not find Switchbot {sensor_type} with address {address}" @@ -77,6 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: password=entry.data.get(CONF_PASSWORD), retry_count=entry.options[CONF_RETRY_COUNT], ) + coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator( hass, _LOGGER, @@ -84,6 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device, entry.unique_id, entry.data.get(CONF_NAME, entry.title), + connectable, ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index af2f43bdaa0..c46a9b2d501 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -16,7 +16,14 @@ from homeassistant.const import CONF_ADDRESS, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult -from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES +from .const import ( + CONF_RETRY_COUNT, + CONNECTABLE_SUPPORTED_MODEL_TYPES, + DEFAULT_RETRY_COUNT, + DOMAIN, + NON_CONNECTABLE_SUPPORTED_MODEL_TYPES, + SUPPORTED_MODEL_TYPES, +) _LOGGER = logging.getLogger(__name__) @@ -67,6 +74,13 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): ) if not parsed or parsed.data.get("modelName") not in SUPPORTED_MODEL_TYPES: return self.async_abort(reason="not_supported") + model_name = parsed.data.get("modelName") + if ( + not discovery_info.connectable + and model_name in CONNECTABLE_SUPPORTED_MODEL_TYPES + ): + # Source is not connectable but the model is connectable + return self.async_abort(reason="not_supported") self._discovered_adv = parsed data = parsed.data self.context["title_placeholders"] = { @@ -133,18 +147,25 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _async_discover_devices(self) -> None: current_addresses = self._async_current_ids() - for discovery_info in async_discovered_service_info(self.hass): - address = discovery_info.address - if ( - format_unique_id(address) in current_addresses - or address in self._discovered_advs - ): - continue - parsed = parse_advertisement_data( - discovery_info.device, discovery_info.advertisement - ) - if parsed and parsed.data.get("modelName") in SUPPORTED_MODEL_TYPES: - self._discovered_advs[address] = parsed + for connectable in (True, False): + for discovery_info in async_discovered_service_info(self.hass, connectable): + address = discovery_info.address + if ( + format_unique_id(address) in current_addresses + or address in self._discovered_advs + ): + continue + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + if not parsed: + continue + model_name = parsed.data.get("modelName") + if ( + discovery_info.connectable + and model_name in CONNECTABLE_SUPPORTED_MODEL_TYPES + ) or model_name in NON_CONNECTABLE_SUPPORTED_MODEL_TYPES: + self._discovered_advs[address] = parsed if not self._discovered_advs: raise AbortFlow("no_unconfigured_devices") diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 6463b9fb4a3..ad06dc7efcf 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -23,16 +23,23 @@ class SupportedModels(StrEnum): MOTION = "motion" -SUPPORTED_MODEL_TYPES = { +CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.BOT: SupportedModels.BOT, SwitchbotModel.CURTAIN: SupportedModels.CURTAIN, - SwitchbotModel.METER: SupportedModels.HYGROMETER, - SwitchbotModel.CONTACT_SENSOR: SupportedModels.CONTACT, SwitchbotModel.PLUG_MINI: SupportedModels.PLUG, - SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION, SwitchbotModel.COLOR_BULB: SupportedModels.BULB, } +NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { + SwitchbotModel.METER: SupportedModels.HYGROMETER, + SwitchbotModel.CONTACT_SENSOR: SupportedModels.CONTACT, + SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION, +} + +SUPPORTED_MODEL_TYPES = { + **CONNECTABLE_SUPPORTED_MODEL_TYPES, + **NON_CONNECTABLE_SUPPORTED_MODEL_TYPES, +} # Config Defaults DEFAULT_RETRY_COUNT = 3 diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index e4e7c25dc70..8b56b2f282f 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -41,10 +41,15 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): device: switchbot.SwitchbotDevice, base_unique_id: str, device_name: str, + connectable: bool, ) -> None: """Initialize global switchbot data updater.""" super().__init__( - hass, logger, ble_device.address, bluetooth.BluetoothScanningMode.ACTIVE + hass, + logger, + ble_device.address, + bluetooth.BluetoothScanningMode.ACTIVE, + connectable, ) self.ble_device = ble_device self.device = device diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 5631134cdf6..5011ed7e306 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -14,10 +14,12 @@ ], "bluetooth": [ { - "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb", + "connectable": false }, { - "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + "connectable": false } ], "iot_class": "local_push", diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 626b7325014..f899600a8d1 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfoBleak, + async_ble_device_from_address, ) from homeassistant.components.bluetooth.active_update_coordinator import ( ActiveBluetoothProcessorCoordinator, @@ -64,7 +65,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_poll(service_info: BluetoothServiceInfoBleak): # BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it # directly to the Xiaomi code - return await data.async_poll(service_info.device) + # Make sure the device we have is one that we can connect with + # in case its coming from a passive scanner + if service_info.connectable: + connectable_device = service_info.device + elif device := async_ble_device_from_address( + hass, service_info.device.address, True + ): + connectable_device = device + else: + # We have no bluetooth controller that is in range of + # the device to poll it + raise RuntimeError( + f"No connectable device found for {service_info.device.address}" + ) + return await data.async_poll(connectable_device) coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id @@ -78,6 +93,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), needs_poll_method=_needs_poll, poll_method=_async_poll, + # We will take advertisements from non-connectable devices + # since we will trade the BLEDevice for a connectable one + # if we need to poll it + connectable=False, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 4ec3b66d0f9..0e0e43d9121 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -232,7 +232,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): return self._async_get_or_create_entry() current_addresses = self._async_current_ids() - for discovery_info in async_discovered_service_info(self.hass): + for discovery_info in async_discovered_service_info(self.hass, False): address = discovery_info.address if address in current_addresses or address in self._discovered_devices: continue diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index e93dace95c6..c01f0846234 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "bluetooth": [ { + "connectable": false, "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 39eab0fb973..55e10c32444 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -6,7 +6,7 @@ from __future__ import annotations # fmt: off -BLUETOOTH: list[dict[str, str | int | list[int]]] = [ +BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ { "domain": "fjaraskupan", "manufacturer_id": 20296, @@ -21,50 +21,60 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ }, { "domain": "govee_ble", - "local_name": "Govee*" + "local_name": "Govee*", + "connectable": False }, { "domain": "govee_ble", - "local_name": "GVH5*" + "local_name": "GVH5*", + "connectable": False }, { "domain": "govee_ble", - "local_name": "B5178*" + "local_name": "B5178*", + "connectable": False }, { "domain": "govee_ble", "manufacturer_id": 6966, - "service_uuid": "00008451-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008451-0000-1000-8000-00805f9b34fb", + "connectable": False }, { "domain": "govee_ble", "manufacturer_id": 26589, - "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": False }, { "domain": "govee_ble", "manufacturer_id": 18994, - "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", + "connectable": False }, { "domain": "govee_ble", "manufacturer_id": 818, - "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", + "connectable": False }, { "domain": "govee_ble", "manufacturer_id": 59970, - "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": False }, { "domain": "govee_ble", "manufacturer_id": 14474, - "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": False }, { "domain": "govee_ble", "manufacturer_id": 10032, - "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb", + "connectable": False }, { "domain": "homekit_controller", @@ -75,46 +85,57 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ }, { "domain": "inkbird", - "local_name": "sps" + "local_name": "sps", + "connectable": False }, { "domain": "inkbird", - "local_name": "Inkbird*" + "local_name": "Inkbird*", + "connectable": False }, { "domain": "inkbird", - "local_name": "iBBQ*" + "local_name": "iBBQ*", + "connectable": False }, { "domain": "inkbird", - "local_name": "xBBQ*" + "local_name": "xBBQ*", + "connectable": False }, { "domain": "inkbird", - "local_name": "tps" + "local_name": "tps", + "connectable": False }, { "domain": "moat", - "local_name": "Moat_S*" + "local_name": "Moat_S*", + "connectable": False }, { "domain": "qingping", - "local_name": "Qingping*" + "local_name": "Qingping*", + "connectable": False }, { "domain": "sensorpush", - "local_name": "SensorPush*" + "local_name": "SensorPush*", + "connectable": False }, { "domain": "switchbot", - "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb", + "connectable": False }, { "domain": "switchbot", - "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + "connectable": False }, { "domain": "xiaomi_ble", + "connectable": False, "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" }, { diff --git a/homeassistant/loader.py b/homeassistant/loader.py index e4aff23d3f1..00d9bfa1e05 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -91,6 +91,7 @@ class BluetoothMatcherOptional(TypedDict, total=False): service_data_uuid: str manufacturer_id: int manufacturer_data_start: list[int] + connectable: bool class BluetoothMatcher(BluetoothMatcherRequired, BluetoothMatcherOptional): diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py index d8277213f27..22241653e1d 100644 --- a/script/hassfest/bluetooth.py +++ b/script/hassfest/bluetooth.py @@ -14,7 +14,7 @@ from __future__ import annotations # fmt: off -BLUETOOTH: list[dict[str, str | int | list[int]]] = {} +BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = {} """.strip() @@ -36,7 +36,11 @@ def generate_and_validate(integrations: list[dict[str, str]]): for entry in match_types: match_list.append({"domain": domain, **entry}) - return BASE.format(json.dumps(match_list, indent=4)) + return BASE.format( + json.dumps(match_list, indent=4) + .replace('": true', '": True') + .replace('": false', '": False') + ) def validate(integrations: dict[str, Integration], config: Config): diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index b0b4f5b0582..53970a4a895 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -197,6 +197,7 @@ MANIFEST_SCHEMA = vol.Schema( vol.Optional("bluetooth"): [ vol.Schema( { + vol.Optional("connectable"): bool, vol.Optional("service_uuid"): vol.All(str, verify_lowercase), vol.Optional("service_data_uuid"): vol.All(str, verify_lowercase), vol.Optional("local_name"): vol.All(str), diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 220432c46c2..7c559e00adc 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -6,7 +6,12 @@ from unittest.mock import patch from bleak.backends.scanner import AdvertisementData, BLEDevice -from homeassistant.components.bluetooth import DOMAIN, SOURCE_LOCAL, models +from homeassistant.components.bluetooth import ( + DOMAIN, + SOURCE_LOCAL, + async_get_advertisement_callback, + models, +) from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS from homeassistant.components.bluetooth.manager import BluetoothManager from homeassistant.core import HomeAssistant @@ -20,38 +25,84 @@ def _get_manager() -> BluetoothManager: return models.MANAGER -def inject_advertisement(device: BLEDevice, adv: AdvertisementData) -> None: +def inject_advertisement( + hass: HomeAssistant, device: BLEDevice, adv: AdvertisementData +) -> None: """Inject an advertisement into the manager.""" - return inject_advertisement_with_source(device, adv, SOURCE_LOCAL) + return inject_advertisement_with_source(hass, device, adv, SOURCE_LOCAL) def inject_advertisement_with_source( - device: BLEDevice, adv: AdvertisementData, source: str + hass: HomeAssistant, device: BLEDevice, adv: AdvertisementData, source: str ) -> None: """Inject an advertisement into the manager from a specific source.""" - inject_advertisement_with_time_and_source(device, adv, time.monotonic(), source) + inject_advertisement_with_time_and_source( + hass, device, adv, time.monotonic(), source + ) def inject_advertisement_with_time_and_source( - device: BLEDevice, adv: AdvertisementData, time: float, source: str + hass: HomeAssistant, + device: BLEDevice, + adv: AdvertisementData, + time: float, + source: str, ) -> None: """Inject an advertisement into the manager from a specific source at a time.""" - return _get_manager().scanner_adv_received(device, adv, time, source) + inject_advertisement_with_time_and_source_connectable( + hass, device, adv, time, source, True + ) + + +def inject_advertisement_with_time_and_source_connectable( + hass: HomeAssistant, + device: BLEDevice, + adv: AdvertisementData, + time: float, + source: str, + connectable: bool, +) -> None: + """Inject an advertisement into the manager from a specific source at a time and connectable status.""" + async_get_advertisement_callback(hass)( + models.BluetoothServiceInfoBleak( + name=adv.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=adv.manufacturer_data, + service_data=adv.service_data, + service_uuids=adv.service_uuids, + source=source, + device=device, + advertisement=adv, + connectable=connectable, + time=time, + ) + ) def patch_all_discovered_devices(mock_discovered: list[BLEDevice]) -> None: """Mock all the discovered devices from all the scanners.""" - manager = _get_manager() return patch.object( - manager, "async_all_discovered_devices", return_value=mock_discovered + _get_manager(), "async_all_discovered_devices", return_value=mock_discovered ) +def patch_history(mock_history: dict[str, models.BluetoothServiceInfoBleak]) -> None: + """Patch the history.""" + return patch.dict(_get_manager()._history, mock_history) + + +def patch_connectable_history( + mock_history: dict[str, models.BluetoothServiceInfoBleak] +) -> None: + """Patch the connectable history.""" + return patch.dict(_get_manager()._connectable_history, mock_history) + + def patch_discovered_devices(mock_discovered: list[BLEDevice]) -> None: """Mock the combined best path to discovered devices from all the scanners.""" - manager = _get_manager() return patch.object( - manager, "async_discovered_devices", return_value=mock_discovered + _get_manager(), "async_discovered_devices", return_value=mock_discovered ) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 57fcb8402a0..ab6137213b2 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,7 +1,8 @@ """Tests for the Bluetooth integration.""" import asyncio from datetime import timedelta -from unittest.mock import MagicMock, patch +import time +from unittest.mock import MagicMock, Mock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice @@ -20,6 +21,7 @@ from homeassistant.components.bluetooth import ( ) from homeassistant.components.bluetooth.const import ( DEFAULT_ADDRESS, + DOMAIN, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) @@ -33,6 +35,7 @@ from . import ( _get_manager, async_setup_with_default_adapter, inject_advertisement, + inject_advertisement_with_time_and_source_connectable, patch_discovered_devices, ) @@ -228,7 +231,7 @@ async def test_discovery_match_by_service_uuid( wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - inject_advertisement(wrong_device, wrong_adv) + inject_advertisement(hass, wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -238,13 +241,160 @@ async def test_discovery_match_by_service_uuid( local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "switchbot" +def _domains_from_mock_config_flow(mock_config_flow: Mock) -> list[str]: + """Get all the domains that were passed to async_init except bluetooth.""" + return [call[1][0] for call in mock_config_flow.mock_calls if call[1][0] != DOMAIN] + + +async def test_discovery_match_by_service_uuid_connectable( + hass, mock_bleak_scanner_start, macos_adapter +): + """Test bluetooth discovery match by service_uuid and the ble device is connectable.""" + mock_bt = [ + { + "domain": "switchbot", + "connectable": True, + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + } + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + await async_setup_with_default_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + + inject_advertisement_with_time_and_source_connectable( + hass, wrong_device, wrong_adv, time.monotonic(), "any", True + ) + await hass.async_block_till_done() + + assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + + inject_advertisement_with_time_and_source_connectable( + hass, switchbot_device, switchbot_adv, time.monotonic(), "any", True + ) + await hass.async_block_till_done() + + called_domains = _domains_from_mock_config_flow(mock_config_flow) + assert len(called_domains) == 1 + assert called_domains == ["switchbot"] + + +async def test_discovery_match_by_service_uuid_not_connectable( + hass, mock_bleak_scanner_start, macos_adapter +): + """Test bluetooth discovery match by service_uuid and the ble device is not connectable.""" + mock_bt = [ + { + "domain": "switchbot", + "connectable": True, + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + } + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + await async_setup_with_default_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + + inject_advertisement_with_time_and_source_connectable( + hass, wrong_device, wrong_adv, time.monotonic(), "any", False + ) + await hass.async_block_till_done() + + assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + + inject_advertisement_with_time_and_source_connectable( + hass, switchbot_device, switchbot_adv, time.monotonic(), "any", False + ) + await hass.async_block_till_done() + + assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0 + + +async def test_discovery_match_by_name_connectable_false( + hass, mock_bleak_scanner_start, macos_adapter +): + """Test bluetooth discovery match by name and the integration will take non-connectable devices.""" + mock_bt = [ + { + "domain": "qingping", + "connectable": False, + "local_name": "Qingping*", + } + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + await async_setup_with_default_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + + inject_advertisement_with_time_and_source_connectable( + hass, wrong_device, wrong_adv, time.monotonic(), "any", False + ) + await hass.async_block_till_done() + + assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0 + + qingping_device = BLEDevice("44:44:33:11:23:45", "Qingping Motion & Light") + qingping_adv = AdvertisementData( + local_name="Qingping Motion & Light", + service_data={ + "0000fdcd-0000-1000-8000-00805f9b34fb": b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x01{" + }, + ) + + inject_advertisement_with_time_and_source_connectable( + hass, qingping_device, qingping_adv, time.monotonic(), "any", False + ) + await hass.async_block_till_done() + + assert _domains_from_mock_config_flow(mock_config_flow) == ["qingping"] + + mock_config_flow.reset_mock() + # Make sure it will also take a connectable device + inject_advertisement_with_time_and_source_connectable( + hass, qingping_device, qingping_adv, time.monotonic(), "any", True + ) + await hass.async_block_till_done() + assert _domains_from_mock_config_flow(mock_config_flow) == ["qingping"] + + async def test_discovery_match_by_local_name( hass, mock_bleak_scanner_start, macos_adapter ): @@ -264,7 +414,7 @@ async def test_discovery_match_by_local_name( wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - inject_advertisement(wrong_device, wrong_adv) + inject_advertisement(hass, wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -272,7 +422,7 @@ async def test_discovery_match_by_local_name( switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -315,21 +465,21 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( # 1st discovery with no manufacturer data # should not trigger config flow - inject_advertisement(hkc_device, hkc_adv_no_mfr_data) + inject_advertisement(hass, hkc_device, hkc_adv_no_mfr_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() # 2nd discovery with manufacturer data # should trigger a config flow - inject_advertisement(hkc_device, hkc_adv) + inject_advertisement(hass, hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller" mock_config_flow.reset_mock() # 3rd discovery should not generate another flow - inject_advertisement(hkc_device, hkc_adv) + inject_advertisement(hass, hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -340,7 +490,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} ) - inject_advertisement(not_hkc_device, not_hkc_adv) + inject_advertisement(hass, not_hkc_device, not_hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -349,7 +499,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} ) - inject_advertisement(not_apple_device, not_apple_adv) + inject_advertisement(hass, not_apple_device, not_apple_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -422,21 +572,21 @@ async def test_discovery_match_by_service_data_uuid_then_others( ) # 1st discovery should not generate a flow because the # service_data_uuid is not in the advertisement - inject_advertisement(device, adv_without_service_data_uuid) + inject_advertisement(hass, device, adv_without_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() # 2nd discovery should not generate a flow because the # service_data_uuid is not in the advertisement - inject_advertisement(device, adv_without_service_data_uuid) + inject_advertisement(hass, device, adv_without_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() # 3rd discovery should generate a flow because the # manufacturer_data is in the advertisement - inject_advertisement(device, adv_with_mfr_data) + inject_advertisement(hass, device, adv_with_mfr_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "other_domain" @@ -445,7 +595,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 4th discovery should generate a flow because the # service_data_uuid is in the advertisement and # we never saw a service_data_uuid before - inject_advertisement(device, adv_with_service_data_uuid) + inject_advertisement(hass, device, adv_with_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "my_domain" @@ -453,14 +603,14 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 5th discovery should not generate a flow because the # we already saw an advertisement with the service_data_uuid - inject_advertisement(device, adv_with_service_data_uuid) + inject_advertisement(hass, device, adv_with_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 6th discovery should not generate a flow because the # manufacturer_data is in the advertisement # and we saw manufacturer_data before - inject_advertisement(device, adv_with_service_data_uuid_and_mfr_data) + inject_advertisement(hass, device, adv_with_service_data_uuid_and_mfr_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() @@ -469,7 +619,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( # service_uuids is in the advertisement # and we never saw service_uuids before inject_advertisement( - device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid + hass, device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid ) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 2 @@ -482,7 +632,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 8th discovery should not generate a flow # since all fields have been seen at this point inject_advertisement( - device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid + hass, device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid ) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -490,19 +640,19 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 9th discovery should not generate a flow # since all fields have been seen at this point - inject_advertisement(device, adv_with_service_uuid) + inject_advertisement(hass, device, adv_with_service_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 10th discovery should not generate a flow # since all fields have been seen at this point - inject_advertisement(device, adv_with_service_data_uuid) + inject_advertisement(hass, device, adv_with_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 11th discovery should not generate a flow # since all fields have been seen at this point - inject_advertisement(device, adv_without_service_data_uuid) + inject_advertisement(hass, device, adv_without_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -546,7 +696,7 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( # 1st discovery with matches service_uuid # should trigger config flow - inject_advertisement(device, adv_service_uuids) + inject_advertisement(hass, device, adv_service_uuids) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "my_domain" @@ -554,19 +704,19 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( # 2nd discovery with manufacturer data # should trigger a config flow - inject_advertisement(device, adv_manufacturer_data) + inject_advertisement(hass, device, adv_manufacturer_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "my_domain" mock_config_flow.reset_mock() # 3rd discovery should not generate another flow - inject_advertisement(device, adv_service_uuids) + inject_advertisement(hass, device, adv_service_uuids) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 4th discovery should not generate another flow - inject_advertisement(device, adv_manufacturer_data) + inject_advertisement(hass, device, adv_manufacturer_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -590,10 +740,10 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -601,7 +751,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): async_rediscover_address(hass, "44:44:33:11:23:45") - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 2 @@ -633,10 +783,10 @@ async def test_async_discovered_device_api( wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - inject_advertisement(wrong_device, wrong_adv) + inject_advertisement(hass, wrong_device, wrong_adv) switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) wrong_device_went_unavailable = False switchbot_device_went_unavailable = False @@ -670,8 +820,8 @@ async def test_async_discovered_device_api( assert wrong_device_went_unavailable is True # See the devices again - inject_advertisement(wrong_device, wrong_adv) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, wrong_device, wrong_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) # Cancel the callbacks wrong_device_unavailable_cancel() switchbot_device_unavailable_cancel() @@ -688,10 +838,11 @@ async def test_async_discovered_device_api( assert len(service_infos) == 1 # wrong_name should not appear because bleak no longer sees it - assert service_infos[0].name == "wohand" - assert service_infos[0].source == SOURCE_LOCAL - assert isinstance(service_infos[0].device, BLEDevice) - assert isinstance(service_infos[0].advertisement, AdvertisementData) + infos = list(service_infos) + assert infos[0].name == "wohand" + assert infos[0].source == SOURCE_LOCAL + assert isinstance(infos[0].device, BLEDevice) + assert isinstance(infos[0].advertisement, AdvertisementData) assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True @@ -736,25 +887,25 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) await hass.async_block_till_done() assert len(callbacks) == 3 @@ -819,25 +970,25 @@ async def test_register_callback_by_address( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) await hass.async_block_till_done() # Now register again with a callback that fails to @@ -907,7 +1058,7 @@ async def test_register_callback_survives_reload( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] assert service_info.name == "wohand" @@ -918,7 +1069,7 @@ async def test_register_callback_survives_reload( await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) assert len(callbacks) == 2 service_info: BluetoothServiceInfo = callbacks[1][0] assert service_info.name == "wohand" @@ -955,9 +1106,9 @@ async def test_process_advertisements_bail_on_good_advertisement( service_data={"00000d00-0000-1000-8000-00805f9b34fa": b"H\x10c"}, ) - inject_advertisement(device, adv) - inject_advertisement(device, adv) - inject_advertisement(device, adv) + inject_advertisement(hass, device, adv) + inject_advertisement(hass, device, adv) + inject_advertisement(hass, device, adv) await asyncio.sleep(0) @@ -997,14 +1148,14 @@ async def test_process_advertisements_ignore_bad_advertisement( # The goal of this loop is to make sure that async_process_advertisements sees at least one # callback that returns False while not done.is_set(): - inject_advertisement(device, adv) + inject_advertisement(hass, device, adv) await asyncio.sleep(0) # Set the return value and mutate the advertisement # Check that scan ends and correct advertisement data is returned return_value.set() adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c" - inject_advertisement(device, adv) + inject_advertisement(hass, device, adv) await asyncio.sleep(0) result = await handle @@ -1062,7 +1213,7 @@ async def test_wrapped_instance_with_filter( ) scanner.register_detection_callback(_device_detected) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() discovered = await scanner.discover(timeout=0) @@ -1082,12 +1233,12 @@ async def test_wrapped_instance_with_filter( assert len(discovered) == 0 assert discovered == [] - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) assert len(detected) == 4 # The filter we created in the wrapped scanner with should be respected # and we should not get another callback - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) assert len(detected) == 4 @@ -1129,14 +1280,14 @@ async def test_wrapped_instance_with_service_uuids( scanner.register_detection_callback(_device_detected) for _ in range(2): - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) assert len(detected) == 2 @@ -1177,9 +1328,9 @@ async def test_wrapped_instance_with_broken_callbacks( ) scanner.register_detection_callback(_device_detected) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 1 @@ -1222,14 +1373,14 @@ async def test_wrapped_instance_changes_uuids( scanner.register_detection_callback(_device_detected) for _ in range(2): - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) assert len(detected) == 2 @@ -1271,14 +1422,14 @@ async def test_wrapped_instance_changes_filters( scanner.register_detection_callback(_device_detected) for _ in range(2): - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - inject_advertisement(empty_device, empty_adv) + inject_advertisement(hass, empty_device, empty_adv) assert len(detected) == 2 @@ -1333,7 +1484,7 @@ async def test_async_ble_device_from_address( switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - inject_advertisement(switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() assert ( diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index eb6363521f8..9ce5985318b 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -24,7 +24,7 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason( local_name="wohand_signal_100", service_uuids=[] ) inject_advertisement_with_source( - switchbot_device_signal_100, switchbot_adv_signal_100, "hci0" + hass, switchbot_device_signal_100, switchbot_adv_signal_100, "hci0" ) assert ( @@ -37,7 +37,7 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason( local_name="wohand_signal_99", service_uuids=[] ) inject_advertisement_with_source( - switchbot_device_signal_99, switchbot_adv_signal_99, "hci0" + hass, switchbot_device_signal_99, switchbot_adv_signal_99, "hci0" ) assert ( @@ -50,7 +50,7 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason( local_name="wohand_good_signal", service_uuids=[] ) inject_advertisement_with_source( - switchbot_device_signal_98, switchbot_adv_signal_98, "hci1" + hass, switchbot_device_signal_98, switchbot_adv_signal_98, "hci1" ) # should not switch to hci1 @@ -70,7 +70,7 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): local_name="wohand_poor_signal", service_uuids=[] ) inject_advertisement_with_source( - switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" ) assert ( @@ -83,7 +83,7 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): local_name="wohand_good_signal", service_uuids=[] ) inject_advertisement_with_source( - switchbot_device_good_signal, switchbot_adv_good_signal, "hci1" + hass, switchbot_device_good_signal, switchbot_adv_good_signal, "hci1" ) assert ( @@ -92,7 +92,7 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): ) inject_advertisement_with_source( - switchbot_device_good_signal, switchbot_adv_poor_signal, "hci0" + hass, switchbot_device_good_signal, switchbot_adv_poor_signal, "hci0" ) assert ( bluetooth.async_ble_device_from_address(hass, address) @@ -108,7 +108,7 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): ) inject_advertisement_with_source( - switchbot_device_similar_signal, switchbot_adv_similar_signal, "hci0" + hass, switchbot_device_similar_signal, switchbot_adv_similar_signal, "hci0" ) assert ( bluetooth.async_ble_device_from_address(hass, address) @@ -129,6 +129,7 @@ async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): local_name="wohand_poor_signal_hci0", service_uuids=[] ) inject_advertisement_with_time_and_source( + hass, switchbot_device_poor_signal_hci0, switchbot_adv_poor_signal_hci0, start_time_monotonic, @@ -147,6 +148,7 @@ async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): local_name="wohand_poor_signal_hci1", service_uuids=[] ) inject_advertisement_with_time_and_source( + hass, switchbot_device_poor_signal_hci1, switchbot_adv_poor_signal_hci1, start_time_monotonic, @@ -163,6 +165,7 @@ async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): # even though the signal is poor because the device is now # likely unreachable via hci0 inject_advertisement_with_time_and_source( + hass, switchbot_device_poor_signal_hci1, switchbot_adv_poor_signal_hci1, start_time_monotonic + STALE_ADVERTISEMENT_SECONDS + 1, diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 9a90f99d11b..2335bf51485 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -20,7 +20,7 @@ from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_manager, patch_all_discovered_devices +from . import patch_all_discovered_devices, patch_history from tests.common import async_fire_time_changed @@ -178,15 +178,9 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True - scanner = _get_manager() - with patch_all_discovered_devices( [MagicMock(address="44:44:33:11:23:45")] - ), patch.object( - scanner, - "history", - {"aa:bb:cc:dd:ee:ff": MagicMock()}, - ): + ), patch_history({"aa:bb:cc:dd:ee:ff": MagicMock()}): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) @@ -198,11 +192,7 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( with patch_all_discovered_devices( [MagicMock(address="44:44:33:11:23:45")] - ), patch.object( - scanner, - "history", - {"aa:bb:cc:dd:ee:ff": MagicMock()}, - ): + ), patch_history({"aa:bb:cc:dd:ee:ff": MagicMock()}): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index ac35e9f2bee..2ae6f77b28d 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -32,7 +32,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_manager, patch_all_discovered_devices +from . import patch_all_discovered_devices, patch_connectable_history, patch_history from tests.common import MockEntityPlatform, async_fire_time_changed @@ -246,12 +246,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True assert processor.available is True - scanner = _get_manager() with patch_all_discovered_devices( [MagicMock(address="44:44:33:11:23:45")] - ), patch.object( - scanner, - "history", + ), patch_history({"aa:bb:cc:dd:ee:ff": MagicMock()}), patch_connectable_history( {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): async_fire_time_changed( @@ -268,9 +265,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): with patch_all_discovered_devices( [MagicMock(address="44:44:33:11:23:45")] - ), patch.object( - scanner, - "history", + ), patch_history({"aa:bb:cc:dd:ee:ff": MagicMock()}), patch_connectable_history( {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): async_fire_time_changed( diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index f9f0a51fc0f..071e8e16d23 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -90,6 +90,8 @@ async def test_preserve_new_tracked_device_name( source="local", device=BLEDevice(address, None), advertisement=AdvertisementData(local_name="empty"), + time=0, + connectable=False, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -113,6 +115,8 @@ async def test_preserve_new_tracked_device_name( source="local", device=BLEDevice(address, None), advertisement=AdvertisementData(local_name="empty"), + time=0, + connectable=False, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -155,6 +159,8 @@ async def test_tracking_battery_times_out( source="local", device=BLEDevice(address, None), advertisement=AdvertisementData(local_name="empty"), + time=0, + connectable=False, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -219,6 +225,8 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ source="local", device=BLEDevice(address, None), advertisement=AdvertisementData(local_name="empty"), + time=0, + connectable=False, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -285,6 +293,8 @@ async def test_tracking_battery_successful( source="local", device=BLEDevice(address, None), advertisement=AdvertisementData(local_name="empty"), + time=0, + connectable=True, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] diff --git a/tests/components/fjaraskupan/__init__.py b/tests/components/fjaraskupan/__init__.py index 35c69f98d65..94acad4df5a 100644 --- a/tests/components/fjaraskupan/__init__.py +++ b/tests/components/fjaraskupan/__init__.py @@ -4,8 +4,18 @@ from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from homeassistant.components.bluetooth import SOURCE_LOCAL, BluetoothServiceInfoBleak +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -COOKER_SERVICE_INFO = BluetoothServiceInfoBleak.from_advertisement( - BLEDevice("1.1.1.1", "COOKERHOOD_FJAR"), AdvertisementData(), source=SOURCE_LOCAL +COOKER_SERVICE_INFO = BluetoothServiceInfoBleak( + name="COOKERHOOD_FJAR", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={}, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="COOKERHOOD_FJAR"), + advertisement=AdvertisementData(), + time=0, + connectable=True, ) diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index fbf764fa4eb..3235a9e8cd3 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -69,6 +69,28 @@ WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), + time=0, + connectable=True, +) + + +WOHAND_SERVICE_INFO_NOT_CONNECTABLE = BluetoothServiceInfoBleak( + name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), + time=0, + connectable=False, ) @@ -87,6 +109,8 @@ WOHAND_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), device=BLEDevice("798A8547-2A3D-C609-55FF-73FA824B923B", "WoHand"), + time=0, + connectable=True, ) @@ -105,6 +129,8 @@ WOHAND_SERVICE_ALT_ADDRESS_INFO = BluetoothServiceInfoBleak( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), + time=0, + connectable=True, ) WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak( name="WoCurtain", @@ -121,6 +147,8 @@ WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoCurtain"), + time=0, + connectable=True, ) WOSENSORTH_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -136,6 +164,8 @@ WOSENSORTH_SERVICE_INFO = BluetoothServiceInfoBleak( service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00d\x00\x96\xac"}, ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoSensorTH"), + time=0, + connectable=False, ) NOT_SWITCHBOT_INFO = BluetoothServiceInfoBleak( @@ -151,4 +181,6 @@ NOT_SWITCHBOT_INFO = BluetoothServiceInfoBleak( service_data={}, ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "unknown"), + time=0, + connectable=True, ) diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 7ad863cc355..71b7018a6b3 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -14,6 +14,7 @@ from . import ( WOHAND_ENCRYPTED_SERVICE_INFO, WOHAND_SERVICE_ALT_ADDRESS_INFO, WOHAND_SERVICE_INFO, + WOHAND_SERVICE_INFO_NOT_CONNECTABLE, WOSENSORTH_SERVICE_INFO, init_integration, patch_async_setup_entry, @@ -112,6 +113,17 @@ async def test_async_step_bluetooth_not_switchbot(hass): assert result["reason"] == "not_supported" +async def test_async_step_bluetooth_not_connectable(hass): + """Test discovery via bluetooth and its not connectable switchbot.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOHAND_SERVICE_INFO_NOT_CONNECTABLE, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + async def test_user_setup_wohand(hass): """Test the user initiated form with password and valid mac.""" @@ -203,7 +215,12 @@ async def test_user_setup_wocurtain_or_bot(hass): with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", - return_value=[WOCURTAIN_SERVICE_INFO, WOHAND_SERVICE_ALT_ADDRESS_INFO], + return_value=[ + NOT_SWITCHBOT_INFO, + WOCURTAIN_SERVICE_INFO, + WOHAND_SERVICE_ALT_ADDRESS_INFO, + WOHAND_SERVICE_INFO_NOT_CONNECTABLE, + ], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -234,7 +251,11 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass): with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", - return_value=[WOCURTAIN_SERVICE_INFO, WOHAND_ENCRYPTED_SERVICE_INFO], + return_value=[ + WOCURTAIN_SERVICE_INFO, + WOHAND_ENCRYPTED_SERVICE_INFO, + WOHAND_SERVICE_INFO_NOT_CONNECTABLE, + ], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index c4424236082..4593e5c01f3 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -15,6 +15,8 @@ NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfoBleak( service_uuids=[], source="local", advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, ) LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -29,6 +31,8 @@ LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfoBleak( service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, ) MMC_T201_1_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -43,6 +47,8 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfoBleak( service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, ) JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -57,6 +63,8 @@ JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfoBleak( service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, ) YLKG07YL_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -71,6 +79,8 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfoBleak( service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, ) MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfoBleak( @@ -85,10 +95,14 @@ MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfoBleak( service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, ) -def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBleak: +def make_advertisement( + address: str, payload: bytes, connectable: bool = True +) -> BluetoothServiceInfoBleak: """Make a dummy advertisement.""" return BluetoothServiceInfoBleak( name="Test Device", @@ -102,4 +116,6 @@ def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBlea service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", advertisement=AdvertisementData(local_name="Test Device"), + time=0, + connectable=connectable, ) diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index b49d65f58ae..c4052d36bf4 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -2,7 +2,10 @@ from unittest.mock import patch -from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.bluetooth import ( + BluetoothChange, + async_get_advertisement_callback, +) from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -294,6 +297,190 @@ async def test_xiaomi_HHCCJCY01(hass): await hass.async_block_till_done() +async def test_xiaomi_HHCCJCY01_not_connectable(hass): + """This device has multiple advertisements before all sensors are visible but not connectable.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7B", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", + b"q \x98\x00fz>j\x8d|\xc4\r\x07\x10\x03\x00\x00\x00", + connectable=False, + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", + b"q \x98\x00hz>j\x8d|\xc4\r\t\x10\x02W\x02", + connectable=False, + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", + b"q \x98\x00Gz>j\x8d|\xc4\r\x08\x10\x01@", + connectable=False, + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", + b"q \x98\x00iz>j\x8d|\xc4\r\x04\x10\x02\xf4\x00", + connectable=False, + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") + illum_sensor_attr = illum_sensor.attributes + assert illum_sensor.state == "0" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" + assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") + cond_sensor_attribtes = cond_sensor.attributes + assert cond_sensor.state == "599" + assert ( + cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" + ) + assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") + moist_sensor_attribtes = moist_sensor.attributes + assert moist_sensor.state == "64" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" + assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "24.4" + assert ( + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" + ) + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + # No battery sensor since its not connectable + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_HHCCJCY01_only_some_sources_connectable(hass): + """This device has multiple advertisements before all sensors are visible and some sources are connectable.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = async_get_advertisement_callback(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", + b"q \x98\x00fz>j\x8d|\xc4\r\x07\x10\x03\x00\x00\x00", + connectable=True, + ), + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", + b"q \x98\x00hz>j\x8d|\xc4\r\t\x10\x02W\x02", + connectable=False, + ), + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", + b"q \x98\x00Gz>j\x8d|\xc4\r\x08\x10\x01@", + connectable=False, + ), + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", + b"q \x98\x00iz>j\x8d|\xc4\r\x04\x10\x02\xf4\x00", + connectable=False, + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 5 + + illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") + illum_sensor_attr = illum_sensor.attributes + assert illum_sensor.state == "0" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" + assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") + cond_sensor_attribtes = cond_sensor.attributes + assert cond_sensor.state == "599" + assert ( + cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" + ) + assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") + moist_sensor_attribtes = moist_sensor.attributes + assert moist_sensor.state == "64" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" + assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "24.4" + assert ( + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" + ) + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + batt_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_battery") + batt_sensor_attribtes = batt_sensor.attributes + assert batt_sensor.state == "5" + assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Battery" + assert batt_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert batt_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_xiaomi_CGDK2(hass): """This device has encrypion so we need to retrieve its bindkey from the configentry.""" entry = MockConfigEntry( diff --git a/tests/components/yalexs_ble/__init__.py b/tests/components/yalexs_ble/__init__.py index eb6800ff83a..36002a49f3e 100644 --- a/tests/components/yalexs_ble/__init__.py +++ b/tests/components/yalexs_ble/__init__.py @@ -17,6 +17,8 @@ YALE_ACCESS_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( source="local", device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="M1012LU"), advertisement=AdvertisementData(), + time=0, + connectable=True, ) @@ -33,6 +35,8 @@ LOCK_DISCOVERY_INFO_UUID_ADDRESS = BluetoothServiceInfoBleak( source="local", device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="M1012LU"), advertisement=AdvertisementData(), + time=0, + connectable=True, ) OLD_FIRMWARE_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( @@ -48,6 +52,8 @@ OLD_FIRMWARE_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( source="local", device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"), advertisement=AdvertisementData(), + time=0, + connectable=True, ) @@ -64,4 +70,6 @@ NOT_YALE_DISCOVERY_INFO = BluetoothServiceInfoBleak( source="local", device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"), advertisement=AdvertisementData(), + time=0, + connectable=True, ) From e4b288ef7cb49be63fd45ac7714851cfc44bd11e Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Mon, 22 Aug 2022 12:03:57 -0600 Subject: [PATCH 559/903] Load sms notify via discovery (#76733) * Fix #76283 Fix #76283 * Update notify.py * Support sending SMS as ANSI * Put back load via discovery * Update homeassistant/components/sms/const.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/sms/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/sms/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/sms/notify.py Co-authored-by: Martin Hjelmare * Fix typo * Apply PR feedback * Fix bad reference * Update homeassistant/components/sms/notify.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/sms/notify.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/sms/notify.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/sms/notify.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/sms/notify.py Co-authored-by: Martin Hjelmare * Apply PR feedback * Add back schema * Update homeassistant/components/sms/notify.py Co-authored-by: Martin Hjelmare * Fix pylint * Remove platform schema * Fix after merge Co-authored-by: Martin Hjelmare --- homeassistant/components/sms/__init__.py | 33 +++++++++++++----------- homeassistant/components/sms/const.py | 1 + homeassistant/components/sms/gateway.py | 9 +++++-- homeassistant/components/sms/notify.py | 27 ++++++++----------- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 83d4bbc31f3..27cb7ac034d 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -6,10 +6,11 @@ import async_timeout import gammu # pylint: disable=import-error import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_DEVICE, Platform +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -19,6 +20,7 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DOMAIN, GATEWAY, + HASS_CONFIG, NETWORK_COORDINATOR, SIGNAL_COORDINATOR, SMS_GATEWAY, @@ -49,17 +51,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Configure Gammu state machine.""" hass.data.setdefault(DOMAIN, {}) - if not (sms_config := config.get(DOMAIN, {})): - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=sms_config, - ) - ) - + hass.data[HASS_CONFIG] = config return True @@ -75,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Connecting mode:%s", connection_mode) gateway = await create_sms_gateway(config, hass) if not gateway: - return False + raise ConfigEntryNotReady(f"Cannot find device {device}") signal_coordinator = SignalCoordinator(hass, gateway) network_coordinator = NetworkCoordinator(hass, gateway) @@ -93,6 +85,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # set up notify platform, no entry support for notify component yet, + # have to use discovery to load platform. + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_NAME: DOMAIN}, + hass.data[HASS_CONFIG], + ) + ) return True diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py index 841c4bd8f89..d055894f402 100644 --- a/homeassistant/components/sms/const.py +++ b/homeassistant/components/sms/const.py @@ -7,6 +7,7 @@ from homeassistant.helpers.entity import EntityCategory DOMAIN = "sms" SMS_GATEWAY = "SMS_GATEWAY" +HASS_CONFIG = "sms_hass_config" SMS_STATE_UNREAD = "UnRead" SIGNAL_COORDINATOR = "signal_coordinator" NETWORK_COORDINATOR = "network_coordinator" diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index c469e688737..7a2f095abd1 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -167,8 +167,13 @@ async def create_sms_gateway(config, hass): """Create the sms gateway.""" try: gateway = Gateway(config, hass) - await gateway.init_async() + try: + await gateway.init_async() + except gammu.GSMError as exc: + _LOGGER.error("Failed to initialize, error %s", exc) + await gateway.terminate_async() + return None return gateway except gammu.GSMError as exc: - _LOGGER.error("Failed to initialize, error %s", exc) + _LOGGER.error("Failed to create async worker, error %s", exc) return None diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index d82e4951c00..13f9cfc9f72 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -2,40 +2,31 @@ import logging import gammu # pylint: disable=import-error -import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import CONF_NAME, CONF_RECIPIENT, CONF_TARGET -import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import BaseNotificationService +from homeassistant.const import CONF_TARGET from .const import CONF_UNICODE, DOMAIN, GATEWAY, SMS_GATEWAY _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_RECIPIENT): cv.string, vol.Optional(CONF_NAME): cv.string} -) - -def get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the SMS notification service.""" if discovery_info is None: - number = config[CONF_RECIPIENT] - else: - number = discovery_info[CONF_RECIPIENT] + return None - return SMSNotificationService(hass, number) + return SMSNotificationService(hass) class SMSNotificationService(BaseNotificationService): """Implement the notification service for SMS.""" - def __init__(self, hass, number): + def __init__(self, hass): """Initialize the service.""" self.hass = hass - self.number = number async def async_send_message(self, message="", **kwargs): """Send SMS message.""" @@ -46,7 +37,11 @@ class SMSNotificationService(BaseNotificationService): gateway = self.hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] - targets = kwargs.get(CONF_TARGET, [self.number]) + targets = kwargs.get(CONF_TARGET) + if targets is None: + _LOGGER.error("No target number specified, cannot send message") + return + is_unicode = kwargs.get(CONF_UNICODE, True) smsinfo = { "Class": -1, From e3210291a5b94b3652525190744e7b65b48ffa68 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 22 Aug 2022 14:10:02 -0400 Subject: [PATCH 560/903] Bump version of pyunifiprotect to 4.1.4 (#77172) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 218516dc81b..6ff2e8bb91d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.1.2", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.1.4", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index bcabcdb7297..6aed6cc56f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2019,7 +2019,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.2 +pyunifiprotect==4.1.4 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65e33123f38..bf605143b33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1382,7 +1382,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.2 +pyunifiprotect==4.1.4 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 03ed552ca9ea9dcd85cd26f62ff0bd1a1134a562 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 20:23:15 +0200 Subject: [PATCH 561/903] Improve type hint in foscam camera entity (#77166) --- homeassistant/components/foscam/camera.py | 52 ++++++++--------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index cf2740ab9cc..c0b6661a392 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -94,18 +94,17 @@ async def async_setup_entry( class HassFoscamCamera(Camera): """An implementation of a Foscam IP camera.""" - def __init__(self, camera, config_entry): + def __init__(self, camera: FoscamCamera, config_entry: ConfigEntry) -> None: """Initialize a Foscam camera.""" super().__init__() self._foscam_session = camera - self._name = config_entry.title + self._attr_name = config_entry.title 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.entry_id + self._attr_unique_id = config_entry.entry_id self._rtsp_port = config_entry.data[CONF_RTSP_PORT] - self._motion_status = False if self._rtsp_port: self._attr_supported_features = CameraEntityFeature.STREAM @@ -119,21 +118,16 @@ class HassFoscamCamera(Camera): if ret == -3: LOGGER.info( "Can't get motion detection status, camera %s configured with non-admin user", - self._name, + self.name, ) elif ret != 0: LOGGER.error( - "Error getting motion detection status of %s: %s", self._name, ret + "Error getting motion detection status of %s: %s", self.name, ret ) else: - self._motion_status = response == 1 - - @property - def unique_id(self): - """Return the entity unique ID.""" - return self._unique_id + self._attr_motion_detection_enabled = response == 1 def camera_image( self, width: int | None = None, height: int | None = None @@ -147,18 +141,13 @@ class HassFoscamCamera(Camera): return response - async def stream_source(self): + async def stream_source(self) -> str | None: """Return the stream source.""" if self._rtsp_port: return f"rtsp://{self._username}:{self._password}@{self._foscam_session.host}:{self._rtsp_port}/video{self._stream}" return None - @property - def motion_detection_enabled(self): - """Camera Motion Detection Status.""" - return self._motion_status - def enable_motion_detection(self) -> None: """Enable motion detection in camera.""" try: @@ -168,15 +157,15 @@ class HassFoscamCamera(Camera): if ret == -3: LOGGER.info( "Can't set motion detection status, camera %s configured with non-admin user", - self._name, + self.name, ) return - self._motion_status = True + self._attr_motion_detection_enabled = True except TypeError: LOGGER.debug( "Failed enabling motion detection on '%s'. Is it supported by the device?", - self._name, + self.name, ) def disable_motion_detection(self) -> None: @@ -188,27 +177,27 @@ class HassFoscamCamera(Camera): if ret == -3: LOGGER.info( "Can't set motion detection status, camera %s configured with non-admin user", - self._name, + self.name, ) return - self._motion_status = False + self._attr_motion_detection_enabled = False except TypeError: LOGGER.debug( "Failed disabling motion detection on '%s'. Is it supported by the device?", - self._name, + self.name, ) async def async_perform_ptz(self, movement, travel_time): """Perform a PTZ action on the camera.""" - LOGGER.debug("PTZ action '%s' on %s", movement, self._name) + LOGGER.debug("PTZ action '%s' on %s", movement, self.name) movement_function = getattr(self._foscam_session, MOVEMENT_ATTRS[movement]) ret, _ = await self.hass.async_add_executor_job(movement_function) if ret != 0: - LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret) + LOGGER.error("Error moving %s '%s': %s", movement, self.name, ret) return await asyncio.sleep(travel_time) @@ -218,12 +207,12 @@ class HassFoscamCamera(Camera): ) if ret != 0: - LOGGER.error("Error stopping movement on '%s': %s", self._name, ret) + LOGGER.error("Error stopping movement on '%s': %s", self.name, ret) return async def async_perform_ptz_preset(self, preset_name): """Perform a PTZ preset action on the camera.""" - LOGGER.debug("PTZ preset '%s' on %s", preset_name, self._name) + LOGGER.debug("PTZ preset '%s' on %s", preset_name, self.name) preset_function = getattr(self._foscam_session, PTZ_GOTO_PRESET_COMMAND) @@ -231,11 +220,6 @@ class HassFoscamCamera(Camera): if ret != 0: LOGGER.error( - "Error moving to preset %s on '%s': %s", preset_name, self._name, ret + "Error moving to preset %s on '%s': %s", preset_name, self.name, ret ) return - - @property - def name(self): - """Return the name of this camera.""" - return self._name From df5f6bdfc1ed4141847dc201e5492bbd81794ba8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 20:30:35 +0200 Subject: [PATCH 562/903] Use _attr_should_poll in camera entities (#77173) --- homeassistant/components/agent_dvr/camera.py | 6 +----- homeassistant/components/amcrest/camera.py | 9 +-------- homeassistant/components/logi_circle/camera.py | 6 +----- homeassistant/components/nest/camera_sdm.py | 5 ----- homeassistant/components/nest/legacy/camera.py | 6 +----- homeassistant/components/uvc/camera.py | 7 ++----- homeassistant/components/zoneminder/camera.py | 7 ++----- 7 files changed, 8 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 2e8568b22b7..e485940034f 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -70,6 +70,7 @@ class AgentCamera(MjpegCamera): """Representation of an Agent Device Stream.""" _attr_attribution = ATTRIBUTION + _attr_should_poll = True # Cameras default to False _attr_supported_features = CameraEntityFeature.ON_OFF def __init__(self, device): @@ -117,11 +118,6 @@ class AgentCamera(MjpegCamera): "alerts_enabled": self.device.alerts_active, } - @property - def should_poll(self) -> bool: - """Update the state periodically.""" - return True - @property def is_recording(self) -> bool: """Return whether the monitor is recording.""" diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index c3d1e0d28d6..da5e046a88a 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -164,6 +164,7 @@ class AmcrestCommandFailed(Exception): class AmcrestCam(Camera): """An implementation of an Amcrest IP camera.""" + _attr_should_poll = True # Cameras default to False _attr_supported_features = CameraEntityFeature.ON_OFF | CameraEntityFeature.STREAM def __init__(self, name: str, device: AmcrestDevice, ffmpeg: FFmpegManager) -> None: @@ -281,14 +282,6 @@ class AmcrestCam(Camera): # Entity property overrides - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return True - @property def name(self) -> str: """Return the name of this camera.""" diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 097b64ac208..231de83a135 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -62,6 +62,7 @@ async def async_setup_entry( class LogiCam(Camera): """An implementation of a Logi Circle camera.""" + _attr_should_poll = True # Cameras default to False _attr_supported_features = CameraEntityFeature.ON_OFF def __init__(self, camera, device_info, ffmpeg): @@ -168,11 +169,6 @@ class LogiCam(Camera): """Enable streaming mode for this camera.""" await self._camera.set_config("streaming", True) - @property - def should_poll(self): - """Update the image periodically.""" - return True - async def set_config(self, mode, value): """Set an configuration property for the target camera.""" if mode == LED_MODE_KEY: diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index c26b216b21f..e148916d7e8 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -74,11 +74,6 @@ class NestCamera(Camera): self._attr_is_streaming = CameraLiveStreamTrait.NAME in self._device.traits self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3 - @property - def should_poll(self) -> bool: - """Disable polling since entities have state pushed via pubsub.""" - return False - @property def unique_id(self) -> str: """Return a unique ID.""" diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py index affe912b6fb..d118b9b8c4a 100644 --- a/homeassistant/components/nest/legacy/camera.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -38,6 +38,7 @@ async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: class NestCamera(Camera): """Representation of a Nest Camera.""" + _attr_should_poll = True # Cameras default to False _attr_supported_features = CameraEntityFeature.ON_OFF def __init__(self, structure, device): @@ -75,11 +76,6 @@ class NestCamera(Camera): name=self.device.name_long, ) - @property - def should_poll(self): - """Nest camera should poll periodically.""" - return True - @property def is_recording(self): """Return true if the device is recording.""" diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index e6365f21dfe..d6c3bd55d63 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -86,6 +86,8 @@ def setup_platform( class UnifiVideoCamera(Camera): """A Ubiquiti Unifi Video Camera.""" + _attr_should_poll = True # Cameras default to False + def __init__(self, camera, uuid, name, password): """Initialize an Unifi camera.""" super().__init__() @@ -104,11 +106,6 @@ class UnifiVideoCamera(Camera): """Return the name of this camera.""" return self._name - @property - def should_poll(self): - """If this entity should be polled.""" - return True - @property def supported_features(self): """Return supported features.""" diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index a7f0cf1d7fa..a627f64d0bf 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -36,6 +36,8 @@ def setup_platform( class ZoneMinderCamera(MjpegCamera): """Representation of a ZoneMinder Monitor Stream.""" + _attr_should_poll = True # Cameras default to False + def __init__(self, monitor, verify_ssl): """Initialize as a subclass of MjpegCamera.""" super().__init__( @@ -48,11 +50,6 @@ class ZoneMinderCamera(MjpegCamera): self._is_available = None self._monitor = monitor - @property - def should_poll(self): - """Update the recording state periodically.""" - return True - def update(self): """Update our recording state from the ZM API.""" _LOGGER.debug("Updating camera state for monitor %i", self._monitor.id) From f0646dfb4295d63de896add9e0c0dc1aef7a0293 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 22 Aug 2022 21:40:33 +0200 Subject: [PATCH 563/903] Improve type hint in filter sensor entity (#77155) --- homeassistant/components/filter/sensor.py | 87 +++++++++-------------- 1 file changed, 35 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index fead10c71a3..4c9c83a8787 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -8,6 +8,7 @@ from functools import partial import logging from numbers import Number import statistics +from typing import Any import voluptuous as vol @@ -34,7 +35,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -180,13 +181,14 @@ async def async_setup_platform( await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - name = config.get(CONF_NAME) - unique_id = config.get(CONF_UNIQUE_ID) - entity_id = config.get(CONF_ENTITY_ID) + name: str | None = config.get(CONF_NAME) + unique_id: str | None = config.get(CONF_UNIQUE_ID) + entity_id: str = config[CONF_ENTITY_ID] + filter_configs: list[dict[str, Any]] = config[CONF_FILTERS] filters = [ FILTERS[_filter.pop(CONF_FILTER_NAME)](entity=entity_id, **_filter) - for _filter in config[CONF_FILTERS] + for _filter in filter_configs ] async_add_entities([SensorFilter(name, unique_id, entity_id, filters)]) @@ -195,30 +197,41 @@ async def async_setup_platform( class SensorFilter(SensorEntity): """Representation of a Filter Sensor.""" - def __init__(self, name, unique_id, entity_id, filters): + _attr_should_poll = False + + def __init__( + self, + name: str | None, + unique_id: str | None, + entity_id: str, + filters: list[Filter], + ) -> None: """Initialize the sensor.""" - self._name = name + self._attr_name = name self._attr_unique_id = unique_id self._entity = entity_id - self._unit_of_measurement = None - self._state = None + self._attr_native_unit_of_measurement = None + self._state: str | None = None self._filters = filters - self._icon = None - self._device_class = None + self._attr_icon = None + self._attr_device_class = None self._attr_state_class = None + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_id} @callback - def _update_filter_sensor_state_event(self, event): + def _update_filter_sensor_state_event(self, event: Event) -> None: """Handle device state changes.""" _LOGGER.debug("Update filter on event: %s", event) self._update_filter_sensor_state(event.data.get("new_state")) @callback - def _update_filter_sensor_state(self, new_state, update_ha=True): + def _update_filter_sensor_state( + self, new_state: State | None, update_ha: bool = True + ) -> None: """Process device state changes.""" if new_state is None: _LOGGER.warning( - "While updating filter %s, the new_state is None", self._name + "While updating filter %s, the new_state is None", self.name ) self._state = None self.async_write_ha_state() @@ -254,14 +267,14 @@ class SensorFilter(SensorEntity): self._state = temp_state.state - if self._icon is None: - self._icon = new_state.attributes.get(ATTR_ICON, ICON) + if self._attr_icon is None: + self._attr_icon = new_state.attributes.get(ATTR_ICON, ICON) if ( - self._device_class is None + self._attr_device_class is None and new_state.attributes.get(ATTR_DEVICE_CLASS) in SENSOR_DEVICE_CLASSES ): - self._device_class = new_state.attributes.get(ATTR_DEVICE_CLASS) + self._attr_device_class = new_state.attributes.get(ATTR_DEVICE_CLASS) if ( self._attr_state_class is None @@ -269,8 +282,8 @@ class SensorFilter(SensorEntity): ): self._attr_state_class = new_state.attributes.get(ATTR_STATE_CLASS) - if self._unit_of_measurement is None: - self._unit_of_measurement = new_state.attributes.get( + if self._attr_native_unit_of_measurement is None: + self._attr_native_unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT ) @@ -348,43 +361,13 @@ class SensorFilter(SensorEntity): ) @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): + def native_value(self) -> datetime | str | None: """Return the state of the sensor.""" - if self._device_class == SensorDeviceClass.TIMESTAMP: + if self._state is not None and self.device_class == SensorDeviceClass.TIMESTAMP: return datetime.fromisoformat(self._state) return self._state - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._icon - - @property - def native_unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return self._unit_of_measurement - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return {ATTR_ENTITY_ID: self._entity} - - @property - def device_class(self): - """Return device class.""" - return self._device_class - class FilterState: """State abstraction for filter usage.""" From 29b502bb0ac900aa4e7e986d9af044c0aecf3002 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Mon, 22 Aug 2022 22:18:17 +0200 Subject: [PATCH 564/903] Add diagnostics for Pure Energie integration (#77151) * Add diagnostics for Pure Energie integration * Update test after feedback --- .../components/pure_energie/diagnostics.py | 36 ++++++++++++++++ .../pure_energie/test_diagnostics.py | 41 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 homeassistant/components/pure_energie/diagnostics.py create mode 100644 tests/components/pure_energie/test_diagnostics.py diff --git a/homeassistant/components/pure_energie/diagnostics.py b/homeassistant/components/pure_energie/diagnostics.py new file mode 100644 index 00000000000..b2e071d58cd --- /dev/null +++ b/homeassistant/components/pure_energie/diagnostics.py @@ -0,0 +1,36 @@ +"""Diagnostics support for Pure Energie.""" +from __future__ import annotations + +from dataclasses import asdict +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from . import PureEnergieDataUpdateCoordinator +from .const import DOMAIN + +TO_REDACT = { + CONF_HOST, + "n2g_id", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: PureEnergieDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + return { + "entry": { + "title": entry.title, + "data": async_redact_data(entry.data, TO_REDACT), + }, + "data": { + "device": async_redact_data(asdict(coordinator.data.device), TO_REDACT), + "smartbridge": asdict(coordinator.data.smartbridge), + }, + } diff --git a/tests/components/pure_energie/test_diagnostics.py b/tests/components/pure_energie/test_diagnostics.py new file mode 100644 index 00000000000..afb76724447 --- /dev/null +++ b/tests/components/pure_energie/test_diagnostics.py @@ -0,0 +1,41 @@ +"""Tests for the diagnostics data provided by the Pure Energie integration.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +) -> None: + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": { + "title": "home", + "data": { + "host": REDACTED, + }, + }, + "data": { + "device": { + "batch": "SBP-HMX-210318", + "firmware": "1.6.16", + "hardware": 1, + "manufacturer": "NET2GRID", + "model": "SBWF3102", + "n2g_id": REDACTED, + }, + "smartbridge": { + "energy_consumption_total": 17762.1, + "energy_production_total": 21214.6, + "power_flow": 338, + }, + }, + } From 9843753f302ff8db3f2c123e8cd03a211ba0af05 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 22 Aug 2022 23:43:09 +0200 Subject: [PATCH 565/903] Add alias support to all triggers (#77184) --- .../components/automation/__init__.py | 10 ++++-- homeassistant/helpers/config_validation.py | 1 + homeassistant/helpers/trigger.py | 12 +++++-- tests/helpers/test_trigger.py | 34 +++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 7421e95f293..e177b76faf3 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -443,9 +443,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity): This method is a coroutine. """ reason = "" - if "trigger" in run_variables and "description" in run_variables["trigger"]: - reason = f' by {run_variables["trigger"]["description"]}' - self._logger.debug("Automation triggered%s", reason) + alias = "" + if "trigger" in run_variables: + if "description" in run_variables["trigger"]: + reason = f' by {run_variables["trigger"]["description"]}' + if "alias" in run_variables["trigger"]: + alias = f' trigger \'{run_variables["trigger"]["alias"]}\'' + self._logger.debug("Automation%s triggered%s", alias, reason) # Create a new context referring to the old context. parent_id = None if context is None else context.id diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 2ed4bc7abab..4c2fed60bb4 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1425,6 +1425,7 @@ CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema( TRIGGER_BASE_SCHEMA = vol.Schema( { + vol.Optional(CONF_ALIAS): str, vol.Required(CONF_PLATFORM): str, vol.Optional(CONF_ID): str, vol.Optional(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA, diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index a48ccbbb7b9..9fde56ec7aa 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -9,7 +9,13 @@ from typing import TYPE_CHECKING, Any, Protocol, TypedDict import voluptuous as vol -from homeassistant.const import CONF_ENABLED, CONF_ID, CONF_PLATFORM, CONF_VARIABLES +from homeassistant.const import ( + CONF_ALIAS, + CONF_ENABLED, + CONF_ID, + CONF_PLATFORM, + CONF_VARIABLES, +) from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -43,6 +49,7 @@ class TriggerData(TypedDict): id: str idx: str + alias: str | None class TriggerInfo(TypedDict): @@ -130,7 +137,8 @@ async def async_initialize_triggers( platform = await _async_get_trigger_platform(hass, conf) trigger_id = conf.get(CONF_ID, f"{idx}") trigger_idx = f"{idx}" - trigger_data = TriggerData(id=trigger_id, idx=trigger_idx) + trigger_alias = conf.get(CONF_ALIAS) + trigger_data = TriggerData(id=trigger_id, idx=trigger_idx, alias=trigger_alias) info = TriggerInfo( domain=domain, name=name, diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 9c59b00c843..7cee307f3ec 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -103,3 +103,37 @@ async def test_if_disabled_trigger_not_firing( hass.bus.async_fire("enabled_trigger_event") await hass.async_block_till_done() assert len(calls) == 1 + + +async def test_trigger_alias( + hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture +) -> None: + """Test triggers support aliases.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": [ + { + "alias": "My event", + "platform": "event", + "event_type": "trigger_event", + } + ], + "action": { + "service": "test.automation", + "data_template": {"alias": "{{ trigger.alias }}"}, + }, + } + }, + ) + + hass.bus.async_fire("trigger_event") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["alias"] == "My event" + assert ( + "Automation trigger 'My event' triggered by event 'trigger_event'" + in caplog.text + ) From 0f0e398945b057336e2d933bc9d3342610b7a823 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 23 Aug 2022 00:32:02 +0000 Subject: [PATCH 566/903] [ci skip] Translation update --- .../android_ip_webcam/translations/el.json | 3 +- .../android_ip_webcam/translations/et.json | 3 +- .../android_ip_webcam/translations/no.json | 3 +- .../components/bluetooth/translations/el.json | 9 ++++ .../components/bluetooth/translations/et.json | 11 +++- .../lacrosse_view/translations/et.json | 3 +- .../components/lametric/translations/et.json | 50 +++++++++++++++++++ .../landisgyr_heat_meter/translations/et.json | 23 +++++++++ .../p1_monitor/translations/el.json | 3 ++ .../p1_monitor/translations/et.json | 3 ++ .../p1_monitor/translations/no.json | 3 ++ .../pure_energie/translations/el.json | 3 ++ .../pure_energie/translations/et.json | 3 ++ .../pure_energie/translations/no.json | 3 ++ .../components/pushover/translations/el.json | 34 +++++++++++++ .../components/pushover/translations/et.json | 34 +++++++++++++ .../components/skybell/translations/el.json | 6 +++ .../components/skybell/translations/et.json | 6 +++ .../components/skybell/translations/no.json | 6 +++ .../components/zha/translations/el.json | 3 ++ .../components/zha/translations/et.json | 3 ++ .../components/zha/translations/no.json | 3 ++ 22 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/lametric/translations/et.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/et.json create mode 100644 homeassistant/components/pushover/translations/el.json create mode 100644 homeassistant/components/pushover/translations/et.json diff --git a/homeassistant/components/android_ip_webcam/translations/el.json b/homeassistant/components/android_ip_webcam/translations/el.json index a4ae676b8a0..9cf2b26492d 100644 --- a/homeassistant/components/android_ip_webcam/translations/el.json +++ b/homeassistant/components/android_ip_webcam/translations/el.json @@ -4,7 +4,8 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/et.json b/homeassistant/components/android_ip_webcam/translations/et.json index 1b2d03a2d0a..bcb8f1bbf30 100644 --- a/homeassistant/components/android_ip_webcam/translations/et.json +++ b/homeassistant/components/android_ip_webcam/translations/et.json @@ -4,7 +4,8 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/no.json b/homeassistant/components/android_ip_webcam/translations/no.json index e9d7be1d409..f2aa1fbf7cb 100644 --- a/homeassistant/components/android_ip_webcam/translations/no.json +++ b/homeassistant/components/android_ip_webcam/translations/no.json @@ -4,7 +4,8 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" }, "step": { "user": { diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 5a0aee96322..e81460bbdec 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Bluetooth;" }, + "multiple_adapters": { + "data": { + "adapter": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1 Bluetooth \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + }, + "single_adapter": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1 Bluetooth {name};" + }, "user": { "data": { "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json index da1dbdb1a5f..e1fdd22a7fb 100644 --- a/homeassistant/components/bluetooth/translations/et.json +++ b/homeassistant/components/bluetooth/translations/et.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", - "no_adapters": "Bluetoothi adaptereid ei leitud" + "no_adapters": "Seadistamata Bluetoothi adaptereid ei leitud" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Kas soovid Bluetoothi seadistada?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "Vali seadistamiseks Bluetooth-adapter" + }, + "single_adapter": { + "description": "Kas seadistada Bluetooth-adapterit {nimi}?" + }, "user": { "data": { "address": "Seade" diff --git a/homeassistant/components/lacrosse_view/translations/et.json b/homeassistant/components/lacrosse_view/translations/et.json index 8f21ccff5f6..a8afe0965da 100644 --- a/homeassistant/components/lacrosse_view/translations/et.json +++ b/homeassistant/components/lacrosse_view/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamine nurjus", diff --git a/homeassistant/components/lametric/translations/et.json b/homeassistant/components/lametric/translations/et.json new file mode 100644 index 00000000000..62340afca03 --- /dev/null +++ b/homeassistant/components/lametric/translations/et.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "authorize_url_timeout": "Tuvastamise URLi loomise ajal\u00f5pp", + "invalid_discovery_info": "Saadud sobimatu avastamisteave", + "link_local_address": "Kohtv\u00f5rgu linke ei toetata", + "missing_configuration": "LaMetricu integratsioon pole konfigureeritud. Palun j\u00e4rgige dokumentatsiooni.", + "no_devices": "Volitatud kasutajal pole LaMetricu seadmeid", + "no_url_available": "URL pole saadaval. Teavet selle veateate kohta saab [check the help section]({docs_url})" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "LaMetricu seadet saab Home Assistantis seadistada kahel erineval viisil. \n\n Saate sisestada kogu seadme teabe ja API m\u00e4rgid ise v\u00f5i koduabiline saab need teie LaMetric.com kontolt importida.", + "menu_options": { + "manual_entry": "Sisesta k\u00e4sitsi", + "pick_implementation": "Import veebisaidilt LaMetric.com (soovitatav)" + } + }, + "manual_entry": { + "data": { + "api_key": "API v\u00f5ti", + "host": "Host" + }, + "data_description": { + "api_key": "Selle API-v\u00f5tme leiad [oma LaMetricu arendajakonto seadmete lehelt](https://developer.lametric.com/user/devices).", + "host": "Teie v\u00f5rgus oleva LaMetricu TIME IP-aadress v\u00f5i hostinimi." + } + }, + "pick_implementation": { + "title": "Vali tuvastusmeetod" + }, + "user_cloud_select_device": { + "data": { + "device": "Vali lisatav LaMetric seade" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "LaMetricu integratsioon on moderniseeritud: see on n\u00fc\u00fcd konfigureeritud ja seadistatud kasutajaliidese kaudu ning \u00fchendused on n\u00fc\u00fcd kohalikud.\n\nKahjuks pole automaatset migreerimisteed v\u00f5imalik ja seega peate oma LaMetricu koduabilisega uuesti seadistama. Palun tutvuge koduabilise LaMetric integratsiooni dokumentatsiooniga, kuidas seda seadistada.\n\nEemaldage vana LaMetric YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "LaMetricu jaoks on n\u00f5utav k\u00e4sitsi migreerimine" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/et.json b/homeassistant/components/landisgyr_heat_meter/translations/et.json new file mode 100644 index 00000000000..9553db6f7ca --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB seadme rada" + } + }, + "user": { + "data": { + "device": "Vali seade" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/el.json b/homeassistant/components/p1_monitor/translations/el.json index fd520c381f8..bd30b70ef63 100644 --- a/homeassistant/components/p1_monitor/translations/el.json +++ b/homeassistant/components/p1_monitor/translations/el.json @@ -9,6 +9,9 @@ "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, + "data_description": { + "host": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c4\u03b7\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03bf\u03b8\u03cc\u03bd\u03b7\u03c2 P1." + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf {intergration} \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/et.json b/homeassistant/components/p1_monitor/translations/et.json index 69090de7640..18a561ea30b 100644 --- a/homeassistant/components/p1_monitor/translations/et.json +++ b/homeassistant/components/p1_monitor/translations/et.json @@ -9,6 +9,9 @@ "host": "Host", "name": "Nimi" }, + "data_description": { + "host": "P1 Monitori paigalduse IP-aadress v\u00f5i hostinimi." + }, "description": "Seadista P1 -monitor Home Assistantiga sidumiseks." } } diff --git a/homeassistant/components/p1_monitor/translations/no.json b/homeassistant/components/p1_monitor/translations/no.json index d0827d0e357..16e58ba7c7b 100644 --- a/homeassistant/components/p1_monitor/translations/no.json +++ b/homeassistant/components/p1_monitor/translations/no.json @@ -9,6 +9,9 @@ "host": "Vert", "name": "Navn" }, + "data_description": { + "host": "IP-adressen eller vertsnavnet til P1 Monitor-installasjonen." + }, "description": "Sett opp P1 Monitor for \u00e5 integreres med Home Assistant." } } diff --git a/homeassistant/components/pure_energie/translations/el.json b/homeassistant/components/pure_energie/translations/el.json index a63ada73fa9..6cd73b3010c 100644 --- a/homeassistant/components/pure_energie/translations/el.json +++ b/homeassistant/components/pure_energie/translations/el.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "data_description": { + "host": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c4\u03bf\u03c5 Pure Energie Meter \u03c3\u03b1\u03c2." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/et.json b/homeassistant/components/pure_energie/translations/et.json index 4df06e2ca04..289d9565f24 100644 --- a/homeassistant/components/pure_energie/translations/et.json +++ b/homeassistant/components/pure_energie/translations/et.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "Pure Energie Meteri IP-aadress v\u00f5i hostinimi." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/no.json b/homeassistant/components/pure_energie/translations/no.json index 6e02b8df2c0..7271f7e88ce 100644 --- a/homeassistant/components/pure_energie/translations/no.json +++ b/homeassistant/components/pure_energie/translations/no.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Vert" + }, + "data_description": { + "host": "IP-adressen eller vertsnavnet til Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/el.json b/homeassistant/components/pushover/translations/el.json new file mode 100644 index 00000000000..d79e3edd8f4 --- /dev/null +++ b/homeassistant/components/pushover/translations/el.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "invalid_user_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "user_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Pushover \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Pushover YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Pushover YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/et.json b/homeassistant/components/pushover/translations/et.json new file mode 100644 index 00000000000..1d309198b5c --- /dev/null +++ b/homeassistant/components/pushover/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Kehtetu API v\u00f5ti", + "invalid_user_key": "Kehtetu kasutajav\u00f5ti" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "name": "Nimi", + "user_key": "Kasutaja v\u00f5ti" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Pushoveri seadistamine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nEemaldage failist configuration.yaml-i pushover-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Pushover YAML konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/el.json b/homeassistant/components/skybell/translations/el.json index 870068a34fd..0f049c3cc9f 100644 --- a/homeassistant/components/skybell/translations/el.json +++ b/homeassistant/components/skybell/translations/el.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Skybell \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd YAML \u03c4\u03bf\u03c5 Skybell \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/et.json b/homeassistant/components/skybell/translations/et.json index f6f6392d7a0..565aabd84d6 100644 --- a/homeassistant/components/skybell/translations/et.json +++ b/homeassistant/components/skybell/translations/et.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Skybell'i konfigureerimine YAML-i abil on eemaldatud.\n\nTeie olemasolevat YAML-konfiguratsiooni ei kasuta Home Assistant.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Skybell YAML konfiguratsioon on eemaldatud" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/no.json b/homeassistant/components/skybell/translations/no.json index 8701b272f12..365009e60d3 100644 --- a/homeassistant/components/skybell/translations/no.json +++ b/homeassistant/components/skybell/translations/no.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Skybell med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Skybell YAML-konfigurasjonen er fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 85bb3ccce6d..68ba1b9f0c4 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -13,6 +13,9 @@ "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ;" }, + "confirm_hardware": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, "pick_radio": { "data": { "radio_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2" diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index cbcc3adfa51..aa3c26144fa 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -13,6 +13,9 @@ "confirm": { "description": "Kas soovid seadistada teenust {name} ?" }, + "confirm_hardware": { + "description": "Kas seadistada {name} ?" + }, "pick_radio": { "data": { "radio_type": "Seadme raadio t\u00fc\u00fcp" diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 47ae51b89f0..72609918b23 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -13,6 +13,9 @@ "confirm": { "description": "Vil du konfigurere {name}?" }, + "confirm_hardware": { + "description": "Vil du konfigurere {name} ?" + }, "pick_radio": { "data": { "radio_type": "Radio type" From ce3502291da758155df31d1160f1da74d9f8d38b Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 22 Aug 2022 20:58:31 -0400 Subject: [PATCH 567/903] Add better support for UniFi Protect Cameras with Removable Lens (#76942) --- homeassistant/components/unifiprotect/camera.py | 5 +++++ homeassistant/components/unifiprotect/sensor.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 76bfc72408d..bff8af7be98 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -190,6 +190,11 @@ class ProtectCamera(ProtectDeviceEntity, Camera): self._attr_is_recording = ( self.device.state == StateType.CONNECTED and self.device.is_recording ) + is_connected = ( + self.data.last_update_success and self.device.state == StateType.CONNECTED + ) + # some cameras have detachable lens that could cause the camera to be offline + self._attr_available = is_connected and self.device.is_video_ready self._async_set_stream_source() self._attr_extra_state_attributes = { diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 7a9f4652a2e..3dac8e46aee 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -200,6 +200,14 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ufp_value="last_ring", entity_registry_enabled_default=False, ), + ProtectSensorEntityDescription( + key="lens_type", + name="Lens Type", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:camera-iris", + ufp_required_field="has_removable_lens", + ufp_value="feature_flags.lens_type", + ), ProtectSensorEntityDescription( key="mic_level", name="Microphone Level", From 325557c3e9157c1ac9e56e9172721eac44f0a16b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 23 Aug 2022 03:38:26 +0200 Subject: [PATCH 568/903] Use _attr_should_poll in zha entities (#77175) --- .../components/zha/device_tracker.py | 3 +- homeassistant/components/zha/entity.py | 7 +-- homeassistant/components/zha/sensor.py | 47 ++++--------------- 3 files changed, 11 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index eed80a05055..5a20273d085 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -49,13 +49,14 @@ async def async_setup_entry( class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): """Represent a tracked device.""" + _attr_should_poll = True # BaseZhaEntity defaults to False + def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA device tracker.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._battery_channel = self.cluster_channels.get(CHANNEL_POWER_CONFIGURATION) self._connected = False self._keepalive_interval = 60 - self._should_poll = True self._battery_level = None async def async_added_to_hass(self): diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 2f609555c79..61dbe5339d0 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -49,12 +49,12 @@ class BaseZhaEntity(LogMixin, entity.Entity): unique_id_suffix: str | None = None _attr_has_entity_name = True + _attr_should_poll = False def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None: """Init ZHA entity.""" self._name: str = "" self._force_update: bool = False - self._should_poll: bool = False self._unique_id: str = unique_id if self.unique_id_suffix: self._unique_id += f"-{self.unique_id_suffix}" @@ -89,11 +89,6 @@ class BaseZhaEntity(LogMixin, entity.Entity): """Force update this entity.""" return self._force_update - @property - def should_poll(self) -> bool: - """Poll state from device.""" - return self._should_poll - @property def device_info(self) -> entity.DeviceInfo: """Return a device description for device registry.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index e0f5bb958cd..61fad097fc8 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -266,15 +266,11 @@ class ElectricalMeasurement(Sensor): SENSOR_ATTR = "active_power" _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER + _attr_should_poll = True # BaseZhaEntity defaults to False _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _unit = POWER_WATT _div_mul_prefix = "ac_power" - @property - def should_poll(self) -> bool: - """Return True if HA needs to poll for state changes.""" - return True - @property def extra_state_attributes(self) -> dict[str, Any]: """Return device state attrs for sensor.""" @@ -312,14 +308,10 @@ class ElectricalMeasurementApparentPower( SENSOR_ATTR = "apparent_power" _attr_device_class: SensorDeviceClass = SensorDeviceClass.APPARENT_POWER + _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor _unit = POWER_VOLT_AMPERE _div_mul_prefix = "ac_power" - @property - def should_poll(self) -> bool: - """Poll indirectly by ElectricalMeasurementSensor.""" - return False - @MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_current"): @@ -327,14 +319,10 @@ class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_curr SENSOR_ATTR = "rms_current" _attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT + _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor _unit = ELECTRIC_CURRENT_AMPERE _div_mul_prefix = "ac_current" - @property - def should_poll(self) -> bool: - """Poll indirectly by ElectricalMeasurementSensor.""" - return False - @MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_voltage"): @@ -342,14 +330,10 @@ class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_volt SENSOR_ATTR = "rms_voltage" _attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT + _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor _unit = ELECTRIC_POTENTIAL_VOLT _div_mul_prefix = "ac_voltage" - @property - def should_poll(self) -> bool: - """Poll indirectly by ElectricalMeasurementSensor.""" - return False - @MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_frequency"): @@ -357,14 +341,10 @@ class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_freque SENSOR_ATTR = "ac_frequency" _attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY + _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor _unit = FREQUENCY_HERTZ _div_mul_prefix = "ac_frequency" - @property - def should_poll(self) -> bool: - """Poll indirectly by ElectricalMeasurementSensor.""" - return False - @MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_factor"): @@ -372,13 +352,9 @@ class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_f SENSOR_ATTR = "power_factor" _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR + _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor _unit = PERCENTAGE - @property - def should_poll(self) -> bool: - """Poll indirectly by ElectricalMeasurementSensor.""" - return False - @MULTI_MATCH( generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER, stop_on_match_group=CHANNEL_HUMIDITY @@ -521,10 +497,7 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered") class PolledSmartEnergySummation(SmartEnergySummation): """Polled Smart Energy Metering summation sensor.""" - @property - def should_poll(self) -> bool: - """Poll the entity for current state.""" - return True + _attr_should_poll = True # BaseZhaEntity defaults to False async def async_update(self) -> None: """Retrieve latest state.""" @@ -770,6 +743,7 @@ class RSSISensor(Sensor, id_suffix="rssi"): _attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False + _attr_should_poll = True # BaseZhaEntity defaults to False unique_id_suffix: str @classmethod @@ -794,11 +768,6 @@ class RSSISensor(Sensor, id_suffix="rssi"): """Return the state of the entity.""" return getattr(self._zha_device.device, self.unique_id_suffix) - @property - def should_poll(self) -> bool: - """Poll the entity for current state.""" - return True - @MULTI_MATCH(channel_names=CHANNEL_BASIC) class LQISensor(RSSISensor, id_suffix="lqi"): From c76dec138acc8272ecbc530cd60a647e4b162d91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Aug 2022 15:45:08 -1000 Subject: [PATCH 569/903] Discover new bluetooth adapters when they are plugged in (#77006) --- .../components/bluetooth/__init__.py | 30 +++++ .../components/bluetooth/manifest.json | 2 +- .../homeassistant_sky_connect/__init__.py | 4 +- homeassistant/components/usb/__init__.py | 116 ++++++++++++------ homeassistant/loader.py | 35 +++++- tests/components/bluetooth/test_init.py | 56 +++++++++ tests/components/usb/test_init.py | 34 ++++- 7 files changed, 230 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index f71ee5aa34c..58ca4a6976b 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -3,16 +3,19 @@ from __future__ import annotations from asyncio import Future from collections.abc import Callable, Iterable +import logging import platform from typing import TYPE_CHECKING, cast import async_timeout from homeassistant import config_entries +from homeassistant.components import usb from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, discovery_flow +from homeassistant.helpers.debounce import Debouncer from homeassistant.loader import async_get_bluetooth from . import models @@ -65,6 +68,8 @@ __all__ = [ "SOURCE_LOCAL", ] +_LOGGER = logging.getLogger(__name__) + def _get_manager(hass: HomeAssistant) -> BluetoothManager: """Get the bluetooth manager.""" @@ -214,6 +219,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_migrate_entries(hass, adapters) await async_discover_adapters(hass, adapters) + async def _async_rediscover_adapters() -> None: + """Rediscover adapters when a new one may be available.""" + discovered_adapters = await manager.async_get_bluetooth_adapters(cached=False) + _LOGGER.debug("Rediscovered adapters: %s", discovered_adapters) + await async_discover_adapters(hass, discovered_adapters) + + discovery_debouncer = Debouncer( + hass, _LOGGER, cooldown=5, immediate=False, function=_async_rediscover_adapters + ) + + def _async_trigger_discovery() -> None: + # There are so many bluetooth adapter models that + # we check the bus whenever a usb device is plugged in + # to see if it is a bluetooth adapter since we can't + # tell if the device is a bluetooth adapter or if its + # actually supported unless we ask DBus if its now + # present. + _LOGGER.debug("Triggering bluetooth usb discovery") + hass.async_create_task(discovery_debouncer.async_call()) + + cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, hass_callback(lambda event: cancel()) + ) + return True diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 29c534322f2..cfe9590b2db 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -2,7 +2,7 @@ "domain": "bluetooth", "name": "Bluetooth", "documentation": "https://www.home-assistant.io/integrations/bluetooth", - "dependencies": ["websocket_api"], + "dependencies": ["usb"], "quality_scale": "internal", "requirements": [ "bleak==0.15.1", diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 981e96ccdee..dd4cf013fab 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -1,6 +1,8 @@ """The Home Assistant Sky Connect integration.""" from __future__ import annotations +from typing import cast + from homeassistant.components import usb from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -17,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manufacturer=entry.data["manufacturer"], description=entry.data["description"], ) - if not usb.async_is_plugged_in(hass, entry.data): + if not usb.async_is_plugged_in(hass, cast(usb.USBCallbackMatcher, entry.data)): # The USB dongle is not plugged in raise ConfigEntryNotReady diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 83c7a6a8a45..55ffe111a73 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -1,7 +1,7 @@ """The USB Discovery integration.""" from __future__ import annotations -from collections.abc import Coroutine, Mapping +from collections.abc import Coroutine import dataclasses import fnmatch import logging @@ -17,12 +17,17 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + callback as hass_callback, +) from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow, system_info from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import async_get_usb +from homeassistant.loader import USBMatcher, async_get_usb from .const import DOMAIN from .models import USBDevice @@ -35,6 +40,36 @@ _LOGGER = logging.getLogger(__name__) REQUEST_SCAN_COOLDOWN = 60 # 1 minute cooldown +__all__ = [ + "async_is_plugged_in", + "async_register_scan_request_callback", + "USBCallbackMatcher", + "UsbServiceInfo", +] + + +class USBCallbackMatcher(USBMatcher): + """Callback matcher for the USB integration.""" + + +@hass_callback +def async_register_scan_request_callback( + hass: HomeAssistant, callback: CALLBACK_TYPE +) -> CALLBACK_TYPE: + """Register to receive a callback when a scan should be initiated.""" + discovery: USBDiscovery = hass.data[DOMAIN] + return discovery.async_register_scan_request_callback(callback) + + +@hass_callback +def async_is_plugged_in(hass: HomeAssistant, matcher: USBCallbackMatcher) -> bool: + """Return True is a USB device is present.""" + usb_discovery: USBDiscovery = hass.data[DOMAIN] + return any( + _is_matching(USBDevice(*device_tuple), matcher) + for device_tuple in usb_discovery.seen + ) + @dataclasses.dataclass class UsbServiceInfo(BaseServiceInfo): @@ -97,7 +132,7 @@ def _fnmatch_lower(name: str | None, pattern: str) -> bool: return fnmatch.fnmatch(name.lower(), pattern) -def _is_matching(device: USBDevice, matcher: Mapping[str, str]) -> bool: +def _is_matching(device: USBDevice, matcher: USBMatcher | USBCallbackMatcher) -> bool: """Return True if a device matches.""" if "vid" in matcher and device.vid != matcher["vid"]: return False @@ -124,7 +159,7 @@ class USBDiscovery: def __init__( self, hass: HomeAssistant, - usb: list[dict[str, str]], + usb: list[USBMatcher], ) -> None: """Init USB Discovery.""" self.hass = hass @@ -132,6 +167,7 @@ class USBDiscovery: self.seen: set[tuple[str, ...]] = set() self.observer_active = False self._request_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None + self._request_callbacks: list[CALLBACK_TYPE] = [] async def async_setup(self) -> None: """Set up USB Discovery.""" @@ -188,9 +224,23 @@ class USBDiscovery: "Discovered Device at path: %s, triggering scan serial", device.device_path, ) - self.scan_serial() + self.hass.create_task(self._async_scan()) - @callback + @hass_callback + def async_register_scan_request_callback( + self, + _callback: CALLBACK_TYPE, + ) -> CALLBACK_TYPE: + """Register a callback.""" + self._request_callbacks.append(_callback) + + @hass_callback + def _async_remove_callback() -> None: + self._request_callbacks.remove(_callback) + + return _async_remove_callback + + @hass_callback def _async_process_discovered_usb_device(self, device: USBDevice) -> None: """Process a USB discovery.""" _LOGGER.debug("Discovered USB Device: %s", device) @@ -198,14 +248,20 @@ class USBDiscovery: if device_tuple in self.seen: return self.seen.add(device_tuple) - matched = [] - for matcher in self.usb: - if _is_matching(device, matcher): - matched.append(matcher) + matched = [matcher for matcher in self.usb if _is_matching(device, matcher)] if not matched: return + service_info = UsbServiceInfo( + device=device.device, + vid=device.vid, + pid=device.pid, + serial_number=device.serial_number, + manufacturer=device.manufacturer, + description=device.description, + ) + sorted_by_most_targeted = sorted(matched, key=lambda item: -len(item)) most_matched_fields = len(sorted_by_most_targeted[0]) @@ -219,17 +275,10 @@ class USBDiscovery: self.hass, matcher["domain"], {"source": config_entries.SOURCE_USB}, - UsbServiceInfo( - device=device.device, - vid=device.vid, - pid=device.pid, - serial_number=device.serial_number, - manufacturer=device.manufacturer, - description=device.description, - ), + service_info, ) - @callback + @hass_callback def _async_process_ports(self, ports: list[ListPortInfo]) -> None: """Process each discovered port.""" for port in ports: @@ -237,15 +286,17 @@ class USBDiscovery: continue self._async_process_discovered_usb_device(usb_device_from_port(port)) - def scan_serial(self) -> None: - """Scan serial ports.""" - self.hass.add_job(self._async_process_ports, comports()) - async def _async_scan_serial(self) -> None: """Scan serial ports.""" self._async_process_ports(await self.hass.async_add_executor_job(comports)) - async def async_request_scan_serial(self) -> None: + async def _async_scan(self) -> None: + """Scan for USB devices and notify callbacks to scan as well.""" + for callback in self._request_callbacks: + callback() + await self._async_scan_serial() + + async def async_request_scan(self) -> None: """Request a serial scan.""" if not self._request_debouncer: self._request_debouncer = Debouncer( @@ -253,7 +304,7 @@ class USBDiscovery: _LOGGER, cooldown=REQUEST_SCAN_COOLDOWN, immediate=True, - function=self._async_scan_serial, + function=self._async_scan, ) await self._request_debouncer.async_call() @@ -269,16 +320,5 @@ async def websocket_usb_scan( """Scan for new usb devices.""" usb_discovery: USBDiscovery = hass.data[DOMAIN] if not usb_discovery.observer_active: - await usb_discovery.async_request_scan_serial() + await usb_discovery.async_request_scan() connection.send_result(msg["id"]) - - -@callback -def async_is_plugged_in(hass: HomeAssistant, matcher: Mapping) -> bool: - """Return True is a USB device is present.""" - usb_discovery: USBDiscovery = hass.data[DOMAIN] - for device_tuple in usb_discovery.seen: - device = USBDevice(*device_tuple) - if _is_matching(device, matcher): - return True - return False diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 00d9bfa1e05..1d100a42d83 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -98,6 +98,26 @@ class BluetoothMatcher(BluetoothMatcherRequired, BluetoothMatcherOptional): """Matcher for the bluetooth integration.""" +class USBMatcherRequired(TypedDict, total=True): + """Matcher for the usb integration for required fields.""" + + domain: str + + +class USBMatcherOptional(TypedDict, total=False): + """Matcher for the usb integration for optional fields.""" + + vid: str + pid: str + serial_number: str + manufacturer: str + description: str + + +class USBMatcher(USBMatcherRequired, USBMatcherOptional): + """Matcher for the bluetooth integration.""" + + class Manifest(TypedDict, total=False): """ Integration manifest. @@ -318,9 +338,9 @@ async def async_get_dhcp(hass: HomeAssistant) -> list[DHCPMatcher]: return dhcp -async def async_get_usb(hass: HomeAssistant) -> list[dict[str, str]]: +async def async_get_usb(hass: HomeAssistant) -> list[USBMatcher]: """Return cached list of usb types.""" - usb: list[dict[str, str]] = USB.copy() + usb = cast(list[USBMatcher], USB.copy()) integrations = await async_get_custom_components(hass) for integration in integrations.values(): @@ -328,10 +348,13 @@ async def async_get_usb(hass: HomeAssistant) -> list[dict[str, str]]: continue for entry in integration.usb: usb.append( - { - "domain": integration.domain, - **{k: v for k, v in entry.items() if k != "known_devices"}, - } + cast( + USBMatcher, + { + "domain": integration.domain, + **{k: v for k, v in entry.items() if k != "known_devices"}, + }, + ) ) return usb diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ab6137213b2..81b25a6c0dd 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1641,3 +1641,59 @@ async def test_migrate_single_entry_linux(hass, mock_bleak_scanner_start, one_ad assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() assert entry.unique_id == "00:00:00:00:00:01" + + +async def test_discover_new_usb_adapters(hass, mock_bleak_scanner_start, one_adapter): + """Test we can discover new usb adapters.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_scan_request_callback(_hass, _callback): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.usb.async_register_scan_request_callback", + _async_register_scan_request_callback, + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + + assert not hass.config_entries.flow.async_progress(DOMAIN) + + saved_callback() + assert not hass.config_entries.flow.async_progress(DOMAIN) + + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + "hci1": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:02", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + for wait_sec in range(10, 20): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=wait_sec) + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index 0d1ad36a9f4..ca978af75f2 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -1,7 +1,7 @@ """Tests for the USB Discovery integration.""" import os import sys -from unittest.mock import MagicMock, call, patch, sentinel +from unittest.mock import MagicMock, Mock, call, patch, sentinel import pytest @@ -875,3 +875,35 @@ async def test_async_is_plugged_in(hass, hass_ws_client): assert response["success"] await hass.async_block_till_done() assert usb.async_is_plugged_in(hass, matcher) + + +async def test_web_socket_triggers_discovery_request_callbacks(hass, hass_ws_client): + """Test the websocket call triggers a discovery request callback.""" + mock_callback = Mock() + + with patch("pyudev.Context", side_effect=ImportError), patch( + "homeassistant.components.usb.async_get_usb", return_value=[] + ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = usb.async_register_scan_request_callback(hass, mock_callback) + + ws_client = await hass_ws_client(hass) + await ws_client.send_json({"id": 1, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + assert len(mock_callback.mock_calls) == 1 + cancel() + + await ws_client.send_json({"id": 2, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + assert len(mock_callback.mock_calls) == 1 From be2366d773293248447cd03637277b2b5c4d40c0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 23 Aug 2022 08:43:07 +0200 Subject: [PATCH 570/903] Add `this` object to MQTT templates (#77142) * Add `this` object to MQTT templates * Only set once, remove hass guard * Set once if there is a state * Add tests TemplateStateFromEntityId calls once --- homeassistant/components/mqtt/models.py | 23 ++++++++++++++--- tests/components/mqtt/test_init.py | 33 +++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 84bf704a262..d40b882d81b 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -17,6 +17,8 @@ from homeassistant.helpers.typing import TemplateVarsType _SENTINEL = object() +ATTR_THIS = "this" + PublishPayloadType = Union[str, bytes, int, float, None] @@ -57,7 +59,8 @@ class MqttCommandTemplate: entity: Entity | None = None, ) -> None: """Instantiate a command template.""" - self._attr_command_template = command_template + self._template_state: template.TemplateStateFromEntityId | None = None + self._command_template = command_template if command_template is None: return @@ -91,17 +94,23 @@ class MqttCommandTemplate: return payload - if self._attr_command_template is None: + if self._command_template is None: return value - values = {"value": value} + values: dict[str, Any] = {"value": value} if self._entity: values[ATTR_ENTITY_ID] = self._entity.entity_id values[ATTR_NAME] = self._entity.name + if not self._template_state: + self._template_state = template.TemplateStateFromEntityId( + self._command_template.hass, self._entity.entity_id + ) + values[ATTR_THIS] = self._template_state + if variables is not None: values.update(variables) return _convert_outgoing_payload( - self._attr_command_template.async_render(values, parse_result=False) + self._command_template.async_render(values, parse_result=False) ) @@ -117,6 +126,7 @@ class MqttValueTemplate: config_attributes: TemplateVarsType = None, ) -> None: """Instantiate a value template.""" + self._template_state: template.TemplateStateFromEntityId | None = None self._value_template = value_template self._config_attributes = config_attributes if value_template is None: @@ -150,6 +160,11 @@ class MqttValueTemplate: if self._entity: values[ATTR_ENTITY_ID] = self._entity.entity_id values[ATTR_NAME] = self._entity.name + if not self._template_state and self._value_template.hass: + self._template_state = template.TemplateStateFromEntityId( + self._value_template.hass, self._entity.entity_id + ) + values[ATTR_THIS] = self._template_state if default == _SENTINEL: return self._value_template.async_render_with_possible_json_value( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index fe8f483adf2..fd77dcec3a7 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -299,7 +299,7 @@ async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config "command_topic": topic, "name": "Test Select", "options": ["milk", "beer"], - "command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}"}', + "command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}", "this_object_state": "{{ this.state }}"}', } }, ) @@ -319,7 +319,7 @@ async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config mqtt_mock.async_publish.assert_called_once_with( topic, - '{"option": "beer", "entity_id": "select.test_select", "name": "Test Select"}', + '{"option": "beer", "entity_id": "select.test_select", "name": "Test Select", "this_object_state": "milk"}', 0, False, ) @@ -327,6 +327,20 @@ async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config state = hass.states.get("select.test_select") assert state.state == "beer" + # Test that TemplateStateFromEntityId is not called again + with patch( + "homeassistant.helpers.template.TemplateStateFromEntityId", MagicMock() + ) as template_state_calls: + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.test_select", "option": "milk"}, + blocking=True, + ) + assert template_state_calls.call_count == 0 + state = hass.states.get("select.test_select") + assert state.state == "milk" + async def test_value_template_value(hass): """Test the rendering of MQTT value template.""" @@ -359,10 +373,25 @@ async def test_value_template_value(hass): # test value template with entity entity = Entity() entity.hass = hass + entity.entity_id = "select.test" tpl = template.Template("{{ value_json.id }}") val_tpl = mqtt.MqttValueTemplate(tpl, entity=entity) assert val_tpl.async_render_with_possible_json_value('{"id": 4321}') == "4321" + # test this object in a template + tpl2 = template.Template("{{ this.entity_id }}") + val_tpl2 = mqtt.MqttValueTemplate(tpl2, entity=entity) + assert val_tpl2.async_render_with_possible_json_value("bla") == "select.test" + + with patch( + "homeassistant.helpers.template.TemplateStateFromEntityId", MagicMock() + ) as template_state_calls: + tpl3 = template.Template("{{ this.entity_id }}") + val_tpl3 = mqtt.MqttValueTemplate(tpl3, entity=entity) + val_tpl3.async_render_with_possible_json_value("call1") + val_tpl3.async_render_with_possible_json_value("call2") + assert template_state_calls.call_count == 1 + async def test_service_call_without_topic_does_not_publish( hass, mqtt_mock_entry_no_yaml_config From 680a477009f4f1f986a55f76988afb86339897b2 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Tue, 23 Aug 2022 09:41:07 +0200 Subject: [PATCH 571/903] Fix frontier silicon EQ Mode not present on all devices (#76200) * Fix #76159: EQ Mode not present on all devices * Address review remarks * Duplicate bookkeeping of sound mode support * reduce length of try-blocks --- .../frontier_silicon/media_player.py | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 9a51847d760..4c1e4390e61 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -3,7 +3,12 @@ from __future__ import annotations import logging -from afsapi import AFSAPI, ConnectionError as FSConnectionError, PlayState +from afsapi import ( + AFSAPI, + ConnectionError as FSConnectionError, + NotImplementedException as FSNotImplementedException, + PlayState, +) import voluptuous as vol from homeassistant.components.media_player import ( @@ -113,10 +118,12 @@ class AFSAPIDevice(MediaPlayerEntity): ) self._attr_name = name - self._max_volume = None + self._max_volume: int | None = None - self.__modes_by_label = None - self.__sound_modes_by_label = None + self.__modes_by_label: dict[str, str] | None = None + self.__sound_modes_by_label: dict[str, str] | None = None + + self._supports_sound_mode: bool = True async def async_update(self): """Get the latest date and update device state.""" @@ -158,12 +165,20 @@ class AFSAPIDevice(MediaPlayerEntity): } self._attr_source_list = list(self.__modes_by_label) - if not self._attr_sound_mode_list: - self.__sound_modes_by_label = { - sound_mode.label: sound_mode.key - for sound_mode in await afsapi.get_equalisers() - } - self._attr_sound_mode_list = list(self.__sound_modes_by_label) + if not self._attr_sound_mode_list and self._supports_sound_mode: + try: + equalisers = await afsapi.get_equalisers() + except FSNotImplementedException: + self._supports_sound_mode = False + # Remove SELECT_SOUND_MODE from the advertised supported features + self._attr_supported_features ^= ( + MediaPlayerEntityFeature.SELECT_SOUND_MODE + ) + else: + self.__sound_modes_by_label = { + sound_mode.label: sound_mode.key for sound_mode in equalisers + } + self._attr_sound_mode_list = list(self.__sound_modes_by_label) # The API seems to include 'zero' in the number of steps (e.g. if the range is # 0-40 then get_volume_steps returns 41) subtract one to get the max volume. @@ -185,8 +200,19 @@ class AFSAPIDevice(MediaPlayerEntity): self._attr_is_volume_muted = await afsapi.get_mute() self._attr_media_image_url = await afsapi.get_play_graphic() - eq_preset = await afsapi.get_eq_preset() - self._attr_sound_mode = eq_preset.label if eq_preset is not None else None + if self._supports_sound_mode: + try: + eq_preset = await afsapi.get_eq_preset() + except FSNotImplementedException: + self._supports_sound_mode = False + # Remove SELECT_SOUND_MODE from the advertised supported features + self._attr_supported_features ^= ( + MediaPlayerEntityFeature.SELECT_SOUND_MODE + ) + else: + self._attr_sound_mode = ( + eq_preset.label if eq_preset is not None else None + ) volume = await self.fs_device.get_volume() From 9e66b30af97063b500fb41a9c82a2565bf431082 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Tue, 23 Aug 2022 10:02:58 +0200 Subject: [PATCH 572/903] Add new sensors for energy produced (via Tibbber) (#76165) The new sensors tibber:energy_(production|profit)_ are like the existing consumption/totalCost ones except that they report outgoing energy instead of incomeing. --- homeassistant/components/tibber/sensor.py | 42 ++++++----- tests/components/tibber/test_common.py | 25 ++++++- tests/components/tibber/test_statistics.py | 81 ++++++++-------------- 3 files changed, 77 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index bc3f6015286..9cfc4ecbb5a 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -558,17 +558,21 @@ class TibberDataCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via API.""" await self._tibber_connection.fetch_consumption_data_active_homes() + await self._tibber_connection.fetch_production_data_active_homes() await self._insert_statistics() async def _insert_statistics(self): """Insert Tibber statistics.""" for home in self._tibber_connection.get_homes(): - if not home.hourly_consumption_data: - continue - for sensor_type in ( - "consumption", - "totalCost", - ): + sensors = [] + if home.hourly_consumption_data: + sensors.append(("consumption", False, ENERGY_KILO_WATT_HOUR)) + sensors.append(("totalCost", False, home.currency)) + if home.hourly_production_data: + sensors.append(("production", True, ENERGY_KILO_WATT_HOUR)) + sensors.append(("profit", True, home.currency)) + + for sensor_type, is_production, unit in sensors: statistic_id = ( f"{TIBBER_DOMAIN}:energy_" f"{sensor_type.lower()}_" @@ -581,20 +585,26 @@ class TibberDataCoordinator(DataUpdateCoordinator): if not last_stats: # First time we insert 5 years of data (if available) - hourly_consumption_data = await home.get_historic_data(5 * 365 * 24) + hourly_data = await home.get_historic_data( + 5 * 365 * 24, production=is_production + ) _sum = 0 last_stats_time = None else: - # hourly_consumption_data contains the last 30 days - # of consumption data. + # hourly_consumption/production_data contains the last 30 days + # of consumption/production data. # We update the statistics with the last 30 days # of data to handle corrections in the data. - hourly_consumption_data = home.hourly_consumption_data + hourly_data = ( + home.hourly_production_data + if is_production + else home.hourly_consumption_data + ) - start = dt_util.parse_datetime( - hourly_consumption_data[0]["from"] - ) - timedelta(hours=1) + start = dt_util.parse_datetime(hourly_data[0]["from"]) - timedelta( + hours=1 + ) stat = await get_instance(self.hass).async_add_executor_job( statistics_during_period, self.hass, @@ -609,7 +619,7 @@ class TibberDataCoordinator(DataUpdateCoordinator): statistics = [] - for data in hourly_consumption_data: + for data in hourly_data: if data.get(sensor_type) is None: continue @@ -627,10 +637,6 @@ class TibberDataCoordinator(DataUpdateCoordinator): ) ) - if sensor_type == "consumption": - unit = ENERGY_KILO_WATT_HOUR - else: - unit = home.currency metadata = StatisticMetaData( has_mean=False, has_sum=True, diff --git a/tests/components/tibber/test_common.py b/tests/components/tibber/test_common.py index 716fa8ce47b..7faa07a6d9f 100644 --- a/tests/components/tibber/test_common.py +++ b/tests/components/tibber/test_common.py @@ -20,6 +20,24 @@ CONSUMPTION_DATA_1 = [ }, ] +PRODUCTION_DATA_1 = [ + { + "from": "2022-01-03T00:00:00.000+01:00", + "profit": 0.1, + "production": 3.1, + }, + { + "from": "2022-01-03T01:00:00.000+01:00", + "profit": 0.2, + "production": 3.2, + }, + { + "from": "2022-01-03T02:00:00.000+01:00", + "profit": 0.3, + "production": 3.3, + }, +] + def mock_get_homes(only_active=True): """Return a list of mocked Tibber homes.""" @@ -32,5 +50,10 @@ def mock_get_homes(only_active=True): tibber_home.country = "NO" tibber_home.last_cons_data_timestamp = dt.datetime(2016, 1, 1, 12, 44, 57) tibber_home.last_data_timestamp = dt.datetime(2016, 1, 1, 12, 48, 57) - tibber_home.get_historic_data.return_value = CONSUMPTION_DATA_1 + + def get_historic_data(n_data, resolution="HOURLY", production=False): + return PRODUCTION_DATA_1 if production else CONSUMPTION_DATA_1 + + tibber_home.get_historic_data.side_effect = get_historic_data + return [tibber_home] diff --git a/tests/components/tibber/test_statistics.py b/tests/components/tibber/test_statistics.py index 17297ffda3a..8e0d24ffa9d 100644 --- a/tests/components/tibber/test_statistics.py +++ b/tests/components/tibber/test_statistics.py @@ -5,7 +5,7 @@ from homeassistant.components.recorder.statistics import statistics_during_perio from homeassistant.components.tibber.sensor import TibberDataCoordinator from homeassistant.util import dt as dt_util -from .test_common import CONSUMPTION_DATA_1, mock_get_homes +from .test_common import CONSUMPTION_DATA_1, PRODUCTION_DATA_1, mock_get_homes from tests.components.recorder.common import async_wait_recording_done @@ -15,62 +15,39 @@ async def test_async_setup_entry(hass, recorder_mock): tibber_connection = AsyncMock() tibber_connection.name = "tibber" tibber_connection.fetch_consumption_data_active_homes.return_value = None + tibber_connection.fetch_production_data_active_homes.return_value = None tibber_connection.get_homes = mock_get_homes coordinator = TibberDataCoordinator(hass, tibber_connection) await coordinator._async_update_data() await async_wait_recording_done(hass) - # Validate consumption - statistic_id = "tibber:energy_consumption_home_id" + for (statistic_id, data, key) in ( + ("tibber:energy_consumption_home_id", CONSUMPTION_DATA_1, "consumption"), + ("tibber:energy_totalcost_home_id", CONSUMPTION_DATA_1, "totalCost"), + ("tibber:energy_production_home_id", PRODUCTION_DATA_1, "production"), + ("tibber:energy_profit_home_id", PRODUCTION_DATA_1, "profit"), + ): + stats = await hass.async_add_executor_job( + statistics_during_period, + hass, + dt_util.parse_datetime(data[0]["from"]), + None, + [statistic_id], + "hour", + True, + ) - stats = await hass.async_add_executor_job( - statistics_during_period, - hass, - dt_util.parse_datetime(CONSUMPTION_DATA_1[0]["from"]), - None, - [statistic_id], - "hour", - True, - ) + assert len(stats) == 1 + assert len(stats[statistic_id]) == 3 + _sum = 0 + for k, stat in enumerate(stats[statistic_id]): + assert stat["start"] == dt_util.parse_datetime(data[k]["from"]) + assert stat["state"] == data[k][key] + assert stat["mean"] is None + assert stat["min"] is None + assert stat["max"] is None + assert stat["last_reset"] is None - assert len(stats) == 1 - assert len(stats[statistic_id]) == 3 - _sum = 0 - for k, stat in enumerate(stats[statistic_id]): - assert stat["start"] == dt_util.parse_datetime(CONSUMPTION_DATA_1[k]["from"]) - assert stat["state"] == CONSUMPTION_DATA_1[k]["consumption"] - assert stat["mean"] is None - assert stat["min"] is None - assert stat["max"] is None - assert stat["last_reset"] is None - - _sum += CONSUMPTION_DATA_1[k]["consumption"] - assert stat["sum"] == _sum - - # Validate cost - statistic_id = "tibber:energy_totalcost_home_id" - - stats = await hass.async_add_executor_job( - statistics_during_period, - hass, - dt_util.parse_datetime(CONSUMPTION_DATA_1[0]["from"]), - None, - [statistic_id], - "hour", - True, - ) - - assert len(stats) == 1 - assert len(stats[statistic_id]) == 3 - _sum = 0 - for k, stat in enumerate(stats[statistic_id]): - assert stat["start"] == dt_util.parse_datetime(CONSUMPTION_DATA_1[k]["from"]) - assert stat["state"] == CONSUMPTION_DATA_1[k]["totalCost"] - assert stat["mean"] is None - assert stat["min"] is None - assert stat["max"] is None - assert stat["last_reset"] is None - - _sum += CONSUMPTION_DATA_1[k]["totalCost"] - assert stat["sum"] == _sum + _sum += data[k][key] + assert stat["sum"] == _sum From bf5ab64b995425f5dff8130151e9e7cda85669ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Aug 2022 12:37:28 +0200 Subject: [PATCH 573/903] Bump actions/cache from 3.0.7 to 3.0.8 (#77196) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e4a47ca2b03..75a200402d6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: cache: "pip" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -185,7 +185,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -211,7 +211,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -222,7 +222,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -260,7 +260,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -271,7 +271,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -312,7 +312,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -323,7 +323,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -353,7 +353,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -364,7 +364,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -480,7 +480,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- @@ -488,7 +488,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: ${{ env.PIP_CACHE }} key: >- @@ -538,7 +538,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- @@ -570,7 +570,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- @@ -603,7 +603,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- @@ -647,7 +647,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- @@ -695,7 +695,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- @@ -749,7 +749,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From c975146146ebd1d52b7e208cb78ba816536619d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Aug 2022 04:35:20 -1000 Subject: [PATCH 574/903] Reduce discovery integration matching overhead (#77194) --- homeassistant/components/bluetooth/match.py | 40 +++++++++++++++---- homeassistant/components/dhcp/__init__.py | 31 ++++++++++++-- homeassistant/components/zeroconf/__init__.py | 32 +++++++++++++-- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 08b3716c50a..333ba020b74 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -2,7 +2,9 @@ from __future__ import annotations from dataclasses import dataclass -import fnmatch +from fnmatch import translate +from functools import lru_cache +import re from typing import TYPE_CHECKING, Final, TypedDict from lru import LRU # pylint: disable=no-name-in-module @@ -136,12 +138,6 @@ def ble_device_matches( return False advertisement_data = service_info.advertisement - if (local_name := matcher.get(LOCAL_NAME)) is not None and not fnmatch.fnmatch( - advertisement_data.local_name or device.name or device.address, - local_name, - ): - return False - if ( service_uuid := matcher.get(SERVICE_UUID) ) is not None and service_uuid not in advertisement_data.service_uuids: @@ -165,4 +161,34 @@ def ble_device_matches( ): return False + if (local_name := matcher.get(LOCAL_NAME)) is not None and ( + (device_name := advertisement_data.local_name or device.name) is None + or not _memorized_fnmatch( + device_name, + local_name, + ) + ): + return False + return True + + +@lru_cache(maxsize=4096, typed=True) +def _compile_fnmatch(pattern: str) -> re.Pattern: + """Compile a fnmatch pattern.""" + return re.compile(translate(pattern)) + + +@lru_cache(maxsize=1024, typed=True) +def _memorized_fnmatch(name: str, pattern: str) -> bool: + """Memorized version of fnmatch that has a larger lru_cache. + + The default version of fnmatch only has a lru_cache of 256 entries. + With many devices we quickly reach that limit and end up compiling + the same pattern over and over again. + + Bluetooth has its own memorized fnmatch with its own lru_cache + since the data is going to be relatively the same + since the devices will not change frequently. + """ + return bool(_compile_fnmatch(pattern).match(name)) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index be9cbd7426d..7a5854fc53e 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -7,10 +7,12 @@ from collections.abc import Callable, Iterable import contextlib from dataclasses import dataclass from datetime import timedelta -import fnmatch +from fnmatch import translate +from functools import lru_cache from ipaddress import ip_address as make_ip_address import logging import os +import re import threading from typing import TYPE_CHECKING, Any, Final, cast @@ -204,12 +206,14 @@ class WatcherBase: if ( matcher_mac := matcher.get(MAC_ADDRESS) - ) is not None and not fnmatch.fnmatch(uppercase_mac, matcher_mac): + ) is not None and not _memorized_fnmatch(uppercase_mac, matcher_mac): continue if ( matcher_hostname := matcher.get(HOSTNAME) - ) is not None and not fnmatch.fnmatch(lowercase_hostname, matcher_hostname): + ) is not None and not _memorized_fnmatch( + lowercase_hostname, matcher_hostname + ): continue _LOGGER.debug("Matched %s against %s", data, matcher) @@ -514,3 +518,24 @@ def _verify_working_pcap(cap_filter: str) -> None: ) compile_filter(cap_filter) + + +@lru_cache(maxsize=4096, typed=True) +def _compile_fnmatch(pattern: str) -> re.Pattern: + """Compile a fnmatch pattern.""" + return re.compile(translate(pattern)) + + +@lru_cache(maxsize=1024, typed=True) +def _memorized_fnmatch(name: str, pattern: str) -> bool: + """Memorized version of fnmatch that has a larger lru_cache. + + The default version of fnmatch only has a lru_cache of 256 entries. + With many devices we quickly reach that limit and end up compiling + the same pattern over and over again. + + DHCP has its own memorized fnmatch with its own lru_cache + since the data is going to be relatively the same + since the devices will not change frequently + """ + return bool(_compile_fnmatch(pattern).match(name)) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 1bfa44f3894..476cbd82cf8 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -5,9 +5,11 @@ import asyncio import contextlib from contextlib import suppress from dataclasses import dataclass -import fnmatch +from fnmatch import translate +from functools import lru_cache from ipaddress import IPv4Address, IPv6Address, ip_address import logging +import re import socket import sys from typing import Any, Final, cast @@ -302,7 +304,8 @@ def _match_against_data( return False match_val = matcher[key] assert isinstance(match_val, str) - if not fnmatch.fnmatch(match_data[key], match_val): + + if not _memorized_fnmatch(match_data[key], match_val): return False return True @@ -312,7 +315,7 @@ def _match_against_props(matcher: dict[str, str], props: dict[str, str]) -> bool return not any( key for key in matcher - if key not in props or not fnmatch.fnmatch(props[key].lower(), matcher[key]) + if key not in props or not _memorized_fnmatch(props[key].lower(), matcher[key]) ) @@ -484,7 +487,7 @@ def async_get_homekit_discovery_domain( if ( model != test_model and not model.startswith((f"{test_model} ", f"{test_model}-")) - and not fnmatch.fnmatch(model, test_model) + and not _memorized_fnmatch(model, test_model) ): continue @@ -575,3 +578,24 @@ def _truncate_location_name_to_valid(location_name: str) -> str: location_name, ) return location_name.encode("utf-8")[:MAX_NAME_LEN].decode("utf-8", "ignore") + + +@lru_cache(maxsize=4096, typed=True) +def _compile_fnmatch(pattern: str) -> re.Pattern: + """Compile a fnmatch pattern.""" + return re.compile(translate(pattern)) + + +@lru_cache(maxsize=1024, typed=True) +def _memorized_fnmatch(name: str, pattern: str) -> bool: + """Memorized version of fnmatch that has a larger lru_cache. + + The default version of fnmatch only has a lru_cache of 256 entries. + With many devices we quickly reach that limit and end up compiling + the same pattern over and over again. + + Zeroconf has its own memorized fnmatch with its own lru_cache + since the data is going to be relatively the same + since the devices will not change frequently + """ + return bool(_compile_fnmatch(pattern).match(name)) From 7f001cc1d1dec87a46d7f783c10b0b72917e2ee6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Aug 2022 04:41:50 -1000 Subject: [PATCH 575/903] ESPHome BLE scanner support (#77123) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .coveragerc | 1 + homeassistant/components/esphome/__init__.py | 4 + homeassistant/components/esphome/bluetooth.py | 113 ++++++++++++++++++ .../components/esphome/manifest.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/esphome/bluetooth.py diff --git a/.coveragerc b/.coveragerc index 14773947be1..693083081f4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -317,6 +317,7 @@ omit = homeassistant/components/escea/__init__.py homeassistant/components/esphome/__init__.py homeassistant/components/esphome/binary_sensor.py + homeassistant/components/esphome/bluetooth.py homeassistant/components/esphome/button.py homeassistant/components/esphome/camera.py homeassistant/components/esphome/climate.py diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 5d7b0efc18d..9df1d1af7d9 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -52,6 +52,8 @@ from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.storage import Store from homeassistant.helpers.template import Template +from .bluetooth import async_connect_scanner + # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData @@ -286,6 +288,8 @@ async def async_setup_entry( # noqa: C901 await cli.subscribe_states(entry_data.async_update_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states(async_on_state_subscription) + if entry_data.device_info.has_bluetooth_proxy: + await async_connect_scanner(hass, entry, cli) hass.async_create_task(entry_data.async_save_to_store()) except APIConnectionError as err: diff --git a/homeassistant/components/esphome/bluetooth.py b/homeassistant/components/esphome/bluetooth.py new file mode 100644 index 00000000000..94351293c7b --- /dev/null +++ b/homeassistant/components/esphome/bluetooth.py @@ -0,0 +1,113 @@ +"""Bluetooth scanner for esphome.""" + +from collections.abc import Callable +import datetime +from datetime import timedelta +import re +import time + +from aioesphomeapi import APIClient, BluetoothLEAdvertisement +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import ( + BaseHaScanner, + async_get_advertisement_callback, + async_register_scanner, +) +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_track_time_interval + +ADV_STALE_TIME = 180 # seconds + +TWO_CHAR = re.compile("..") + + +async def async_connect_scanner( + hass: HomeAssistant, entry: ConfigEntry, cli: APIClient +) -> None: + """Connect scanner.""" + assert entry.unique_id is not None + new_info_callback = async_get_advertisement_callback(hass) + scanner = ESPHomeScannner(hass, entry.unique_id, new_info_callback) + entry.async_on_unload(async_register_scanner(hass, scanner, False)) + entry.async_on_unload(scanner.async_setup()) + await cli.subscribe_bluetooth_le_advertisements(scanner.async_on_advertisement) + + +class ESPHomeScannner(BaseHaScanner): + """Scanner for esphome.""" + + def __init__( + self, + hass: HomeAssistant, + scanner_id: str, + new_info_callback: Callable[[BluetoothServiceInfoBleak], None], + ) -> None: + """Initialize the scanner.""" + self._hass = hass + self._new_info_callback = new_info_callback + self._discovered_devices: dict[str, BLEDevice] = {} + self._discovered_device_timestamps: dict[str, float] = {} + self._source = scanner_id + + @callback + def async_setup(self) -> CALLBACK_TYPE: + """Set up the scanner.""" + return async_track_time_interval( + self._hass, self._async_expire_devices, timedelta(seconds=30) + ) + + def _async_expire_devices(self, _datetime: datetime.datetime) -> None: + """Expire old devices.""" + now = time.monotonic() + expired = [ + address + for address, timestamp in self._discovered_device_timestamps.items() + if now - timestamp > ADV_STALE_TIME + ] + for address in expired: + del self._discovered_devices[address] + del self._discovered_device_timestamps[address] + + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + return list(self._discovered_devices.values()) + + @callback + def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: + """Call the registered callback.""" + now = time.monotonic() + address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper + advertisement_data = AdvertisementData( # type: ignore[no-untyped-call] + local_name=None if adv.name == "" else adv.name, + manufacturer_data=adv.manufacturer_data, + service_data=adv.service_data, + service_uuids=adv.service_uuids, + ) + device = BLEDevice( # type: ignore[no-untyped-call] + address=address, + name=adv.name, + details={}, + rssi=adv.rssi, + ) + self._discovered_devices[address] = device + self._discovered_device_timestamps[address] = now + self._new_info_callback( + BluetoothServiceInfoBleak( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=self._source, + device=device, + advertisement=advertisement_data, + connectable=False, + time=now, + ) + ) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index cf748b27170..4739c2904ac 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,11 +3,11 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.11.0"], + "requirements": ["aioesphomeapi==10.13.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], - "after_dependencies": ["zeroconf", "tag"], + "after_dependencies": ["bluetooth", "zeroconf", "tag"], "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6aed6cc56f4..56e2fdd72c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -150,7 +150,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.11.0 +aioesphomeapi==10.13.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf605143b33..c97130ee6aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.11.0 +aioesphomeapi==10.13.0 # homeassistant.components.flo aioflo==2021.11.0 From b1d249c39128824fc175def1fafadaf275517b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Tue, 23 Aug 2022 16:25:58 +0100 Subject: [PATCH 576/903] Update Whirlpool integration for 0.17.0 library (#76780) * Update Whirlpool integration for 0.17.0 library * Use dataclass for integration shared data --- .../components/whirlpool/__init__.py | 28 +++++++- homeassistant/components/whirlpool/climate.py | 65 ++++++++++--------- .../components/whirlpool/config_flow.py | 7 +- homeassistant/components/whirlpool/const.py | 1 - .../components/whirlpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/whirlpool/conftest.py | 22 +++++-- tests/components/whirlpool/test_climate.py | 22 ++----- tests/components/whirlpool/test_init.py | 10 +++ 10 files changed, 100 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 1991ec3806c..c6721197abc 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -1,15 +1,18 @@ """The Whirlpool Sixth Sense integration.""" +from dataclasses import dataclass import logging import aiohttp +from whirlpool.appliancesmanager import AppliancesManager from whirlpool.auth import Auth +from whirlpool.backendselector import BackendSelector, Brand, Region from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import AUTH_INSTANCE_KEY, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -20,7 +23,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Whirlpool Sixth Sense from a config entry.""" hass.data.setdefault(DOMAIN, {}) - auth = Auth(entry.data["username"], entry.data["password"]) + backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) + auth = Auth(backend_selector, entry.data["username"], entry.data["password"]) try: await auth.do_auth(store=False) except aiohttp.ClientError as ex: @@ -30,7 +34,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Authentication failed") return False - hass.data[DOMAIN][entry.entry_id] = {AUTH_INSTANCE_KEY: auth} + appliances_manager = AppliancesManager(backend_selector, auth) + if not await appliances_manager.fetch_appliances(): + _LOGGER.error("Cannot fetch appliances") + return False + + hass.data[DOMAIN][entry.entry_id] = WhirlpoolData( + appliances_manager, + auth, + backend_selector, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -44,3 +57,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +@dataclass +class WhirlpoolData: + """Whirlpool integaration shared data.""" + + appliances_manager: AppliancesManager + auth: Auth + backend_selector: BackendSelector diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index c3786fd5f55..60de41b46f9 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -1,14 +1,14 @@ """Platform for climate integration.""" from __future__ import annotations -import asyncio import logging +from typing import Any -import aiohttp from whirlpool.aircon import Aircon, FanSpeed as AirconFanSpeed, Mode as AirconMode from whirlpool.auth import Auth +from whirlpool.backendselector import BackendSelector -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity from homeassistant.components.climate.const import ( FAN_AUTO, FAN_HIGH, @@ -23,9 +23,11 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import AUTH_INSTANCE_KEY, DOMAIN +from . import WhirlpoolData +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -67,14 +69,21 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entry.""" - auth: Auth = hass.data[DOMAIN][config_entry.entry_id][AUTH_INSTANCE_KEY] - if not (said_list := auth.get_said_list()): - _LOGGER.debug("No appliances found") + whirlpool_data: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] + if not (aircons := whirlpool_data.appliances_manager.aircons): + _LOGGER.debug("No aircons found") return - # the whirlpool library needs to be updated to be able to support more - # than one device, so we use only the first one for now - aircons = [AirConEntity(said, auth) for said in said_list] + aircons = [ + AirConEntity( + hass, + ac_data["SAID"], + ac_data["NAME"], + whirlpool_data.backend_selector, + whirlpool_data.auth, + ) + for ac_data in aircons + ] async_add_entities(aircons, True) @@ -95,50 +104,44 @@ class AirConEntity(ClimateEntity): _attr_temperature_unit = TEMP_CELSIUS _attr_should_poll = False - def __init__(self, said, auth: Auth): + def __init__(self, hass, said, name, backend_selector: BackendSelector, auth: Auth): """Initialize the entity.""" - self._aircon = Aircon(auth, said, self.async_write_ha_state) + self._aircon = Aircon(backend_selector, auth, said, self.async_write_ha_state) - self._attr_name = said + self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, said, hass=hass) + self._attr_name = name if name is not None else said self._attr_unique_id = said async def async_added_to_hass(self) -> None: """Connect aircon to the cloud.""" await self._aircon.connect() - try: - name = await self._aircon.fetch_name() - if name is not None: - self._attr_name = name - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Failed to get name") - @property def available(self) -> bool: """Return True if entity is available.""" return self._aircon.get_online() @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self._aircon.get_current_temp() @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" return self._aircon.get_temp() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self._aircon.set_temp(kwargs.get(ATTR_TEMPERATURE)) @property - def current_humidity(self): + def current_humidity(self) -> int: """Return the current humidity.""" return self._aircon.get_current_humidity() @property - def target_humidity(self): + def target_humidity(self) -> int: """Return the humidity we try to reach.""" return self._aircon.get_humidity() @@ -169,30 +172,30 @@ class AirConEntity(ClimateEntity): await self._aircon.set_power_on(True) @property - def fan_mode(self): + def fan_mode(self) -> str: """Return the fan setting.""" fanspeed = self._aircon.get_fanspeed() return AIRCON_FANSPEED_MAP.get(fanspeed, FAN_OFF) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set fan mode.""" if not (fanspeed := FAN_MODE_TO_AIRCON_FANSPEED.get(fan_mode)): raise ValueError(f"Invalid fan mode {fan_mode}") await self._aircon.set_fanspeed(fanspeed) @property - def swing_mode(self): + def swing_mode(self) -> str: """Return the swing setting.""" return SWING_HORIZONTAL if self._aircon.get_h_louver_swing() else SWING_OFF - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target temperature.""" await self._aircon.set_h_louver_swing(swing_mode == SWING_HORIZONTAL) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn device on.""" await self._aircon.set_power_on(True) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn device off.""" await self._aircon.set_power_on(False) diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index ac6cb3d568e..dbc59f82416 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -7,9 +7,11 @@ import logging import aiohttp import voluptuous as vol from whirlpool.auth import Auth +from whirlpool.backendselector import BackendSelector, Brand, Region from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -27,7 +29,8 @@ async def validate_input( Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - auth = Auth(data[CONF_USERNAME], data[CONF_PASSWORD]) + backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) + auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD]) try: await auth.do_auth() except (asyncio.TimeoutError, aiohttp.ClientConnectionError) as exc: @@ -44,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/whirlpool/const.py b/homeassistant/components/whirlpool/const.py index 16ba293e3b2..8a030d8fab2 100644 --- a/homeassistant/components/whirlpool/const.py +++ b/homeassistant/components/whirlpool/const.py @@ -1,4 +1,3 @@ """Constants for the Whirlpool Sixth Sense integration.""" DOMAIN = "whirlpool" -AUTH_INSTANCE_KEY = "auth" diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 4c7471e4715..a7c99e9066c 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -3,7 +3,7 @@ "name": "Whirlpool Sixth Sense", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/whirlpool", - "requirements": ["whirlpool-sixth-sense==0.15.1"], + "requirements": ["whirlpool-sixth-sense==0.17.0"], "codeowners": ["@abmantis"], "iot_class": "cloud_push", "loggers": ["whirlpool"] diff --git a/requirements_all.txt b/requirements_all.txt index 56e2fdd72c8..95afa8a04f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ waterfurnace==1.1.0 webexteamssdk==1.1.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.15.1 +whirlpool-sixth-sense==0.17.0 # homeassistant.components.whois whois==0.9.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c97130ee6aa..a558bbe5b28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1675,7 +1675,7 @@ wallbox==0.4.9 watchdog==2.1.9 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.15.1 +whirlpool-sixth-sense==0.17.0 # homeassistant.components.whois whois==0.9.16 diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index 3a5fd0e3d2e..eba58b07faa 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -12,19 +12,31 @@ MOCK_SAID2 = "said2" @pytest.fixture(name="mock_auth_api") def fixture_mock_auth_api(): - """Set up air conditioner Auth fixture.""" + """Set up Auth fixture.""" with mock.patch("homeassistant.components.whirlpool.Auth") as mock_auth: mock_auth.return_value.do_auth = AsyncMock() mock_auth.return_value.is_access_token_valid.return_value = True - mock_auth.return_value.get_said_list.return_value = [MOCK_SAID1, MOCK_SAID2] yield mock_auth +@pytest.fixture(name="mock_appliances_manager_api") +def fixture_mock_appliances_manager_api(): + """Set up AppliancesManager fixture.""" + with mock.patch( + "homeassistant.components.whirlpool.AppliancesManager" + ) as mock_appliances_manager: + mock_appliances_manager.return_value.fetch_appliances = AsyncMock() + mock_appliances_manager.return_value.aircons = [ + {"SAID": MOCK_SAID1, "NAME": "TestZone"}, + {"SAID": MOCK_SAID2, "NAME": "TestZone"}, + ] + yield mock_appliances_manager + + def get_aircon_mock(said): """Get a mock of an air conditioner.""" mock_aircon = mock.Mock(said=said) mock_aircon.connect = AsyncMock() - mock_aircon.fetch_name = AsyncMock(return_value="TestZone") mock_aircon.get_online.return_value = True mock_aircon.get_power_on.return_value = True mock_aircon.get_mode.return_value = whirlpool.aircon.Mode.Cool @@ -47,13 +59,13 @@ def get_aircon_mock(said): @pytest.fixture(name="mock_aircon1_api", autouse=True) -def fixture_mock_aircon1_api(mock_auth_api): +def fixture_mock_aircon1_api(mock_auth_api, mock_appliances_manager_api): """Set up air conditioner API fixture.""" yield get_aircon_mock(MOCK_SAID1) @pytest.fixture(name="mock_aircon2_api", autouse=True) -def fixture_mock_aircon2_api(mock_auth_api): +def fixture_mock_aircon2_api(mock_auth_api, mock_appliances_manager_api): """Set up air conditioner API fixture.""" yield get_aircon_mock(MOCK_SAID2) diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index a1f2ed38aba..f0d4c93f8d6 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -1,7 +1,6 @@ """Test the Whirlpool Sixth Sense climate domain.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import MagicMock -import aiohttp from attr import dataclass import pytest import whirlpool @@ -58,30 +57,21 @@ async def update_ac_state( """Simulate an update trigger from the API.""" update_ha_state_cb = mock_aircon_api_instances.call_args_list[ mock_instance_idx - ].args[2] + ].args[3] update_ha_state_cb() await hass.async_block_till_done() return hass.states.get(entity_id) -async def test_no_appliances(hass: HomeAssistant, mock_auth_api: MagicMock): +async def test_no_appliances( + hass: HomeAssistant, mock_appliances_manager_api: MagicMock +): """Test the setup of the climate entities when there are no appliances available.""" - mock_auth_api.return_value.get_said_list.return_value = [] + mock_appliances_manager_api.return_value.aircons = [] await init_integration(hass) assert len(hass.states.async_all()) == 0 -async def test_name_fallback_on_exception( - hass: HomeAssistant, mock_aircon1_api: MagicMock -): - """Test name property.""" - mock_aircon1_api.fetch_name = AsyncMock(side_effect=aiohttp.ClientError()) - - await init_integration(hass) - state = hass.states.get("climate.said1") - assert state.attributes[ATTR_FRIENDLY_NAME] == "said1" - - async def test_static_attributes(hass: HomeAssistant, mock_aircon1_api: MagicMock): """Test static climate attributes.""" await init_integration(hass) diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index 00fc27ddc63..626c127b61a 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -36,6 +36,16 @@ async def test_setup_auth_failed(hass: HomeAssistant, mock_auth_api: MagicMock): assert entry.state is ConfigEntryState.SETUP_ERROR +async def test_setup_fetch_appliances_failed( + hass: HomeAssistant, mock_appliances_manager_api: MagicMock +): + """Test setup with failed fetch_appliances.""" + mock_appliances_manager_api.return_value.fetch_appliances.return_value = False + entry = await init_integration(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.SETUP_ERROR + + async def test_unload_entry(hass: HomeAssistant): """Test successful unload of entry.""" entry = await init_integration(hass) From f61edf07788c87681fe06985b80a0994e09df5b9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Aug 2022 17:51:17 +0200 Subject: [PATCH 577/903] Fix updating of statistics metadata name (#77207) * Fix updating of statistics metadata name * Fix test * Test renaming --- .../components/recorder/statistics.py | 2 ++ tests/components/recorder/test_statistics.py | 30 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index fcd8e4f3930..1b0a4e64897 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -267,12 +267,14 @@ def _update_or_add_metadata( if ( old_metadata["has_mean"] != new_metadata["has_mean"] or old_metadata["has_sum"] != new_metadata["has_sum"] + or old_metadata["name"] != new_metadata["name"] or old_metadata["unit_of_measurement"] != new_metadata["unit_of_measurement"] ): session.query(StatisticsMeta).filter_by(statistic_id=statistic_id).update( { StatisticsMeta.has_mean: new_metadata["has_mean"], StatisticsMeta.has_sum: new_metadata["has_sum"], + StatisticsMeta.name: new_metadata["name"], StatisticsMeta.unit_of_measurement: new_metadata["unit_of_measurement"], }, synchronize_session=False, diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 8db4587f1cf..5bc8aa76a00 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -161,6 +161,7 @@ def mock_sensor_statistics(): "unit_of_measurement": "dogs", "has_mean": True, "has_sum": False, + "name": None, }, "stat": {"start": start}, } @@ -599,7 +600,7 @@ async def test_import_statistics( ] } - # Update the previously inserted statistics + # Update the previously inserted statistics + rename external_statistics = { "start": period1, "max": 1, @@ -609,8 +610,34 @@ async def test_import_statistics( "state": 4, "sum": 5, } + external_metadata["name"] = "Total imported energy renamed" import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "has_mean": False, + "has_sum": True, + "statistic_id": statistic_id, + "name": "Total imported energy renamed", + "source": source, + "unit_of_measurement": "kWh", + } + ] + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) + assert metadata == { + statistic_id: ( + 1, + { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy renamed", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + }, + ) + } stats = statistics_during_period(hass, zero, period="hour") assert stats == { statistic_id: [ @@ -639,6 +666,7 @@ async def test_import_statistics( ] } + # Adjust the statistics await client.send_json( { "id": 1, From 99ec341d6ee9370fb7563fed65d83bde2e2b3f4c Mon Sep 17 00:00:00 2001 From: y34hbuddy <47507530+y34hbuddy@users.noreply.github.com> Date: Tue, 23 Aug 2022 14:58:17 -0400 Subject: [PATCH 578/903] Refactor volvooncall to use ConfigFlow (#76680) * refactor volvooncall to use ConfigFlow * remove unused constant SIGNAL_STATE_UPDATED * implemented feedback * improve ConfigFlow UX by giving an option for region=None * implemented more feedback * next round of feedback * implemented more feedback * improve test coverage * more test coverage * Apply suggestions from code review * implemented feedback on tests * Apply suggestions from code review Co-authored-by: Martin Hjelmare --- .coveragerc | 9 +- CODEOWNERS | 1 + .../components/volvooncall/__init__.py | 206 +++++++++--------- .../components/volvooncall/binary_sensor.py | 46 +++- .../components/volvooncall/config_flow.py | 86 ++++++++ homeassistant/components/volvooncall/const.py | 62 ++++++ .../components/volvooncall/device_tracker.py | 97 ++++++--- .../components/volvooncall/errors.py | 6 + homeassistant/components/volvooncall/lock.py | 46 +++- .../components/volvooncall/manifest.json | 4 +- .../components/volvooncall/sensor.py | 45 +++- .../components/volvooncall/strings.json | 28 +++ .../components/volvooncall/switch.py | 47 +++- .../volvooncall/translations/en.json | 29 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/volvooncall/__init__.py | 1 + .../volvooncall/test_config_flow.py | 169 ++++++++++++++ 18 files changed, 705 insertions(+), 181 deletions(-) create mode 100644 homeassistant/components/volvooncall/config_flow.py create mode 100644 homeassistant/components/volvooncall/const.py create mode 100644 homeassistant/components/volvooncall/errors.py create mode 100644 homeassistant/components/volvooncall/strings.json create mode 100644 homeassistant/components/volvooncall/translations/en.json create mode 100644 tests/components/volvooncall/__init__.py create mode 100644 tests/components/volvooncall/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 693083081f4..942f32eb9a5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1427,7 +1427,14 @@ omit = homeassistant/components/volumio/__init__.py homeassistant/components/volumio/browse_media.py homeassistant/components/volumio/media_player.py - homeassistant/components/volvooncall/* + homeassistant/components/volvooncall/__init__.py + homeassistant/components/volvooncall/binary_sensor.py + homeassistant/components/volvooncall/const.py + homeassistant/components/volvooncall/device_tracker.py + homeassistant/components/volvooncall/errors.py + homeassistant/components/volvooncall/lock.py + homeassistant/components/volvooncall/sensor.py + homeassistant/components/volvooncall/switch.py homeassistant/components/vulcan/__init__.py homeassistant/components/vulcan/calendar.py homeassistant/components/vulcan/fetch_data.py diff --git a/CODEOWNERS b/CODEOWNERS index 2513e290230..799c81bfa81 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1201,6 +1201,7 @@ build.json @home-assistant/supervisor /homeassistant/components/volumio/ @OnFreund /tests/components/volumio/ @OnFreund /homeassistant/components/volvooncall/ @molobrakos +/tests/components/volvooncall/ @molobrakos /homeassistant/components/vulcan/ @Antoni-Czaplicki /tests/components/vulcan/ @Antoni-Czaplicki /homeassistant/components/wake_on_lan/ @ntilley905 diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index cf385d320ca..52890ce9d55 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -1,12 +1,15 @@ """Support for Volvo On Call.""" -from datetime import timedelta + import logging +from aiohttp.client_exceptions import ClientResponseError import async_timeout import voluptuous as vol from volvooncall import Connection from volvooncall.dashboard import Instrument +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -16,7 +19,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -27,121 +30,119 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -DOMAIN = "volvooncall" - -DATA_KEY = DOMAIN +from .const import ( + CONF_MUTABLE, + CONF_SCANDINAVIAN_MILES, + CONF_SERVICE_URL, + DEFAULT_UPDATE_INTERVAL, + DOMAIN, + PLATFORMS, + RESOURCES, + VOLVO_DISCOVERY_NEW, +) +from .errors import InvalidAuth _LOGGER = logging.getLogger(__name__) -DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) - -CONF_SERVICE_URL = "service_url" -CONF_SCANDINAVIAN_MILES = "scandinavian_miles" -CONF_MUTABLE = "mutable" - -SIGNAL_STATE_UPDATED = f"{DOMAIN}.updated" - -PLATFORMS = { - "sensor": "sensor", - "binary_sensor": "binary_sensor", - "lock": "lock", - "device_tracker": "device_tracker", - "switch": "switch", -} - -RESOURCES = [ - "position", - "lock", - "heater", - "odometer", - "trip_meter1", - "trip_meter2", - "average_speed", - "fuel_amount", - "fuel_amount_level", - "average_fuel_consumption", - "distance_to_empty", - "washer_fluid_level", - "brake_fluid", - "service_warning_status", - "bulb_failures", - "battery_range", - "battery_level", - "time_to_fully_charged", - "battery_charge_status", - "engine_start", - "last_trip", - "is_engine_running", - "doors_hood_open", - "doors_tailgate_open", - "doors_front_left_door_open", - "doors_front_right_door_open", - "doors_rear_left_door_open", - "doors_rear_right_door_open", - "windows_front_left_window_open", - "windows_front_right_window_open", - "windows_rear_left_window_open", - "windows_rear_right_window_open", - "tyre_pressure_front_left_tyre_pressure", - "tyre_pressure_front_right_tyre_pressure", - "tyre_pressure_rear_left_tyre_pressure", - "tyre_pressure_rear_right_tyre_pressure", - "any_door_open", - "any_window_open", -] - CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.deprecated(CONF_SCAN_INTERVAL), - cv.deprecated(CONF_NAME), - cv.deprecated(CONF_RESOURCES), - vol.Schema( + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL - ): vol.All( - cv.time_period, vol.Clamp(min=DEFAULT_UPDATE_INTERVAL) - ), # ignored, using DataUpdateCoordinator instead + ): vol.All(cv.time_period, vol.Clamp(min=DEFAULT_UPDATE_INTERVAL)), vol.Optional(CONF_NAME, default={}): cv.schema_with_slug_keys( cv.string - ), # ignored, users can modify names of entities in the UI + ), vol.Optional(CONF_RESOURCES): vol.All( cv.ensure_list, [vol.In(RESOURCES)] - ), # ignored, users can disable entities in the UI + ), vol.Optional(CONF_REGION): cv.string, vol.Optional(CONF_SERVICE_URL): cv.string, vol.Optional(CONF_MUTABLE, default=True): cv.boolean, vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, } - ), - ) - }, + ) + }, + ), extra=vol.ALLOW_EXTRA, ) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Volvo On Call component.""" + """Migrate from YAML to ConfigEntry.""" + if DOMAIN not in config: + return True + + hass.data[DOMAIN] = {} + + if not hass.config_entries.async_entries(DOMAIN): + new_conf = {} + new_conf[CONF_USERNAME] = config[DOMAIN][CONF_USERNAME] + new_conf[CONF_PASSWORD] = config[DOMAIN][CONF_PASSWORD] + new_conf[CONF_REGION] = config[DOMAIN].get(CONF_REGION) + new_conf[CONF_SCANDINAVIAN_MILES] = config[DOMAIN][CONF_SCANDINAVIAN_MILES] + new_conf[CONF_MUTABLE] = config[DOMAIN][CONF_MUTABLE] + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=new_conf + ) + ) + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version=None, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Volvo On Call component from a ConfigEntry.""" session = async_get_clientsession(hass) connection = Connection( session=session, - username=config[DOMAIN].get(CONF_USERNAME), - password=config[DOMAIN].get(CONF_PASSWORD), - service_url=config[DOMAIN].get(CONF_SERVICE_URL), - region=config[DOMAIN].get(CONF_REGION), + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + service_url=None, + region=entry.data[CONF_REGION], ) - hass.data[DATA_KEY] = {} + hass.data.setdefault(DOMAIN, {}) - volvo_data = VolvoData(hass, connection, config) + volvo_data = VolvoData(hass, connection, entry) - hass.data[DATA_KEY] = VolvoUpdateCoordinator(hass, volvo_data) + coordinator = hass.data[DOMAIN][entry.entry_id] = VolvoUpdateCoordinator( + hass, volvo_data + ) - return await volvo_data.update() + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryAuthFailed: + return False + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok class VolvoData: @@ -151,13 +152,13 @@ class VolvoData: self, hass: HomeAssistant, connection: Connection, - config: ConfigType, + entry: ConfigEntry, ) -> None: """Initialize the component state.""" self.hass = hass self.vehicles: set[str] = set() self.instruments: set[Instrument] = set() - self.config = config + self.config_entry = entry self.connection = connection def instrument(self, vin, component, attr, slug_attr): @@ -184,8 +185,8 @@ class VolvoData: self.vehicles.add(vehicle.vin) dashboard = vehicle.dashboard( - mutable=self.config[DOMAIN][CONF_MUTABLE], - scandinavian_miles=self.config[DOMAIN][CONF_SCANDINAVIAN_MILES], + mutable=self.config_entry.data[CONF_MUTABLE], + scandinavian_miles=self.config_entry.data[CONF_SCANDINAVIAN_MILES], ) for instrument in ( @@ -193,23 +194,8 @@ class VolvoData: for instrument in dashboard.instruments if instrument.component in PLATFORMS ): - self.instruments.add(instrument) - - self.hass.async_create_task( - discovery.async_load_platform( - self.hass, - PLATFORMS[instrument.component], - DOMAIN, - ( - vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - ), - self.config, - ) - ) + async_dispatcher_send(self.hass, VOLVO_DISCOVERY_NEW, [instrument]) async def update(self): """Update status from the online service.""" @@ -220,11 +206,15 @@ class VolvoData: if vehicle.vin not in self.vehicles: self.discover_vehicle(vehicle) - # this is currently still needed for device_tracker, which isn't using the update coordinator yet - async_dispatcher_send(self.hass, SIGNAL_STATE_UPDATED) - return True + async def auth_is_valid(self): + """Check if provided username/password/region authenticate.""" + try: + await self.connection.get("customeraccounts") + except ClientResponseError as exc: + raise InvalidAuth from exc + class VolvoUpdateCoordinator(DataUpdateCoordinator): """Volvo coordinator.""" diff --git a/homeassistant/components/volvooncall/binary_sensor.py b/homeassistant/components/volvooncall/binary_sensor.py index 2aeaeff93e4..77e1b7183db 100644 --- a/homeassistant/components/volvooncall/binary_sensor.py +++ b/homeassistant/components/volvooncall/binary_sensor.py @@ -4,28 +4,54 @@ from __future__ import annotations from contextlib import suppress import voluptuous as vol +from volvooncall.dashboard import Instrument from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, BinarySensorEntity, ) -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator +from . import VolvoEntity, VolvoUpdateCoordinator +from .const import DOMAIN, VOLVO_DISCOVERY_NEW -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Volvo sensors.""" - if discovery_info is None: - return - async_add_entities([VolvoSensor(hass.data[DATA_KEY], *discovery_info)]) + """Configure binary_sensors from a config entry created in the integrations UI.""" + coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + volvo_data = coordinator.volvo_data + + @callback + def async_discover_device(instruments: list[Instrument]) -> None: + """Discover and add a discovered Volvo On Call binary sensor.""" + entities: list[VolvoSensor] = [] + + for instrument in instruments: + if instrument.component == "binary_sensor": + entities.append( + VolvoSensor( + coordinator, + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + ) + ) + + async_add_entities(entities) + + async_discover_device([*volvo_data.instruments]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) + ) class VolvoSensor(VolvoEntity, BinarySensorEntity): diff --git a/homeassistant/components/volvooncall/config_flow.py b/homeassistant/components/volvooncall/config_flow.py new file mode 100644 index 00000000000..5a0756b5725 --- /dev/null +++ b/homeassistant/components/volvooncall/config_flow.py @@ -0,0 +1,86 @@ +"""Config flow for Volvo On Call integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol +from volvooncall import Connection + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from . import VolvoData +from .const import CONF_MUTABLE, CONF_SCANDINAVIAN_MILES, DOMAIN +from .errors import InvalidAuth + +_LOGGER = logging.getLogger(__name__) + +USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_REGION, default=None): vol.In( + {"na": "North America", "cn": "China", None: "Rest of world"} + ), + vol.Optional(CONF_MUTABLE, default=True): cv.boolean, + vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, + }, +) + + +class VolvoOnCallConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """VolvoOnCall config flow.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle user step.""" + errors = {} + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() + + try: + await self.is_valid(user_input) + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unhandled exception in user step") + errors["base"] = "unknown" + if not errors: + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=USER_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_data) -> FlowResult: + """Import volvooncall config from configuration.yaml.""" + return await self.async_step_user(import_data) + + async def is_valid(self, user_input): + """Check for user input errors.""" + + session = async_get_clientsession(self.hass) + + region: str | None = user_input.get(CONF_REGION) + + connection = Connection( + session=session, + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + service_url=None, + region=region, + ) + + test_volvo_data = VolvoData(self.hass, connection, user_input) + + await test_volvo_data.auth_is_valid() diff --git a/homeassistant/components/volvooncall/const.py b/homeassistant/components/volvooncall/const.py new file mode 100644 index 00000000000..bc72f9c5267 --- /dev/null +++ b/homeassistant/components/volvooncall/const.py @@ -0,0 +1,62 @@ +"""Constants for volvooncall.""" + +from datetime import timedelta + +DOMAIN = "volvooncall" + +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) + +CONF_SERVICE_URL = "service_url" +CONF_SCANDINAVIAN_MILES = "scandinavian_miles" +CONF_MUTABLE = "mutable" + +PLATFORMS = { + "sensor": "sensor", + "binary_sensor": "binary_sensor", + "lock": "lock", + "device_tracker": "device_tracker", + "switch": "switch", +} + +RESOURCES = [ + "position", + "lock", + "heater", + "odometer", + "trip_meter1", + "trip_meter2", + "average_speed", + "fuel_amount", + "fuel_amount_level", + "average_fuel_consumption", + "distance_to_empty", + "washer_fluid_level", + "brake_fluid", + "service_warning_status", + "bulb_failures", + "battery_range", + "battery_level", + "time_to_fully_charged", + "battery_charge_status", + "engine_start", + "last_trip", + "is_engine_running", + "doors_hood_open", + "doors_tailgate_open", + "doors_front_left_door_open", + "doors_front_right_door_open", + "doors_rear_left_door_open", + "doors_rear_right_door_open", + "windows_front_left_window_open", + "windows_front_right_window_open", + "windows_rear_left_window_open", + "windows_rear_right_window_open", + "tyre_pressure_front_left_tyre_pressure", + "tyre_pressure_front_right_tyre_pressure", + "tyre_pressure_rear_left_tyre_pressure", + "tyre_pressure_rear_right_tyre_pressure", + "any_door_open", + "any_window_open", +] + +VOLVO_DISCOVERY_NEW = "volvo_discovery_new" diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index ffed8005f36..159cb39cf6a 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -1,45 +1,80 @@ """Support for tracking a Volvo.""" from __future__ import annotations -from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType -from homeassistant.core import HomeAssistant +from volvooncall.dashboard import Instrument + +from homeassistant.components.device_tracker import SourceType +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import slugify +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DATA_KEY, SIGNAL_STATE_UPDATED, VolvoUpdateCoordinator +from . import VolvoEntity, VolvoUpdateCoordinator +from .const import DOMAIN, VOLVO_DISCOVERY_NEW -async def async_setup_scanner( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - async_see: AsyncSeeCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> bool: - """Set up the Volvo tracker.""" - if discovery_info is None: - return False - - vin, component, attr, slug_attr = discovery_info - coordinator: VolvoUpdateCoordinator = hass.data[DATA_KEY] + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Configure device_trackers from a config entry created in the integrations UI.""" + coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] volvo_data = coordinator.volvo_data - instrument = volvo_data.instrument(vin, component, attr, slug_attr) - if instrument is None: - return False + @callback + def async_discover_device(instruments: list[Instrument]) -> None: + """Discover and add a discovered Volvo On Call device tracker.""" + entities: list[VolvoTrackerEntity] = [] - async def see_vehicle() -> None: - """Handle the reporting of the vehicle position.""" - host_name = instrument.vehicle_name - dev_id = f"volvo_{slugify(host_name)}" - await async_see( - dev_id=dev_id, - host_name=host_name, - source_type=SourceType.GPS, - gps=instrument.state, - icon="mdi:car", + for instrument in instruments: + if instrument.component == "device_tracker": + entities.append( + VolvoTrackerEntity( + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + coordinator, + ) + ) + + async_add_entities(entities) + + async_discover_device([*volvo_data.instruments]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) + ) + + +class VolvoTrackerEntity(VolvoEntity, TrackerEntity): + """A tracked Volvo vehicle.""" + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + latitude, _ = self._get_pos() + return latitude + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + _, longitude = self._get_pos() + return longitude + + @property + def source_type(self) -> SourceType | str: + """Return the source type (GPS).""" + return SourceType.GPS + + def _get_pos(self) -> tuple[float, float]: + volvo_data = self.coordinator.volvo_data + instrument = volvo_data.instrument( + self.vin, self.component, self.attribute, self.slug_attr ) - async_dispatcher_connect(hass, SIGNAL_STATE_UPDATED, see_vehicle) + latitude, longitude, _, _, _ = instrument.state - return True + return (float(latitude), float(longitude)) diff --git a/homeassistant/components/volvooncall/errors.py b/homeassistant/components/volvooncall/errors.py new file mode 100644 index 00000000000..a3af1125b48 --- /dev/null +++ b/homeassistant/components/volvooncall/errors.py @@ -0,0 +1,6 @@ +"""Exceptions specific to volvooncall.""" +from homeassistant.exceptions import HomeAssistantError + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/volvooncall/lock.py b/homeassistant/components/volvooncall/lock.py index da36ca2e573..a48b5dc6b65 100644 --- a/homeassistant/components/volvooncall/lock.py +++ b/homeassistant/components/volvooncall/lock.py @@ -4,27 +4,51 @@ from __future__ import annotations from typing import Any -from volvooncall.dashboard import Lock +from volvooncall.dashboard import Instrument, Lock from homeassistant.components.lock import LockEntity -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator +from . import VolvoEntity, VolvoUpdateCoordinator +from .const import DOMAIN, VOLVO_DISCOVERY_NEW -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Volvo On Call lock.""" - if discovery_info is None: - return + """Configure locks from a config entry created in the integrations UI.""" + coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + volvo_data = coordinator.volvo_data - async_add_entities([VolvoLock(hass.data[DATA_KEY], *discovery_info)]) + @callback + def async_discover_device(instruments: list[Instrument]) -> None: + """Discover and add a discovered Volvo On Call lock.""" + entities: list[VolvoLock] = [] + + for instrument in instruments: + if instrument.component == "lock": + entities.append( + VolvoLock( + coordinator, + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + ) + ) + + async_add_entities(entities) + + async_discover_device([*volvo_data.instruments]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) + ) class VolvoLock(VolvoEntity, LockEntity): diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index fe7e384f72a..16628d0a5d2 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -3,7 +3,9 @@ "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", "requirements": ["volvooncall==0.10.0"], + "dependencies": ["repairs"], "codeowners": ["@molobrakos"], "iot_class": "cloud_polling", - "loggers": ["geopy", "hbmqtt", "volvooncall"] + "loggers": ["geopy", "hbmqtt", "volvooncall"], + "config_flow": true } diff --git a/homeassistant/components/volvooncall/sensor.py b/homeassistant/components/volvooncall/sensor.py index 41426ff878a..0f4269732e3 100644 --- a/homeassistant/components/volvooncall/sensor.py +++ b/homeassistant/components/volvooncall/sensor.py @@ -1,24 +1,51 @@ """Support for Volvo On Call sensors.""" from __future__ import annotations +from volvooncall.dashboard import Instrument + from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator +from . import VolvoEntity, VolvoUpdateCoordinator +from .const import DOMAIN, VOLVO_DISCOVERY_NEW -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Volvo sensors.""" - if discovery_info is None: - return - async_add_entities([VolvoSensor(hass.data[DATA_KEY], *discovery_info)]) + """Configure sensors from a config entry created in the integrations UI.""" + coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + volvo_data = coordinator.volvo_data + + @callback + def async_discover_device(instruments: list[Instrument]) -> None: + """Discover and add a discovered Volvo On Call sensor.""" + entities: list[VolvoSensor] = [] + + for instrument in instruments: + if instrument.component == "sensor": + entities.append( + VolvoSensor( + coordinator, + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + ) + ) + + async_add_entities(entities) + + async_discover_device([*volvo_data.instruments]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) + ) class VolvoSensor(VolvoEntity, SensorEntity): diff --git a/homeassistant/components/volvooncall/strings.json b/homeassistant/components/volvooncall/strings.json new file mode 100644 index 00000000000..1982b3c353c --- /dev/null +++ b/homeassistant/components/volvooncall/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "region": "Region", + "mutable": "Allow Remote Start / Lock / etc.", + "scandinavian_miles": "Use Scandinavian Miles" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "Account is already configured" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Volvo On Call YAML configuration is being removed", + "description": "Configuring the Volvo On Call platform using YAML is being removed in a future release of Home Assistant.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/volvooncall/switch.py b/homeassistant/components/volvooncall/switch.py index 6c8519f12e8..4d7b65bce95 100644 --- a/homeassistant/components/volvooncall/switch.py +++ b/homeassistant/components/volvooncall/switch.py @@ -1,24 +1,51 @@ """Support for Volvo heater.""" from __future__ import annotations +from volvooncall.dashboard import Instrument + from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator +from . import VolvoEntity, VolvoUpdateCoordinator +from .const import DOMAIN, VOLVO_DISCOVERY_NEW -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up a Volvo switch.""" - if discovery_info is None: - return - async_add_entities([VolvoSwitch(hass.data[DATA_KEY], *discovery_info)]) + """Configure binary_sensors from a config entry created in the integrations UI.""" + coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + volvo_data = coordinator.volvo_data + + @callback + def async_discover_device(instruments: list[Instrument]) -> None: + """Discover and add a discovered Volvo On Call switch.""" + entities: list[VolvoSwitch] = [] + + for instrument in instruments: + if instrument.component == "switch": + entities.append( + VolvoSwitch( + coordinator, + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + ) + ) + + async_add_entities(entities) + + async_discover_device([*volvo_data.instruments]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) + ) class VolvoSwitch(VolvoEntity, SwitchEntity): diff --git a/homeassistant/components/volvooncall/translations/en.json b/homeassistant/components/volvooncall/translations/en.json new file mode 100644 index 00000000000..b3468cb78e2 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "title": "Login to Volvo On Call", + "data": { + "username": "Username", + "password": "Password", + "region": "Region", + "mutable": "Allow Remote Start / Lock / etc.", + "scandinavian_miles": "Use Scandinavian Miles" + } + } + }, + "error": { + "invalid_auth": "Authentication failed. Please check your username, password, and region.", + "unknown": "Unknown error." + }, + "abort": { + "already_configured": "Account is already configured" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Volvo On Call YAML configuration is being removed", + "description": "Configuring the Volvo On Call platform using YAML is being removed in a future release of Home Assistant.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c604c5e95cd..59070af31c7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -415,6 +415,7 @@ FLOWS = { "vizio", "vlc_telnet", "volumio", + "volvooncall", "vulcan", "wallbox", "watttime", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a558bbe5b28..99caab39cf7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1655,6 +1655,9 @@ venstarcolortouch==0.18 # homeassistant.components.vilfo vilfo-api-client==0.3.2 +# homeassistant.components.volvooncall +volvooncall==0.10.0 + # homeassistant.components.verisure vsure==1.8.1 diff --git a/tests/components/volvooncall/__init__.py b/tests/components/volvooncall/__init__.py new file mode 100644 index 00000000000..b49a0974c59 --- /dev/null +++ b/tests/components/volvooncall/__init__.py @@ -0,0 +1 @@ +"""Tests for the Volvo On Call integration.""" diff --git a/tests/components/volvooncall/test_config_flow.py b/tests/components/volvooncall/test_config_flow.py new file mode 100644 index 00000000000..3f64b052824 --- /dev/null +++ b/tests/components/volvooncall/test_config_flow.py @@ -0,0 +1,169 @@ +"""Test the Volvo On Call config flow.""" +from unittest.mock import Mock, patch + +from aiohttp import ClientResponseError + +from homeassistant import config_entries +from homeassistant.components.volvooncall.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert len(result["errors"]) == 0 + + with patch("volvooncall.Connection.get"), patch( + "homeassistant.components.volvooncall.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "region": "na", + "mutable": True, + "scandinavian_miles": False, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "test-username" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + "region": "na", + "mutable": True, + "scandinavian_miles": False, + } + 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} + ) + + exc = ClientResponseError(Mock(), (), status=401) + + with patch( + "volvooncall.Connection.get", + side_effect=exc, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "region": "na", + "mutable": True, + "scandinavian_miles": False, + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_flow_already_configured(hass: HomeAssistant) -> None: + """Test we handle a flow that has already been configured.""" + first_entry = MockConfigEntry(domain=DOMAIN, unique_id="test-username") + first_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert len(result["errors"]) == 0 + + with patch("volvooncall.Connection.get"), patch( + "homeassistant.components.volvooncall.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "region": "na", + "mutable": True, + "scandinavian_miles": False, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_other_exception(hass: HomeAssistant) -> None: + """Test we handle other exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "volvooncall.Connection.get", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "region": "na", + "mutable": True, + "scandinavian_miles": False, + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_import(hass: HomeAssistant) -> None: + """Test a YAML import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + assert result["type"] == FlowResultType.FORM + assert len(result["errors"]) == 0 + + with patch("volvooncall.Connection.get"), patch( + "homeassistant.components.volvooncall.async_setup", + return_value=True, + ), patch( + "homeassistant.components.volvooncall.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "region": "na", + "mutable": True, + "scandinavian_miles": False, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "test-username" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + "region": "na", + "mutable": True, + "scandinavian_miles": False, + } + assert len(mock_setup_entry.mock_calls) == 1 From 5ce64539c5f248f30adac79ad8df98ce3fc81946 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 23 Aug 2022 17:57:38 -0400 Subject: [PATCH 579/903] Bump AIOAladdinConnect to 0.1.42 (#77205) --- homeassistant/components/aladdin_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/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index febba16170a..e2a15757cdc 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.41"], + "requirements": ["AIOAladdinConnect==0.1.42"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 95afa8a04f1..b1ae90221f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.41 +AIOAladdinConnect==0.1.42 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99caab39cf7..5d83beb5c5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.41 +AIOAladdinConnect==0.1.42 # homeassistant.components.adax Adax-local==0.1.4 From f1075644f8198abfdce925848386bd1c891594dc Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 24 Aug 2022 00:00:54 +0200 Subject: [PATCH 580/903] Bump pymysensors to 0.24.0 (#77201) --- homeassistant/components/mysensors/gateway.py | 3 --- homeassistant/components/mysensors/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 7f13035f55c..c93e0380757 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -196,7 +196,6 @@ async def _get_gateway( in_prefix=topic_in_prefix, out_prefix=topic_out_prefix, retain=retain, - loop=hass.loop, event_callback=None, persistence=persistence, persistence_file=persistence_file, @@ -206,7 +205,6 @@ async def _get_gateway( gateway = mysensors.AsyncSerialGateway( device, baud=baud_rate, - loop=hass.loop, event_callback=None, persistence=persistence, persistence_file=persistence_file, @@ -216,7 +214,6 @@ async def _get_gateway( gateway = mysensors.AsyncTCPGateway( device, port=tcp_port, - loop=hass.loop, event_callback=None, persistence=persistence, persistence_file=persistence_file, diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index dafdd7c86bc..a340e1ef4da 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -2,7 +2,7 @@ "domain": "mysensors", "name": "MySensors", "documentation": "https://www.home-assistant.io/integrations/mysensors", - "requirements": ["pymysensors==0.22.1"], + "requirements": ["pymysensors==0.24.0"], "after_dependencies": ["mqtt"], "codeowners": ["@MartinHjelmare", "@functionpointer"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index b1ae90221f5..ad81c61920d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1692,7 +1692,7 @@ pymsteams==0.1.12 pymyq==3.1.4 # homeassistant.components.mysensors -pymysensors==0.22.1 +pymysensors==0.24.0 # homeassistant.components.netgear pynetgear==0.10.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d83beb5c5f..d34de5e090a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1181,7 +1181,7 @@ pymonoprice==0.3 pymyq==3.1.4 # homeassistant.components.mysensors -pymysensors==0.22.1 +pymysensors==0.24.0 # homeassistant.components.netgear pynetgear==0.10.7 From dc17bca00cc164bd3a038bffc56ea976aeab8b45 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Aug 2022 00:29:30 +0200 Subject: [PATCH 581/903] Add config entry selector (#77108) --- homeassistant/helpers/selector.py | 28 ++++++++++++++++++++++++++++ tests/helpers/test_selector.py | 20 ++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 93abd6ca4e4..c2d221bcc78 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -318,6 +318,34 @@ class ColorTempSelector(Selector): return value +class ConfigEntrySelectorConfig(TypedDict, total=False): + """Class to represent a config entry selector config.""" + + integration: str + + +@SELECTORS.register("config_entry") +class ConfigEntrySelector(Selector): + """Selector of a config entry.""" + + selector_type = "config_entry" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("integration"): str, + } + ) + + def __init__(self, config: ConfigEntrySelectorConfig | None = None) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> dict[str, str]: + """Validate the passed selection.""" + config: dict[str, str] = vol.Schema(str)(data) + return config + + class DateSelectorConfig(TypedDict): """Class to represent a date selector config.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index c809eaea8bd..5472e6609f0 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -285,6 +285,26 @@ def test_boolean_selector_schema(schema, valid_selections, invalid_selections): ) +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + ("6b68b250388cbe0d620c92dd3acc93ec", "76f2e8f9a6491a1b580b3a8967c27ddd"), + (None, True, 1), + ), + ( + {"integration": "adguard"}, + ("6b68b250388cbe0d620c92dd3acc93ec", "76f2e8f9a6491a1b580b3a8967c27ddd"), + (None, True, 1), + ), + ), +) +def test_config_entry_selector_schema(schema, valid_selections, invalid_selections): + """Test boolean selector.""" + _test_selector("config_entry", schema, valid_selections, invalid_selections) + + @pytest.mark.parametrize( "schema,valid_selections,invalid_selections", (({}, ("00:00:00",), ("blah", None)),), From a4dcb3a9c18f49f8f335b362722df6aad87a3f1e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 24 Aug 2022 00:27:09 +0000 Subject: [PATCH 582/903] [ci skip] Translation update --- .../android_ip_webcam/translations/hu.json | 3 +- .../components/awair/translations/hu.json | 8 ++- .../components/bluetooth/translations/hu.json | 11 +++- .../derivative/translations/he.json | 20 ++++++++ .../fully_kiosk/translations/hu.json | 20 ++++++++ .../components/hue/translations/hu.json | 13 +++-- .../lacrosse_view/translations/hu.json | 3 +- .../components/lametric/translations/hu.json | 50 +++++++++++++++++++ .../landisgyr_heat_meter/translations/hu.json | 23 +++++++++ .../p1_monitor/translations/hu.json | 3 ++ .../pure_energie/translations/hu.json | 3 ++ .../components/pushover/translations/hu.json | 34 +++++++++++++ .../components/qingping/translations/hu.json | 2 +- .../components/skybell/translations/hu.json | 6 +++ .../volvooncall/translations/de.json | 28 +++++++++++ .../volvooncall/translations/en.json | 49 +++++++++--------- .../volvooncall/translations/fr.json | 22 ++++++++ .../xiaomi_miio/translations/select.hu.json | 10 ++++ .../components/zha/translations/hu.json | 3 ++ 19 files changed, 276 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/derivative/translations/he.json create mode 100644 homeassistant/components/fully_kiosk/translations/hu.json create mode 100644 homeassistant/components/lametric/translations/hu.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/hu.json create mode 100644 homeassistant/components/pushover/translations/hu.json create mode 100644 homeassistant/components/volvooncall/translations/de.json create mode 100644 homeassistant/components/volvooncall/translations/fr.json diff --git a/homeassistant/components/android_ip_webcam/translations/hu.json b/homeassistant/components/android_ip_webcam/translations/hu.json index e728b9eee54..a87aa7a0800 100644 --- a/homeassistant/components/android_ip_webcam/translations/hu.json +++ b/homeassistant/components/android_ip_webcam/translations/hu.json @@ -4,7 +4,8 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "user": { diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index eae3a375bdf..1c940eed6c2 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -29,7 +29,13 @@ "data": { "host": "IP c\u00edm" }, - "description": "Az Awair lok\u00e1lis API-t az al\u00e1bbi l\u00e9p\u00e9sekkel kell enged\u00e9lyezni: {url}" + "description": "K\u00f6vesse [az utas\u00edt\u00e1sokat]({url}) az Awair lok\u00e1lis API enged\u00e9lyez\u00e9s\u00e9hez. \n\n Ha k\u00e9sz, kattintson a MEHET gombra." + }, + "local_pick": { + "data": { + "device": "Eszk\u00f6z", + "host": "IP c\u00edm" + } }, "reauth": { "data": { diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json index 8b51191e938..591362de0e3 100644 --- a/homeassistant/components/bluetooth/translations/hu.json +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "no_adapters": "Nem tal\u00e1lhat\u00f3 Bluetooth adapter" + "no_adapters": "Nem tal\u00e1ltak konfigur\u00e1latlan Bluetooth-adaptert" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a Bluetooth-ot?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "V\u00e1lasszon ki egy Bluetooth-adaptert a be\u00e1ll\u00edt\u00e1shoz" + }, + "single_adapter": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Bluetooth-adaptert: {name}?" + }, "user": { "data": { "address": "Eszk\u00f6z" diff --git a/homeassistant/components/derivative/translations/he.json b/homeassistant/components/derivative/translations/he.json new file mode 100644 index 00000000000..8f81df2df20 --- /dev/null +++ b/homeassistant/components/derivative/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/hu.json b/homeassistant/components/fully_kiosk/translations/hu.json new file mode 100644 index 00000000000..cd08f258be2 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index dd5c13c5f59..af8f616190a 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -44,6 +44,8 @@ "button_2": "M\u00e1sodik gomb", "button_3": "Harmadik gomb", "button_4": "Negyedik gomb", + "clock_wise": "Forgat\u00e1s az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val megegyez\u0151 ir\u00e1nyba", + "counter_clock_wise": "Forgat\u00e1s az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val ellent\u00e9tes ir\u00e1nyba", "dim_down": "S\u00f6t\u00e9t\u00edt", "dim_up": "Vil\u00e1gos\u00edt", "double_buttons_1_3": "Els\u0151 \u00e9s harmadik gomb", @@ -55,13 +57,14 @@ "double_short_release": "Mindk\u00e9t \"{subtype}\" felengedve", "initial_press": "\"{subtype}\" lenyomva el\u0151sz\u00f6r", "long_release": "\"{subtype}\" felengedve hossz\u00fa nyomva tart\u00e1s ut\u00e1n", - "remote_button_long_release": "A \"{subtype}\" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", - "remote_button_short_press": "\"{subtype}\" gomb lenyomva", - "remote_button_short_release": "\"{subtype}\" gomb elengedve", + "remote_button_long_release": "\"{subtype}\" hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", + "remote_button_short_press": "\"{subtype}\" lenyomva", + "remote_button_short_release": "\"{subtype}\" elengedve", "remote_double_button_long_press": "Mindk\u00e9t \"{subtype}\" hossz\u00fa megnyom\u00e1st k\u00f6vet\u0151en megjelent", "remote_double_button_short_press": "Mindk\u00e9t \"{subtype}\" megjelent", - "repeat": "\"{subtype}\"gomb lenyomva tartava", - "short_release": "\"{subtype}\" felengedve r\u00f6vid nyomva tart\u00e1s ut\u00e1n" + "repeat": "\"{subtype}\" lenyomva tartava", + "short_release": "\"{subtype}\" felengedve r\u00f6vid nyomva tart\u00e1s ut\u00e1n", + "start": "\"{subtype}\" lenyomva el\u0151sz\u00f6r" } }, "options": { diff --git a/homeassistant/components/lacrosse_view/translations/hu.json b/homeassistant/components/lacrosse_view/translations/hu.json index a040bd96b91..c1330c5cb83 100644 --- a/homeassistant/components/lacrosse_view/translations/hu.json +++ b/homeassistant/components/lacrosse_view/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", diff --git a/homeassistant/components/lametric/translations/hu.json b/homeassistant/components/lametric/translations/hu.json new file mode 100644 index 00000000000..c27848279e7 --- /dev/null +++ b/homeassistant/components/lametric/translations/hu.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "invalid_discovery_info": "\u00c9rv\u00e9nytelen felfedez\u00e9si inform\u00e1ci\u00f3 \u00e9rkezett", + "link_local_address": "A linklocal c\u00edmek nem t\u00e1mogatottak", + "missing_configuration": "A LaMetric integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "no_devices": "A jogosult felhaszn\u00e1l\u00f3 nem rendelkezik LaMetric-eszk\u00f6z\u00f6kkel", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "A LaMetric eszk\u00f6zt k\u00e9tf\u00e9lek\u00e9ppen lehet be\u00e1ll\u00edtani a Home Assistantben. \n\n Az \u00f6sszes eszk\u00f6zinform\u00e1ci\u00f3t \u00e9s API tokent saj\u00e1t maga is megadhatja, vagy a Home Assistant import\u00e1lhatja azokat a LaMetric.com-fi\u00f3kj\u00e1b\u00f3l.", + "menu_options": { + "manual_entry": "\u00cdrja be manu\u00e1lisan", + "pick_implementation": "Import\u00e1l\u00e1s a LaMetric.com webhelyr\u0151l (aj\u00e1nlott)" + } + }, + "manual_entry": { + "data": { + "api_key": "API kulcs", + "host": "C\u00edm" + }, + "data_description": { + "api_key": "Ezt az API-kulcsot a [LaMetric fejleszt\u0151i fi\u00f3kj\u00e1nak eszk\u00f6z\u00f6k oldal\u00e1n] (https://developer.lametric.com/user/devices) tal\u00e1lja.", + "host": "A LaMetric TIME IP-c\u00edme vagy hostneve a h\u00e1l\u00f3zaton." + } + }, + "pick_implementation": { + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "user_cloud_select_device": { + "data": { + "device": "V\u00e1lassza ki a hozz\u00e1adand\u00f3 LaMetric eszk\u00f6zt" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "A LaMetric integr\u00e1ci\u00f3 moderniz\u00e1l\u00e1sra ker\u00fclt: A konfigur\u00e1l\u00e1s \u00e9s be\u00e1ll\u00edt\u00e1s mostant\u00f3l a felhaszn\u00e1l\u00f3i fel\u00fcleten kereszt\u00fcl t\u00f6rt\u00e9nik, \u00e9s a kommunik\u00e1ci\u00f3 mostant\u00f3l helyi.\n\nSajnos nincs lehet\u0151s\u00e9g automatikus migr\u00e1ci\u00f3s \u00fatvonalra, \u00edgy a LaMetric-et \u00fajra be kell \u00e1ll\u00edtania a Home Assistant seg\u00edts\u00e9g\u00e9vel. K\u00e9rj\u00fck, tekintse meg a Home Assistant LaMetric integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t a be\u00e1ll\u00edt\u00e1ssal kapcsolatban.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a r\u00e9gi LaMetric YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot.", + "title": "Manu\u00e1lis migr\u00e1ci\u00f3 sz\u00fcks\u00e9ges a LaMetric eset\u00e9ben" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/hu.json b/homeassistant/components/landisgyr_heat_meter/translations/hu.json new file mode 100644 index 00000000000..030fe6853f2 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + } + }, + "user": { + "data": { + "device": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/hu.json b/homeassistant/components/p1_monitor/translations/hu.json index af1f1e62fdb..0b6071be83f 100644 --- a/homeassistant/components/p1_monitor/translations/hu.json +++ b/homeassistant/components/p1_monitor/translations/hu.json @@ -9,6 +9,9 @@ "host": "C\u00edm", "name": "Elnevez\u00e9s" }, + "data_description": { + "host": "A P1 Monitor rendszer\u00e9nek IP-c\u00edme vagy hostneve." + }, "description": "\u00c1ll\u00edtsa be a P1 monitort az Otthoni asszisztenssel val\u00f3 integr\u00e1ci\u00f3hoz." } } diff --git a/homeassistant/components/pure_energie/translations/hu.json b/homeassistant/components/pure_energie/translations/hu.json index d4bd60def2c..be5e943e252 100644 --- a/homeassistant/components/pure_energie/translations/hu.json +++ b/homeassistant/components/pure_energie/translations/hu.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "C\u00edm" + }, + "data_description": { + "host": "A Pure Energie Meter IP-c\u00edme vagy hostneve." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/hu.json b/homeassistant/components/pushover/translations/hu.json new file mode 100644 index 00000000000..cdb09291937 --- /dev/null +++ b/homeassistant/components/pushover/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "invalid_user_key": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3i kulcs" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "api_key": "API kulcs", + "name": "Elnevez\u00e9s", + "user_key": "Felhaszn\u00e1l\u00f3i kulcs" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A Pushover konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Pushover YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Pushover YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/hu.json b/homeassistant/components/qingping/translations/hu.json index 8a1bc9a1c42..140271f7840 100644 --- a/homeassistant/components/qingping/translations/hu.json +++ b/homeassistant/components/qingping/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r be van \u00e1ll\u00edtva", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", - "no_devices_found": "Nincs felder\u00edtett eszk\u00f6z a h\u00e1l\u00f3zaton", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" }, "flow_title": "{name}", diff --git a/homeassistant/components/skybell/translations/hu.json b/homeassistant/components/skybell/translations/hu.json index 98b9ee3f016..08a151e711b 100644 --- a/homeassistant/components/skybell/translations/hu.json +++ b/homeassistant/components/skybell/translations/hu.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "A Skybell YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Skybell YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + } } } \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/de.json b/homeassistant/components/volvooncall/translations/de.json new file mode 100644 index 00000000000..9f7687ebc80 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Konto ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "mutable": "Fernstart / -verriegelung / etc. zulassen", + "password": "Passwort", + "region": "Region", + "scandinavian_miles": "Skandinavische Meilen verwenden", + "username": "Benutzername" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration der Volvo On Call Plattform mittels YAML wird in einer zuk\u00fcnftigen Version von Home Assistant entfernt.\n\nDeine bestehende Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Volvo On Call YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/en.json b/homeassistant/components/volvooncall/translations/en.json index b3468cb78e2..aeabdbc8d45 100644 --- a/homeassistant/components/volvooncall/translations/en.json +++ b/homeassistant/components/volvooncall/translations/en.json @@ -1,29 +1,28 @@ { - "config": { - "step": { - "user": { - "title": "Login to Volvo On Call", - "data": { - "username": "Username", - "password": "Password", - "region": "Region", - "mutable": "Allow Remote Start / Lock / etc.", - "scandinavian_miles": "Use Scandinavian Miles" + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "mutable": "Allow Remote Start / Lock / etc.", + "password": "Password", + "region": "Region", + "scandinavian_miles": "Use Scandinavian Miles", + "username": "Username" + } + } } - } }, - "error": { - "invalid_auth": "Authentication failed. Please check your username, password, and region.", - "unknown": "Unknown error." - }, - "abort": { - "already_configured": "Account is already configured" + "issues": { + "deprecated_yaml": { + "description": "Configuring the Volvo On Call platform using YAML is being removed in a future release of Home Assistant.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Volvo On Call YAML configuration is being removed" + } } - }, - "issues": { - "deprecated_yaml": { - "title": "The Volvo On Call YAML configuration is being removed", - "description": "Configuring the Volvo On Call platform using YAML is being removed in a future release of Home Assistant.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/fr.json b/homeassistant/components/volvooncall/translations/fr.json new file mode 100644 index 00000000000..ac007625497 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "mutable": "Autoriser le d\u00e9marrage, le verrouillage, etc. \u00e0 distance", + "password": "Mot de passe", + "region": "R\u00e9gion", + "scandinavian_miles": "Utiliser les miles scandinaves", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.hu.json b/homeassistant/components/xiaomi_miio/translations/select.hu.json index 4e6df2b4a33..7c74e7a4730 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.hu.json +++ b/homeassistant/components/xiaomi_miio/translations/select.hu.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "El\u0151re", + "left": "Bal", + "right": "Jobb" + }, "xiaomi_miio__led_brightness": { "bright": "F\u00e9nyes", "dim": "Hom\u00e1lyos", "off": "Ki" + }, + "xiaomi_miio__ptc_level": { + "high": "Magas", + "low": "Alacsony", + "medium": "K\u00f6zepes" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 2b4fe26e0d6..65a93dcf79e 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -13,6 +13,9 @@ "confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, + "confirm_hardware": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, "pick_radio": { "data": { "radio_type": "R\u00e1di\u00f3 t\u00edpusa" From 8167cd615a32c4e9a37b38616d9aaf71e86e64d3 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 23 Aug 2022 20:53:43 -0400 Subject: [PATCH 583/903] Bump ZHA dependencies (#77125) --- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 1eb2536fed6..0648cbd86f7 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,15 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.32.0", + "bellows==0.33.1", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.49.1", + "zigpy==0.50.2", "zigpy-xbee==0.15.0", - "zigpy-zigate==0.9.1", - "zigpy-znp==0.8.1" + "zigpy-zigate==0.9.2", + "zigpy-znp==0.8.2" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index ad81c61920d..ba9426ef75c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.32.0 +bellows==0.33.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.2 @@ -2554,13 +2554,13 @@ zigpy-deconz==0.18.0 zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.9.1 +zigpy-zigate==0.9.2 # homeassistant.components.zha -zigpy-znp==0.8.1 +zigpy-znp==0.8.2 # homeassistant.components.zha -zigpy==0.49.1 +zigpy==0.50.2 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d34de5e090a..c87ec95790b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.32.0 +bellows==0.33.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.2 @@ -1743,13 +1743,13 @@ zigpy-deconz==0.18.0 zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.9.1 +zigpy-zigate==0.9.2 # homeassistant.components.zha -zigpy-znp==0.8.1 +zigpy-znp==0.8.2 # homeassistant.components.zha -zigpy==0.49.1 +zigpy==0.50.2 # homeassistant.components.zwave_js zwave-js-server-python==0.40.0 From de7fdeddf94b7d313500bb4c0c472849cdfb0799 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Aug 2022 21:18:47 -0700 Subject: [PATCH 584/903] Update qingping matcher to support additional models (#77225) * Update qingping matcher to support additional models * tweak * bump * Update BinarySensorEntityDescription Co-authored-by: Marcel van der Veldt --- homeassistant/components/qingping/binary_sensor.py | 4 ++++ homeassistant/components/qingping/manifest.json | 11 +++++++++-- homeassistant/generated/bluetooth.py | 10 ++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py index 273a9a93351..cdc75af0b09 100644 --- a/homeassistant/components/qingping/binary_sensor.py +++ b/homeassistant/components/qingping/binary_sensor.py @@ -35,6 +35,10 @@ BINARY_SENSOR_DESCRIPTIONS = { key=QingpingBinarySensorDeviceClass.LIGHT, device_class=BinarySensorDeviceClass.LIGHT, ), + QingpingBinarySensorDeviceClass.DOOR: BinarySensorEntityDescription( + key=QingpingBinarySensorDeviceClass.DOOR, + device_class=BinarySensorDeviceClass.DOOR, + ), } diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 212011b834e..1eef6e2c471 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -3,8 +3,15 @@ "name": "Qingping", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qingping", - "bluetooth": [{ "local_name": "Qingping*", "connectable": false }], - "requirements": ["qingping-ble==0.3.0"], + "bluetooth": [ + { "local_name": "Qingping*", "connectable": false }, + { "local_name": "Lee Guitars*", "connectable": false }, + { + "service_data_uuid": "0000fdcd-0000-1000-8000-00805f9b34fb", + "connectable": false + } + ], + "requirements": ["qingping-ble==0.5.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 55e10c32444..f33f26c366a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -118,6 +118,16 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "local_name": "Qingping*", "connectable": False }, + { + "domain": "qingping", + "local_name": "Lee Guitars*", + "connectable": False + }, + { + "domain": "qingping", + "service_data_uuid": "0000fdcd-0000-1000-8000-00805f9b34fb", + "connectable": False + }, { "domain": "sensorpush", "local_name": "SensorPush*", diff --git a/requirements_all.txt b/requirements_all.txt index ba9426ef75c..f7ac5476861 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2070,7 +2070,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.3.0 +qingping-ble==0.5.0 # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c87ec95790b..b360e5f2407 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1418,7 +1418,7 @@ pyws66i==1.1 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.3.0 +qingping-ble==0.5.0 # homeassistant.components.rachio rachiopy==1.0.3 From 671f1293174964fb4a25365e4c1c261decad2ee6 Mon Sep 17 00:00:00 2001 From: donoghdb <85128952+donoghdb@users.noreply.github.com> Date: Wed, 24 Aug 2022 07:18:10 +0100 Subject: [PATCH 585/903] Fix met_eireann default wind speed unit (#77229) Update default units --- homeassistant/components/met_eireann/weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index f20f0e1254a..b872e9a8df6 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_NAME, LENGTH_MILLIMETERS, PRESSURE_HPA, - SPEED_METERS_PER_SECOND, + SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -58,7 +58,7 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): _attr_native_precipitation_unit = LENGTH_MILLIMETERS _attr_native_pressure_unit = PRESSURE_HPA _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__(self, coordinator, config, hourly): """Initialise the platform with a data instance and site.""" From a40ddb5e838d62095ea1dc1c60fec9a41cf46491 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 24 Aug 2022 09:11:01 +0200 Subject: [PATCH 586/903] Use _attr_should_poll in xiaomi_aqara entities (#77197) * Use _attr_should_poll in xiaomi_aqara entities * Adjust switch --- homeassistant/components/xiaomi_aqara/__init__.py | 7 ++----- .../components/xiaomi_aqara/binary_sensor.py | 14 ++++---------- homeassistant/components/xiaomi_aqara/switch.py | 7 ++----- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index da3303494e5..e99851dae9f 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -225,6 +225,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class XiaomiDevice(Entity): """Representation a base Xiaomi device.""" + _attr_should_poll = False + def __init__(self, device, device_type, xiaomi_hub, config_entry): """Initialize the Xiaomi device.""" self._state = None @@ -309,11 +311,6 @@ class XiaomiDevice(Entity): """Return True if entity is available.""" return self._is_available - @property - def should_poll(self): - """Return the polling state. No polling needed.""" - return False - @property def extra_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 04e3945e7a2..44c17b634cb 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -140,15 +140,9 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity): """Initialize the XiaomiSmokeSensor.""" self._data_key = data_key self._device_class = device_class - self._should_poll = False self._density = 0 super().__init__(device, name, xiaomi_hub, config_entry) - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return self._should_poll - @property def is_on(self): """Return true if sensor is on.""" @@ -340,7 +334,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity): def parse_data(self, data, raw_data): """Parse data sent by gateway.""" - self._should_poll = False + self._attr_should_poll = False if NO_CLOSE in data: # handle push from the hub self._open_since = data[NO_CLOSE] return True @@ -350,7 +344,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity): return False if value == "open": - self._should_poll = True + self._attr_should_poll = True if self._state: return False self._state = True @@ -388,14 +382,14 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): def parse_data(self, data, raw_data): """Parse data sent by gateway.""" - self._should_poll = False + self._attr_should_poll = False value = data.get(self._data_key) if value is None: return False if value == "leak": - self._should_poll = True + self._attr_should_poll = True if self._state: return False self._state = True diff --git a/homeassistant/components/xiaomi_aqara/switch.py b/homeassistant/components/xiaomi_aqara/switch.py index 86acd4100a2..39bf637256f 100644 --- a/homeassistant/components/xiaomi_aqara/switch.py +++ b/homeassistant/components/xiaomi_aqara/switch.py @@ -149,6 +149,8 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): self._load_power = None self._power_consumed = None self._supports_power_consumption = supports_power_consumption + # Polling needed for Zigbee plug only. + self._attr_should_poll = supports_power_consumption super().__init__(device, name, xiaomi_hub, config_entry) @property @@ -177,11 +179,6 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): attrs.update(super().extra_state_attributes) return attrs - @property - def should_poll(self): - """Return the polling state. Polling needed for Zigbee plug only.""" - return self._supports_power_consumption - def turn_on(self, **kwargs): """Turn the switch on.""" if self._write_to_hub(self._sid, **{self._data_key: "on"}): From a926e7062c2f01f3d06858bd2f87bb62fb767cfb Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 24 Aug 2022 03:53:17 -0400 Subject: [PATCH 587/903] Fix Aladdin connect multiple doors on one device (#77226) Fixed Multiple doors device_info --- homeassistant/components/aladdin_connect/cover.py | 4 +++- homeassistant/components/aladdin_connect/sensor.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index f032fcecbe0..ee0955cbb3d 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -90,6 +90,7 @@ class AladdinDevice(CoverEntity): self._number = device["door_number"] self._name = device["name"] self._serial = device["serial"] + self._model = device["model"] self._attr_unique_id = f"{self._device_id}-{self._number}" self._attr_has_entity_name = True @@ -97,9 +98,10 @@ class AladdinDevice(CoverEntity): def device_info(self) -> DeviceInfo | None: """Device information for Aladdin Connect cover.""" return DeviceInfo( - identifiers={(DOMAIN, self._device_id)}, + identifiers={(DOMAIN, f"{self._device_id}-{self._number}")}, name=self._name, manufacturer="Overhead Door", + model=self._model, ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 3d319a724c1..5fcc75fa27c 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -114,7 +114,7 @@ class AladdinConnectSensor(SensorEntity): def device_info(self) -> DeviceInfo | None: """Device information for Aladdin Connect sensors.""" return DeviceInfo( - identifiers={(DOMAIN, self._device_id)}, + identifiers={(DOMAIN, f"{self._device_id}-{self._number}")}, name=self._name, manufacturer="Overhead Door", model=self._model, From 619e99d24c1c7e1766f2fa0a01ab0d30813ed730 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 24 Aug 2022 09:57:57 +0200 Subject: [PATCH 588/903] Update xknx to 1.0.1 (#77244) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 0f9b6b4b95a..bb5599939db 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==1.0.0"], + "requirements": ["xknx==1.0.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index f7ac5476861..82c49c0a121 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2495,7 +2495,7 @@ xboxapi==2.0.1 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==1.0.0 +xknx==1.0.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b360e5f2407..0c9ad0f0d46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1702,7 +1702,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==1.0.0 +xknx==1.0.1 # homeassistant.components.bluesound # homeassistant.components.fritz From c26d6879ae351c065e466cabd92788892edbdb30 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Aug 2022 10:10:36 +0200 Subject: [PATCH 589/903] Add button platform to LaMetric (#76768) * Add button platform to LaMetric * coveragerc * Fix docstring Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + homeassistant/components/lametric/button.py | 86 +++++++++++++++++++++ homeassistant/components/lametric/const.py | 2 +- 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lametric/button.py diff --git a/.coveragerc b/.coveragerc index 942f32eb9a5..44b654d05ef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -641,6 +641,7 @@ omit = homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lametric/__init__.py + homeassistant/components/lametric/button.py homeassistant/components/lametric/coordinator.py homeassistant/components/lametric/entity.py homeassistant/components/lametric/notify.py diff --git a/homeassistant/components/lametric/button.py b/homeassistant/components/lametric/button.py new file mode 100644 index 00000000000..4d8c75f0ab0 --- /dev/null +++ b/homeassistant/components/lametric/button.py @@ -0,0 +1,86 @@ +"""Support for LaMetric buttons.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from demetriek import LaMetricDevice + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator +from .entity import LaMetricEntity + + +@dataclass +class LaMetricButtonEntityDescriptionMixin: + """Mixin values for LaMetric entities.""" + + press_fn: Callable[[LaMetricDevice], Awaitable[Any]] + + +@dataclass +class LaMetricButtonEntityDescription( + ButtonEntityDescription, LaMetricButtonEntityDescriptionMixin +): + """Class describing LaMetric button entities.""" + + +BUTTONS = [ + LaMetricButtonEntityDescription( + key="app_next", + name="Next app", + icon="mdi:arrow-right-bold", + entity_category=EntityCategory.CONFIG, + press_fn=lambda api: api.app_next(), + ), + LaMetricButtonEntityDescription( + key="app_previous", + name="Previous app", + icon="mdi:arrow-left-bold", + entity_category=EntityCategory.CONFIG, + press_fn=lambda api: api.app_previous(), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LaMetric button based on a config entry.""" + coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LaMetricButtonEntity( + coordinator=coordinator, + description=description, + ) + for description in BUTTONS + ) + + +class LaMetricButtonEntity(LaMetricEntity, ButtonEntity): + """Representation of a LaMetric number.""" + + entity_description: LaMetricButtonEntityDescription + + def __init__( + self, + coordinator: LaMetricDataUpdateCoordinator, + description: LaMetricButtonEntityDescription, + ) -> None: + """Initialize the button entity.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}" + + async def async_press(self) -> None: + """Send out a command to LaMetric.""" + await self.entity_description.press_fn(self.coordinator.lametric) diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py index 0fe4b3f21d8..da84450e784 100644 --- a/homeassistant/components/lametric/const.py +++ b/homeassistant/components/lametric/const.py @@ -7,7 +7,7 @@ from typing import Final from homeassistant.const import Platform DOMAIN: Final = "lametric" -PLATFORMS = [Platform.NUMBER] +PLATFORMS = [Platform.BUTTON, Platform.NUMBER] LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=30) From 853fab0a680075b1339d870c3e00239c4c292c9f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Aug 2022 10:48:23 +0200 Subject: [PATCH 590/903] Mock MQTT setup in hassio tests (#77245) * Mock MQTT setup in hassio tests * Tweak --- tests/components/hassio/test_discovery.py | 127 ++++++++++++---------- 1 file changed, 71 insertions(+), 56 deletions(-) diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 30013c34f21..655cc4b23b5 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,14 +1,38 @@ """Test config flow.""" from http import HTTPStatus -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch +import pytest + +from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED from homeassistant.setup import async_setup_component +from tests.common import MockModule, mock_entity_platform, mock_integration -async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): + +@pytest.fixture +async def mock_mqtt(hass): + """Mock the MQTT integration's config flow.""" + mock_integration(hass, MockModule(MQTT_DOMAIN)) + mock_entity_platform(hass, f"config_flow.{MQTT_DOMAIN}", None) + + with patch.dict(config_entries.HANDLERS): + + class MqttFlow(config_entries.ConfigFlow, domain=MQTT_DOMAIN): + """Test flow.""" + + VERSION = 1 + + async_step_hassio = AsyncMock(return_value={"type": "abort"}) + + yield MqttFlow + + +async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client, mock_mqtt): """Test startup and discovery after event.""" aioclient_mock.get( "http://127.0.0.1/discovery", @@ -39,31 +63,29 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): assert aioclient_mock.call_count == 0 - with patch( - "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", - return_value={"type": "abort"}, - ) as mock_mqtt: - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 - assert mock_mqtt.called - mock_mqtt.assert_called_with( - HassioServiceInfo( - config={ - "broker": "mock-broker", - "port": 1883, - "username": "mock-user", - "password": "mock-pass", - "protocol": "3.1.1", - "addon": "Mosquitto Test", - } - ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert aioclient_mock.call_count == 2 + assert mock_mqtt.async_step_hassio.called + mock_mqtt.async_step_hassio.assert_called_with( + HassioServiceInfo( + config={ + "broker": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", + "addon": "Mosquitto Test", + } ) + ) -async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client): +async def test_hassio_discovery_startup_done( + hass, aioclient_mock, hassio_client, mock_mqtt +): """Test startup and discovery with hass discovery.""" aioclient_mock.post( "http://127.0.0.1/supervisor/options", @@ -102,17 +124,14 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client ), patch( "homeassistant.components.hassio.HassIO.get_info", Mock(side_effect=HassioAPIError()), - ), patch( - "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", - return_value={"type": "abort"}, - ) as mock_mqtt: + ): await hass.async_start() await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 2 - assert mock_mqtt.called - mock_mqtt.assert_called_with( + assert mock_mqtt.async_step_hassio.called + mock_mqtt.async_step_hassio.assert_called_with( HassioServiceInfo( config={ "broker": "mock-broker", @@ -126,7 +145,7 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client ) -async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): +async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client, mock_mqtt): """Test discovery webhook.""" aioclient_mock.get( "http://127.0.0.1/discovery/testuuid", @@ -151,30 +170,26 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): json={"result": "ok", "data": {"name": "Mosquitto Test"}}, ) - with patch( - "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", - return_value={"type": "abort"}, - ) as mock_mqtt: - resp = await hassio_client.post( - "/api/hassio_push/discovery/testuuid", - json={"addon": "mosquitto", "service": "mqtt", "uuid": "testuuid"}, - ) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() + resp = await hassio_client.post( + "/api/hassio_push/discovery/testuuid", + json={"addon": "mosquitto", "service": "mqtt", "uuid": "testuuid"}, + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() - assert resp.status == HTTPStatus.OK - assert aioclient_mock.call_count == 2 - assert mock_mqtt.called - mock_mqtt.assert_called_with( - HassioServiceInfo( - config={ - "broker": "mock-broker", - "port": 1883, - "username": "mock-user", - "password": "mock-pass", - "protocol": "3.1.1", - "addon": "Mosquitto Test", - } - ) + assert resp.status == HTTPStatus.OK + assert aioclient_mock.call_count == 2 + assert mock_mqtt.async_step_hassio.called + mock_mqtt.async_step_hassio.assert_called_with( + HassioServiceInfo( + config={ + "broker": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", + "addon": "Mosquitto Test", + } ) + ) From 2497ff5a39292b7167733206fe2e1e63a6a3b86e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Aug 2022 11:09:23 +0200 Subject: [PATCH 591/903] Add energy and gas sensors to demo integration (#77206) --- homeassistant/components/demo/__init__.py | 84 +++++++++++++++-- homeassistant/components/demo/sensor.py | 105 ++++++++++++++++++++-- tests/components/demo/test_init.py | 6 +- tests/components/sensor/test_recorder.py | 2 +- 4 files changed, 181 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 2e01e2c3c6b..6d0f6499001 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -6,6 +6,7 @@ from random import random from homeassistant import config_entries, setup from homeassistant.components import persistent_notification from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.models import StatisticMetaData from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, @@ -260,15 +261,16 @@ def _generate_sum_statistics(start, end, init_value, max_diff): return statistics -async def _insert_statistics(hass): +async def _insert_statistics(hass: HomeAssistant) -> None: """Insert some fake statistics.""" now = dt_util.now() yesterday = now - datetime.timedelta(days=1) yesterday_midnight = yesterday.replace(hour=0, minute=0, second=0, microsecond=0) # Fake yesterday's temperatures - metadata = { + metadata: StatisticMetaData = { "source": DOMAIN, + "name": "Outdoor temperature", "statistic_id": f"{DOMAIN}:temperature_outdoor", "unit_of_measurement": "°C", "has_mean": True, @@ -279,26 +281,94 @@ async def _insert_statistics(hass): ) async_add_external_statistics(hass, metadata, statistics) - # Fake yesterday's energy consumption + # Add external energy consumption in kWh, ~ 12 kWh / day + # This should be possible to pick for the energy dashboard + statistic_id = f"{DOMAIN}:energy_consumption_kwh" metadata = { "source": DOMAIN, - "statistic_id": f"{DOMAIN}:energy_consumption", + "name": "Energy consumption 1", + "statistic_id": statistic_id, "unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, } - statistic_id = f"{DOMAIN}:energy_consumption" sum_ = 0 last_stats = await get_instance(hass).async_add_executor_job( get_last_statistics, hass, 1, statistic_id, True ) - if "domain:energy_consumption" in last_stats: - sum_ = last_stats["domain.electricity_total"]["sum"] or 0 + if statistic_id in last_stats: + sum_ = last_stats[statistic_id][0]["sum"] or 0 + statistics = _generate_sum_statistics( + yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), sum_, 2 + ) + async_add_external_statistics(hass, metadata, statistics) + + # Add external energy consumption in MWh, ~ 12 kWh / day + # This should not be possible to pick for the energy dashboard + statistic_id = f"{DOMAIN}:energy_consumption_mwh" + metadata = { + "source": DOMAIN, + "name": "Energy consumption 2", + "statistic_id": statistic_id, + "unit_of_measurement": "MWh", + "has_mean": False, + "has_sum": True, + } + sum_ = 0 + last_stats = await get_instance(hass).async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, True + ) + if statistic_id in last_stats: + sum_ = last_stats[statistic_id][0]["sum"] or 0 + statistics = _generate_sum_statistics( + yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), sum_, 0.002 + ) + async_add_external_statistics(hass, metadata, statistics) + + # Add external gas consumption in m³, ~6 m3/day + # This should be possible to pick for the energy dashboard + statistic_id = f"{DOMAIN}:gas_consumption_m3" + metadata = { + "source": DOMAIN, + "name": "Gas consumption 1", + "statistic_id": statistic_id, + "unit_of_measurement": "m³", + "has_mean": False, + "has_sum": True, + } + sum_ = 0 + last_stats = await get_instance(hass).async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, True + ) + if statistic_id in last_stats: + sum_ = last_stats[statistic_id][0]["sum"] or 0 statistics = _generate_sum_statistics( yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), sum_, 1 ) async_add_external_statistics(hass, metadata, statistics) + # Add external gas consumption in ft³, ~180 ft3/day + # This should not be possible to pick for the energy dashboard + statistic_id = f"{DOMAIN}:gas_consumption_ft3" + metadata = { + "source": DOMAIN, + "name": "Gas consumption 2", + "statistic_id": statistic_id, + "unit_of_measurement": "ft³", + "has_mean": False, + "has_sum": True, + } + sum_ = 0 + last_stats = await get_instance(hass).async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, True + ) + if statistic_id in last_stats: + sum_ = last_stats[statistic_id][0]["sum"] or 0 + statistics = _generate_sum_statistics( + yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), sum_, 30 + ) + async_add_external_statistics(hass, metadata, statistics) + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set the config entry up.""" diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index aa93bdd8b71..1adc8616593 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -1,7 +1,12 @@ """Demo platform that has a couple of fake sensors.""" from __future__ import annotations +from datetime import datetime, timedelta +from typing import cast + from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + RestoreSensor, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -11,13 +16,17 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONCENTRATION_PARTS_PER_MILLION, ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, PERCENTAGE, POWER_WATT, TEMP_CELSIUS, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from . import DOMAIN @@ -77,14 +86,45 @@ async def async_setup_platform( POWER_WATT, None, ), - DemoSensor( + DemoSumSensor( "sensor_6", - "Today energy", - 15, + "Total energy 1", + 0.5, # 6kWh / h SensorDeviceClass.ENERGY, - SensorStateClass.MEASUREMENT, + SensorStateClass.TOTAL, ENERGY_KILO_WATT_HOUR, None, + "total_energy_kwh", + ), + DemoSumSensor( + "sensor_7", + "Total energy 2", + 0.00025, # 0.003 MWh/h (3 kWh / h) + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL, + ENERGY_MEGA_WATT_HOUR, + None, + "total_energy_mwh", + ), + DemoSumSensor( + "sensor_8", + "Total gas 1", + 0.025, # 0.30 m³/h (10.6 ft³ / h) + SensorDeviceClass.GAS, + SensorStateClass.TOTAL, + VOLUME_CUBIC_METERS, + None, + "total_gas_m3", + ), + DemoSumSensor( + "sensor_9", + "Total gas 2", + 1.0, # 12 ft³/h (0.34 m³ / h) + SensorDeviceClass.GAS, + SensorStateClass.TOTAL, + VOLUME_CUBIC_FEET, + None, + "total_gas_ft3", ), ] ) @@ -129,3 +169,58 @@ class DemoSensor(SensorEntity): if battery: self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery} + + +class DemoSumSensor(RestoreSensor): + """Representation of a Demo sensor.""" + + _attr_should_poll = False + _attr_native_value: float + + def __init__( + self, + unique_id: str, + name: str, + five_minute_increase: float, + device_class: SensorDeviceClass, + state_class: SensorStateClass | None, + unit_of_measurement: str | None, + battery: StateType, + suggested_entity_id: str, + ) -> None: + """Initialize the sensor.""" + self.entity_id = f"{SENSOR_DOMAIN}.{suggested_entity_id}" + self._attr_device_class = device_class + self._attr_name = name + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_native_value = 0 + self._attr_state_class = state_class + self._attr_unique_id = unique_id + self._five_minute_increase = five_minute_increase + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) + + if battery: + self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery} + + @callback + def _async_bump_sum(self, now: datetime) -> None: + """Bump the sum.""" + self._attr_native_value += self._five_minute_increase + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + state = await self.async_get_last_sensor_data() + if state: + self._attr_native_value = cast(float, state.native_value) + + self.async_on_remove( + async_track_time_interval( + self.hass, self._async_bump_sum, timedelta(minutes=5) + ), + ) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index ba8baa0d487..413badde18d 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -57,7 +57,7 @@ async def test_demo_statistics(hass, recorder_mock): assert { "has_mean": True, "has_sum": False, - "name": None, + "name": "Outdoor temperature", "source": "demo", "statistic_id": "demo:temperature_outdoor", "unit_of_measurement": "°C", @@ -65,9 +65,9 @@ async def test_demo_statistics(hass, recorder_mock): assert { "has_mean": False, "has_sum": True, - "name": None, + "name": "Energy consumption 1", "source": "demo", - "statistic_id": "demo:energy_consumption", + "statistic_id": "demo:energy_consumption_kwh", "unit_of_measurement": "kWh", } in statistic_ids diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index cc2f9c76f1f..64fd1884ae6 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -767,7 +767,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( "bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue", ), ( - "sensor.today_energy", + "sensor.power_consumption", "from integration demo ", "bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+demo%22", ), From 635eda584dc8f932af235b72bb36ad76e74662f5 Mon Sep 17 00:00:00 2001 From: On Freund Date: Wed, 24 Aug 2022 14:09:54 +0300 Subject: [PATCH 592/903] Support for local push in Risco integration (#75874) * Local config flow * Local entities * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Address code review comments * More type hints * Apply suggestions from code review Co-authored-by: Martin Hjelmare * More annotations * Even more annonations * New entity naming * Move fixtures to conftest * Improve state tests for local * Remove mutable default arguments * Remove assertions for lack of state * Add missing file * Switch setup to fixtures * Use error fixtures in test_config_flow * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/risco/__init__.py | 100 ++- .../components/risco/alarm_control_panel.py | 144 ++- .../components/risco/binary_sensor.py | 154 ++-- homeassistant/components/risco/config_flow.py | 77 +- homeassistant/components/risco/const.py | 4 + homeassistant/components/risco/entity.py | 6 +- homeassistant/components/risco/manifest.json | 2 +- homeassistant/components/risco/sensor.py | 38 +- homeassistant/components/risco/strings.json | 13 + .../components/risco/translations/en.json | 15 +- tests/components/risco/conftest.py | 148 ++++ .../risco/test_alarm_control_panel.py | 822 ++++++++++++++---- tests/components/risco/test_binary_sensor.py | 209 +++-- tests/components/risco/test_config_flow.py | 188 +++- tests/components/risco/test_sensor.py | 86 +- tests/components/risco/util.py | 70 +- 16 files changed, 1596 insertions(+), 480 deletions(-) create mode 100644 tests/components/risco/conftest.py diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index fea3ee63aac..e95b3016139 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -1,14 +1,27 @@ """The Risco integration.""" +from collections.abc import Callable +from dataclasses import dataclass, field from datetime import timedelta import logging +from typing import Any -from pyrisco import CannotConnectError, OperationError, RiscoCloud, UnauthorizedError +from pyrisco import ( + CannotConnectError, + OperationError, + RiscoCloud, + RiscoLocal, + UnauthorizedError, +) +from pyrisco.common import Partition, Zone from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PIN, + CONF_PORT, CONF_SCAN_INTERVAL, + CONF_TYPE, CONF_USERNAME, Platform, ) @@ -18,17 +31,94 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.storage import Store from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENTS_COORDINATOR +from .const import ( + DATA_COORDINATOR, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + EVENTS_COORDINATOR, + TYPE_LOCAL, +) PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.SENSOR] -UNDO_UPDATE_LISTENER = "undo_update_listener" LAST_EVENT_STORAGE_VERSION = 1 LAST_EVENT_TIMESTAMP_KEY = "last_event_timestamp" _LOGGER = logging.getLogger(__name__) +@dataclass +class LocalData: + """A data class for local data passed to the platforms.""" + + system: RiscoLocal + zone_updates: dict[int, Callable[[], Any]] = field(default_factory=dict) + partition_updates: dict[int, Callable[[], Any]] = field(default_factory=dict) + + +def is_local(entry: ConfigEntry) -> bool: + """Return whether the entry represents an instance with local communication.""" + return entry.data.get(CONF_TYPE) == TYPE_LOCAL + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Risco from a config entry.""" + if is_local(entry): + return await _async_setup_local_entry(hass, entry) + + return await _async_setup_cloud_entry(hass, entry) + + +async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + data = entry.data + risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN]) + + try: + await risco.connect() + except CannotConnectError as error: + raise ConfigEntryNotReady() from error + except UnauthorizedError: + _LOGGER.exception("Failed to login to Risco cloud") + return False + + async def _error(error: Exception) -> None: + _LOGGER.error("Error in Risco library: %s", error) + + entry.async_on_unload(risco.add_error_handler(_error)) + + async def _default(command: str, result: str, *params: list[str]) -> None: + _LOGGER.debug( + "Unhandled update from Risco library: %s, %s, %s", command, result, params + ) + + entry.async_on_unload(risco.add_default_handler(_default)) + + local_data = LocalData(risco) + + async def _zone(zone_id: int, zone: Zone) -> None: + _LOGGER.debug("Risco zone update for %d", zone_id) + callback = local_data.zone_updates.get(zone_id) + if callback: + callback() + + entry.async_on_unload(risco.add_zone_handler(_zone)) + + async def _partition(partition_id: int, partition: Partition) -> None: + _LOGGER.debug("Risco partition update for %d", partition_id) + callback = local_data.partition_updates.get(partition_id) + if callback: + callback() + + entry.async_on_unload(risco.add_partition_handler(_partition)) + + entry.async_on_unload(entry.add_update_listener(_update_listener)) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = local_data + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def _async_setup_cloud_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = entry.data risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) try: @@ -46,12 +136,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, risco, entry.entry_id, 60 ) - undo_listener = entry.add_update_listener(_update_listener) + entry.async_on_unload(entry.add_update_listener(_update_listener)) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, - UNDO_UPDATE_LISTENER: undo_listener, EVENTS_COORDINATOR: events_coordinator, } @@ -65,7 +154,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 3bad03fda10..4196ee0cf42 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -1,7 +1,11 @@ """Support for Risco alarms.""" from __future__ import annotations +from collections.abc import Callable import logging +from typing import Any + +from pyrisco.common import Partition from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, @@ -23,6 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import LocalData, RiscoDataUpdateCoordinator, is_local from .const import ( CONF_CODE_ARM_REQUIRED, CONF_CODE_DISARM_REQUIRED, @@ -53,57 +58,61 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Risco alarm control panel.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] options = {**DEFAULT_OPTIONS, **config_entry.options} - entities = [ - RiscoAlarm(coordinator, partition_id, config_entry.data[CONF_PIN], options) - for partition_id in coordinator.data.partitions - ] - - async_add_entities(entities, False) + if is_local(config_entry): + local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + RiscoLocalAlarm( + local_data.system.id, + partition_id, + partition, + local_data.partition_updates, + config_entry.data[CONF_PIN], + options, + ) + for partition_id, partition in local_data.system.partitions.items() + ) + else: + coordinator: RiscoDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ][DATA_COORDINATOR] + async_add_entities( + RiscoCloudAlarm( + coordinator, partition_id, config_entry.data[CONF_PIN], options + ) + for partition_id in coordinator.data.partitions + ) -class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): - """Representation of a Risco partition.""" +class RiscoAlarm(AlarmControlPanelEntity): + """Representation of a Risco cloud partition.""" _attr_code_format = CodeFormat.NUMBER - def __init__(self, coordinator, partition_id, code, options): + def __init__( + self, + *, + partition_id: int, + partition: Partition, + code: str, + options: dict[str, Any], + **kwargs: Any, + ) -> None: """Init the partition.""" - super().__init__(coordinator) + super().__init__(**kwargs) self._partition_id = partition_id - self._partition = self.coordinator.data.partitions[self._partition_id] + self._partition = partition self._code = code self._attr_code_arm_required = options[CONF_CODE_ARM_REQUIRED] self._code_disarm_required = options[CONF_CODE_DISARM_REQUIRED] self._risco_to_ha = options[CONF_RISCO_STATES_TO_HA] self._ha_to_risco = options[CONF_HA_STATES_TO_RISCO] self._attr_supported_features = 0 + self._attr_has_entity_name = True + self._attr_name = None for state in self._ha_to_risco: self._attr_supported_features |= STATES_TO_SUPPORTED_FEATURES[state] - def _get_data_from_coordinator(self): - self._partition = self.coordinator.data.partitions[self._partition_id] - - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, - name=self.name, - manufacturer="Risco", - ) - - @property - def name(self) -> str: - """Return the name of the partition.""" - return f"Risco {self._risco.site_name} Partition {self._partition_id}" - - @property - def unique_id(self) -> str: - """Return a unique id for that partition.""" - return f"{self._risco.site_uuid}_{self._partition_id}" - @property def state(self) -> str | None: """Return the state of the device.""" @@ -165,7 +174,74 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): else: await self._call_alarm_method(risco_state) + async def _call_alarm_method(self, method: str, *args: Any) -> None: + raise NotImplementedError + + +class RiscoCloudAlarm(RiscoAlarm, RiscoEntity): + """Representation of a Risco partition.""" + + def __init__( + self, + coordinator: RiscoDataUpdateCoordinator, + partition_id: int, + code: str, + options: dict[str, Any], + ) -> None: + """Init the partition.""" + super().__init__( + partition_id=partition_id, + partition=coordinator.data.partitions[partition_id], + coordinator=coordinator, + code=code, + options=options, + ) + self._attr_unique_id = f"{self._risco.site_uuid}_{partition_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, + name=f"Risco {self._risco.site_name} Partition {partition_id}", + manufacturer="Risco", + ) + + def _get_data_from_coordinator(self) -> None: + self._partition = self.coordinator.data.partitions[self._partition_id] + async def _call_alarm_method(self, method, *args): alarm = await getattr(self._risco, method)(self._partition_id, *args) self._partition = alarm.partitions[self._partition_id] self.async_write_ha_state() + + +class RiscoLocalAlarm(RiscoAlarm): + """Representation of a Risco local, partition.""" + + _attr_should_poll = False + + def __init__( + self, + system_id: str, + partition_id: int, + partition: Partition, + partition_updates: dict[int, Callable[[], Any]], + code: str, + options: dict[str, Any], + ) -> None: + """Init the partition.""" + super().__init__( + partition_id=partition_id, partition=partition, code=code, options=options + ) + self._system_id = system_id + self._partition_updates = partition_updates + self._attr_unique_id = f"{system_id}_{partition_id}_local" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, + name=f"Risco {system_id} Partition {partition_id}", + manufacturer="Risco", + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to updates.""" + self._partition_updates[self._partition_id] = self.async_write_ha_state + + async def _call_alarm_method(self, method: str, *args: Any) -> None: + await getattr(self._partition, method)(*args) diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index acb62113235..9f98be09f0d 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -1,4 +1,11 @@ """Support for Risco alarm zones.""" +from __future__ import annotations + +from collections.abc import Callable, Mapping +from typing import Any + +from pyrisco.common import Zone + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -9,6 +16,7 @@ from homeassistant.helpers import entity_platform from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import LocalData, RiscoDataUpdateCoordinator, is_local from .const import DATA_COORDINATOR, DOMAIN from .entity import RiscoEntity, binary_sensor_unique_id @@ -28,69 +36,117 @@ async def async_setup_entry( SERVICE_UNBYPASS_ZONE, {}, "async_unbypass_zone" ) - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - entities = [ - RiscoBinarySensor(coordinator, zone_id, zone) - for zone_id, zone in coordinator.data.zones.items() - ] - async_add_entities(entities, False) - - -class RiscoBinarySensor(BinarySensorEntity, RiscoEntity): - """Representation of a Risco zone as a binary sensor.""" - - def __init__(self, coordinator, zone_id, zone): - """Init the zone.""" - super().__init__(coordinator) - self._zone_id = zone_id - self._zone = zone - - def _get_data_from_coordinator(self): - self._zone = self.coordinator.data.zones[self._zone_id] - - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, - manufacturer="Risco", - name=self.name, + if is_local(config_entry): + local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + RiscoLocalBinarySensor( + local_data.system.id, zone_id, zone, local_data.zone_updates + ) + for zone_id, zone in local_data.system.zones.items() + ) + else: + coordinator: RiscoDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ][DATA_COORDINATOR] + async_add_entities( + RiscoCloudBinarySensor(coordinator, zone_id, zone) + for zone_id, zone in coordinator.data.zones.items() ) - @property - def name(self): - """Return the name of the zone.""" - return self._zone.name + +class RiscoBinarySensor(BinarySensorEntity): + """Representation of a Risco zone as a binary sensor.""" + + _attr_device_class = BinarySensorDeviceClass.MOTION + + def __init__(self, *, zone_id: int, zone: Zone, **kwargs: Any) -> None: + """Init the zone.""" + super().__init__(**kwargs) + self._zone_id = zone_id + self._zone = zone + self._attr_has_entity_name = True + self._attr_name = None @property - def unique_id(self): - """Return a unique id for this zone.""" - return binary_sensor_unique_id(self._risco, self._zone_id) - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes.""" return {"zone_id": self._zone_id, "bypassed": self._zone.bypassed} @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._zone.triggered - @property - def device_class(self): - """Return the class of this sensor, from BinarySensorDeviceClass.""" - return BinarySensorDeviceClass.MOTION + async def async_bypass_zone(self) -> None: + """Bypass this zone.""" + await self._bypass(True) - async def _bypass(self, bypass): + async def async_unbypass_zone(self) -> None: + """Unbypass this zone.""" + await self._bypass(False) + + async def _bypass(self, bypass: bool) -> None: + raise NotImplementedError + + +class RiscoCloudBinarySensor(RiscoBinarySensor, RiscoEntity): + """Representation of a Risco cloud zone as a binary sensor.""" + + def __init__( + self, coordinator: RiscoDataUpdateCoordinator, zone_id: int, zone: Zone + ) -> None: + """Init the zone.""" + super().__init__(zone_id=zone_id, zone=zone, coordinator=coordinator) + self._attr_unique_id = binary_sensor_unique_id(self._risco, zone_id) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, + manufacturer="Risco", + name=self._zone.name, + ) + + def _get_data_from_coordinator(self) -> None: + self._zone = self.coordinator.data.zones[self._zone_id] + + async def _bypass(self, bypass: bool) -> None: alarm = await self._risco.bypass_zone(self._zone_id, bypass) self._zone = alarm.zones[self._zone_id] self.async_write_ha_state() - async def async_bypass_zone(self): - """Bypass this zone.""" - await self._bypass(True) - async def async_unbypass_zone(self): - """Unbypass this zone.""" - await self._bypass(False) +class RiscoLocalBinarySensor(RiscoBinarySensor): + """Representation of a Risco local zone as a binary sensor.""" + + _attr_should_poll = False + + def __init__( + self, + system_id: str, + zone_id: int, + zone: Zone, + zone_updates: dict[int, Callable[[], Any]], + ) -> None: + """Init the zone.""" + super().__init__(zone_id=zone_id, zone=zone) + self._system_id = system_id + self._zone_updates = zone_updates + self._attr_unique_id = f"{system_id}_zone_{zone_id}_local" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, + manufacturer="Risco", + name=self._zone.name, + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to updates.""" + self._zone_updates[self._zone_id] = self.async_write_ha_state + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return the state attributes.""" + return { + **(super().extra_state_attributes or {}), + "groups": self._zone.groups, + } + + async def _bypass(self, bypass: bool) -> None: + await self._zone.bypass(bypass) diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 5f8f40cb5f7..1befe626347 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -1,16 +1,21 @@ """Config flow for Risco integration.""" from __future__ import annotations +import asyncio +from collections.abc import Mapping import logging -from pyrisco import CannotConnectError, RiscoCloud, UnauthorizedError +from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedError import voluptuous as vol from homeassistant import config_entries, core from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PIN, + CONF_PORT, CONF_SCAN_INTERVAL, + CONF_TYPE, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -27,18 +32,27 @@ from .const import ( DEFAULT_OPTIONS, DOMAIN, RISCO_STATES, + SLEEP_INTERVAL, + TYPE_LOCAL, ) _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema( +CLOUD_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PIN): str, } ) +LOCAL_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=1000): int, + vol.Required(CONF_PIN): str, + } +) HA_STATES = [ STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -47,10 +61,10 @@ HA_STATES = [ ] -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect. +async def validate_cloud_input(hass: core.HomeAssistant, data) -> dict[str, str]: + """Validate the user input allows us to connect to Risco Cloud. - Data has the keys from DATA_SCHEMA with values provided by the user. + Data has the keys from CLOUD_SCHEMA with values provided by the user. """ risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) @@ -62,6 +76,20 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": risco.site_name} +async def validate_local_input( + hass: core.HomeAssistant, data: Mapping[str, str] +) -> dict[str, str]: + """Validate the user input allows us to connect to a local panel. + + Data has the keys from LOCAL_SCHEMA with values provided by the user. + """ + risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN]) + await risco.connect() + site_id = risco.id + await risco.disconnect() + return {"title": site_id} + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Risco.""" @@ -77,13 +105,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" + return self.async_show_menu( + step_id="user", + menu_options=["cloud", "local"], + ) + + async def async_step_cloud(self, user_input=None): + """Configure a cloud based alarm.""" errors = {} if user_input is not None: await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() try: - info = await validate_input(self.hass, user_input) + info = await validate_cloud_input(self.hass, user_input) except CannotConnectError: errors["base"] = "cannot_connect" except UnauthorizedError: @@ -95,7 +130,35 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 + step_id="cloud", data_schema=CLOUD_SCHEMA, errors=errors + ) + + async def async_step_local(self, user_input=None): + """Configure a local based alarm.""" + errors = {} + if user_input is not None: + try: + info = await validate_local_input(self.hass, user_input) + except CannotConnectError: + errors["base"] = "cannot_connect" + except UnauthorizedError: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info["title"]) + self._abort_if_unique_id_configured() + + # Risco can hang if we don't wait before creating a new connection + await asyncio.sleep(SLEEP_INTERVAL) + + return self.async_create_entry( + title=info["title"], data={**user_input, **{CONF_TYPE: TYPE_LOCAL}} + ) + + return self.async_show_form( + step_id="local", data_schema=LOCAL_SCHEMA, errors=errors ) diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index 46eb011ba5b..f4ac170d3c7 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -15,6 +15,8 @@ EVENTS_COORDINATOR = "risco_events" DEFAULT_SCAN_INTERVAL = 30 +TYPE_LOCAL = "local" + CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_DISARM_REQUIRED = "code_disarm_required" CONF_RISCO_STATES_TO_HA = "risco_states_to_ha" @@ -44,3 +46,5 @@ DEFAULT_OPTIONS = { CONF_RISCO_STATES_TO_HA: DEFAULT_RISCO_STATES_TO_HA, CONF_HA_STATES_TO_RISCO: DEFAULT_HA_STATES_TO_RISCO, } + +SLEEP_INTERVAL = 1 diff --git a/homeassistant/components/risco/entity.py b/homeassistant/components/risco/entity.py index 04b521156b1..e49b632ac78 100644 --- a/homeassistant/components/risco/entity.py +++ b/homeassistant/components/risco/entity.py @@ -1,13 +1,15 @@ """A risco entity base class.""" from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import RiscoDataUpdateCoordinator -def binary_sensor_unique_id(risco, zone_id): + +def binary_sensor_unique_id(risco, zone_id: int) -> str: """Return unique id for the binary sensor.""" return f"{risco.site_uuid}_zone_{zone_id}" -class RiscoEntity(CoordinatorEntity): +class RiscoEntity(CoordinatorEntity[RiscoDataUpdateCoordinator]): """Risco entity base class.""" def _get_data_from_coordinator(self): diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index fb4b8203aac..0136e8f54de 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -6,6 +6,6 @@ "requirements": ["pyrisco==0.5.2"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", - "iot_class": "cloud_polling", + "iot_class": "local_push", "loggers": ["pyrisco"] } diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index 6038c2911c9..c4bd047e260 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -1,4 +1,9 @@ """Sensor for Risco Events.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + from homeassistant.components.binary_sensor import DOMAIN as BS_DOMAIN from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -8,6 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util +from . import RiscoEventsDataUpdateCoordinator, is_local from .const import DOMAIN, EVENTS_COORDINATOR from .entity import binary_sensor_unique_id @@ -38,7 +44,13 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for device.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][EVENTS_COORDINATOR] + if is_local(config_entry): + # no events in local comm + return + + coordinator: RiscoEventsDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ][EVENTS_COORDINATOR] sensors = [ RiscoSensor(coordinator, id, [], name, config_entry.entry_id) for id, name in CATEGORIES.items() @@ -62,19 +74,12 @@ class RiscoSensor(CoordinatorEntity, SensorEntity): self._excludes = excludes self._name = name self._entry_id = entry_id - self._entity_registry = None + self._entity_registry: er.EntityRegistry | None = None + self._attr_unique_id = f"events_{name}_{self.coordinator.risco.site_uuid}" + self._attr_name = f"Risco {self.coordinator.risco.site_name} {name} Events" + self._attr_device_class = SensorDeviceClass.TIMESTAMP - @property - def name(self): - """Return the name of the sensor.""" - return f"Risco {self.coordinator.risco.site_name} {self._name} Events" - - @property - def unique_id(self): - """Return a unique id for this sensor.""" - return f"events_{self._name}_{self.coordinator.risco.site_uuid}" - - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self._entity_registry = er.async_get(self.hass) self.async_on_remove( @@ -103,7 +108,7 @@ class RiscoSensor(CoordinatorEntity, SensorEntity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """State attributes.""" if self._event is None: return None @@ -120,8 +125,3 @@ class RiscoSensor(CoordinatorEntity, SensorEntity): attrs["zone_entity_id"] = zone_entity_id return attrs - - @property - def device_class(self): - """Device class of sensor.""" - return SensorDeviceClass.TIMESTAMP diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json index ebce9dda514..1cc2fe7317c 100644 --- a/homeassistant/components/risco/strings.json +++ b/homeassistant/components/risco/strings.json @@ -2,11 +2,24 @@ "config": { "step": { "user": { + "menu_options": { + "cloud": "Risco Cloud (recommended)", + "local": "Local Risco Panel (advanced)" + } + }, + "cloud": { "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "pin": "[%key:common::config_flow::data::pin%]" } + }, + "local": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "pin": "[%key:common::config_flow::data::pin%]" + } } }, "error": { diff --git a/homeassistant/components/risco/translations/en.json b/homeassistant/components/risco/translations/en.json index c98cd82ede8..95dd395e501 100644 --- a/homeassistant/components/risco/translations/en.json +++ b/homeassistant/components/risco/translations/en.json @@ -9,12 +9,25 @@ "unknown": "Unexpected error" }, "step": { - "user": { + "cloud": { "data": { "password": "Password", "pin": "PIN Code", "username": "Username" } + }, + "local": { + "data": { + "host": "Host", + "pin": "PIN Code", + "port": "Port" + } + }, + "user": { + "menu_options": { + "cloud": "Risco Cloud (recommended)", + "local": "Local Risco Panel (advanced)" + } } } }, diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py new file mode 100644 index 00000000000..006e57b9ae5 --- /dev/null +++ b/tests/components/risco/conftest.py @@ -0,0 +1,148 @@ +"""Fixtures for Risco tests.""" +from unittest.mock import MagicMock, PropertyMock, patch + +from pytest import fixture + +from homeassistant.components.risco.const import DOMAIN, TYPE_LOCAL +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PIN, + CONF_PORT, + CONF_TYPE, + CONF_USERNAME, +) + +from .util import TEST_SITE_NAME, TEST_SITE_UUID, zone_mock + +from tests.common import MockConfigEntry + +TEST_CLOUD_CONFIG = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PIN: "1234", +} +TEST_LOCAL_CONFIG = { + CONF_TYPE: TYPE_LOCAL, + CONF_HOST: "test-host", + CONF_PORT: 5004, + CONF_PIN: "1234", +} + + +@fixture +def two_zone_cloud(): + """Fixture to mock alarm with two zones.""" + zone_mocks = {0: zone_mock(), 1: zone_mock()} + alarm_mock = MagicMock() + with patch.object( + zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), patch.object( + zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") + ), patch.object( + zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), patch.object( + zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") + ), patch.object( + alarm_mock, + "zones", + new_callable=PropertyMock(return_value=zone_mocks), + ), patch( + "homeassistant.components.risco.RiscoCloud.get_state", + return_value=alarm_mock, + ): + yield zone_mocks + + +@fixture +def options(): + """Fixture for default (empty) options.""" + return {} + + +@fixture +def events(): + """Fixture for default (empty) events.""" + return [] + + +@fixture +def cloud_config_entry(hass, options): + """Fixture for a cloud config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=TEST_CLOUD_CONFIG, options=options + ) + config_entry.add_to_hass(hass) + return config_entry + + +@fixture +def login_with_error(exception): + """Fixture to simulate error on login.""" + with patch( + "homeassistant.components.risco.RiscoCloud.login", + side_effect=exception, + ): + yield + + +@fixture +async def setup_risco_cloud(hass, cloud_config_entry, events): + """Set up a Risco integration for testing.""" + with patch( + "homeassistant.components.risco.RiscoCloud.login", + return_value=True, + ), patch( + "homeassistant.components.risco.RiscoCloud.site_uuid", + new_callable=PropertyMock(return_value=TEST_SITE_UUID), + ), patch( + "homeassistant.components.risco.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.RiscoCloud.close" + ), patch( + "homeassistant.components.risco.RiscoCloud.get_events", + return_value=events, + ): + await hass.config_entries.async_setup(cloud_config_entry.entry_id) + await hass.async_block_till_done() + + yield cloud_config_entry + + +@fixture +def local_config_entry(hass, options): + """Fixture for a local config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=TEST_LOCAL_CONFIG, options=options + ) + config_entry.add_to_hass(hass) + return config_entry + + +@fixture +def connect_with_error(exception): + """Fixture to simulate error on connect.""" + with patch( + "homeassistant.components.risco.RiscoLocal.connect", + side_effect=exception, + ): + yield + + +@fixture +async def setup_risco_local(hass, local_config_entry): + """Set up a local Risco integration for testing.""" + with patch( + "homeassistant.components.risco.RiscoLocal.connect", + return_value=True, + ), patch( + "homeassistant.components.risco.RiscoLocal.id", + new_callable=PropertyMock(return_value=TEST_SITE_UUID), + ), patch( + "homeassistant.components.risco.RiscoLocal.disconnect" + ): + await hass.config_entries.async_setup(local_config_entry.entry_id) + await hass.async_block_till_done() + + yield local_config_entry diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 4a82656147d..ca0eb604eef 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -1,5 +1,5 @@ """Tests for the Risco alarm control panel device.""" -from unittest.mock import MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest @@ -31,12 +31,13 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco +from .util import TEST_SITE_UUID -from tests.common import MockConfigEntry +FIRST_CLOUD_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0" +SECOND_CLOUD_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_1" -FIRST_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0" -SECOND_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_1" +FIRST_LOCAL_ENTITY_ID = "alarm_control_panel.risco_test_site_uuid_partition_0" +SECOND_LOCAL_ENTITY_ID = "alarm_control_panel.risco_test_site_uuid_partition_1" CODES_REQUIRED_OPTIONS = {"code_arm_required": True, "code_disarm_required": True} TEST_RISCO_TO_HA = { @@ -86,7 +87,7 @@ def _partition_mock(): @pytest.fixture -def two_part_alarm(): +def two_part_cloud_alarm(): """Fixture to mock alarm with two partitions.""" partition_mocks = {0: _partition_mock(), 1: _partition_mock()} alarm_mock = MagicMock() @@ -102,52 +103,42 @@ def two_part_alarm(): "homeassistant.components.risco.RiscoCloud.get_state", return_value=alarm_mock, ): - yield alarm_mock + yield partition_mocks -async def test_cannot_connect(hass): - """Test connection error.""" - - with patch( - "homeassistant.components.risco.RiscoCloud.login", - side_effect=CannotConnectError, +@pytest.fixture +def two_part_local_alarm(): + """Fixture to mock alarm with two partitions.""" + partition_mocks = {0: _partition_mock(), 1: _partition_mock()} + with patch.object( + partition_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), patch.object( + partition_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), patch( + "homeassistant.components.risco.RiscoLocal.zones", + new_callable=PropertyMock(return_value={}), + ), patch( + "homeassistant.components.risco.RiscoLocal.partitions", + new_callable=PropertyMock(return_value=partition_mocks), ): - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - registry = er.async_get(hass) - assert not registry.async_is_registered(FIRST_ENTITY_ID) - assert not registry.async_is_registered(SECOND_ENTITY_ID) + yield partition_mocks -async def test_unauthorized(hass): - """Test unauthorized error.""" - - with patch( - "homeassistant.components.risco.RiscoCloud.login", - side_effect=UnauthorizedError, - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - registry = er.async_get(hass) - assert not registry.async_is_registered(FIRST_ENTITY_ID) - assert not registry.async_is_registered(SECOND_ENTITY_ID) +@pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) +async def test_error_on_login(hass, login_with_error, cloud_config_entry): + """Test error on login.""" + await hass.config_entries.async_setup(cloud_config_entry.entry_id) + await hass.async_block_till_done() + registry = er.async_get(hass) + assert not registry.async_is_registered(FIRST_CLOUD_ENTITY_ID) + assert not registry.async_is_registered(SECOND_CLOUD_ENTITY_ID) -async def test_setup(hass, two_part_alarm): +async def test_cloud_setup(hass, two_part_cloud_alarm, setup_risco_cloud): """Test entity setup.""" registry = er.async_get(hass) - - assert not registry.async_is_registered(FIRST_ENTITY_ID) - assert not registry.async_is_registered(SECOND_ENTITY_ID) - - await setup_risco(hass) - - assert registry.async_is_registered(FIRST_ENTITY_ID) - assert registry.async_is_registered(SECOND_ENTITY_ID) + assert registry.async_is_registered(FIRST_CLOUD_ENTITY_ID) + assert registry.async_is_registered(SECOND_CLOUD_ENTITY_ID) registry = dr.async_get(hass) device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0")}) @@ -159,50 +150,59 @@ async def test_setup(hass, two_part_alarm): assert device.manufacturer == "Risco" -async def _check_state(hass, alarm, property, state, entity_id, partition_id): - with patch.object(alarm.partitions[partition_id], property, return_value=True): +async def _check_cloud_state( + hass, partitions, property, state, entity_id, partition_id +): + with patch.object(partitions[partition_id], property, return_value=True): await async_update_entity(hass, entity_id) await hass.async_block_till_done() assert hass.states.get(entity_id).state == state -async def test_states(hass, two_part_alarm): +@pytest.mark.parametrize("options", [CUSTOM_MAPPING_OPTIONS]) +async def test_cloud_states(hass, two_part_cloud_alarm, setup_risco_cloud): """Test the various alarm states.""" - await setup_risco(hass, [], CUSTOM_MAPPING_OPTIONS) - - assert hass.states.get(FIRST_ENTITY_ID).state == STATE_UNKNOWN - for partition_id, entity_id in {0: FIRST_ENTITY_ID, 1: SECOND_ENTITY_ID}.items(): - await _check_state( + assert hass.states.get(FIRST_CLOUD_ENTITY_ID).state == STATE_UNKNOWN + for partition_id, entity_id in { + 0: FIRST_CLOUD_ENTITY_ID, + 1: SECOND_CLOUD_ENTITY_ID, + }.items(): + await _check_cloud_state( hass, - two_part_alarm, + two_part_cloud_alarm, "triggered", STATE_ALARM_TRIGGERED, entity_id, partition_id, ) - await _check_state( - hass, two_part_alarm, "arming", STATE_ALARM_ARMING, entity_id, partition_id - ) - await _check_state( + await _check_cloud_state( hass, - two_part_alarm, + two_part_cloud_alarm, + "arming", + STATE_ALARM_ARMING, + entity_id, + partition_id, + ) + await _check_cloud_state( + hass, + two_part_cloud_alarm, "armed", STATE_ALARM_ARMED_AWAY, entity_id, partition_id, ) - await _check_state( + await _check_cloud_state( hass, - two_part_alarm, + two_part_cloud_alarm, "partially_armed", STATE_ALARM_ARMED_HOME, entity_id, partition_id, ) - await _check_state( + await _check_cloud_state( hass, - two_part_alarm, + two_part_cloud_alarm, "disarmed", STATE_ALARM_DISARMED, entity_id, @@ -211,13 +211,13 @@ async def test_states(hass, two_part_alarm): groups = {"A": False, "B": False, "C": True, "D": False} with patch.object( - two_part_alarm.partitions[partition_id], + two_part_cloud_alarm[partition_id], "groups", new_callable=PropertyMock(return_value=groups), ): - await _check_state( + await _check_cloud_state( hass, - two_part_alarm, + two_part_cloud_alarm, "partially_armed", STATE_ALARM_ARMED_NIGHT, entity_id, @@ -225,22 +225,6 @@ async def test_states(hass, two_part_alarm): ) -async def _test_service_call( - hass, service, method, entity_id, partition_id, *args, **kwargs -): - with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: - await _call_alarm_service(hass, service, entity_id, **kwargs) - set_mock.assert_awaited_once_with(partition_id, *args) - - -async def _test_no_service_call( - hass, service, method, entity_id, partition_id, **kwargs -): - with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: - await _call_alarm_service(hass, service, entity_id, **kwargs) - set_mock.assert_not_awaited() - - async def _call_alarm_service(hass, service, entity_id, **kwargs): data = {"entity_id": entity_id, **kwargs} @@ -249,181 +233,695 @@ async def _call_alarm_service(hass, service, entity_id, **kwargs): ) -async def test_sets_custom_mapping(hass, two_part_alarm): - """Test settings the various modes when mapping some states.""" - await setup_risco(hass, [], CUSTOM_MAPPING_OPTIONS) +async def _test_cloud_service_call( + hass, service, method, entity_id, partition_id, *args, **kwargs +): + with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: + await _call_alarm_service(hass, service, entity_id, **kwargs) + set_mock.assert_awaited_once_with(partition_id, *args) + +async def _test_cloud_no_service_call( + hass, service, method, entity_id, partition_id, **kwargs +): + with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: + await _call_alarm_service(hass, service, entity_id, **kwargs) + set_mock.assert_not_awaited() + + +@pytest.mark.parametrize("options", [CUSTOM_MAPPING_OPTIONS]) +async def test_cloud_sets_custom_mapping(hass, two_part_cloud_alarm, setup_risco_cloud): + """Test settings the various modes when mapping some states.""" registry = er.async_get(hass) - entity = registry.async_get(FIRST_ENTITY_ID) + entity = registry.async_get(FIRST_CLOUD_ENTITY_ID) assert entity.supported_features == EXPECTED_FEATURES - await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0) - await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1) - await _test_service_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0) - await _test_service_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1) - await _test_service_call( - hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0 + await _test_cloud_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", FIRST_CLOUD_ENTITY_ID, 0 ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1 + await _test_cloud_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", SECOND_CLOUD_ENTITY_ID, 1 ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, "C" + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_CLOUD_ENTITY_ID, 0 ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C" + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_CLOUD_ENTITY_ID, 1 + ) + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_CLOUD_ENTITY_ID, 0 + ) + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_CLOUD_ENTITY_ID, 1 + ) + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_CLOUD_ENTITY_ID, 0, "C" + ) + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_CLOUD_ENTITY_ID, 1, "C" ) -async def test_sets_full_custom_mapping(hass, two_part_alarm): +@pytest.mark.parametrize("options", [FULL_CUSTOM_MAPPING]) +async def test_cloud_sets_full_custom_mapping( + hass, two_part_cloud_alarm, setup_risco_cloud +): """Test settings the various modes when mapping all states.""" - await setup_risco(hass, [], FULL_CUSTOM_MAPPING) - registry = er.async_get(hass) - entity = registry.async_get(FIRST_ENTITY_ID) + entity = registry.async_get(FIRST_CLOUD_ENTITY_ID) assert ( entity.supported_features == EXPECTED_FEATURES | SUPPORT_ALARM_ARM_CUSTOM_BYPASS ) - await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0) - await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1) - await _test_service_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0) - await _test_service_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1) - await _test_service_call( - hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0 + await _test_cloud_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", FIRST_CLOUD_ENTITY_ID, 0 ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1 + await _test_cloud_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", SECOND_CLOUD_ENTITY_ID, 1 ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, "C" + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_CLOUD_ENTITY_ID, 0 ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C" + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_CLOUD_ENTITY_ID, 1 ) - await _test_service_call( + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_CLOUD_ENTITY_ID, 0 + ) + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_CLOUD_ENTITY_ID, 1 + ) + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_CLOUD_ENTITY_ID, 0, "C" + ) + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_CLOUD_ENTITY_ID, 1, "C" + ) + await _test_cloud_service_call( hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", - FIRST_ENTITY_ID, + FIRST_CLOUD_ENTITY_ID, 0, "D", ) - await _test_service_call( + await _test_cloud_service_call( hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", - SECOND_ENTITY_ID, + SECOND_CLOUD_ENTITY_ID, 1, "D", ) -async def test_sets_with_correct_code(hass, two_part_alarm): +@pytest.mark.parametrize( + "options", [{**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}] +) +async def test_cloud_sets_with_correct_code( + hass, two_part_cloud_alarm, setup_risco_cloud +): """Test settings the various modes when code is required.""" - await setup_risco(hass, [], {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) - code = {"code": 1234} - await _test_service_call( - hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0, **code + await _test_cloud_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", FIRST_CLOUD_ENTITY_ID, 0, **code ) - await _test_service_call( - hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1, **code + await _test_cloud_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", SECOND_CLOUD_ENTITY_ID, 1, **code ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0, **code + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_CLOUD_ENTITY_ID, 0, **code ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1, **code + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_CLOUD_ENTITY_ID, 1, **code ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0, **code + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_CLOUD_ENTITY_ID, 0, **code ) - await _test_service_call( - hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1, **code + await _test_cloud_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_CLOUD_ENTITY_ID, 1, **code ) - await _test_service_call( + await _test_cloud_service_call( hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", - FIRST_ENTITY_ID, + FIRST_CLOUD_ENTITY_ID, 0, "C", **code, ) - await _test_service_call( + await _test_cloud_service_call( hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", - SECOND_ENTITY_ID, + SECOND_CLOUD_ENTITY_ID, 1, "C", **code, ) with pytest.raises(HomeAssistantError): - await _test_no_service_call( + await _test_cloud_no_service_call( hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", - FIRST_ENTITY_ID, + FIRST_CLOUD_ENTITY_ID, 0, **code, ) with pytest.raises(HomeAssistantError): - await _test_no_service_call( + await _test_cloud_no_service_call( hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", - SECOND_ENTITY_ID, + SECOND_CLOUD_ENTITY_ID, 1, **code, ) -async def test_sets_with_incorrect_code(hass, two_part_alarm): +@pytest.mark.parametrize( + "options", [{**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}] +) +async def test_cloud_sets_with_incorrect_code( + hass, two_part_cloud_alarm, setup_risco_cloud +): """Test settings the various modes when code is required and incorrect.""" - await setup_risco(hass, [], {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) - code = {"code": 4321} - await _test_no_service_call( - hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0, **code + await _test_cloud_no_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", FIRST_CLOUD_ENTITY_ID, 0, **code ) - await _test_no_service_call( - hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1, **code + await _test_cloud_no_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", SECOND_CLOUD_ENTITY_ID, 1, **code ) - await _test_no_service_call( - hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0, **code + await _test_cloud_no_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_CLOUD_ENTITY_ID, 0, **code ) - await _test_no_service_call( - hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1, **code + await _test_cloud_no_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_CLOUD_ENTITY_ID, 1, **code ) - await _test_no_service_call( - hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0, **code + await _test_cloud_no_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_CLOUD_ENTITY_ID, 0, **code ) - await _test_no_service_call( - hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1, **code + await _test_cloud_no_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_CLOUD_ENTITY_ID, 1, **code ) - await _test_no_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, **code + await _test_cloud_no_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_CLOUD_ENTITY_ID, 0, **code ) - await _test_no_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, **code + await _test_cloud_no_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_CLOUD_ENTITY_ID, 1, **code ) with pytest.raises(HomeAssistantError): - await _test_no_service_call( + await _test_cloud_no_service_call( hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", - FIRST_ENTITY_ID, + FIRST_CLOUD_ENTITY_ID, 0, **code, ) with pytest.raises(HomeAssistantError): - await _test_no_service_call( + await _test_cloud_no_service_call( hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", - SECOND_ENTITY_ID, + SECOND_CLOUD_ENTITY_ID, 1, **code, ) + + +@pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) +async def test_error_on_connect(hass, connect_with_error, local_config_entry): + """Test error on connect.""" + await hass.config_entries.async_setup(local_config_entry.entry_id) + await hass.async_block_till_done() + registry = er.async_get(hass) + assert not registry.async_is_registered(FIRST_LOCAL_ENTITY_ID) + assert not registry.async_is_registered(SECOND_LOCAL_ENTITY_ID) + + +async def test_local_setup(hass, two_part_local_alarm, setup_risco_local): + """Test entity setup.""" + registry = er.async_get(hass) + assert registry.async_is_registered(FIRST_LOCAL_ENTITY_ID) + assert registry.async_is_registered(SECOND_LOCAL_ENTITY_ID) + + registry = dr.async_get(hass) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0_local")}) + assert device is not None + assert device.manufacturer == "Risco" + + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1_local")}) + assert device is not None + assert device.manufacturer == "Risco" + + +async def _check_local_state( + hass, partitions, property, state, entity_id, partition_id, callback +): + with patch.object(partitions[partition_id], property, return_value=True): + await callback(partition_id, partitions[partition_id]) + + assert hass.states.get(entity_id).state == state + + +@pytest.fixture +def _mock_partition_handler(): + with patch( + "homeassistant.components.risco.RiscoLocal.add_partition_handler" + ) as mock: + yield mock + + +@pytest.mark.parametrize("options", [CUSTOM_MAPPING_OPTIONS]) +async def test_local_states( + hass, two_part_local_alarm, _mock_partition_handler, setup_risco_local +): + """Test the various alarm states.""" + callback = _mock_partition_handler.call_args.args[0] + + assert callback is not None + + assert hass.states.get(FIRST_LOCAL_ENTITY_ID).state == STATE_UNKNOWN + for partition_id, entity_id in { + 0: FIRST_LOCAL_ENTITY_ID, + 1: SECOND_LOCAL_ENTITY_ID, + }.items(): + await _check_local_state( + hass, + two_part_local_alarm, + "triggered", + STATE_ALARM_TRIGGERED, + entity_id, + partition_id, + callback, + ) + await _check_local_state( + hass, + two_part_local_alarm, + "arming", + STATE_ALARM_ARMING, + entity_id, + partition_id, + callback, + ) + await _check_local_state( + hass, + two_part_local_alarm, + "armed", + STATE_ALARM_ARMED_AWAY, + entity_id, + partition_id, + callback, + ) + await _check_local_state( + hass, + two_part_local_alarm, + "partially_armed", + STATE_ALARM_ARMED_HOME, + entity_id, + partition_id, + callback, + ) + await _check_local_state( + hass, + two_part_local_alarm, + "disarmed", + STATE_ALARM_DISARMED, + entity_id, + partition_id, + callback, + ) + + groups = {"A": False, "B": False, "C": True, "D": False} + with patch.object( + two_part_local_alarm[partition_id], + "groups", + new_callable=PropertyMock(return_value=groups), + ): + await _check_local_state( + hass, + two_part_local_alarm, + "partially_armed", + STATE_ALARM_ARMED_NIGHT, + entity_id, + partition_id, + callback, + ) + + +async def _test_local_service_call( + hass, service, method, entity_id, partition, *args, **kwargs +): + with patch.object(partition, method, AsyncMock()) as set_mock: + await _call_alarm_service(hass, service, entity_id, **kwargs) + set_mock.assert_awaited_once_with(*args) + + +async def _test_local_no_service_call( + hass, service, method, entity_id, partition, **kwargs +): + with patch.object(partition, method, AsyncMock()) as set_mock: + await _call_alarm_service(hass, service, entity_id, **kwargs) + set_mock.assert_not_awaited() + + +@pytest.mark.parametrize("options", [CUSTOM_MAPPING_OPTIONS]) +async def test_local_sets_custom_mapping(hass, two_part_local_alarm, setup_risco_local): + """Test settings the various modes when mapping some states.""" + registry = er.async_get(hass) + entity = registry.async_get(FIRST_LOCAL_ENTITY_ID) + assert entity.supported_features == EXPECTED_FEATURES + + await _test_local_service_call( + hass, + SERVICE_ALARM_DISARM, + "disarm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_DISARM, + "disarm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_AWAY, + "arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_AWAY, + "arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_HOME, + "partial_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_HOME, + "partial_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + "C", + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + "C", + ) + + +@pytest.mark.parametrize("options", [FULL_CUSTOM_MAPPING]) +async def test_local_sets_full_custom_mapping( + hass, two_part_local_alarm, setup_risco_local +): + """Test settings the various modes when mapping all states.""" + registry = er.async_get(hass) + entity = registry.async_get(FIRST_LOCAL_ENTITY_ID) + assert ( + entity.supported_features == EXPECTED_FEATURES | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + ) + + await _test_local_service_call( + hass, + SERVICE_ALARM_DISARM, + "disarm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_DISARM, + "disarm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_AWAY, + "arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_AWAY, + "arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_HOME, + "partial_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_HOME, + "partial_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + "C", + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + "C", + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "group_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + "D", + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "group_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + "D", + ) + + +@pytest.mark.parametrize( + "options", [{**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}] +) +async def test_local_sets_with_correct_code( + hass, two_part_local_alarm, setup_risco_local +): + """Test settings the various modes when code is required.""" + code = {"code": 1234} + await _test_local_service_call( + hass, + SERVICE_ALARM_DISARM, + "disarm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + **code, + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_DISARM, + "disarm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + **code, + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_AWAY, + "arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + **code, + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_AWAY, + "arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + **code, + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_HOME, + "partial_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + **code, + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_HOME, + "partial_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + **code, + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + "C", + **code, + ) + await _test_local_service_call( + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + "C", + **code, + ) + with pytest.raises(HomeAssistantError): + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + **code, + ) + with pytest.raises(HomeAssistantError): + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + **code, + ) + + +@pytest.mark.parametrize( + "options", [{**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}] +) +async def test_local_sets_with_incorrect_code( + hass, two_part_local_alarm, setup_risco_local +): + """Test settings the various modes when code is required and incorrect.""" + code = {"code": 4321} + await _test_local_no_service_call( + hass, + SERVICE_ALARM_DISARM, + "disarm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + **code, + ) + await _test_local_no_service_call( + hass, + SERVICE_ALARM_DISARM, + "disarm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + **code, + ) + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_AWAY, + "arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + **code, + ) + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_AWAY, + "arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + **code, + ) + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_HOME, + "partial_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + **code, + ) + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_HOME, + "partial_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + **code, + ) + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + **code, + ) + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + **code, + ) + with pytest.raises(HomeAssistantError): + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + FIRST_LOCAL_ENTITY_ID, + two_part_local_alarm[0], + **code, + ) + with pytest.raises(HomeAssistantError): + await _test_local_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + SECOND_LOCAL_ENTITY_ID, + two_part_local_alarm[1], + **code, + ) diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index a7c11c9cb00..2325d88c03f 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -1,62 +1,55 @@ """Tests for the Risco binary sensors.""" from unittest.mock import PropertyMock, patch +import pytest + from homeassistant.components.risco import CannotConnectError, UnauthorizedError from homeassistant.components.risco.const import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco -from .util import two_zone_alarm # noqa: F401 - -from tests.common import MockConfigEntry +from .util import TEST_SITE_UUID, zone_mock FIRST_ENTITY_ID = "binary_sensor.zone_0" SECOND_ENTITY_ID = "binary_sensor.zone_1" -async def test_cannot_connect(hass): - """Test connection error.""" - - with patch( - "homeassistant.components.risco.RiscoCloud.login", - side_effect=CannotConnectError, +@pytest.fixture +def two_zone_local(): + """Fixture to mock alarm with two zones.""" + zone_mocks = {0: zone_mock(), 1: zone_mock()} + with patch.object( + zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), patch.object( + zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") + ), patch.object( + zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), patch.object( + zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") + ), patch( + "homeassistant.components.risco.RiscoLocal.partitions", + new_callable=PropertyMock(return_value={}), + ), patch( + "homeassistant.components.risco.RiscoLocal.zones", + new_callable=PropertyMock(return_value=zone_mocks), ): - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - registry = er.async_get(hass) - assert not registry.async_is_registered(FIRST_ENTITY_ID) - assert not registry.async_is_registered(SECOND_ENTITY_ID) + yield zone_mocks -async def test_unauthorized(hass): - """Test unauthorized error.""" - - with patch( - "homeassistant.components.risco.RiscoCloud.login", - side_effect=UnauthorizedError, - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - registry = er.async_get(hass) - assert not registry.async_is_registered(FIRST_ENTITY_ID) - assert not registry.async_is_registered(SECOND_ENTITY_ID) - - -async def test_setup(hass, two_zone_alarm): # noqa: F811 - """Test entity setup.""" +@pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) +async def test_error_on_login(hass, login_with_error, cloud_config_entry): + """Test error on login.""" + await hass.config_entries.async_setup(cloud_config_entry.entry_id) + await hass.async_block_till_done() registry = er.async_get(hass) - assert not registry.async_is_registered(FIRST_ENTITY_ID) assert not registry.async_is_registered(SECOND_ENTITY_ID) - await setup_risco(hass) +async def test_cloud_setup(hass, two_zone_cloud, setup_risco_cloud): + """Test entity setup.""" + registry = er.async_get(hass) assert registry.async_is_registered(FIRST_ENTITY_ID) assert registry.async_is_registered(SECOND_ENTITY_ID) @@ -70,13 +63,13 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 assert device.manufacturer == "Risco" -async def _check_state(hass, alarm, triggered, bypassed, entity_id, zone_id): +async def _check_cloud_state(hass, zones, triggered, bypassed, entity_id, zone_id): with patch.object( - alarm.zones[zone_id], + zones[zone_id], "triggered", new_callable=PropertyMock(return_value=triggered), ), patch.object( - alarm.zones[zone_id], + zones[zone_id], "bypassed", new_callable=PropertyMock(return_value=bypassed), ): @@ -89,23 +82,20 @@ async def _check_state(hass, alarm, triggered, bypassed, entity_id, zone_id): assert hass.states.get(entity_id).attributes["zone_id"] == zone_id -async def test_states(hass, two_zone_alarm): # noqa: F811 +async def test_cloud_states(hass, two_zone_cloud, setup_risco_cloud): """Test the various alarm states.""" - await setup_risco(hass) - - await _check_state(hass, two_zone_alarm, True, True, FIRST_ENTITY_ID, 0) - await _check_state(hass, two_zone_alarm, True, False, FIRST_ENTITY_ID, 0) - await _check_state(hass, two_zone_alarm, False, True, FIRST_ENTITY_ID, 0) - await _check_state(hass, two_zone_alarm, False, False, FIRST_ENTITY_ID, 0) - await _check_state(hass, two_zone_alarm, True, True, SECOND_ENTITY_ID, 1) - await _check_state(hass, two_zone_alarm, True, False, SECOND_ENTITY_ID, 1) - await _check_state(hass, two_zone_alarm, False, True, SECOND_ENTITY_ID, 1) - await _check_state(hass, two_zone_alarm, False, False, SECOND_ENTITY_ID, 1) + await _check_cloud_state(hass, two_zone_cloud, True, True, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, True, False, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, False, True, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, False, False, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, True, True, SECOND_ENTITY_ID, 1) + await _check_cloud_state(hass, two_zone_cloud, True, False, SECOND_ENTITY_ID, 1) + await _check_cloud_state(hass, two_zone_cloud, False, True, SECOND_ENTITY_ID, 1) + await _check_cloud_state(hass, two_zone_cloud, False, False, SECOND_ENTITY_ID, 1) -async def test_bypass(hass, two_zone_alarm): # noqa: F811 +async def test_cloud_bypass(hass, two_zone_cloud, setup_risco_cloud): """Test bypassing a zone.""" - await setup_risco(hass) with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} @@ -116,9 +106,8 @@ async def test_bypass(hass, two_zone_alarm): # noqa: F811 mock.assert_awaited_once_with(0, True) -async def test_unbypass(hass, two_zone_alarm): # noqa: F811 +async def test_cloud_unbypass(hass, two_zone_cloud, setup_risco_cloud): """Test unbypassing a zone.""" - await setup_risco(hass) with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} @@ -127,3 +116,113 @@ async def test_unbypass(hass, two_zone_alarm): # noqa: F811 ) mock.assert_awaited_once_with(0, False) + + +@pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) +async def test_error_on_connect(hass, connect_with_error, local_config_entry): + """Test error on connect.""" + await hass.config_entries.async_setup(local_config_entry.entry_id) + await hass.async_block_till_done() + registry = er.async_get(hass) + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + +async def test_local_setup(hass, two_zone_local, setup_risco_local): + """Test entity setup.""" + registry = er.async_get(hass) + assert registry.async_is_registered(FIRST_ENTITY_ID) + assert registry.async_is_registered(SECOND_ENTITY_ID) + + registry = dr.async_get(hass) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0_local")}) + assert device is not None + assert device.manufacturer == "Risco" + + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_1_local")}) + assert device is not None + assert device.manufacturer == "Risco" + + +async def _check_local_state( + hass, zones, triggered, bypassed, entity_id, zone_id, callback +): + with patch.object( + zones[zone_id], + "triggered", + new_callable=PropertyMock(return_value=triggered), + ), patch.object( + zones[zone_id], + "bypassed", + new_callable=PropertyMock(return_value=bypassed), + ): + await callback(zone_id, zones[zone_id]) + + expected_triggered = STATE_ON if triggered else STATE_OFF + assert hass.states.get(entity_id).state == expected_triggered + assert hass.states.get(entity_id).attributes["bypassed"] == bypassed + assert hass.states.get(entity_id).attributes["zone_id"] == zone_id + + +@pytest.fixture +def _mock_zone_handler(): + with patch("homeassistant.components.risco.RiscoLocal.add_zone_handler") as mock: + yield mock + + +async def test_local_states( + hass, two_zone_local, _mock_zone_handler, setup_risco_local +): + """Test the various alarm states.""" + callback = _mock_zone_handler.call_args.args[0] + + assert callback is not None + + await _check_local_state( + hass, two_zone_local, True, True, FIRST_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, True, False, FIRST_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, False, True, FIRST_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, False, False, FIRST_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, True, True, SECOND_ENTITY_ID, 1, callback + ) + await _check_local_state( + hass, two_zone_local, True, False, SECOND_ENTITY_ID, 1, callback + ) + await _check_local_state( + hass, two_zone_local, False, True, SECOND_ENTITY_ID, 1, callback + ) + await _check_local_state( + hass, two_zone_local, False, False, SECOND_ENTITY_ID, 1, callback + ) + + +async def test_local_bypass(hass, two_zone_local, setup_risco_local): + """Test bypassing a zone.""" + with patch.object(two_zone_local[0], "bypass") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + DOMAIN, "bypass_zone", service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(True) + + +async def test_local_unbypass(hass, two_zone_local, setup_risco_local): + """Test unbypassing a zone.""" + with patch.object(two_zone_local[0], "bypass") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + DOMAIN, "unbypass_zone", service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(False) diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 8d04f478e44..a39a724d7b9 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -4,22 +4,29 @@ from unittest.mock import PropertyMock, patch import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.risco.config_flow import ( CannotConnectError, UnauthorizedError, ) from homeassistant.components.risco.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry TEST_SITE_NAME = "test-site-name" -TEST_DATA = { +TEST_CLOUD_DATA = { "username": "test-username", "password": "test-password", "pin": "1234", } +TEST_LOCAL_DATA = { + "host": "test-host", + "port": 5004, + "pin": "1234", +} + TEST_RISCO_TO_HA = { "arm": "armed_away", "partial_arm": "armed_home", @@ -42,13 +49,19 @@ TEST_OPTIONS = { } -async def test_form(hass): - """Test we get the form.""" +async def test_cloud_form(hass): + """Test we get the cloud form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" - assert result["errors"] == {} + assert result["type"] == FlowResultType.MENU + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "cloud"} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {} with patch( "homeassistant.components.risco.config_flow.RiscoCloud.login", @@ -59,17 +72,20 @@ async def test_form(hass): ), patch( "homeassistant.components.risco.config_flow.RiscoCloud.close" ) as mock_close, patch( + "homeassistant.components.risco.config_flow.SLEEP_INTERVAL", + 0, + ), patch( "homeassistant.components.risco.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], TEST_CLOUD_DATA ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" - assert result2["title"] == TEST_SITE_NAME - assert result2["data"] == TEST_DATA + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == TEST_SITE_NAME + assert result3["data"] == TEST_CLOUD_DATA assert len(mock_setup_entry.mock_calls) == 1 mock_close.assert_awaited_once() @@ -82,33 +98,33 @@ async def test_form(hass): (Exception, "unknown"), ], ) -async def test_error(hass, exception, error): +async def test_cloud_error(hass, login_with_error, error): """Test we handle config flow errors.""" 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"], {"next_step_id": "cloud"} + ) with patch( - "homeassistant.components.risco.config_flow.RiscoCloud.login", - side_effect=exception, - ), patch( "homeassistant.components.risco.config_flow.RiscoCloud.close" ) as mock_close: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], TEST_CLOUD_DATA ) mock_close.assert_awaited_once() - assert result2["type"] == "form" - assert result2["errors"] == {"base": error} + assert result3["type"] == FlowResultType.FORM + assert result3["errors"] == {"base": error} -async def test_form_already_exists(hass): +async def test_form_cloud_already_exists(hass): """Test that a flow with an existing username aborts.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=TEST_DATA["username"], - data=TEST_DATA, + unique_id=TEST_CLOUD_DATA["username"], + data=TEST_CLOUD_DATA, ) entry.add_to_hass(hass) @@ -118,33 +134,139 @@ async def test_form_already_exists(hass): ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA + result["flow_id"], {"next_step_id": "cloud"} ) - assert result2["type"] == "abort" - assert result2["reason"] == "already_configured" + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], TEST_CLOUD_DATA + ) + + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "already_configured" + + +async def test_local_form(hass): + """Test we get the local form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "local"} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {} + + with patch( + "homeassistant.components.risco.config_flow.RiscoLocal.connect", + return_value=True, + ), patch( + "homeassistant.components.risco.config_flow.RiscoLocal.id", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.config_flow.RiscoLocal.disconnect" + ) as mock_close, patch( + "homeassistant.components.risco.config_flow.SLEEP_INTERVAL", + 0, + ), patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], TEST_LOCAL_DATA + ) + await hass.async_block_till_done() + + expected_data = {**TEST_LOCAL_DATA, **{"type": "local"}} + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == TEST_SITE_NAME + assert result3["data"] == expected_data + assert len(mock_setup_entry.mock_calls) == 1 + mock_close.assert_awaited_once() + + +@pytest.mark.parametrize( + "exception, error", + [ + (UnauthorizedError, "invalid_auth"), + (CannotConnectError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_local_error(hass, connect_with_error, error): + """Test we handle config flow errors.""" + 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"], {"next_step_id": "local"} + ) + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], TEST_LOCAL_DATA + ) + + assert result3["type"] == FlowResultType.FORM + assert result3["errors"] == {"base": error} + + +async def test_form_local_already_exists(hass): + """Test that a flow with an existing host aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_SITE_NAME, + data=TEST_LOCAL_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "local"} + ) + + with patch( + "homeassistant.components.risco.config_flow.RiscoLocal.connect", + return_value=True, + ), patch( + "homeassistant.components.risco.config_flow.RiscoLocal.id", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.config_flow.RiscoLocal.disconnect" + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], TEST_LOCAL_DATA + ) + + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "already_configured" async def test_options_flow(hass): """Test options flow.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=TEST_DATA["username"], - data=TEST_DATA, + unique_id=TEST_CLOUD_DATA["username"], + data=TEST_CLOUD_DATA, ) entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=TEST_OPTIONS, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "risco_to_ha" result = await hass.config_entries.options.async_configure( @@ -152,7 +274,7 @@ async def test_options_flow(hass): user_input=TEST_RISCO_TO_HA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "ha_to_risco" with patch("homeassistant.components.risco.async_setup_entry", return_value=True): @@ -161,7 +283,7 @@ async def test_options_flow(hass): user_input=TEST_HA_TO_RISCO, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert entry.options == { **TEST_OPTIONS, "risco_states_to_ha": TEST_RISCO_TO_HA, @@ -173,8 +295,8 @@ async def test_ha_to_risco_schema(hass): """Test that the schema for the ha-to-risco mapping step is generated properly.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=TEST_DATA["username"], - data=TEST_DATA, + unique_id=TEST_CLOUD_DATA["username"], + data=TEST_CLOUD_DATA, ) entry.add_to_hass(hass) diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 8fb4daf8624..55c75823a38 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -2,19 +2,17 @@ from datetime import timedelta from unittest.mock import MagicMock, PropertyMock, patch +import pytest + from homeassistant.components.risco import ( LAST_EVENT_TIMESTAMP_KEY, CannotConnectError, UnauthorizedError, ) -from homeassistant.components.risco.const import DOMAIN from homeassistant.helpers import entity_registry as er from homeassistant.util import dt -from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco -from .util import two_zone_alarm # noqa: F401 - -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import async_fire_time_changed ENTITY_IDS = { "Alarm": "sensor.risco_test_site_name_alarm_events", @@ -109,34 +107,23 @@ CATEGORIES_TO_EVENTS = { } -async def test_cannot_connect(hass): - """Test connection error.""" - +@pytest.fixture +def _no_zones_and_partitions(): with patch( - "homeassistant.components.risco.RiscoCloud.login", - side_effect=CannotConnectError, + "homeassistant.components.risco.RiscoLocal.zones", + new_callable=PropertyMock(return_value=[]), + ), patch( + "homeassistant.components.risco.RiscoLocal.partitions", + new_callable=PropertyMock(return_value=[]), ): - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - registry = er.async_get(hass) - for id in ENTITY_IDS.values(): - assert not registry.async_is_registered(id) + yield -async def test_unauthorized(hass): - """Test unauthorized error.""" - - with patch( - "homeassistant.components.risco.RiscoCloud.login", - side_effect=UnauthorizedError, - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() +@pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) +async def test_error_on_login(hass, login_with_error, cloud_config_entry): + """Test error on login.""" + await hass.config_entries.async_setup(cloud_config_entry.entry_id) + await hass.async_block_till_done() registry = er.async_get(hass) for id in ENTITY_IDS.values(): @@ -166,29 +153,31 @@ def _check_state(hass, category, entity_id): assert "zone_entity_id" not in state.attributes -async def test_setup(hass, two_zone_alarm): # noqa: F811 - """Test entity setup.""" +@pytest.fixture +def _set_utc_time_zone(hass): hass.config.set_time_zone("UTC") - registry = er.async_get(hass) - for id in ENTITY_IDS.values(): - assert not registry.async_is_registered(id) +@pytest.fixture +def _save_mock(): with patch( - "homeassistant.components.risco.RiscoCloud.site_uuid", - new_callable=PropertyMock(return_value=TEST_SITE_UUID), - ), patch( "homeassistant.components.risco.Store.async_save", ) as save_mock: - await setup_risco(hass, TEST_EVENTS) - for id in ENTITY_IDS.values(): - assert registry.async_is_registered(id) + yield save_mock - save_mock.assert_awaited_once_with( - {LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time} - ) - for category, entity_id in ENTITY_IDS.items(): - _check_state(hass, category, entity_id) + +@pytest.mark.parametrize("events", [TEST_EVENTS]) +async def test_cloud_setup( + hass, two_zone_cloud, _set_utc_time_zone, _save_mock, setup_risco_cloud +): + """Test entity setup.""" + registry = er.async_get(hass) + for id in ENTITY_IDS.values(): + assert registry.async_is_registered(id) + + _save_mock.assert_awaited_once_with({LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}) + for category, entity_id in ENTITY_IDS.items(): + _check_state(hass, category, entity_id) with patch( "homeassistant.components.risco.RiscoCloud.get_events", return_value=[] @@ -202,3 +191,10 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 for category, entity_id in ENTITY_IDS.items(): _check_state(hass, category, entity_id) + + +async def test_local_setup(hass, setup_risco_local, _no_zones_and_partitions): + """Test entity setup.""" + registry = er.async_get(hass) + for id in ENTITY_IDS.values(): + assert not registry.async_is_registered(id) diff --git a/tests/components/risco/util.py b/tests/components/risco/util.py index 3fa81586d27..b2600383f2a 100644 --- a/tests/components/risco/util.py +++ b/tests/components/risco/util.py @@ -1,74 +1,12 @@ """Utilities for Risco tests.""" -from unittest.mock import MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock -from pytest import fixture - -from homeassistant.components.risco.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME - -from tests.common import MockConfigEntry - -TEST_CONFIG = { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_PIN: "1234", -} TEST_SITE_UUID = "test-site-uuid" TEST_SITE_NAME = "test-site-name" -async def setup_risco(hass, events=[], options={}): - """Set up a Risco integration for testing.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, options=options) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.risco.RiscoCloud.login", - return_value=True, - ), patch( - "homeassistant.components.risco.RiscoCloud.site_uuid", - new_callable=PropertyMock(return_value=TEST_SITE_UUID), - ), patch( - "homeassistant.components.risco.RiscoCloud.site_name", - new_callable=PropertyMock(return_value=TEST_SITE_NAME), - ), patch( - "homeassistant.components.risco.RiscoCloud.close" - ), patch( - "homeassistant.components.risco.RiscoCloud.get_events", - return_value=events, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -def _zone_mock(): +def zone_mock(): + """Return a mocked zone.""" return MagicMock( - triggered=False, - bypassed=False, + triggered=False, bypassed=False, bypass=AsyncMock(return_value=True) ) - - -@fixture -def two_zone_alarm(): - """Fixture to mock alarm with two zones.""" - zone_mocks = {0: _zone_mock(), 1: _zone_mock()} - alarm_mock = MagicMock() - with patch.object( - zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) - ), patch.object( - zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") - ), patch.object( - zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) - ), patch.object( - zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") - ), patch.object( - alarm_mock, - "zones", - new_callable=PropertyMock(return_value=zone_mocks), - ), patch( - "homeassistant.components.risco.RiscoCloud.get_state", - return_value=alarm_mock, - ): - yield alarm_mock From a4d7130d7a03e3ae9e3e8b3aac19cbd55e91fe84 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Aug 2022 15:01:24 +0200 Subject: [PATCH 593/903] Fix unneeded inheritance in LaMetric base entity (#77260) --- homeassistant/components/lametric/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lametric/entity.py b/homeassistant/components/lametric/entity.py index 1e31b2968af..35810df0273 100644 --- a/homeassistant/components/lametric/entity.py +++ b/homeassistant/components/lametric/entity.py @@ -2,14 +2,14 @@ from __future__ import annotations from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import LaMetricDataUpdateCoordinator -class LaMetricEntity(CoordinatorEntity[LaMetricDataUpdateCoordinator], Entity): +class LaMetricEntity(CoordinatorEntity[LaMetricDataUpdateCoordinator]): """Defines a LaMetric entity.""" _attr_has_entity_name = True From 7f4c5c04d39885ea31d316ebbd9306dc0cf1d1a8 Mon Sep 17 00:00:00 2001 From: McYars <70546784+McYars@users.noreply.github.com> Date: Wed, 24 Aug 2022 16:09:01 +0300 Subject: [PATCH 594/903] Add Xiaomi Smartmi Fresh Air System XFXTDFR02ZM (#76637) --- homeassistant/components/xiaomi_miio/const.py | 12 ++++++++++++ homeassistant/components/xiaomi_miio/number.py | 3 +++ homeassistant/components/xiaomi_miio/select.py | 4 ++++ homeassistant/components/xiaomi_miio/sensor.py | 2 ++ homeassistant/components/xiaomi_miio/switch.py | 3 +++ 5 files changed, 24 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 54289bb8389..c0711a02a36 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -78,6 +78,7 @@ MODEL_AIRHUMIDIFIER_MJJSQ = "deerma.humidifier.mjjsq" MODEL_AIRFRESH_A1 = "dmaker.airfresh.a1" MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2" +MODEL_AIRFRESH_VA4 = "zhimi.airfresh.va4" MODEL_AIRFRESH_T2017 = "dmaker.airfresh.t2017" MODEL_FAN_1C = "dmaker.fan.1c" @@ -136,6 +137,7 @@ MODELS_PURIFIER_MIIO = [ MODEL_AIRPURIFIER_2H, MODEL_AIRFRESH_A1, MODEL_AIRFRESH_VA2, + MODEL_AIRFRESH_VA4, MODEL_AIRFRESH_T2017, ] MODELS_HUMIDIFIER_MIIO = [ @@ -415,6 +417,16 @@ FEATURE_FLAGS_AIRFRESH = ( | FEATURE_SET_EXTRA_FEATURES ) +FEATURE_FLAGS_AIRFRESH_VA4 = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED + | FEATURE_SET_LED_BRIGHTNESS + | FEATURE_RESET_FILTER + | FEATURE_SET_EXTRA_FEATURES + | FEATURE_SET_PTC +) + FEATURE_FLAGS_AIRFRESH_T2017 = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_DISPLAY | FEATURE_SET_PTC ) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 2d3106ddd4e..1bc843463e8 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -23,6 +23,7 @@ from .const import ( FEATURE_FLAGS_AIRFRESH, FEATURE_FLAGS_AIRFRESH_A1, FEATURE_FLAGS_AIRFRESH_T2017, + FEATURE_FLAGS_AIRFRESH_VA4, FEATURE_FLAGS_AIRHUMIDIFIER_CA4, FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, FEATURE_FLAGS_AIRPURIFIER_2S, @@ -54,6 +55,7 @@ from .const import ( MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, + MODEL_AIRFRESH_VA4, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -226,6 +228,7 @@ NUMBER_TYPES = { MODEL_TO_FEATURES_MAP = { MODEL_AIRFRESH_A1: FEATURE_FLAGS_AIRFRESH_A1, MODEL_AIRFRESH_VA2: FEATURE_FLAGS_AIRFRESH, + MODEL_AIRFRESH_VA4: FEATURE_FLAGS_AIRFRESH_VA4, MODEL_AIRFRESH_T2017: FEATURE_FLAGS_AIRFRESH_T2017, MODEL_AIRHUMIDIFIER_CA1: FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, MODEL_AIRHUMIDIFIER_CA4: FEATURE_FLAGS_AIRHUMIDIFIER_CA4, diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 14ceac7f2f4..69f8dbfad30 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -40,6 +40,7 @@ from .const import ( KEY_DEVICE, MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, + MODEL_AIRFRESH_VA4, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -89,6 +90,9 @@ MODEL_TO_ATTR_MAP: dict[str, list] = { MODEL_AIRFRESH_VA2: [ AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirfreshLedBrightness) ], + MODEL_AIRFRESH_VA4: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirfreshLedBrightness) + ], MODEL_AIRHUMIDIFIER_CA1: [ AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness) ], diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 5370357c58c..819d95f208c 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -57,6 +57,7 @@ from .const import ( MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, + MODEL_AIRFRESH_VA4, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRPURIFIER_3C, @@ -516,6 +517,7 @@ FAN_ZA5_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) MODEL_TO_SENSORS_MAP: dict[str, tuple[str, ...]] = { MODEL_AIRFRESH_A1: AIRFRESH_SENSORS_A1, MODEL_AIRFRESH_VA2: AIRFRESH_SENSORS, + MODEL_AIRFRESH_VA4: AIRFRESH_SENSORS, MODEL_AIRFRESH_T2017: AIRFRESH_SENSORS_T2017, MODEL_AIRHUMIDIFIER_CA1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRHUMIDIFIER_CB1: HUMIDIFIER_CA1_CB1_SENSORS, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 89cc80ce74f..577f9af837f 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -37,6 +37,7 @@ from .const import ( FEATURE_FLAGS_AIRFRESH, FEATURE_FLAGS_AIRFRESH_A1, FEATURE_FLAGS_AIRFRESH_T2017, + FEATURE_FLAGS_AIRFRESH_VA4, FEATURE_FLAGS_AIRHUMIDIFIER, FEATURE_FLAGS_AIRHUMIDIFIER_CA4, FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, @@ -72,6 +73,7 @@ from .const import ( MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, + MODEL_AIRFRESH_VA4, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -182,6 +184,7 @@ SERVICE_TO_METHOD = { MODEL_TO_FEATURES_MAP = { MODEL_AIRFRESH_A1: FEATURE_FLAGS_AIRFRESH_A1, MODEL_AIRFRESH_VA2: FEATURE_FLAGS_AIRFRESH, + MODEL_AIRFRESH_VA4: FEATURE_FLAGS_AIRFRESH_VA4, MODEL_AIRFRESH_T2017: FEATURE_FLAGS_AIRFRESH_T2017, MODEL_AIRHUMIDIFIER_CA1: FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, MODEL_AIRHUMIDIFIER_CA4: FEATURE_FLAGS_AIRHUMIDIFIER_CA4, From 4d02cccd113680698a15f2b737647ea0a74e508a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Aug 2022 15:15:29 +0200 Subject: [PATCH 595/903] Fix typing of ConfigEntrySelector (#77259) --- homeassistant/helpers/selector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c2d221bcc78..7f7a4d16595 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -340,9 +340,9 @@ class ConfigEntrySelector(Selector): """Instantiate a selector.""" super().__init__(config) - def __call__(self, data: Any) -> dict[str, str]: + def __call__(self, data: Any) -> str: """Validate the passed selection.""" - config: dict[str, str] = vol.Schema(str)(data) + config: str = vol.Schema(str)(data) return config From 7ee47f0f2603a70f4b09d6e00115313f100fdf1d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 24 Aug 2022 15:41:35 +0200 Subject: [PATCH 596/903] Adjust inheritance in homeworks (#77265) --- .../components/homeworks/__init__.py | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index 689709aca52..3d9023d16cb 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant, callback 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.entity import Entity from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -95,30 +96,18 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: return True -class HomeworksDevice: +class HomeworksDevice(Entity): """Base class of a Homeworks device.""" + _attr_should_poll = False + def __init__(self, controller, addr, name): """Initialize Homeworks device.""" self._addr = addr - self._name = name + self._attr_name = name + self._attr_unique_id = f"homeworks.{self._addr}" self._controller = controller - @property - def unique_id(self): - """Return a unique identifier.""" - return f"homeworks.{self._addr}" - - @property - def name(self): - """Device name.""" - return self._name - - @property - def should_poll(self): - """No need to poll.""" - return False - class HomeworksKeypadEvent: """When you want signals instead of entities. From d1486d04d9aca2bf5cd407fce02e64e2fa5452c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 12:17:28 -0500 Subject: [PATCH 597/903] Add support for bleak passive scanning on linux (#75542) --- .../components/bluetooth/__init__.py | 32 +++--- .../components/bluetooth/config_flow.py | 52 ++++++++- homeassistant/components/bluetooth/const.py | 1 + homeassistant/components/bluetooth/scanner.py | 30 ++++- .../components/bluetooth/strings.json | 10 ++ .../components/bluetooth/translations/en.json | 8 +- tests/components/bluetooth/conftest.py | 5 + .../components/bluetooth/test_config_flow.py | 104 ++++++++++++++++++ tests/components/bluetooth/test_init.py | 47 ++++++++ 9 files changed, 260 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 58ca4a6976b..208bbe6952b 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -9,8 +9,8 @@ from typing import TYPE_CHECKING, cast import async_timeout -from homeassistant import config_entries from homeassistant.components import usb +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady @@ -25,6 +25,7 @@ from .const import ( ADAPTER_SW_VERSION, CONF_ADAPTER, CONF_DETAILS, + CONF_PASSIVE, DATA_MANAGER, DEFAULT_ADDRESS, DOMAIN, @@ -51,7 +52,6 @@ if TYPE_CHECKING: from homeassistant.helpers.typing import ConfigType - __all__ = [ "async_ble_device_from_address", "async_discovered_service_info", @@ -213,7 +213,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.data[DATA_MANAGER] = models.MANAGER = manager - adapters = await manager.async_get_bluetooth_adapters() async_migrate_entries(hass, adapters) @@ -249,8 +248,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @hass_callback def async_migrate_entries( - hass: HomeAssistant, - adapters: dict[str, AdapterDetails], + hass: HomeAssistant, adapters: dict[str, AdapterDetails] ) -> None: """Migrate config entries to support multiple.""" current_entries = hass.config_entries.async_entries(DOMAIN) @@ -284,15 +282,13 @@ async def async_discover_adapters( discovery_flow.async_create_flow( hass, DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: adapter, CONF_DETAILS: details}, ) async def async_update_device( - hass: HomeAssistant, - entry: config_entries.ConfigEntry, - adapter: str, + hass: HomeAssistant, entry: ConfigEntry, adapter: str ) -> None: """Update device registry entry. @@ -314,9 +310,7 @@ async def async_update_device( ) -async def async_setup_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for a bluetooth scanner.""" address = entry.unique_id assert address is not None @@ -326,8 +320,10 @@ async def async_setup_entry( f"Bluetooth adapter {adapter} with address {address} not found" ) + passive = entry.options.get(CONF_PASSIVE) + mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE try: - bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter) + bleak_scanner = create_bleak_scanner(mode, adapter) except RuntimeError as err: raise ConfigEntryNotReady( f"{adapter_human_name(adapter, address)}: {err}" @@ -342,12 +338,16 @@ async def async_setup_entry( entry.async_on_unload(async_register_scanner(hass, scanner, True)) await async_update_device(hass, entry, adapter) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner + entry.async_on_unload(entry.add_update_listener(async_update_listener)) return True -async def async_unload_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> bool: +async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" scanner: HaScanner = hass.data[DOMAIN].pop(entry.entry_id) await scanner.async_stop() diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 2435a1e39ed..0fa2304468f 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,15 +1,24 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations +import platform from typing import TYPE_CHECKING, Any, cast import voluptuous as vol from homeassistant.components import onboarding -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.core import callback from homeassistant.helpers.typing import DiscoveryInfoType -from .const import ADAPTER_ADDRESS, CONF_ADAPTER, CONF_DETAILS, DOMAIN, AdapterDetails +from .const import ( + ADAPTER_ADDRESS, + CONF_ADAPTER, + CONF_DETAILS, + CONF_PASSIVE, + DOMAIN, + AdapterDetails, +) from .util import adapter_human_name, adapter_unique_name, async_get_bluetooth_adapters if TYPE_CHECKING: @@ -112,3 +121,42 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle a flow initialized by the user.""" return await self.async_step_multiple_adapters() + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + @classmethod + @callback + def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: + """Return options flow support for this handler.""" + return platform.system() == "Linux" + + +class OptionsFlowHandler(OptionsFlow): + """Handle the option flow for bluetooth.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """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_PASSIVE, + default=self.config_entry.options.get(CONF_PASSIVE, False), + ): bool, + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index d6f7b515532..540310e9747 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -8,6 +8,7 @@ DOMAIN = "bluetooth" CONF_ADAPTER = "adapter" CONF_DETAILS = "details" +CONF_PASSIVE = "passive" WINDOWS_DEFAULT_BLUETOOTH_ADAPTER = "bluetooth" MACOS_DEFAULT_BLUETOOTH_ADAPTER = "Core Bluetooth" diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 8805b0adaf2..d186f613c94 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -7,10 +7,14 @@ from datetime import datetime import logging import platform import time +from typing import Any import async_timeout import bleak from bleak import BleakError +from bleak.assigned_numbers import AdvertisementDataType +from bleak.backends.bluezdbus.advertisement_monitor import OrPattern +from bleak.backends.bluezdbus.scanner import BlueZScannerArgs from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from dbus_next import InvalidMessageError @@ -38,7 +42,15 @@ from .util import adapter_human_name, async_reset_adapter OriginalBleakScanner = bleak.BleakScanner MONOTONIC_TIME = time.monotonic - +# or_patterns is a workaround for the fact that passive scanning +# needs at least one matcher to be set. The below matcher +# will match all devices. +PASSIVE_SCANNER_ARGS = BlueZScannerArgs( + or_patterns=[ + OrPattern(0, AdvertisementDataType.FLAGS, b"\x06"), + OrPattern(0, AdvertisementDataType.FLAGS, b"\x1a"), + ] +) _LOGGER = logging.getLogger(__name__) @@ -81,13 +93,19 @@ def create_bleak_scanner( scanning_mode: BluetoothScanningMode, adapter: str | None ) -> bleak.BleakScanner: """Create a Bleak scanner.""" - scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} - # Only Linux supports multiple adapters - if adapter and platform.system() == "Linux": - scanner_kwargs["adapter"] = adapter + scanner_kwargs: dict[str, Any] = { + "scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode] + } + if platform.system() == "Linux": + # Only Linux supports multiple adapters + if adapter: + scanner_kwargs["adapter"] = adapter + if scanning_mode == BluetoothScanningMode.PASSIVE: + scanner_kwargs["bluez"] = PASSIVE_SCANNER_ARGS _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) + try: - return OriginalBleakScanner(**scanner_kwargs) # type: ignore[arg-type] + return OriginalBleakScanner(**scanner_kwargs) except (FileNotFoundError, BleakError) as ex: raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index 269995192a8..1912242ea6a 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -25,5 +25,15 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "no_adapters": "No unconfigured Bluetooth adapters found" } + }, + "options": { + "step": { + "init": { + "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled.", + "data": { + "passive": "Passive listening" + } + } + } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 5b40308cd3c..940cf5ebf88 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -33,8 +30,9 @@ "step": { "init": { "data": { - "adapter": "The Bluetooth Adapter to use for scanning" - } + "passive": "Passive listening" + }, + "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled." } } } diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 5ddd0fbc15f..1ea9b8706d4 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -28,6 +28,11 @@ def windows_adapter(): def one_adapter_fixture(): """Fixture that mocks one adapter on Linux.""" with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ), patch( + "homeassistant.components.bluetooth.scanner.platform.system", + return_value="Linux", + ), patch( "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" ), patch( "bluetooth_adapters.get_bluetooth_adapter_details", diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index e16208b3d70..61763eef257 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -6,11 +6,13 @@ from homeassistant import config_entries from homeassistant.components.bluetooth.const import ( CONF_ADAPTER, CONF_DETAILS, + CONF_PASSIVE, DEFAULT_ADDRESS, DOMAIN, AdapterDetails, ) from homeassistant.data_entry_flow import FlowResultType +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -235,3 +237,105 @@ async def test_async_step_integration_discovery_already_exists(hass): ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter): + """Test options on Linux.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="00:00:00:00:00:01", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_PASSIVE: True, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_PASSIVE] is True + + # Verify we can change it to False + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_PASSIVE: False, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_PASSIVE] is False + + +@patch( + "homeassistant.components.bluetooth.config_flow.platform.system", + return_value="Darwin", +) +async def test_options_flow_disabled_macos(mock_system, hass, hass_ws_client): + """Test options are disabled on MacOS.""" + await async_setup_component(hass, "config", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + ) + entry.add_to_hass(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/get", + "domain": "bluetooth", + "type_filter": "integration", + } + ) + response = await ws_client.receive_json() + assert response["result"][0]["supports_options"] is False + + +@patch( + "homeassistant.components.bluetooth.config_flow.platform.system", + return_value="Linux", +) +async def test_options_flow_enabled_linux(mock_system, hass, hass_ws_client): + """Test options are enabled on Linux.""" + await async_setup_component(hass, "config", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + ) + entry.add_to_hass(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/get", + "domain": "bluetooth", + "type_filter": "integration", + } + ) + response = await ws_client.receive_json() + assert response["result"][0]["supports_options"] is True diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 81b25a6c0dd..bfd327bfc03 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -20,6 +20,7 @@ from homeassistant.components.bluetooth import ( scanner, ) from homeassistant.components.bluetooth.const import ( + CONF_PASSIVE, DEFAULT_ADDRESS, DOMAIN, SOURCE_LOCAL, @@ -61,6 +62,52 @@ async def test_setup_and_stop(hass, mock_bleak_scanner_start, enable_bluetooth): assert len(mock_bleak_scanner_start.mock_calls) == 1 +async def test_setup_and_stop_passive(hass, mock_bleak_scanner_start, one_adapter): + """Test we and setup and stop the scanner the passive scanner.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, + data={}, + options={CONF_PASSIVE: True}, + unique_id="00:00:00:00:00:01", + ) + entry.add_to_hass(hass) + init_kwargs = None + + class MockPassiveBleakScanner: + def __init__(self, *args, **kwargs): + """Init the scanner.""" + nonlocal init_kwargs + init_kwargs = kwargs + + async def start(self, *args, **kwargs): + """Start the scanner.""" + + async def stop(self, *args, **kwargs): + """Stop the scanner.""" + + def register_detection_callback(self, *args, **kwargs): + """Register a callback.""" + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", + MockPassiveBleakScanner, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + 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 init_kwargs == { + "adapter": "hci0", + "bluez": scanner.PASSIVE_SCANNER_ARGS, + "scanning_mode": "passive", + } + + async def test_setup_and_stop_no_bluetooth(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth is not available.""" mock_bt = [ From cfa26ae0cab721f984050f08e11b2e9a8ebebfac Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 25 Aug 2022 03:53:22 +1000 Subject: [PATCH 598/903] Migrate Hunter Douglas Powerview to aiopvapi 2.0.0 (#76998) --- .../hunterdouglas_powerview/cover.py | 52 +++++++++++-------- .../hunterdouglas_powerview/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 7c4c90a2132..d444354b7a8 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -19,8 +19,7 @@ from aiopvapi.resources.shade import ( MAX_POSITION, MIN_POSITION, BaseShade, - ShadeTdbu, - Silhouette, + ShadeTopDownBottomUp, factory as PvShade, ) import async_timeout @@ -116,17 +115,18 @@ def create_powerview_shade_entity( name_before_refresh: str, ) -> Iterable[ShadeEntity]: """Create a PowerViewShade entity.""" + classes: list[BaseShade] = [] - # order here is important as both ShadeTDBU are listed in aiovapi as can_tilt - # and both require their own class here to work - if isinstance(shade, ShadeTdbu): + if isinstance(shade, ShadeTopDownBottomUp): classes.extend([PowerViewShadeTDBUTop, PowerViewShadeTDBUBottom]) - elif isinstance(shade, Silhouette): - classes.append(PowerViewShadeSilhouette) - elif shade.can_tilt: + elif ( # this will be extended further in next release for more defined control + shade.capability.capabilities.tiltOnClosed + or shade.capability.capabilities.tiltAnywhere + ): classes.append(PowerViewShadeWithTilt) else: classes.append(PowerViewShade) + _LOGGER.debug("%s (%s) detected as %a", shade.name, shade.capability.type, classes) return [ cls(coordinator, device_info, room_name, shade, name_before_refresh) for cls in classes @@ -392,8 +392,13 @@ class PowerViewShade(PowerViewShadeBase): ) -class PowerViewShadeTDBU(PowerViewShade): - """Representation of a PowerView shade with top/down bottom/up capabilities.""" +class PowerViewShadeDualRailBase(PowerViewShade): + """Representation of a shade with top/down bottom/up capabilities. + + Base methods shared between the two shades created + Child Classes: PowerViewShadeTDBUBottom / PowerViewShadeTDBUTop + API Class: ShadeTopDownBottomUp + """ @property def transition_steps(self) -> int: @@ -403,8 +408,13 @@ class PowerViewShadeTDBU(PowerViewShade): ) + hd_position_to_hass(self.positions.secondary, MAX_POSITION) -class PowerViewShadeTDBUBottom(PowerViewShadeTDBU): - """Representation of a top down bottom up powerview shade.""" +class PowerViewShadeTDBUBottom(PowerViewShadeDualRailBase): + """Representation of the bottom PowerViewShadeDualRailBase shade. + + These shades have top/down bottom up functionality and two entiites. + Sibling Class: PowerViewShadeTDBUTop + API Class: ShadeTopDownBottomUp + """ def __init__( self, @@ -440,8 +450,13 @@ class PowerViewShadeTDBUBottom(PowerViewShadeTDBU): ) -class PowerViewShadeTDBUTop(PowerViewShadeTDBU): - """Representation of a top down bottom up powerview shade.""" +class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase): + """Representation of the top PowerViewShadeDualRailBase shade. + + These shades have top/down bottom up functionality and two entiites. + Sibling Class: PowerViewShadeTDBUBottom + API Class: ShadeTopDownBottomUp + """ def __init__( self, @@ -516,8 +531,6 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): class PowerViewShadeWithTilt(PowerViewShade): """Representation of a PowerView shade with tilt capabilities.""" - _max_tilt = MAX_POSITION - def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, @@ -535,6 +548,7 @@ class PowerViewShadeWithTilt(PowerViewShade): ) if self._device_info.model != LEGACY_DEVICE_MODEL: self._attr_supported_features |= CoverEntityFeature.STOP_TILT + self._max_tilt = self._shade.shade_limits.tilt_max @property def current_cover_tilt_position(self) -> int: @@ -628,9 +642,3 @@ class PowerViewShadeWithTilt(PowerViewShade): async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilting.""" await self.async_stop_cover() - - -class PowerViewShadeSilhouette(PowerViewShadeWithTilt): - """Representation of a Silhouette PowerView shade.""" - - _max_tilt = 32767 diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 8e2206b2778..d3711313c1d 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,7 +2,7 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==1.6.19"], + "requirements": ["aiopvapi==2.0.0"], "codeowners": ["@bdraco", "@kingy444", "@trullock"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 82c49c0a121..47ceace6e0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -223,7 +223,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==1.6.19 +aiopvapi==2.0.0 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c9ad0f0d46..53c18eee40d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,7 +198,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==1.6.19 +aiopvapi==2.0.0 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 From 647eb9650de54bc437481134960c492e00a72a68 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 24 Aug 2022 23:50:07 +0300 Subject: [PATCH 599/903] Add remote learn command to BraviaTV (#76655) * add bravia remote learn * unwrap --- homeassistant/components/braviatv/coordinator.py | 14 ++++++++++++++ homeassistant/components/braviatv/remote.py | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index b5d91263b34..bb6bf59f681 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -10,6 +10,7 @@ from typing import Any, Final, TypeVar from pybravia import BraviaTV, BraviaTVError from typing_extensions import Concatenate, ParamSpec +from homeassistant.components import persistent_notification from homeassistant.components.media_player.const import ( MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, @@ -256,3 +257,16 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): for _ in range(repeats): for cmd in command: await self.client.send_command(cmd) + + @catch_braviatv_errors + async def async_learn_command(self, entity_id: str) -> None: + """Display a list of available commands in a persistent notification.""" + commands = await self.client.get_command_list() + codes = ", ".join(commands.keys()) + title = "Bravia TV" + message = f"**List of available commands for `{entity_id}`**:\n\n{codes}" + persistent_notification.async_create( + self.hass, + title=title, + message=message, + ) diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index f45b2d74004..411b459fced 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -47,3 +47,7 @@ class BraviaTVRemote(BraviaTVEntity, RemoteEntity): """Send a command to device.""" repeats = kwargs[ATTR_NUM_REPEATS] await self.coordinator.async_send_command(command, repeats) + + async def async_learn_command(self, **kwargs: Any) -> None: + """Learn commands from the device.""" + await self.coordinator.async_learn_command(self.entity_id) From 5d1c9a2e94016793bd302b2ecd50dae168585ea3 Mon Sep 17 00:00:00 2001 From: yllar Date: Wed, 24 Aug 2022 23:54:59 +0300 Subject: [PATCH 600/903] Songpal dependency upgrade (#77278) Bump python-songpal to 0.15 --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 8825de877e5..2aa58b16a7e 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Sony Songpal", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": ["python-songpal==0.14.1"], + "requirements": ["python-songpal==0.15"], "codeowners": ["@rytilahti", "@shenxn"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 47ceace6e0b..57ba371c2b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1975,7 +1975,7 @@ python-ripple-api==0.0.3 python-smarttub==0.0.32 # homeassistant.components.songpal -python-songpal==0.14.1 +python-songpal==0.15 # homeassistant.components.tado python-tado==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53c18eee40d..bed1b9ad2da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1350,7 +1350,7 @@ python-picnic-api==1.1.0 python-smarttub==0.0.32 # homeassistant.components.songpal -python-songpal==0.14.1 +python-songpal==0.15 # homeassistant.components.tado python-tado==0.12.0 From 109d5c70841fad369c07e873c53d6dd57aac162c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 17:36:21 -0500 Subject: [PATCH 601/903] Fix bluetooth discovery when advertisement format changes (#77286) --- homeassistant/components/bluetooth/match.py | 22 ++++-- tests/components/bluetooth/test_init.py | 81 +++++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 333ba020b74..4a0aa8ee995 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -49,8 +49,8 @@ class IntegrationMatchHistory: """Track which fields have been seen.""" manufacturer_data: bool - service_data: bool - service_uuids: bool + service_data: set[str] + service_uuids: set[str] def seen_all_fields( @@ -59,9 +59,15 @@ def seen_all_fields( """Return if we have seen all fields.""" if not previous_match.manufacturer_data and advertisement_data.manufacturer_data: return False - if not previous_match.service_data and advertisement_data.service_data: + if advertisement_data.service_data and ( + not previous_match.service_data + or not previous_match.service_data.issuperset(advertisement_data.service_data) + ): return False - if not previous_match.service_uuids and advertisement_data.service_uuids: + if advertisement_data.service_uuids and ( + not previous_match.service_uuids + or not previous_match.service_uuids.issuperset(advertisement_data.service_uuids) + ): return False return True @@ -114,13 +120,13 @@ class IntegrationMatcher: previous_match.manufacturer_data |= bool( advertisement_data.manufacturer_data ) - previous_match.service_data |= bool(advertisement_data.service_data) - previous_match.service_uuids |= bool(advertisement_data.service_uuids) + previous_match.service_data |= set(advertisement_data.service_data) + previous_match.service_uuids |= set(advertisement_data.service_uuids) else: matched[device.address] = IntegrationMatchHistory( manufacturer_data=bool(advertisement_data.manufacturer_data), - service_data=bool(advertisement_data.service_data), - service_uuids=bool(advertisement_data.service_uuids), + service_data=set(advertisement_data.service_data), + service_uuids=set(advertisement_data.service_uuids), ) return matched_domains diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index bfd327bfc03..9b958e2fade 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -704,6 +704,87 @@ async def test_discovery_match_by_service_data_uuid_then_others( assert len(mock_config_flow.mock_calls) == 0 +async def test_discovery_match_by_service_data_uuid_when_format_changes( + hass, mock_bleak_scanner_start, macos_adapter +): + """Test bluetooth discovery match by service_data_uuid when format changes.""" + mock_bt = [ + { + "domain": "xiaomi_ble", + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb", + }, + { + "domain": "qingping", + "service_data_uuid": "0000fdcd-0000-1000-8000-00805f9b34fb", + }, + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + device = BLEDevice("44:44:33:11:23:45", "lock") + adv_without_service_data_uuid = AdvertisementData( + local_name="Qingping Temp RH M", + service_uuids=[], + manufacturer_data={}, + ) + xiaomi_format_adv = AdvertisementData( + local_name="Qingping Temp RH M", + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"0XH\x0b\x06\xa7%\x144-X\x08" + }, + ) + qingping_format_adv = AdvertisementData( + local_name="Qingping Temp RH M", + service_data={ + "0000fdcd-0000-1000-8000-00805f9b34fb": b"\x08\x16\xa7%\x144-X\x01\x04\xdb\x00\xa6\x01\x02\x01d" + }, + ) + # 1st discovery should not generate a flow because the + # service_data_uuid is not in the advertisement + inject_advertisement(hass, device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 2nd discovery should generate a flow because the + # service_data_uuid matches xiaomi format + inject_advertisement(hass, device, xiaomi_format_adv) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "xiaomi_ble" + mock_config_flow.reset_mock() + + # 4th discovery should generate a flow because the + # service_data_uuid matches qingping format + inject_advertisement(hass, device, qingping_format_adv) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "qingping" + mock_config_flow.reset_mock() + + # 5th discovery should not generate a flow because the + # we already saw an advertisement with the service_data_uuid + inject_advertisement(hass, device, qingping_format_adv) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 6th discovery should not generate a flow because the + # we already saw an advertisement with the service_data_uuid + inject_advertisement(hass, device, xiaomi_format_adv) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( hass, mock_bleak_scanner_start, macos_adapter ): From ae481948a3471f91de1c0fc15851c4ee9f51cf09 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 25 Aug 2022 01:46:22 +0200 Subject: [PATCH 602/903] Bump Accuweather library (#77285) Bump accuweather library --- 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 cbb74585778..f92dca9dfee 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.3.0"], + "requirements": ["accuweather==0.4.0"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 57ba371c2b0..32bd5984de4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -71,7 +71,7 @@ WazeRouteCalculator==0.14 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.3.0 +accuweather==0.4.0 # homeassistant.components.adax adax==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bed1b9ad2da..c6902049de3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -61,7 +61,7 @@ WazeRouteCalculator==0.14 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.3.0 +accuweather==0.4.0 # homeassistant.components.adax adax==0.2.0 From e79f1de9e954a9ad8025a7f9912e9de4b4e48c0b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 18:46:34 -0500 Subject: [PATCH 603/903] Bump qingping-ble to 0.6.0 (#77289) Adds support for the Qingping Temp RH M Changelog: https://github.com/Bluetooth-Devices/qingping-ble/compare/v0.5.0...v0.6.0 --- homeassistant/components/qingping/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 1eef6e2c471..4e1189f3782 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -11,7 +11,7 @@ "connectable": false } ], - "requirements": ["qingping-ble==0.5.0"], + "requirements": ["qingping-ble==0.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 32bd5984de4..ff3a248be34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2070,7 +2070,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.5.0 +qingping-ble==0.6.0 # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6902049de3..bad3671b282 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1418,7 +1418,7 @@ pyws66i==1.1 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.5.0 +qingping-ble==0.6.0 # homeassistant.components.rachio rachiopy==1.0.3 From d33f93d5caf6ea130ed1d52cab808537ea176aca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Aug 2022 01:46:59 +0200 Subject: [PATCH 604/903] Remove unnecessary property from hvv_departures (#77267) --- homeassistant/components/hvv_departures/binary_sensor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index aeaa0b1176c..f9ab469a48d 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -135,11 +135,6 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): """Return entity state.""" return self.coordinator.data[self.idx]["state"] - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - @property def available(self): """Return if entity is available.""" From 16b93d1af29743372788dbd2fbae11af43a7bbe3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 25 Aug 2022 00:28:02 +0000 Subject: [PATCH 605/903] [ci skip] Translation update --- .../android_ip_webcam/translations/ru.json | 3 +- .../components/bluetooth/translations/de.json | 6 ++-- .../components/bluetooth/translations/en.json | 4 +++ .../components/bluetooth/translations/es.json | 6 ++-- .../components/bluetooth/translations/id.json | 6 ++-- .../components/bluetooth/translations/it.json | 8 ++++-- .../components/lametric/translations/it.json | 4 +++ .../components/lyric/translations/ru.json | 2 +- .../p1_monitor/translations/it.json | 3 ++ .../p1_monitor/translations/ru.json | 3 ++ .../pure_energie/translations/it.json | 3 ++ .../pure_energie/translations/ru.json | 3 ++ .../components/pushover/translations/ru.json | 14 ++++++++++ .../components/risco/translations/de.json | 18 ++++++++++++ .../components/risco/translations/en.json | 5 ++++ .../components/risco/translations/es.json | 18 ++++++++++++ .../components/risco/translations/et.json | 18 ++++++++++++ .../components/risco/translations/fr.json | 18 ++++++++++++ .../components/risco/translations/id.json | 18 ++++++++++++ .../components/risco/translations/it.json | 18 ++++++++++++ .../risco/translations/zh-Hant.json | 18 ++++++++++++ .../components/senz/translations/ru.json | 2 +- .../simplepush/translations/ru.json | 2 +- .../components/skybell/translations/ru.json | 6 ++++ .../components/spotify/translations/ru.json | 2 +- .../steam_online/translations/ru.json | 2 +- .../volvooncall/translations/el.json | 28 +++++++++++++++++++ .../volvooncall/translations/es.json | 28 +++++++++++++++++++ .../volvooncall/translations/et.json | 28 +++++++++++++++++++ .../volvooncall/translations/hu.json | 28 +++++++++++++++++++ .../volvooncall/translations/id.json | 28 +++++++++++++++++++ .../volvooncall/translations/it.json | 27 ++++++++++++++++++ .../volvooncall/translations/no.json | 28 +++++++++++++++++++ .../volvooncall/translations/ru.json | 28 +++++++++++++++++++ .../volvooncall/translations/zh-Hant.json | 28 +++++++++++++++++++ .../components/zha/translations/ru.json | 3 ++ 36 files changed, 449 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/volvooncall/translations/el.json create mode 100644 homeassistant/components/volvooncall/translations/es.json create mode 100644 homeassistant/components/volvooncall/translations/et.json create mode 100644 homeassistant/components/volvooncall/translations/hu.json create mode 100644 homeassistant/components/volvooncall/translations/id.json create mode 100644 homeassistant/components/volvooncall/translations/it.json create mode 100644 homeassistant/components/volvooncall/translations/no.json create mode 100644 homeassistant/components/volvooncall/translations/ru.json create mode 100644 homeassistant/components/volvooncall/translations/zh-Hant.json diff --git a/homeassistant/components/android_ip_webcam/translations/ru.json b/homeassistant/components/android_ip_webcam/translations/ru.json index deeac93856b..6ef52da4383 100644 --- a/homeassistant/components/android_ip_webcam/translations/ru.json +++ b/homeassistant/components/android_ip_webcam/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." }, "error": { - "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.", + "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/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index 50723328c4a..be3d0fa074a 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "Der zum Scannen zu verwendende Bluetooth-Adapter" - } + "adapter": "Der zum Scannen zu verwendende Bluetooth-Adapter", + "passive": "Passives Mith\u00f6ren" + }, + "description": "Passives Mith\u00f6ren erfordert BlueZ 5.63 oder h\u00f6her mit aktivierten experimentellen Funktionen." } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 940cf5ebf88..a3a7b97260e 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,6 +9,9 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -30,6 +33,7 @@ "step": { "init": { "data": { + "adapter": "The Bluetooth Adapter to use for scanning", "passive": "Passive listening" }, "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled." diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json index 170f44e446f..e533454d929 100644 --- a/homeassistant/components/bluetooth/translations/es.json +++ b/homeassistant/components/bluetooth/translations/es.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "El adaptador Bluetooth que se usar\u00e1 para escanear" - } + "adapter": "El adaptador Bluetooth que se usar\u00e1 para escanear", + "passive": "Escucha pasiva" + }, + "description": "La escucha pasiva requiere BlueZ 5.63 o posterior con funciones experimentales habilitadas." } } } diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index a0ce38665b7..4caaa9bdd8a 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "Adaptor Bluetooth yang digunakan untuk pemindaian" - } + "adapter": "Adaptor Bluetooth yang digunakan untuk pemindaian", + "passive": "Mendengarkan secara pasif" + }, + "description": "Mendengarkan secara pasif memerlukan BlueZ 5.63 atau lebih baru dengan fitur eksperimental yang diaktifkan." } } } diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index fd03835ebce..244a0e84d59 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", - "no_adapters": "Nessun adattatore Bluetooth trovato" + "no_adapters": "Nessun adattatore Bluetooth non configurato trovato" }, "flow_title": "{name}", "step": { @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "L'adattatore Bluetooth da utilizzare per la scansione" - } + "adapter": "L'adattatore Bluetooth da utilizzare per la scansione", + "passive": "Ascolto passivo" + }, + "description": "L'ascolto passivo richiede BlueZ 5.63 o successivo con funzionalit\u00e0 sperimentali abilitate." } } } diff --git a/homeassistant/components/lametric/translations/it.json b/homeassistant/components/lametric/translations/it.json index 61159f16def..e41d40d7605 100644 --- a/homeassistant/components/lametric/translations/it.json +++ b/homeassistant/components/lametric/translations/it.json @@ -25,6 +25,10 @@ "data": { "api_key": "Chiave API", "host": "Host" + }, + "data_description": { + "api_key": "Puoi trovare questa chiave API nella [pagina dei dispositivi nel tuo account sviluppatore LaMetric](https://developer.lametric.com/user/devices).", + "host": "L'indirizzo IP o il nome host di LaMetric TIME sulla rete." } }, "pick_implementation": { diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json index 98fd17ed407..a1d3c342c87 100644 --- a/homeassistant/components/lyric/translations/ru.json +++ b/homeassistant/components/lyric/translations/ru.json @@ -20,7 +20,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Honeywell Lyric\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Honeywell Lyric\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Honeywell Lyric \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } diff --git a/homeassistant/components/p1_monitor/translations/it.json b/homeassistant/components/p1_monitor/translations/it.json index 61b6d9d75f0..ae529d7cb92 100644 --- a/homeassistant/components/p1_monitor/translations/it.json +++ b/homeassistant/components/p1_monitor/translations/it.json @@ -9,6 +9,9 @@ "host": "Host", "name": "Nome" }, + "data_description": { + "host": "L'indirizzo IP o il nome host dell'installazione di P1 Monitor." + }, "description": "Configura P1 Monitor per l'integrazione con Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/ru.json b/homeassistant/components/p1_monitor/translations/ru.json index 4f118fe952c..75a179abdc3 100644 --- a/homeassistant/components/p1_monitor/translations/ru.json +++ b/homeassistant/components/p1_monitor/translations/ru.json @@ -9,6 +9,9 @@ "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, + "data_description": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 P1 Monitor." + }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 P1 Monitor." } } diff --git a/homeassistant/components/pure_energie/translations/it.json b/homeassistant/components/pure_energie/translations/it.json index b7fb47b1ea4..f3b7419fc1d 100644 --- a/homeassistant/components/pure_energie/translations/it.json +++ b/homeassistant/components/pure_energie/translations/it.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "L'indirizzo IP o il nome host del tuo contatore Pure Energie." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/ru.json b/homeassistant/components/pure_energie/translations/ru.json index 7673757b245..2aa39757104 100644 --- a/homeassistant/components/pure_energie/translations/ru.json +++ b/homeassistant/components/pure_energie/translations/ru.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442" + }, + "data_description": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0412\u0430\u0448\u0435\u0433\u043e Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/ru.json b/homeassistant/components/pushover/translations/ru.json index ae879b1fd27..c6a1aff57b0 100644 --- a/homeassistant/components/pushover/translations/ru.json +++ b/homeassistant/components/pushover/translations/ru.json @@ -13,8 +13,22 @@ "reauth_confirm": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "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": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "user_key": "\u041a\u043b\u044e\u0447 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Pushover \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Pushover \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index 77d842353fc..6b356a473ce 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -9,11 +9,29 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "cloud": { + "data": { + "password": "Passwort", + "pin": "PIN-Code", + "username": "Benutzername" + } + }, + "local": { + "data": { + "host": "Host", + "pin": "PIN-Code", + "port": "Port" + } + }, "user": { "data": { "password": "Passwort", "pin": "PIN-Code", "username": "Benutzername" + }, + "menu_options": { + "cloud": "Risco-Cloud (empfohlen)", + "local": "Lokales Risco-Panel (fortgeschritten)" } } } diff --git a/homeassistant/components/risco/translations/en.json b/homeassistant/components/risco/translations/en.json index 95dd395e501..0d72ba3cca2 100644 --- a/homeassistant/components/risco/translations/en.json +++ b/homeassistant/components/risco/translations/en.json @@ -24,6 +24,11 @@ } }, "user": { + "data": { + "password": "Password", + "pin": "PIN Code", + "username": "Username" + }, "menu_options": { "cloud": "Risco Cloud (recommended)", "local": "Local Risco Panel (advanced)" diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json index ff6176afc5a..690887b66d2 100644 --- a/homeassistant/components/risco/translations/es.json +++ b/homeassistant/components/risco/translations/es.json @@ -9,11 +9,29 @@ "unknown": "Error inesperado" }, "step": { + "cloud": { + "data": { + "password": "Contrase\u00f1a", + "pin": "C\u00f3digo PIN", + "username": "Nombre de usuario" + } + }, + "local": { + "data": { + "host": "Host", + "pin": "C\u00f3digo PIN", + "port": "Puerto" + } + }, "user": { "data": { "password": "Contrase\u00f1a", "pin": "C\u00f3digo PIN", "username": "Nombre de usuario" + }, + "menu_options": { + "cloud": "Risco Cloud (recomendado)", + "local": "Panel Risco local (avanzado)" } } } diff --git a/homeassistant/components/risco/translations/et.json b/homeassistant/components/risco/translations/et.json index c57d5d73fec..4f8140c12fc 100644 --- a/homeassistant/components/risco/translations/et.json +++ b/homeassistant/components/risco/translations/et.json @@ -9,11 +9,29 @@ "unknown": "Tundmatu viga" }, "step": { + "cloud": { + "data": { + "password": "Salas\u00f5na", + "pin": "PIN kood", + "username": "Kasutajanimi" + } + }, + "local": { + "data": { + "host": "Host", + "pin": "PIN kood", + "port": "Port" + } + }, "user": { "data": { "password": "Salas\u00f5na", "pin": "PIN kood", "username": "Kasutajanimi" + }, + "menu_options": { + "cloud": "Risco Cloud (soovitatav)", + "local": "Kohalik Risco paneel (t\u00e4psem)" } } } diff --git a/homeassistant/components/risco/translations/fr.json b/homeassistant/components/risco/translations/fr.json index c83e0f8263f..3d12d0be5c8 100644 --- a/homeassistant/components/risco/translations/fr.json +++ b/homeassistant/components/risco/translations/fr.json @@ -9,11 +9,29 @@ "unknown": "Erreur inattendue" }, "step": { + "cloud": { + "data": { + "password": "Mot de passe", + "pin": "Code PIN", + "username": "Nom d'utilisateur" + } + }, + "local": { + "data": { + "host": "H\u00f4te", + "pin": "Code PIN", + "port": "Port" + } + }, "user": { "data": { "password": "Mot de passe", "pin": "Code PIN", "username": "Nom d'utilisateur" + }, + "menu_options": { + "cloud": "Risco Cloud (recommand\u00e9)", + "local": "Risco Panel local (avanc\u00e9)" } } } diff --git a/homeassistant/components/risco/translations/id.json b/homeassistant/components/risco/translations/id.json index eef1f00ffa6..438ff72a4ed 100644 --- a/homeassistant/components/risco/translations/id.json +++ b/homeassistant/components/risco/translations/id.json @@ -9,11 +9,29 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "cloud": { + "data": { + "password": "Kata Sandi", + "pin": "Kode PIN", + "username": "Nama Pengguna" + } + }, + "local": { + "data": { + "host": "Host", + "pin": "Kode PIN", + "port": "Port" + } + }, "user": { "data": { "password": "Kata Sandi", "pin": "Kode PIN", "username": "Nama Pengguna" + }, + "menu_options": { + "cloud": "Risco Cloud (disarankan)", + "local": "Panel Risco Lokal (lanjutan)" } } } diff --git a/homeassistant/components/risco/translations/it.json b/homeassistant/components/risco/translations/it.json index 503dca18db4..ebe78f338d9 100644 --- a/homeassistant/components/risco/translations/it.json +++ b/homeassistant/components/risco/translations/it.json @@ -9,11 +9,29 @@ "unknown": "Errore imprevisto" }, "step": { + "cloud": { + "data": { + "password": "Password", + "pin": "Codice PIN", + "username": "Nome utente" + } + }, + "local": { + "data": { + "host": "Host", + "pin": "Codice PIN", + "port": "Porta" + } + }, "user": { "data": { "password": "Password", "pin": "Codice PIN", "username": "Nome utente" + }, + "menu_options": { + "cloud": "Risco Cloud (consigliato)", + "local": "Pannello Risco locale (avanzato)" } } } diff --git a/homeassistant/components/risco/translations/zh-Hant.json b/homeassistant/components/risco/translations/zh-Hant.json index 7553ec3e36a..852ba76e208 100644 --- a/homeassistant/components/risco/translations/zh-Hant.json +++ b/homeassistant/components/risco/translations/zh-Hant.json @@ -9,11 +9,29 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "cloud": { + "data": { + "password": "\u5bc6\u78bc", + "pin": "PIN \u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, + "local": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "pin": "PIN \u78bc", + "port": "\u901a\u8a0a\u57e0" + } + }, "user": { "data": { "password": "\u5bc6\u78bc", "pin": "PIN \u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "menu_options": { + "cloud": "Risco Cloud\uff08\u63a8\u85a6\uff09", + "local": "\u672c\u5730\u7aef Risco \u9762\u677f\uff08\u9032\u968e\uff09" } } } diff --git a/homeassistant/components/senz/translations/ru.json b/homeassistant/components/senz/translations/ru.json index 0c8f912d2fe..11ddbf5a98c 100644 --- a/homeassistant/components/senz/translations/ru.json +++ b/homeassistant/components/senz/translations/ru.json @@ -19,7 +19,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"nVent RAYCHEM SENZ\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"nVent RAYCHEM SENZ\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 nVent RAYCHEM SENZ \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json index 1e7a61fb1cb..be00e5deb89 100644 --- a/homeassistant/components/simplepush/translations/ru.json +++ b/homeassistant/components/simplepush/translations/ru.json @@ -24,7 +24,7 @@ "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" }, "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Simplepush \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Simplepush\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } diff --git a/homeassistant/components/skybell/translations/ru.json b/homeassistant/components/skybell/translations/ru.json index 8c30e7c8ad6..50fe51447c8 100644 --- a/homeassistant/components/skybell/translations/ru.json +++ b/homeassistant/components/skybell/translations/ru.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Skybell\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Skybell \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index 869d839947c..c4cb0294e23 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -21,7 +21,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Spotify \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Spotify\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Spotify \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } }, diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index 48ec5e0c944..94f3fb33bb3 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Steam\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Steam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } }, diff --git a/homeassistant/components/volvooncall/translations/el.json b/homeassistant/components/volvooncall/translations/el.json new file mode 100644 index 00000000000..1911ff66fe3 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/el.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "mutable": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 / \u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1 / \u03ba.\u03bb\u03c0.", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae", + "scandinavian_miles": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03a3\u03ba\u03b1\u03bd\u03b4\u03b9\u03bd\u03b1\u03b2\u03b9\u03ba\u03ce\u03bd \u039c\u03b9\u03bb\u03af\u03c9\u03bd", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 Volvo On Call \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03bc\u03b5\u03bb\u03bb\u03bf\u03bd\u03c4\u03b9\u03ba\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Home Assistant. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Volvo On Call YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/es.json b/homeassistant/components/volvooncall/translations/es.json new file mode 100644 index 00000000000..2eff271f511 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/es.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "mutable": "Permitir el arranque / bloqueo a distancia / etc.", + "password": "Contrase\u00f1a", + "region": "Regi\u00f3n", + "scandinavian_miles": "Utilizar millas escandinavas", + "username": "Nombre de usuario" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de la plataforma Volvo On Call mediante YAML se eliminar\u00e1 en una versi\u00f3n futura de Home Assistant. \n\nTu configuraci\u00f3n existente se ha importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Volvo On Call" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/et.json b/homeassistant/components/volvooncall/translations/et.json new file mode 100644 index 00000000000..0469a09a381 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/et.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "mutable": "Luba kaugk\u00e4ivitus / lukustamine / jne.", + "password": "Salas\u00f5na", + "region": "Piirkond", + "scandinavian_miles": "Kasuta Scandinavian Miles", + "username": "Kasutajanimi" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Volvo On Call platvormi konfigureerimine YAML-i abil eemaldatakse Home Assistant'i tulevases versioonis.\n\nTeie olemasolev konfiguratsioon on automaatselt kasutajaliidesesse imporditud. Probleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Volvo On Call YAML-i konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/hu.json b/homeassistant/components/volvooncall/translations/hu.json new file mode 100644 index 00000000000..c8b3ff45d8c --- /dev/null +++ b/homeassistant/components/volvooncall/translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "mutable": "Enged\u00e9lyezze a t\u00e1voli ind\u00edt\u00e1st / z\u00e1r\u00e1st / stb.", + "password": "Jelsz\u00f3", + "region": "R\u00e9gi\u00f3", + "scandinavian_miles": "Skandin\u00e1v m\u00e9rf\u00f6ld haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A Volvo On Call platform YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa a Home Assistant egy j\u00f6v\u0151beli kiad\u00e1s\u00e1b\u00f3l elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletbe. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot.", + "title": "A Volvo On Call YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/id.json b/homeassistant/components/volvooncall/translations/id.json new file mode 100644 index 00000000000..df0b1efc0d6 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/id.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "mutable": "Izinkan Mulai/Kunci Jarak Jauh, dll.", + "password": "Kata Sandi", + "region": "Wilayah", + "scandinavian_miles": "Gunakan Mil Skandinavia", + "username": "Nama Pengguna" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi platform Volvo On Call lewat YAML dalam proses penghapusan di versi mendatang Home Assistant.\n\nKonfigurasi yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Volvo On Call dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/it.json b/homeassistant/components/volvooncall/translations/it.json new file mode 100644 index 00000000000..c933ff732b1 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "region": "Regione", + "scandinavian_miles": "Usa le miglia scandinave", + "username": "Nome utente" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione della piattaforma Volvo On Call tramite YAML sar\u00e0 rimossa in una release futura di Home Assistant.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Volvo On Call sar\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/no.json b/homeassistant/components/volvooncall/translations/no.json new file mode 100644 index 00000000000..51fd5531c57 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/no.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "mutable": "Tillat fjernstart / l\u00e5s / etc.", + "password": "Passord", + "region": "Region", + "scandinavian_miles": "Bruk Skandinaviske Miles", + "username": "Brukernavn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Volvo On Call-plattformen ved hjelp av YAML fjernes i en fremtidig versjon av Home Assistant. \n\n Din eksisterende konfigurasjon har blitt importert til brukergrensesnittet automatisk. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Volvo On Call YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/ru.json b/homeassistant/components/volvooncall/translations/ru.json new file mode 100644 index 00000000000..79d45c47f4d --- /dev/null +++ b/homeassistant/components/volvooncall/translations/ru.json @@ -0,0 +1,28 @@ +{ + "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": { + "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": { + "mutable": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0434\u0438\u0441\u0442\u0430\u043d\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0437\u0430\u043f\u0443\u0441\u043a / \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0443 \u0438 \u0442.\u0434.", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c", + "scandinavian_miles": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043a\u0430\u043d\u0434\u0438\u043d\u0430\u0432\u0441\u043a\u0438\u0435 \u043c\u0438\u043b\u0438", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Volvo On Call \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u0440\u0435\u043b\u0438\u0437\u0435 Home Assistant.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Volvo On Call \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/zh-Hant.json b/homeassistant/components/volvooncall/translations/zh-Hant.json new file mode 100644 index 00000000000..8167ca9b9b8 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/zh-Hant.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "mutable": "\u5141\u8a31\u9060\u7aef\u555f\u52d5/\u4e0a\u9396/\u7b49\u529f\u80fd\u3002", + "password": "\u5bc6\u78bc", + "region": "\u5340\u57df", + "scandinavian_miles": "\u4f7f\u7528\u7d0d\u7dad\u4e9e\u82f1\u91cc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Volvo On Call \u5373\u5c07\u65bc Home Assistant \u672a\u4f86\u7248\u672c\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Volvo On Call YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 6eae6313424..ef705784be8 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -13,6 +13,9 @@ "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "confirm_hardware": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, "pick_radio": { "data": { "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" From 859effee56ac13435d45aae3ea30ccc3047c1dc6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Aug 2022 03:59:00 +0200 Subject: [PATCH 606/903] Remove unnecessary property from fritz (#77269) --- homeassistant/components/fritz/common.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index d748bdcf7dd..610dcab6aaa 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -877,11 +877,6 @@ class FritzDeviceBase(update_coordinator.CoordinatorEntity[AvmWrapper]): return self._avm_wrapper.devices[self._mac].hostname return None - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - async def async_process_update(self) -> None: """Update device.""" raise NotImplementedError() From 2161b6f0498cc734151a128bc1d31ab24be7847d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 25 Aug 2022 04:00:54 +0200 Subject: [PATCH 607/903] Fix level controllable output controls in deCONZ (#77223) Fix level controllable output controls --- homeassistant/components/deconz/cover.py | 17 ++- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_cover.py | 108 ++++++++++++++++++ 5 files changed, 125 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 8df974cf146..3eac9cafd52 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any, cast from pydeconz.interfaces.lights import CoverAction +from pydeconz.models import ResourceType from pydeconz.models.event import EventType from pydeconz.models.light.cover import Cover @@ -23,9 +24,9 @@ from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_TYPE_TO_DEVICE_CLASS = { - "Level controllable output": CoverDeviceClass.DAMPER, - "Window covering controller": CoverDeviceClass.SHADE, - "Window covering device": CoverDeviceClass.SHADE, + ResourceType.LEVEL_CONTROLLABLE_OUTPUT.value: CoverDeviceClass.DAMPER, + ResourceType.WINDOW_COVERING_CONTROLLER.value: CoverDeviceClass.SHADE, + ResourceType.WINDOW_COVERING_DEVICE.value: CoverDeviceClass.SHADE, } @@ -71,6 +72,8 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): self._attr_device_class = DECONZ_TYPE_TO_DEVICE_CLASS.get(cover.type) + self.legacy_mode = cover.type == ResourceType.LEVEL_CONTROLLABLE_OUTPUT.value + @property def current_cover_position(self) -> int: """Return the current position of the cover.""" @@ -87,6 +90,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): await self.gateway.api.lights.covers.set_state( id=self._device.resource_id, lift=position, + legacy_mode=self.legacy_mode, ) async def async_open_cover(self, **kwargs: Any) -> None: @@ -94,6 +98,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): await self.gateway.api.lights.covers.set_state( id=self._device.resource_id, action=CoverAction.OPEN, + legacy_mode=self.legacy_mode, ) async def async_close_cover(self, **kwargs: Any) -> None: @@ -101,6 +106,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): await self.gateway.api.lights.covers.set_state( id=self._device.resource_id, action=CoverAction.CLOSE, + legacy_mode=self.legacy_mode, ) async def async_stop_cover(self, **kwargs: Any) -> None: @@ -108,6 +114,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): await self.gateway.api.lights.covers.set_state( id=self._device.resource_id, action=CoverAction.STOP, + legacy_mode=self.legacy_mode, ) @property @@ -123,6 +130,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): await self.gateway.api.lights.covers.set_state( id=self._device.resource_id, tilt=position, + legacy_mode=self.legacy_mode, ) async def async_open_cover_tilt(self, **kwargs: Any) -> None: @@ -130,6 +138,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): await self.gateway.api.lights.covers.set_state( id=self._device.resource_id, tilt=0, + legacy_mode=self.legacy_mode, ) async def async_close_cover_tilt(self, **kwargs: Any) -> None: @@ -137,6 +146,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): await self.gateway.api.lights.covers.set_state( id=self._device.resource_id, tilt=100, + legacy_mode=self.legacy_mode, ) async def async_stop_cover_tilt(self, **kwargs: Any) -> None: @@ -144,4 +154,5 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): await self.gateway.api.lights.covers.set_state( id=self._device.resource_id, action=CoverAction.STOP, + legacy_mode=self.legacy_mode, ) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 51de538324f..38c78d849da 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==103"], + "requirements": ["pydeconz==104"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index ff3a248be34..81e1cf594c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1467,7 +1467,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==103 +pydeconz==104 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bad3671b282..c2cebb49b1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1022,7 +1022,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==103 +pydeconz==104 # homeassistant.components.dexcom pydexcom==0.2.3 diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 0c37edc221d..f2f4c7d7a2d 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -213,3 +213,111 @@ async def test_tilt_cover(hass, aioclient_mock): blocking=True, ) assert aioclient_mock.mock_calls[4][2] == {"stop": True} + + +async def test_level_controllable_output_cover(hass, aioclient_mock): + """Test that tilting a cover works.""" + data = { + "lights": { + "0": { + "etag": "4cefc909134c8e99086b55273c2bde67", + "hascolor": False, + "lastannounced": "2022-08-08T12:06:18Z", + "lastseen": "2022-08-14T14:22Z", + "manufacturername": "Keen Home Inc", + "modelid": "SV01-410-MP-1.0", + "name": "Vent", + "state": { + "alert": "none", + "bri": 242, + "on": False, + "reachable": True, + "sat": 10, + }, + "swversion": "0x00000012", + "type": "Level controllable output", + "uniqueid": "00:22:a3:00:00:00:00:00-01", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 1 + covering_device = hass.states.get("cover.vent") + assert covering_device.state == STATE_OPEN + assert covering_device.attributes[ATTR_CURRENT_TILT_POSITION] == 97 + + # Verify service calls for tilting cover + + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") + + # Service open cover + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.vent"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"on": False} + + # Service close cover + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.vent"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"on": True} + + # Service set cover position + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.vent", ATTR_POSITION: 40}, + blocking=True, + ) + assert aioclient_mock.mock_calls[3][2] == {"bri": 152} + + # Service set tilt cover + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: "cover.vent", ATTR_TILT_POSITION: 40}, + blocking=True, + ) + assert aioclient_mock.mock_calls[4][2] == {"sat": 152} + + # Service open tilt cover + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: "cover.vent"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[5][2] == {"sat": 0} + + # Service close tilt cover + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: "cover.vent"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[6][2] == {"sat": 254} + + # Service stop cover movement + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: "cover.vent"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[7][2] == {"bri_inc": 0} From f6a03625bac609223280b0a4a79b03ac4f9b99c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Aug 2022 22:50:48 -0500 Subject: [PATCH 608/903] Implement websocket message coalescing (#77238) Co-authored-by: Paulus Schoutsen --- .../components/websocket_api/commands.py | 16 ++ .../components/websocket_api/connection.py | 1 + .../components/websocket_api/const.py | 2 + .../components/websocket_api/http.py | 72 +++++-- .../components/websocket_api/test_commands.py | 186 +++++++++++++++++- tests/conftest.py | 106 +++++++++- 6 files changed, 361 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 6c18fd96627..1761323a60d 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -74,6 +74,7 @@ def async_register_commands( async_reg(hass, handle_validate_config) async_reg(hass, handle_subscribe_entities) async_reg(hass, handle_supported_brands) + async_reg(hass, handle_supported_features) def pong_message(iden: int) -> dict[str, Any]: @@ -723,3 +724,18 @@ async def handle_supported_brands( raise int_or_exc data[int_or_exc.domain] = int_or_exc.manifest["supported_brands"] connection.send_result(msg["id"], data) + + +@callback +@decorators.websocket_command( + { + vol.Required("type"): "supported_features", + vol.Required("features"): {str: int}, + } +) +def handle_supported_features( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle setting supported features.""" + connection.supported_features = msg["features"] + connection.send_result(msg["id"]) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 87c52288bcc..c344e1c6a9f 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -42,6 +42,7 @@ class ActiveConnection: self.refresh_token_id = refresh_token.id self.subscriptions: dict[Hashable, Callable[[], Any]] = {} self.last_id = 0 + self.supported_features: dict[str, float] = {} current_connection.set(self) def context(self, msg: dict[str, Any]) -> Context: diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 60a00126092..6135a821d53 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -55,3 +55,5 @@ COMPRESSED_STATE_ATTRIBUTES = "a" COMPRESSED_STATE_CONTEXT = "c" COMPRESSED_STATE_LAST_CHANGED = "lc" COMPRESSED_STATE_LAST_UPDATED = "lu" + +FEATURE_COALESCE_MESSAGES = "coalesce_messages" diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index e8972a227c8..7336fa1c0d2 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -6,7 +6,7 @@ from collections.abc import Callable from contextlib import suppress import datetime as dt import logging -from typing import Any, Final +from typing import TYPE_CHECKING, Any, Final from aiohttp import WSMsgType, web import async_timeout @@ -16,11 +16,13 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.json import json_loads from .auth import AuthPhase, auth_required_message from .const import ( CANCELLATION_ERRORS, DATA_CONNECTIONS, + FEATURE_COALESCE_MESSAGES, MAX_PENDING_MSG, PENDING_MSG_PEAK, PENDING_MSG_PEAK_TIME, @@ -31,6 +33,10 @@ from .const import ( from .error import Disconnect from .messages import message_to_json +if TYPE_CHECKING: + from .connection import ActiveConnection + + _WS_LOGGER: Final = logging.getLogger(f"{__name__}.connection") @@ -67,26 +73,47 @@ class WebSocketHandler: self._writer_task: asyncio.Task | None = None self._logger = WebSocketAdapter(_WS_LOGGER, {"connid": id(self)}) self._peak_checker_unsub: Callable[[], None] | None = None + self.connection: ActiveConnection | None = None async def _writer(self) -> None: """Write outgoing messages.""" # Exceptions if Socket disconnected or cancelled by connection handler - with suppress(RuntimeError, ConnectionResetError, *CANCELLATION_ERRORS): - while not self.wsock.closed: - if (process := await self._to_write.get()) is None: - break + to_write = self._to_write + logger = self._logger + wsock = self.wsock + try: + with suppress(RuntimeError, ConnectionResetError, *CANCELLATION_ERRORS): + while not self.wsock.closed: + if (process := await to_write.get()) is None: + return + message = process if isinstance(process, str) else process() - if not isinstance(process, str): - message: str = process() - else: - message = process - self._logger.debug("Sending %s", message) - await self.wsock.send_str(message) + if ( + to_write.empty() + or not self.connection + or FEATURE_COALESCE_MESSAGES + not in self.connection.supported_features + ): + logger.debug("Sending %s", message) + await wsock.send_str(message) + continue - # Clean up the peaker checker when we shut down the writer - if self._peak_checker_unsub is not None: - self._peak_checker_unsub() - self._peak_checker_unsub = None + messages: list[str] = [message] + while not to_write.empty(): + if (process := to_write.get_nowait()) is None: + return + messages.append( + process if isinstance(process, str) else process() + ) + + coalesced_messages = "[" + ",".join(messages) + "]" + self._logger.debug("Sending %s", coalesced_messages) + await self.wsock.send_str(coalesced_messages) + finally: + # Clean up the peaker checker when we shut down the writer + if self._peak_checker_unsub is not None: + self._peak_checker_unsub() + self._peak_checker_unsub = None @callback def _send_message(self, message: str | dict[str, Any] | Callable[[], str]) -> None: @@ -194,13 +221,13 @@ class WebSocketHandler: raise Disconnect try: - msg_data = msg.json() + msg_data = msg.json(loads=json_loads) except ValueError as err: disconnect_warn = "Received invalid JSON." raise Disconnect from err self._logger.debug("Received %s", msg_data) - connection = await auth.async_handle(msg_data) + self.connection = connection = await auth.async_handle(msg_data) self.hass.data[DATA_CONNECTIONS] = ( self.hass.data.get(DATA_CONNECTIONS, 0) + 1 ) @@ -218,13 +245,18 @@ class WebSocketHandler: break try: - msg_data = msg.json() + msg_data = msg.json(loads=json_loads) except ValueError: disconnect_warn = "Received invalid JSON." break self._logger.debug("Received %s", msg_data) - connection.async_handle(msg_data) + if not isinstance(msg_data, list): + connection.async_handle(msg_data) + continue + + for split_msg in msg_data: + connection.async_handle(split_msg) except asyncio.CancelledError: self._logger.info("Connection closed by client") @@ -257,6 +289,8 @@ class WebSocketHandler: if connection is not None: self.hass.data[DATA_CONNECTIONS] -= 1 + self.connection = None + async_dispatcher_send(self.hass, SIGNAL_WEBSOCKET_DISCONNECTED) return wsock diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index f1065061c73..fe748e2c47c 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -13,12 +13,13 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_OK, TYPE_AUTH_REQUIRED, ) -from homeassistant.components.websocket_api.const import URL +from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAGES, URL from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATONS from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.json import json_loads from homeassistant.loader import async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_setup_component @@ -1788,3 +1789,186 @@ async def test_supported_brands(hass, websocket_client): "hello": "World", }, } + + +async def test_message_coalescing(hass, websocket_client, hass_admin_user): + """Test enabling message coalescing.""" + await websocket_client.send_json( + { + "id": 1, + "type": "supported_features", + "features": {FEATURE_COALESCE_MESSAGES: 1}, + } + ) + hass.states.async_set("light.permitted", "on", {"color": "red"}) + + data = await websocket_client.receive_str() + msg = json_loads(data) + assert msg["id"] == 1 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + await websocket_client.send_json({"id": 7, "type": "subscribe_entities"}) + + data = await websocket_client.receive_str() + msgs = json_loads(data) + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "a": { + "light.permitted": {"a": {"color": "red"}, "c": ANY, "lc": ANY, "s": "on"} + } + } + + hass.states.async_set("light.permitted", "on", {"color": "yellow"}) + hass.states.async_set("light.permitted", "on", {"color": "green"}) + hass.states.async_set("light.permitted", "on", {"color": "blue"}) + + data = await websocket_client.receive_str() + msgs = json_loads(data) + + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": {"light.permitted": {"+": {"a": {"color": "yellow"}, "c": ANY, "lu": ANY}}} + } + + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": {"light.permitted": {"+": {"a": {"color": "green"}, "c": ANY, "lu": ANY}}} + } + + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": {"light.permitted": {"+": {"a": {"color": "blue"}, "c": ANY, "lu": ANY}}} + } + + hass.states.async_set("light.permitted", "on", {"color": "yellow"}) + hass.states.async_set("light.permitted", "on", {"color": "green"}) + hass.states.async_set("light.permitted", "on", {"color": "blue"}) + await websocket_client.close() + await hass.async_block_till_done() + + +async def test_message_coalescing_not_supported_by_websocket_client( + hass, websocket_client, hass_admin_user +): + """Test enabling message coalescing not supported by websocket client.""" + await websocket_client.send_json({"id": 7, "type": "subscribe_entities"}) + + data = await websocket_client.receive_str() + msg = json_loads(data) + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + hass.states.async_set("light.permitted", "on", {"color": "red"}) + hass.states.async_set("light.permitted", "on", {"color": "blue"}) + + data = await websocket_client.receive_str() + msg = json_loads(data) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == {"a": {}} + + data = await websocket_client.receive_str() + msg = json_loads(data) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "a": { + "light.permitted": {"a": {"color": "red"}, "c": ANY, "lc": ANY, "s": "on"} + } + } + + data = await websocket_client.receive_str() + msg = json_loads(data) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": {"light.permitted": {"+": {"a": {"color": "blue"}, "c": ANY, "lu": ANY}}} + } + await websocket_client.close() + await hass.async_block_till_done() + + +async def test_client_message_coalescing(hass, websocket_client, hass_admin_user): + """Test client message coalescing.""" + await websocket_client.send_json( + [ + { + "id": 1, + "type": "supported_features", + "features": {FEATURE_COALESCE_MESSAGES: 1}, + }, + {"id": 7, "type": "subscribe_entities"}, + ] + ) + hass.states.async_set("light.permitted", "on", {"color": "red"}) + + data = await websocket_client.receive_str() + msgs = json_loads(data) + + msg = msgs.pop(0) + assert msg["id"] == 1 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "a": { + "light.permitted": {"a": {"color": "red"}, "c": ANY, "lc": ANY, "s": "on"} + } + } + + hass.states.async_set("light.permitted", "on", {"color": "yellow"}) + hass.states.async_set("light.permitted", "on", {"color": "green"}) + hass.states.async_set("light.permitted", "on", {"color": "blue"}) + + data = await websocket_client.receive_str() + msgs = json_loads(data) + + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": {"light.permitted": {"+": {"a": {"color": "yellow"}, "c": ANY, "lu": ANY}}} + } + + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": {"light.permitted": {"+": {"a": {"color": "green"}, "c": ANY, "lu": ANY}}} + } + + msg = msgs.pop(0) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": {"light.permitted": {"+": {"a": {"color": "blue"}, "c": ANY, "lu": ANY}}} + } + + hass.states.async_set("light.permitted", "on", {"color": "yellow"}) + hass.states.async_set("light.permitted", "on", {"color": "green"}) + hass.states.async_set("light.permitted", "on", {"color": "blue"}) + await websocket_client.close() + await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index 0c0a654059b..889568127cb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,15 +2,25 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Callable, Generator from contextlib import asynccontextmanager import functools +from json import JSONDecoder, loads import logging import ssl import threading +from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch -from aiohttp.test_utils import make_mocked_request +from aiohttp import client +from aiohttp.pytest_plugin import AiohttpClient +from aiohttp.test_utils import ( + BaseTestServer, + TestClient, + TestServer, + make_mocked_request, +) +from aiohttp.web import Application import freezegun import multidict import pytest @@ -57,6 +67,7 @@ from tests.components.recorder.common import ( # noqa: E402, isort:skip async_recorder_block_till_done, ) + _LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) @@ -203,6 +214,97 @@ def load_registries(): return True +class CoalescingResponse(client.ClientWebSocketResponse): + """ClientWebSocketResponse client that mimics the websocket js code.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Init the ClientWebSocketResponse.""" + super().__init__(*args, **kwargs) + self._recv_buffer: list[Any] = [] + + async def receive_json( + self, + *, + loads: JSONDecoder = loads, + timeout: float | None = None, + ) -> Any: + """receive_json or from buffer.""" + if self._recv_buffer: + return self._recv_buffer.pop(0) + data = await self.receive_str(timeout=timeout) + decoded = loads(data) + if isinstance(decoded, list): + self._recv_buffer = decoded + return self._recv_buffer.pop(0) + return decoded + + +class CoalescingClient(TestClient): + """Client that mimics the websocket js code.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Init TestClient.""" + super().__init__(*args, ws_response_class=CoalescingResponse, **kwargs) + + +@pytest.fixture +def aiohttp_client_cls(): + """Override the test class for aiohttp.""" + return CoalescingClient + + +@pytest.fixture +def aiohttp_client( + loop: asyncio.AbstractEventLoop, +) -> Generator[AiohttpClient, None, None]: + """Override the default aiohttp_client since 3.x does not support aiohttp_client_cls. + + Remove this when upgrading to 4.x as aiohttp_client_cls + will do the same thing + + aiohttp_client(app, **kwargs) + aiohttp_client(server, **kwargs) + aiohttp_client(raw_server, **kwargs) + """ + clients = [] + + async def go( + __param: Application | BaseTestServer, + *args: Any, + server_kwargs: dict[str, Any] | None = None, + **kwargs: Any, + ) -> TestClient: + + if isinstance(__param, Callable) and not isinstance( # type: ignore[arg-type] + __param, (Application, BaseTestServer) + ): + __param = __param(loop, *args, **kwargs) + kwargs = {} + else: + assert not args, "args should be empty" + + if isinstance(__param, Application): + server_kwargs = server_kwargs or {} + server = TestServer(__param, loop=loop, **server_kwargs) + client = CoalescingClient(server, loop=loop, **kwargs) + elif isinstance(__param, BaseTestServer): + client = TestClient(__param, loop=loop, **kwargs) + else: + raise ValueError("Unknown argument type: %r" % type(__param)) + + await client.start_server() + clients.append(client) + return client + + yield go + + async def finalize() -> None: + while clients: + await clients.pop().close() + + loop.run_until_complete(finalize()) + + @pytest.fixture def hass(loop, load_registries, hass_storage, request): """Fixture to provide a test instance of Home Assistant.""" From cd7625d4a25c0cc442a8654ff38bae49b8209ac3 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 25 Aug 2022 01:05:57 -0400 Subject: [PATCH 609/903] Bump AIOAladdinConnect to 0.1.43 (#77263) Bumped AIOAladdin Connect 0.1.43 Added door to callback key --- homeassistant/components/aladdin_connect/cover.py | 2 +- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index ee0955cbb3d..18dcafeb71c 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -111,7 +111,7 @@ class AladdinDevice(CoverEntity): """Schedule a state update.""" self.async_write_ha_state() - self._acc.register_callback(update_callback, self._serial) + self._acc.register_callback(update_callback, self._serial, self._number) await self._acc.get_doors(self._serial) async def async_will_remove_from_hass(self) -> None: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index e2a15757cdc..f099694f309 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.42"], + "requirements": ["AIOAladdinConnect==0.1.43"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 81e1cf594c4..2d3404483ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.42 +AIOAladdinConnect==0.1.43 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2cebb49b1d..dc284136d8f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.42 +AIOAladdinConnect==0.1.43 # homeassistant.components.adax Adax-local==0.1.4 From bbc2c28ef32d9a0e875f51b4d856de20f9e01ca6 Mon Sep 17 00:00:00 2001 From: mletenay Date: Thu, 25 Aug 2022 07:52:05 +0200 Subject: [PATCH 610/903] Add Synchronize inverter clock button (#69220) * Add Synchronize inverter clock button * Use generic GoodweButtonEntityDescription * Replace deprecated code * Fix DT inverter export limit type * Remove fix to DT inverter export limit time --- .coveragerc | 1 + homeassistant/components/goodwe/button.py | 84 +++++++++++++++++++++++ homeassistant/components/goodwe/const.py | 2 +- 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/goodwe/button.py diff --git a/.coveragerc b/.coveragerc index 44b654d05ef..95af2a269d1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -443,6 +443,7 @@ omit = homeassistant/components/glances/sensor.py homeassistant/components/goalfeed/* homeassistant/components/goodwe/__init__.py + homeassistant/components/goodwe/button.py homeassistant/components/goodwe/const.py homeassistant/components/goodwe/number.py homeassistant/components/goodwe/select.py diff --git a/homeassistant/components/goodwe/button.py b/homeassistant/components/goodwe/button.py new file mode 100644 index 00000000000..1ebc984e48c --- /dev/null +++ b/homeassistant/components/goodwe/button.py @@ -0,0 +1,84 @@ +"""GoodWe PV inverter selection settings entities.""" +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from datetime import datetime +import logging + +from goodwe import Inverter, InverterError + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class GoodweButtonEntityDescriptionRequired: + """Required attributes of GoodweButtonEntityDescription.""" + + action: Callable[[Inverter], Awaitable[None]] + + +@dataclass +class GoodweButtonEntityDescription( + ButtonEntityDescription, GoodweButtonEntityDescriptionRequired +): + """Class describing Goodwe button entities.""" + + +SYNCHRONIZE_CLOCK = GoodweButtonEntityDescription( + key="synchronize_clock", + name="Synchronize inverter clock", + icon="mdi:clock-check-outline", + entity_category=EntityCategory.CONFIG, + action=lambda inv: inv.write_setting("time", datetime.now()), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the inverter button entities from a config entry.""" + inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] + device_info = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE_INFO] + + # read current time from the inverter + try: + await inverter.read_setting("time") + except (InverterError, ValueError): + # Inverter model does not support clock synchronization + _LOGGER.debug("Could not read inverter current clock time") + else: + async_add_entities( + [GoodweButtonEntity(device_info, SYNCHRONIZE_CLOCK, inverter)] + ) + + +class GoodweButtonEntity(ButtonEntity): + """Entity representing the inverter clock synchronization button.""" + + _attr_should_poll = False + entity_description: GoodweButtonEntityDescription + + def __init__( + self, + device_info: DeviceInfo, + description: GoodweButtonEntityDescription, + inverter: Inverter, + ) -> None: + """Initialize the inverter operation mode setting entity.""" + self.entity_description = description + self._attr_unique_id = f"{description.key}-{inverter.serial_number}" + self._attr_device_info = device_info + self._inverter: Inverter = inverter + + async def async_press(self) -> None: + """Triggers the button press service.""" + await self.entity_description.action(self._inverter) diff --git a/homeassistant/components/goodwe/const.py b/homeassistant/components/goodwe/const.py index 0e40601ccdb..c5dbaaa49e5 100644 --- a/homeassistant/components/goodwe/const.py +++ b/homeassistant/components/goodwe/const.py @@ -5,7 +5,7 @@ from homeassistant.const import Platform DOMAIN = "goodwe" -PLATFORMS = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SELECT, Platform.SENSOR] DEFAULT_NAME = "GoodWe" SCAN_INTERVAL = timedelta(seconds=10) From cef6ffb552ae8fb417345780e47f3403201d7d4f Mon Sep 17 00:00:00 2001 From: mletenay Date: Thu, 25 Aug 2022 08:43:09 +0200 Subject: [PATCH 611/903] Fix grid_export_limit unit for DT inverters (#77290) FIx grid_export_limit unit for DT inverters --- homeassistant/components/goodwe/number.py | 30 ++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index 00d8d9d0cae..f8d3979879f 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -25,6 +25,7 @@ class GoodweNumberEntityDescriptionBase: getter: Callable[[Inverter], Awaitable[int]] setter: Callable[[Inverter, int], Awaitable[None]] + filter: Callable[[Inverter], bool] @dataclass @@ -35,17 +36,33 @@ class GoodweNumberEntityDescription( NUMBERS = ( + # non DT inverters (limit in W) GoodweNumberEntityDescription( key="grid_export_limit", name="Grid export limit", icon="mdi:transmission-tower", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=POWER_WATT, - getter=lambda inv: inv.get_grid_export_limit(), - setter=lambda inv, val: inv.set_grid_export_limit(val), native_step=100, native_min_value=0, native_max_value=10000, + getter=lambda inv: inv.get_grid_export_limit(), + setter=lambda inv, val: inv.set_grid_export_limit(val), + filter=lambda inv: type(inv).__name__ != "DT", + ), + # DT inverters (limit is in %) + GoodweNumberEntityDescription( + key="grid_export_limit", + name="Grid export limit", + icon="mdi:transmission-tower", + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=PERCENTAGE, + native_step=1, + native_min_value=0, + native_max_value=100, + getter=lambda inv: inv.get_grid_export_limit(), + setter=lambda inv, val: inv.set_grid_export_limit(val), + filter=lambda inv: type(inv).__name__ == "DT", ), GoodweNumberEntityDescription( key="battery_discharge_depth", @@ -53,11 +70,12 @@ NUMBERS = ( icon="mdi:battery-arrow-down", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, - getter=lambda inv: inv.get_ongrid_battery_dod(), - setter=lambda inv, val: inv.set_ongrid_battery_dod(val), native_step=1, native_min_value=0, native_max_value=99, + getter=lambda inv: inv.get_ongrid_battery_dod(), + setter=lambda inv, val: inv.set_ongrid_battery_dod(val), + filter=lambda inv: True, ), ) @@ -73,7 +91,7 @@ async def async_setup_entry( entities = [] - for description in NUMBERS: + for description in filter(lambda dsc: dsc.filter(inverter), NUMBERS): try: current_value = await description.getter(inverter) except (InverterError, ValueError): @@ -82,7 +100,7 @@ async def async_setup_entry( continue entities.append( - InverterNumberEntity(device_info, description, inverter, current_value), + InverterNumberEntity(device_info, description, inverter, current_value) ) async_add_entities(entities) From c55505b47b108142f3625326645c065496f7418b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Aug 2022 09:27:38 +0200 Subject: [PATCH 612/903] Use mock_restore_cache in mqtt tests (#77297) --- tests/components/mqtt/test_binary_sensor.py | 8 +- tests/components/mqtt/test_init.py | 38 ++++---- tests/components/mqtt/test_light.py | 13 +-- tests/components/mqtt/test_light_json.py | 92 +++++++++----------- tests/components/mqtt/test_light_template.py | 12 +-- tests/components/mqtt/test_scene.py | 33 ++++--- tests/components/mqtt/test_select.py | 74 ++++++++-------- tests/components/mqtt/test_sensor.py | 13 +-- tests/components/mqtt/test_switch.py | 39 ++++----- 9 files changed, 152 insertions(+), 170 deletions(-) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 658af79f20f..7d4edace988 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -53,6 +53,7 @@ from tests.common import ( assert_setup_component, async_fire_mqtt_message, async_fire_time_changed, + mock_restore_cache, ) DEFAULT_CONFIG = { @@ -1062,10 +1063,9 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( {}, last_changed=datetime.fromisoformat("2022-02-02 12:01:35+01:00"), ) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ), assert_setup_component(1, domain): + mock_restore_cache(hass, (fake_state,)) + + with assert_setup_component(1, domain): assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index fd77dcec3a7..020fae1732b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -43,6 +43,7 @@ from tests.common import ( async_fire_time_changed, mock_device_registry, mock_registry, + mock_restore_cache, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @@ -284,27 +285,24 @@ async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config """Test the rendering of entity variables.""" topic = "test/select" - fake_state = ha.State("select.test", "milk") + fake_state = ha.State("select.test_select", "milk") + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - assert await async_setup_component( - hass, - "select", - { - "select": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], - "command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}", "this_object_state": "{{ this.state }}"}', - } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, + "select", + { + "select": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}", "this_object_state": "{{ this.state }}"}', + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index ff529b3dda4..e916276cb4a 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -226,7 +226,11 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import assert_setup_component, async_fire_mqtt_message +from tests.common import ( + assert_setup_component, + async_fire_mqtt_message, + mock_restore_cache, +) from tests.components.light import common DEFAULT_CONFIG = { @@ -792,10 +796,9 @@ async def test_sending_mqtt_commands_and_optimistic( "color_mode": "hs", }, ) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ), assert_setup_component(1, light.DOMAIN): + mock_restore_cache(hass, (fake_state,)) + + with assert_setup_component(1, light.DOMAIN): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 89e966112c1..8fac9092e3f 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -124,7 +124,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message +from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.light import common DEFAULT_CONFIG = { @@ -614,32 +614,29 @@ async def test_sending_mqtt_commands_and_optimistic( "color_temp": 100, }, ) + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "json", - "name": "test", - "command_topic": "test_light_rgb/set", - "brightness": True, - "color_temp": True, - "effect": True, - "hs": True, - "rgb": True, - "xy": True, - "qos": 2, - } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "command_topic": "test_light_rgb/set", + "brightness": True, + "color_temp": True, + "effect": True, + "hs": True, + "rgb": True, + "xy": True, + "qos": 2, + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -754,30 +751,27 @@ async def test_sending_mqtt_commands_and_optimistic2( "hs_color": [100, 100], }, ) + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "brightness": True, - "color_mode": True, - "command_topic": "test_light_rgb/set", - "effect": True, - "name": "test", - "platform": "mqtt", - "qos": 2, - "schema": "json", - "supported_color_modes": supported_color_modes, - } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "brightness": True, + "color_mode": True, + "command_topic": "test_light_rgb/set", + "effect": True, + "name": "test", + "platform": "mqtt", + "qos": 2, + "schema": "json", + "supported_color_modes": supported_color_modes, + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index e4f755eab4e..3b330b1434d 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -74,7 +74,11 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import assert_setup_component, async_fire_mqtt_message +from tests.common import ( + assert_setup_component, + async_fire_mqtt_message, + mock_restore_cache, +) from tests.components.light import common DEFAULT_CONFIG = { @@ -334,11 +338,9 @@ async def test_sending_mqtt_commands_and_optimistic( "color_temp": 100, }, ) + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ), assert_setup_component(1, light.DOMAIN): + with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( hass, light.DOMAIN, diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 713410059fe..d676429bf3e 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -25,6 +25,8 @@ from .test_common import ( help_test_unload_config_entry_with_platform, ) +from tests.common import mock_restore_cache + DEFAULT_CONFIG = { scene.DOMAIN: { "platform": "mqtt", @@ -45,25 +47,22 @@ def scene_platform_only(): async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" fake_state = ha.State("scene.test", STATE_UNKNOWN) + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - assert await async_setup_component( - hass, - scene.DOMAIN, - { - scene.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": "beer on", - }, + assert await async_setup_component( + hass, + scene.DOMAIN, + { + scene.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("scene.test") assert state.state == STATE_UNKNOWN diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 4c3a0523951..085c4d5df00 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -53,7 +53,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message +from tests.common import async_fire_mqtt_message, mock_restore_cache DEFAULT_CONFIG = { select.DOMAIN: { @@ -152,26 +152,23 @@ async def test_run_select_service_optimistic(hass, mqtt_mock_entry_with_yaml_con """Test that set_value service works in optimistic mode.""" topic = "test/select" - fake_state = ha.State("select.test", "milk") + fake_state = ha.State("select.test_select", "milk") + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - assert await async_setup_component( - hass, - select.DOMAIN, - { - "select": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], - } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, + select.DOMAIN, + { + "select": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -196,27 +193,24 @@ async def test_run_select_service_optimistic_with_command_template( """Test that set_value service works in optimistic mode and with a command_template.""" topic = "test/select" - fake_state = ha.State("select.test", "milk") + fake_state = ha.State("select.test_select", "milk") + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - assert await async_setup_component( - hass, - select.DOMAIN, - { - "select": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], - "command_template": '{"option": "{{ value }}"}', - } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, + select.DOMAIN, + { + "select": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}"}', + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index ab094c20b6f..b446b1a8b76 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -67,6 +67,7 @@ from tests.common import ( assert_setup_component, async_fire_mqtt_message, async_fire_time_changed, + mock_restore_cache_with_extra_data, ) DEFAULT_CONFIG = { @@ -1160,15 +1161,9 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( last_changed=datetime.fromisoformat("2022-02-02 12:01:35+01:00"), ) fake_extra_data = MagicMock() - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ), patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_extra_data", - return_value=fake_extra_data, - ), assert_setup_component( - 1, domain - ): + mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) + + with assert_setup_component(1, domain): assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index af6c0f99f50..ac69b17e18e 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -47,7 +47,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message +from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.switch import common DEFAULT_CONFIG = { @@ -108,27 +108,24 @@ async def test_sending_mqtt_commands_and_optimistic( ): """Test the sending MQTT commands in optimistic mode.""" fake_state = ha.State("switch.test", "on") + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - assert await async_setup_component( - hass, - switch.DOMAIN, - { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "qos": "2", - } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, + switch.DOMAIN, + { + switch.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "qos": "2", + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_ON From 3d723bddf842110fd1448b2800621aecbb4b750d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Aug 2022 09:28:53 +0200 Subject: [PATCH 613/903] Use mock_restore_cache in tests (#77298) --- tests/components/knx/test_binary_sensor.py | 63 +++++++++++----------- tests/components/knx/test_select.py | 29 +++++----- tests/components/knx/test_switch.py | 27 +++++----- tests/components/unifi/test_switch.py | 30 +++++------ 4 files changed, 69 insertions(+), 80 deletions(-) diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index ff58a36391c..36c311d7979 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -1,6 +1,5 @@ """Test KNX binary sensor.""" from datetime import timedelta -from unittest.mock import patch from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE from homeassistant.components.knx.schema import BinarySensorSchema @@ -12,7 +11,11 @@ from homeassistant.util import dt from .conftest import KNXTestKit -from tests.common import async_capture_events, async_fire_time_changed +from tests.common import ( + async_capture_events, + async_fire_time_changed, + mock_restore_cache, +) async def test_binary_sensor_entity_category(hass: HomeAssistant, knx: KNXTestKit): @@ -245,22 +248,19 @@ async def test_binary_sensor_restore_and_respond(hass, knx): """Test restoring KNX binary sensor state and respond to read.""" _ADDRESS = "2/2/2" fake_state = State("binary_sensor.test", STATE_ON) + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - await knx.setup_integration( - { - BinarySensorSchema.PLATFORM: [ - { - CONF_NAME: "test", - CONF_STATE_ADDRESS: _ADDRESS, - CONF_SYNC_STATE: False, - }, - ] - } - ) + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM: [ + { + CONF_NAME: "test", + CONF_STATE_ADDRESS: _ADDRESS, + CONF_SYNC_STATE: False, + }, + ] + } + ) # restored state - doesn't send telegram state = hass.states.get("binary_sensor.test") @@ -277,23 +277,20 @@ async def test_binary_sensor_restore_invert(hass, knx): """Test restoring KNX binary sensor state with invert.""" _ADDRESS = "2/2/2" fake_state = State("binary_sensor.test", STATE_ON) + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - await knx.setup_integration( - { - BinarySensorSchema.PLATFORM: [ - { - CONF_NAME: "test", - CONF_STATE_ADDRESS: _ADDRESS, - BinarySensorSchema.CONF_INVERT: True, - CONF_SYNC_STATE: False, - }, - ] - } - ) + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM: [ + { + CONF_NAME: "test", + CONF_STATE_ADDRESS: _ADDRESS, + BinarySensorSchema.CONF_INVERT: True, + CONF_SYNC_STATE: False, + }, + ] + } + ) # restored state - doesn't send telegram state = hass.states.get("binary_sensor.test") diff --git a/tests/components/knx/test_select.py b/tests/components/knx/test_select.py index c8db3625c05..1c0f3dc5c7c 100644 --- a/tests/components/knx/test_select.py +++ b/tests/components/knx/test_select.py @@ -1,6 +1,4 @@ """Test KNX select.""" -from unittest.mock import patch - import pytest from homeassistant.components.knx.const import ( @@ -17,6 +15,8 @@ from homeassistant.core import HomeAssistant, State from .conftest import KNXTestKit +from tests.common import mock_restore_cache + async def test_select_dpt_2_simple(hass: HomeAssistant, knx: KNXTestKit): """Test simple KNX select.""" @@ -98,22 +98,19 @@ async def test_select_dpt_2_restore(hass: HomeAssistant, knx: KNXTestKit): test_address = "1/1/1" test_passive_address = "3/3/3" fake_state = State("select.test", "Control - On") + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - await knx.setup_integration( - { - SelectSchema.PLATFORM: { - CONF_NAME: "test", - KNX_ADDRESS: [test_address, test_passive_address], - CONF_RESPOND_TO_READ: True, - CONF_PAYLOAD_LENGTH: 0, - SelectSchema.CONF_OPTIONS: _options, - } + await knx.setup_integration( + { + SelectSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: [test_address, test_passive_address], + CONF_RESPOND_TO_READ: True, + CONF_PAYLOAD_LENGTH: 0, + SelectSchema.CONF_OPTIONS: _options, } - ) + } + ) # restored state - doesn't send telegram state = hass.states.get("select.test") assert state.state == "Control - On" diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py index 07fe8793fd8..55671193bdf 100644 --- a/tests/components/knx/test_switch.py +++ b/tests/components/knx/test_switch.py @@ -1,6 +1,4 @@ """Test KNX switch.""" -from unittest.mock import patch - from homeassistant.components.knx.const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -12,6 +10,8 @@ from homeassistant.core import HomeAssistant, State from .conftest import KNXTestKit +from tests.common import mock_restore_cache + async def test_switch_simple(hass: HomeAssistant, knx: KNXTestKit): """Test simple KNX switch.""" @@ -115,20 +115,17 @@ async def test_switch_restore_and_respond(hass, knx): """Test restoring KNX switch state and respond to read.""" _ADDRESS = "1/1/1" fake_state = State("switch.test", "on") + mock_restore_cache(hass, (fake_state,)) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - await knx.setup_integration( - { - SwitchSchema.PLATFORM: { - CONF_NAME: "test", - KNX_ADDRESS: _ADDRESS, - CONF_RESPOND_TO_READ: True, - }, - } - ) + await knx.setup_integration( + { + SwitchSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: _ADDRESS, + CONF_RESPOND_TO_READ: True, + }, + } + ) # restored state - doesn't send telegram state = hass.states.get("switch.test") diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 8e2802738af..9965c25a1b5 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,6 +1,5 @@ """UniFi Network switch platform tests.""" from copy import deepcopy -from unittest.mock import patch from aiounifi.controller import MESSAGE_CLIENT_REMOVED, MESSAGE_DEVICE, MESSAGE_EVENT @@ -32,6 +31,8 @@ from .test_controller import ( setup_unifi_integration, ) +from tests.common import mock_restore_cache + CLIENT_1 = { "hostname": "client_1", "ip": "10.0.0.1", @@ -1249,6 +1250,7 @@ async def test_restore_client_succeed(hass, aioclient_mock): "poe_mode": "auto", }, ) + mock_restore_cache(hass, (fake_state,)) config_entry = config_entries.ConfigEntry( version=1, @@ -1269,21 +1271,17 @@ async def test_restore_client_succeed(hass, aioclient_mock): config_entry=config_entry, ) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - await setup_unifi_integration( - hass, - aioclient_mock, - options={ - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - }, - clients_response=[], - devices_response=[POE_DEVICE], - clients_all_response=[POE_CLIENT], - ) + await setup_unifi_integration( + hass, + aioclient_mock, + options={ + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + }, + clients_response=[], + devices_response=[POE_DEVICE], + clients_all_response=[POE_CLIENT], + ) assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 From 79ab794e6aa48f93289c1b46116d4532b3da79c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 25 Aug 2022 09:39:00 +0200 Subject: [PATCH 614/903] Add .strict-typing to prettier ignore list (#77177) --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 99dcbe1a117..950741ec8b2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ *.md +.strict-typing azure-*.yml docs/source/_templates/* homeassistant/components/*/translations/*.json From dfed3ba75e7e24f143244fbdcbe6c22e72cbc1d3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Aug 2022 11:32:06 +0200 Subject: [PATCH 615/903] Move issue_registry to homeassistant.helpers (#77299) * Move issue_registry to homeassistant.helpers * Add backwards compatibility --- homeassistant/bootstrap.py | 11 +- homeassistant/components/repairs/__init__.py | 17 ++- .../components/repairs/issue_handler.py | 122 ++--------------- homeassistant/components/repairs/models.py | 12 +- .../components/repairs/websocket_api.py | 6 +- .../repairs => helpers}/issue_registry.py | 125 +++++++++++++++++- tests/common.py | 4 +- tests/components/repairs/test_init.py | 3 +- .../components/repairs/test_websocket_api.py | 7 +- .../test_issue_registry.py | 26 ++-- 10 files changed, 174 insertions(+), 159 deletions(-) rename homeassistant/{components/repairs => helpers}/issue_registry.py (73%) rename tests/{components/repairs => helpers}/test_issue_registry.py (92%) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index d2858bfcdf1..f2339e6bd1a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,7 +24,13 @@ from .const import ( SIGNAL_BOOTSTRAP_INTEGRATONS, ) from .exceptions import HomeAssistantError -from .helpers import area_registry, device_registry, entity_registry, recorder +from .helpers import ( + area_registry, + device_registry, + entity_registry, + issue_registry, + recorder, +) from .helpers.dispatcher import async_dispatcher_send from .helpers.typing import ConfigType from .setup import ( @@ -521,9 +527,10 @@ async def _async_set_up_integrations( # Load the registries and cache the result of platform.uname().processor await asyncio.gather( + area_registry.async_load(hass), device_registry.async_load(hass), entity_registry.async_load(hass), - area_registry.async_load(hass), + issue_registry.async_load(hass), hass.async_add_executor_job(_cache_uname_processor), ) diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index 5014baff834..726102d4b08 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -2,19 +2,19 @@ from __future__ import annotations from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -from . import issue_handler, websocket_api -from .const import DOMAIN -from .issue_handler import ( - ConfirmRepairFlow, +from homeassistant.helpers.issue_registry import ( + IssueSeverity, async_create_issue, async_delete_issue, create_issue, delete_issue, ) -from .issue_registry import async_load as async_load_issue_registry -from .models import IssueSeverity, RepairsFlow +from homeassistant.helpers.typing import ConfigType + +from . import issue_handler, websocket_api +from .const import DOMAIN +from .issue_handler import ConfirmRepairFlow +from .models import RepairsFlow __all__ = [ "async_create_issue", @@ -34,6 +34,5 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: issue_handler.async_setup(hass) websocket_api.async_setup(hass) - await async_load_issue_registry(hass) return True diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index eecbfe59bde..1201497f0c1 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -1,10 +1,8 @@ """The repairs integration.""" from __future__ import annotations -import functools as ft from typing import Any -from awesomeversion import AwesomeVersion, AwesomeVersionStrategy import voluptuous as vol from homeassistant import data_entry_flow @@ -13,11 +11,16 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) -from homeassistant.util.async_ import run_callback_threadsafe + +# pylint: disable-next=unused-import +from homeassistant.helpers.issue_registry import ( # noqa: F401; Remove when integrations have been updated + async_create_issue, + async_delete_issue, + async_get as async_get_issue_registry, +) from .const import DOMAIN -from .issue_registry import async_get as async_get_issue_registry -from .models import IssueSeverity, RepairsFlow, RepairsProtocol +from .models import RepairsFlow, RepairsProtocol class ConfirmRepairFlow(RepairsFlow): @@ -111,112 +114,3 @@ async def _register_repairs_platform( if not hasattr(platform, "async_create_fix_flow"): raise HomeAssistantError(f"Invalid repairs platform {platform}") hass.data[DOMAIN]["platforms"][integration_domain] = platform - - -@callback -def async_create_issue( - hass: HomeAssistant, - domain: str, - issue_id: str, - *, - breaks_in_ha_version: str | None = None, - data: dict[str, str | int | float | None] | None = None, - is_fixable: bool, - is_persistent: bool = False, - issue_domain: str | None = None, - learn_more_url: str | None = None, - severity: IssueSeverity, - translation_key: str, - translation_placeholders: dict[str, str] | None = None, -) -> None: - """Create an issue, or replace an existing one.""" - # Verify the breaks_in_ha_version is a valid version string - if breaks_in_ha_version: - AwesomeVersion( - breaks_in_ha_version, - ensure_strategy=AwesomeVersionStrategy.CALVER, - find_first_match=False, - ) - - issue_registry = async_get_issue_registry(hass) - issue_registry.async_get_or_create( - domain, - issue_id, - breaks_in_ha_version=breaks_in_ha_version, - data=data, - is_fixable=is_fixable, - is_persistent=is_persistent, - issue_domain=issue_domain, - learn_more_url=learn_more_url, - severity=severity, - translation_key=translation_key, - translation_placeholders=translation_placeholders, - ) - - -def create_issue( - hass: HomeAssistant, - domain: str, - issue_id: str, - *, - breaks_in_ha_version: str | None = None, - data: dict[str, str | int | float | None] | None = None, - is_fixable: bool, - is_persistent: bool = False, - issue_domain: str | None = None, - learn_more_url: str | None = None, - severity: IssueSeverity, - translation_key: str, - translation_placeholders: dict[str, str] | None = None, -) -> None: - """Create an issue, or replace an existing one.""" - return run_callback_threadsafe( - hass.loop, - ft.partial( - async_create_issue, - hass, - domain, - issue_id, - breaks_in_ha_version=breaks_in_ha_version, - data=data, - is_fixable=is_fixable, - is_persistent=is_persistent, - issue_domain=issue_domain, - learn_more_url=learn_more_url, - severity=severity, - translation_key=translation_key, - translation_placeholders=translation_placeholders, - ), - ).result() - - -@callback -def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: - """Delete an issue. - - It is not an error to delete an issue that does not exist. - """ - issue_registry = async_get_issue_registry(hass) - issue_registry.async_delete(domain, issue_id) - - -def delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: - """Delete an issue. - - It is not an error to delete an issue that does not exist. - """ - return run_callback_threadsafe( - hass.loop, async_delete_issue, hass, domain, issue_id - ).result() - - -@callback -def async_ignore_issue( - hass: HomeAssistant, domain: str, issue_id: str, ignore: bool -) -> None: - """Ignore an issue. - - Will raise if the issue does not exist. - """ - issue_registry = async_get_issue_registry(hass) - issue_registry.async_ignore(domain, issue_id, ignore) diff --git a/homeassistant/components/repairs/models.py b/homeassistant/components/repairs/models.py index 1022c50e1f2..045b7bd55dc 100644 --- a/homeassistant/components/repairs/models.py +++ b/homeassistant/components/repairs/models.py @@ -4,16 +4,12 @@ from __future__ import annotations from typing import Protocol from homeassistant import data_entry_flow -from homeassistant.backports.enum import StrEnum from homeassistant.core import HomeAssistant - -class IssueSeverity(StrEnum): - """Issue severity.""" - - CRITICAL = "critical" - ERROR = "error" - WARNING = "warning" +# pylint: disable-next=unused-import +from homeassistant.helpers.issue_registry import ( # noqa: F401; Remove when integrations have been updated + IssueSeverity, +) class RepairsFlow(data_entry_flow.FlowHandler): diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 192c9f5ac66..b4ccca7c894 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -18,10 +18,12 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, ) +from homeassistant.helpers.issue_registry import ( + async_get as async_get_issue_registry, + async_ignore_issue, +) from .const import DOMAIN -from .issue_handler import async_ignore_issue -from .issue_registry import async_get as async_get_issue_registry @callback diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/helpers/issue_registry.py similarity index 73% rename from homeassistant/components/repairs/issue_registry.py rename to homeassistant/helpers/issue_registry.py index a8843011023..b01d56942ac 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -3,14 +3,18 @@ from __future__ import annotations import dataclasses from datetime import datetime +import functools as ft from typing import Any, cast +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.backports.enum import StrEnum from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.storage import Store +from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util -from .models import IssueSeverity +from .storage import Store DATA_REGISTRY = "issue_registry" EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED = "repairs_issue_registry_updated" @@ -20,6 +24,14 @@ STORAGE_VERSION_MINOR = 2 SAVE_DELAY = 10 +class IssueSeverity(StrEnum): + """Issue severity.""" + + CRITICAL = "critical" + ERROR = "error" + WARNING = "warning" + + @dataclasses.dataclass(frozen=True) class IssueEntry: """Issue Registry Entry.""" @@ -267,3 +279,112 @@ async def async_load(hass: HomeAssistant) -> None: assert DATA_REGISTRY not in hass.data hass.data[DATA_REGISTRY] = IssueRegistry(hass) await hass.data[DATA_REGISTRY].async_load() + + +@callback +def async_create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, + is_fixable: bool, + is_persistent: bool = False, + issue_domain: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + # Verify the breaks_in_ha_version is a valid version string + if breaks_in_ha_version: + AwesomeVersion( + breaks_in_ha_version, + ensure_strategy=AwesomeVersionStrategy.CALVER, + find_first_match=False, + ) + + issue_registry = async_get(hass) + issue_registry.async_get_or_create( + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + data=data, + is_fixable=is_fixable, + is_persistent=is_persistent, + issue_domain=issue_domain, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + +def create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, + is_fixable: bool, + is_persistent: bool = False, + issue_domain: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + return run_callback_threadsafe( + hass.loop, + ft.partial( + async_create_issue, + hass, + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + data=data, + is_fixable=is_fixable, + is_persistent=is_persistent, + issue_domain=issue_domain, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ), + ).result() + + +@callback +def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + issue_registry = async_get(hass) + issue_registry.async_delete(domain, issue_id) + + +def delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + return run_callback_threadsafe( + hass.loop, async_delete_issue, hass, domain, issue_id + ).result() + + +@callback +def async_ignore_issue( + hass: HomeAssistant, domain: str, issue_id: str, ignore: bool +) -> None: + """Ignore an issue. + + Will raise if the issue does not exist. + """ + issue_registry = async_get(hass) + issue_registry.async_ignore(domain, issue_id, ignore) diff --git a/tests/common.py b/tests/common.py index acc50e26889..89d1a1d9116 100644 --- a/tests/common.py +++ b/tests/common.py @@ -50,6 +50,7 @@ from homeassistant.helpers import ( entity_platform, entity_registry, intent, + issue_registry, recorder as recorder_helper, restore_state, storage, @@ -297,9 +298,10 @@ async def async_test_home_assistant(loop, load_registries=True): # Load the registries if load_registries: await asyncio.gather( + area_registry.async_load(hass), device_registry.async_load(hass), entity_registry.async_load(hass), - area_registry.async_load(hass), + issue_registry.async_load(hass), ) await hass.async_block_till_done() diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 1fc8367e4c3..9071785aeea 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -14,12 +14,11 @@ from homeassistant.components.repairs import ( ) from homeassistant.components.repairs.const import DOMAIN from homeassistant.components.repairs.issue_handler import ( - async_ignore_issue, async_process_repairs_platforms, ) -from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import IssueSeverity, async_ignore_issue from homeassistant.setup import async_setup_component from tests.common import mock_platform diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 2a506c7a248..508e2edeb92 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -9,14 +9,11 @@ import pytest import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.repairs import ( - RepairsFlow, - async_create_issue, - issue_registry, -) +from homeassistant.components.repairs import RepairsFlow, async_create_issue from homeassistant.components.repairs.const import DOMAIN from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry from homeassistant.setup import async_setup_component from tests.common import mock_platform diff --git a/tests/components/repairs/test_issue_registry.py b/tests/helpers/test_issue_registry.py similarity index 92% rename from tests/components/repairs/test_issue_registry.py rename to tests/helpers/test_issue_registry.py index 76faafce1c7..01c2d83fb46 100644 --- a/tests/components/repairs/test_issue_registry.py +++ b/tests/helpers/test_issue_registry.py @@ -1,20 +1,14 @@ """Test the repairs websocket API.""" -from homeassistant.components.repairs import async_create_issue, issue_registry -from homeassistant.components.repairs.const import DOMAIN -from homeassistant.components.repairs.issue_handler import ( - async_delete_issue, - async_ignore_issue, -) +import pytest + from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.helpers import issue_registry from tests.common import async_capture_events, flush_store async def test_load_issues(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" - assert await async_setup_component(hass, DOMAIN, {}) - issues = [ { "breaks_in_ha_version": "2022.9", @@ -68,7 +62,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: ) for issue in issues: - async_create_issue( + issue_registry.async_create_issue( hass, issue["domain"], issue["issue_id"], @@ -105,7 +99,9 @@ async def test_load_issues(hass: HomeAssistant) -> None: "issue_id": "issue_4", } - async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) + issue_registry.async_ignore_issue( + hass, issues[0]["domain"], issues[0]["issue_id"], True + ) await hass.async_block_till_done() assert len(events) == 5 @@ -115,7 +111,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "issue_id": "issue_1", } - async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) + issue_registry.async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) await hass.async_block_till_done() assert len(events) == 6 @@ -175,6 +171,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: assert issue4_registry2 == issue4 +@pytest.mark.parametrize("load_registries", [False]) async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> None: """Test loading stored issues on start.""" hass_storage[issue_registry.STORAGE_KEY] = { @@ -215,12 +212,13 @@ async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> }, } - assert await async_setup_component(hass, DOMAIN, {}) + await issue_registry.async_load(hass) registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] assert len(registry.issues) == 3 +@pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_1(hass: HomeAssistant, hass_storage) -> None: """Test migration from version 1.1.""" hass_storage[issue_registry.STORAGE_KEY] = { @@ -244,7 +242,7 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage) -> None: }, } - assert await async_setup_component(hass, DOMAIN, {}) + await issue_registry.async_load(hass) registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] assert len(registry.issues) == 2 From ad6beac5352c66033a270b68bf67ea6ca75ac036 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 25 Aug 2022 11:55:33 +0200 Subject: [PATCH 616/903] Add `hw_version` to MQTT device info (#77210) * Add hw_version * Add abbreviation for hw_version * Update tests * Update discovery tests --- homeassistant/components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/mixins.py | 6 ++++++ tests/components/mqtt/test_common.py | 4 ++++ tests/components/mqtt/test_device_trigger.py | 4 ++++ tests/components/mqtt/test_discovery.py | 5 +++++ tests/components/mqtt/test_tag.py | 4 ++++ 6 files changed, 24 insertions(+) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 32b67874a45..6f2eeeeedd0 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -263,6 +263,7 @@ DEVICE_ABBREVIATIONS = { "name": "name", "mf": "manufacturer", "mdl": "model", + "hw": "hw_version", "sw": "sw_version", "sa": "suggested_area", } diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 4b7ae92c6f4..bb035198611 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CONFIGURATION_URL, + ATTR_HW_VERSION, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, @@ -107,6 +108,7 @@ CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template" CONF_IDENTIFIERS = "identifiers" CONF_CONNECTIONS = "connections" CONF_MANUFACTURER = "manufacturer" +CONF_HW_VERSION = "hw_version" CONF_SW_VERSION = "sw_version" CONF_VIA_DEVICE = "via_device" CONF_DEPRECATED_VIA_HUB = "via_hub" @@ -199,6 +201,7 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( vol.Optional(CONF_MANUFACTURER): cv.string, vol.Optional(CONF_MODEL): cv.string, vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_HW_VERSION): cv.string, vol.Optional(CONF_SW_VERSION): cv.string, vol.Optional(CONF_VIA_DEVICE): cv.string, vol.Optional(CONF_SUGGESTED_AREA): cv.string, @@ -880,6 +883,9 @@ def device_info_from_config(config) -> DeviceInfo | None: if CONF_NAME in config: info[ATTR_NAME] = config[CONF_NAME] + if CONF_HW_VERSION in config: + info[ATTR_HW_VERSION] = config[CONF_HW_VERSION] + if CONF_SW_VERSION in config: info[ATTR_SW_VERSION] = config[CONF_SW_VERSION] diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index fe1f4003f0b..ac8ef98531e 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -29,6 +29,7 @@ DEFAULT_CONFIG_DEVICE_INFO_ID = { "manufacturer": "Whatever", "name": "Beer", "model": "Glass", + "hw_version": "rev1", "sw_version": "0.1-beta", "suggested_area": "default_area", "configuration_url": "http://example.com", @@ -39,6 +40,7 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { "manufacturer": "Whatever", "name": "Beer", "model": "Glass", + "hw_version": "rev1", "sw_version": "0.1-beta", "suggested_area": "default_area", "configuration_url": "http://example.com", @@ -958,6 +960,7 @@ async def help_test_entity_device_info_with_identifier( assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" + assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" assert device.suggested_area == "default_area" assert device.configuration_url == "http://example.com" @@ -990,6 +993,7 @@ async def help_test_entity_device_info_with_connection( assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" + assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" assert device.suggested_area == "default_area" assert device.configuration_url == "http://example.com" diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 37a59ef6b53..b49276b8f85 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -947,6 +947,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ "manufacturer": "Whatever", "name": "Beer", "model": "Glass", + "hw_version": "rev1", "sw_version": "0.1-beta", }, } @@ -962,6 +963,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" + assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" @@ -981,6 +983,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ "manufacturer": "Whatever", "name": "Beer", "model": "Glass", + "hw_version": "rev1", "sw_version": "0.1-beta", }, } @@ -994,6 +997,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" + assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 2bc330f8495..29ca1f11743 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -947,6 +947,7 @@ async def test_discovery_expansion(hass, mqtt_mock_entry_no_yaml_config, caplog) ' "ids":["5706DF"],' ' "name":"DiscoveryExpansionTest1 Device",' ' "mdl":"Generic",' + ' "hw":"rev1",' ' "sw":"1.2.3.4",' ' "mf":"None",' ' "sa":"default_area"' @@ -999,6 +1000,7 @@ async def test_discovery_expansion_2(hass, mqtt_mock_entry_no_yaml_config, caplo ' "ids":["5706DF"],' ' "name":"DiscoveryExpansionTest1 Device",' ' "mdl":"Generic",' + ' "hw":"rev1",' ' "sw":"1.2.3.4",' ' "mf":"None",' ' "sa":"default_area"' @@ -1037,6 +1039,7 @@ async def test_discovery_expansion_3(hass, mqtt_mock_entry_no_yaml_config, caplo ' "ids":["5706DF"],' ' "name":"DiscoveryExpansionTest1 Device",' ' "mdl":"Generic",' + ' "hw":"rev1",' ' "sw":"1.2.3.4",' ' "mf":"None",' ' "sa":"default_area"' @@ -1076,6 +1079,7 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( ' "ids":["5706DF"],' ' "name":"DiscoveryExpansionTest1 Device",' ' "mdl":"Generic",' + ' "hw":"rev1",' ' "sw":"1.2.3.4",' ' "mf":"None",' ' "sa":"default_area"' @@ -1124,6 +1128,7 @@ async def test_discovery_expansion_without_encoding_and_value_template_2( ' "ids":["5706DF"],' ' "name":"DiscoveryExpansionTest1 Device",' ' "mdl":"Generic",' + ' "hw":"rev1",' ' "sw":"1.2.3.4",' ' "mf":"None",' ' "sa":"default_area"' diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 507c6d99bed..ed33ed0dcdf 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -437,6 +437,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ "manufacturer": "Whatever", "name": "Beer", "model": "Glass", + "hw_version": "rev1", "sw_version": "0.1-beta", }, } @@ -452,6 +453,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" + assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" @@ -468,6 +470,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ "manufacturer": "Whatever", "name": "Beer", "model": "Glass", + "hw_version": "rev1", "sw_version": "0.1-beta", }, } @@ -481,6 +484,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" + assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" From 7fc294d9b172a3e16fd2c7871c9c5c844c46207c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 25 Aug 2022 12:29:31 +0200 Subject: [PATCH 617/903] Set cv hass in hass fixture (#77271) * Set cv hass in hass fixture * Move test_hass_cv and update docstring * Update tests/test_test_fixtures.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- tests/conftest.py | 2 ++ tests/test_test_fixtures.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 889568127cb..4a02083610b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -329,6 +329,8 @@ def hass(loop, load_registries, hass_storage, request): exceptions = [] hass = loop.run_until_complete(async_test_home_assistant(loop, load_registries)) + ha._cv_hass.set(hass) + orig_exception_handler = loop.get_exception_handler() loop.set_exception_handler(exc_handle) diff --git a/tests/test_test_fixtures.py b/tests/test_test_fixtures.py index 90362e95819..376cd79488f 100644 --- a/tests/test_test_fixtures.py +++ b/tests/test_test_fixtures.py @@ -4,6 +4,8 @@ import socket import pytest import pytest_socket +from homeassistant.core import async_get_hass + def test_sockets_disabled(): """Test we can't open sockets.""" @@ -16,3 +18,12 @@ def test_sockets_enabled(socket_enabled): mysocket = socket.socket() with pytest.raises(pytest_socket.SocketConnectBlockedError): mysocket.connect(("127.0.0.2", 1234)) + + +async def test_hass_cv(hass): + """Test hass context variable. + + When tests are using the `hass`, this tests that the hass context variable was set + in the fixture and that async_get_hass() works correctly. + """ + assert async_get_hass() is hass From e18dd4da165bc82c1a3833b356de7652c5e7dcf1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Aug 2022 13:16:12 +0200 Subject: [PATCH 618/903] Add pressure to openweathermap weather forecast (#77303) --- homeassistant/components/openweathermap/weather.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 9948ecc0428..814dc7dfd02 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -6,6 +6,7 @@ from typing import cast from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRESSURE, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_NATIVE_WIND_SPEED, @@ -33,6 +34,7 @@ from .const import ( ATTR_API_FORECAST_CONDITION, ATTR_API_FORECAST_PRECIPITATION, ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRESSURE, ATTR_API_FORECAST_TEMP, ATTR_API_FORECAST_TEMP_LOW, ATTR_API_FORECAST_TIME, @@ -56,6 +58,7 @@ FORECAST_MAP = { ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRESSURE: ATTR_FORECAST_NATIVE_PRESSURE, ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, From 5d9e462118769e455fff2efcb79fe5632d40d75f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 25 Aug 2022 13:30:05 +0200 Subject: [PATCH 619/903] Add repair for deprecated MQTT yaml config (#77174) * Add repair for deprecated MQTT yaml config * Update homeassistant/components/mqtt/strings.json Co-authored-by: Erik Montnemery * Update homeassistant/components/mqtt/strings.json Co-authored-by: Martin Hjelmare * Add restart instruction * Update homeassistant/components/mqtt/strings.json Co-authored-by: Martin Hjelmare * Update English translation * update issue_registry imports * Update homeassistant/components/mqtt/manifest.json Co-authored-by: Martin Hjelmare Co-authored-by: Erik Montnemery Co-authored-by: Martin Hjelmare --- homeassistant/components/mqtt/mixins.py | 17 ++++++++++++++++- homeassistant/components/mqtt/strings.json | 6 ++++++ .../components/mqtt/translations/en.json | 8 +++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index bb035198611..e4a24c0f5ad 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -28,7 +28,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, async_get_hass, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -48,6 +48,7 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -245,6 +246,20 @@ def warn_for_legacy_schema(domain: str) -> Callable: domain, ) warned.add(domain) + # Register a repair + async_create_issue( + async_get_hass(), + DOMAIN, + f"deprecated_yaml_{domain}", + breaks_in_ha_version="2022.12.0", # Warning first added in 2022.6.0 + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "more_info_url": f"https://www.home-assistant.io/integrations/{domain}.mqtt/#new_format", + "platform": domain, + }, + ) return config return validator diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 155f9fcb4f2..e11f0a685e7 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -1,4 +1,10 @@ { + "issues": { + "deprecated_yaml": { + "title": "Your manually configured MQTT {platform}(s) needs attention", + "description": "Manually configured MQTT {platform}(s) found under platform key `{platform}`.\n\nPlease move the configuration to the `mqtt` integration key and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information." + } + }, "config": { "step": { "broker": { diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 23012946a71..724387dc923 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -1,5 +1,11 @@ { - "config": { + "issues": { + "deprecated_yaml": { + "title": "Your manually configured MQTT {platform}(s) needs attention", + "description": "Manually configured MQTT {platform}(s) found under platform key `{platform}`.\n\nPlease move the configuration to the `mqtt` integration key and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information." + } + }, + "config": { "abort": { "already_configured": "Service is already configured", "single_instance_allowed": "Already configured. Only a single configuration possible." From 8ddee30787a4f34cc0ae4589b0c156ee4f32a6b9 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 25 Aug 2022 15:23:11 +0300 Subject: [PATCH 620/903] Revert "Add remote learn command to BraviaTV" (#77306) --- homeassistant/components/braviatv/coordinator.py | 14 -------------- homeassistant/components/braviatv/remote.py | 4 ---- 2 files changed, 18 deletions(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index bb6bf59f681..b5d91263b34 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -10,7 +10,6 @@ from typing import Any, Final, TypeVar from pybravia import BraviaTV, BraviaTVError from typing_extensions import Concatenate, ParamSpec -from homeassistant.components import persistent_notification from homeassistant.components.media_player.const import ( MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, @@ -257,16 +256,3 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): for _ in range(repeats): for cmd in command: await self.client.send_command(cmd) - - @catch_braviatv_errors - async def async_learn_command(self, entity_id: str) -> None: - """Display a list of available commands in a persistent notification.""" - commands = await self.client.get_command_list() - codes = ", ".join(commands.keys()) - title = "Bravia TV" - message = f"**List of available commands for `{entity_id}`**:\n\n{codes}" - persistent_notification.async_create( - self.hass, - title=title, - message=message, - ) diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 411b459fced..f45b2d74004 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -47,7 +47,3 @@ class BraviaTVRemote(BraviaTVEntity, RemoteEntity): """Send a command to device.""" repeats = kwargs[ATTR_NUM_REPEATS] await self.coordinator.async_send_command(command, repeats) - - async def async_learn_command(self, **kwargs: Any) -> None: - """Learn commands from the device.""" - await self.coordinator.async_learn_command(self.entity_id) From 4f526a92120f2d4430807c01517d315e0f0dec5e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 25 Aug 2022 09:24:09 -0400 Subject: [PATCH 621/903] Add reauth flow to Skybell (#75682) Co-authored-by: J. Nick Koston --- homeassistant/components/skybell/__init__.py | 6 +- .../components/skybell/config_flow.py | 34 +++++++ homeassistant/components/skybell/strings.json | 7 ++ .../components/skybell/translations/en.json | 7 ++ tests/components/skybell/test_config_flow.py | 94 ++++++++++++++++--- 5 files changed, 132 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 1e272dba27f..d986707dfe7 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -11,7 +11,7 @@ from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -59,8 +59,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: devices = await api.async_initialize() - except SkybellAuthenticationException: - return False + except SkybellAuthenticationException as ex: + raise ConfigEntryAuthFailed from ex except SkybellException as ex: raise ConfigEntryNotReady(f"Unable to connect to Skybell service: {ex}") from ex diff --git a/homeassistant/components/skybell/config_flow.py b/homeassistant/components/skybell/config_flow.py index 908eab4c46d..5e63ae4f929 100644 --- a/homeassistant/components/skybell/config_flow.py +++ b/homeassistant/components/skybell/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Skybell integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from aioskybell import Skybell, exceptions @@ -17,6 +18,39 @@ from .const import DOMAIN class SkybellFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Skybell.""" + reauth_email: str + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a reauthorization flow request.""" + self.reauth_email = entry_data[CONF_EMAIL] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle user's reauth credentials.""" + errors = {} + if user_input: + password = user_input[CONF_PASSWORD] + entry_id = self.context["entry_id"] + if entry := self.hass.config_entries.async_get_entry(entry_id): + _, error = await self._async_validate_input(self.reauth_email, password) + if error is None: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | user_input, + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + + errors["base"] = error + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + description_placeholders={CONF_EMAIL: self.reauth_email}, + errors=errors, + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/skybell/strings.json b/homeassistant/components/skybell/strings.json index 949223250df..f9122a1e100 100644 --- a/homeassistant/components/skybell/strings.json +++ b/homeassistant/components/skybell/strings.json @@ -6,6 +6,13 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "description": "Please update your password for {email}", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { diff --git a/homeassistant/components/skybell/translations/en.json b/homeassistant/components/skybell/translations/en.json index 767f6bfe64e..0a7be932056 100644 --- a/homeassistant/components/skybell/translations/en.json +++ b/homeassistant/components/skybell/translations/en.json @@ -15,6 +15,13 @@ "email": "Email", "password": "Password" } + }, + "reauth_confirm": { + "description": "Please update your password for {email}", + "title": "Reauthenticate Integration", + "data": { + "password": "Password" + } } } }, diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index 21ead201b54..9b7afd12c93 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -3,12 +3,20 @@ from unittest.mock import patch from aioskybell import exceptions +from homeassistant import config_entries from homeassistant.components.skybell.const import DOMAIN from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_PASSWORD, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import CONF_CONFIG_FLOW, _patch_skybell, _patch_skybell_devices +from . import ( + CONF_CONFIG_FLOW, + PASSWORD, + USER_ID, + _patch_skybell, + _patch_skybell_devices, +) from tests.common import MockConfigEntry @@ -20,16 +28,9 @@ def _patch_setup_entry() -> None: ) -def _patch_setup() -> None: - return patch( - "homeassistant.components.skybell.async_setup", - return_value=True, - ) - - async def test_flow_user(hass: HomeAssistant) -> None: """Test that the user step works.""" - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(), _patch_setup(): + with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -45,6 +46,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_CONFIG_FLOW + assert result["result"].unique_id == USER_ID async def test_flow_user_already_configured(hass: HomeAssistant) -> None: @@ -55,10 +57,10 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) + with _patch_skybell(), _patch_skybell_devices(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -99,3 +101,69 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} + + +async def test_step_reauth(hass: HomeAssistant) -> None: + """Test the reauth flow.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id=USER_ID, data=CONF_CONFIG_FLOW) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_step_reauth_failed(hass: HomeAssistant) -> None: + """Test the reauth flow fails and recovers.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id=USER_ID, data=CONF_CONFIG_FLOW) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch("homeassistant.components.skybell.Skybell.async_login") as skybell_mock: + skybell_mock.side_effect = exceptions.SkybellAuthenticationException(hass) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" From 0f86bb94d5ffa0601e5d1a380714aa800d676bd5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Aug 2022 08:24:55 -0500 Subject: [PATCH 622/903] Add thermopro integration (BLE) (#77242) --- CODEOWNERS | 2 + .../components/thermopro/__init__.py | 49 +++++ .../components/thermopro/config_flow.py | 94 ++++++++ homeassistant/components/thermopro/const.py | 3 + .../components/thermopro/manifest.json | 11 + homeassistant/components/thermopro/sensor.py | 148 +++++++++++++ .../components/thermopro/strings.json | 21 ++ .../components/thermopro/translations/en.json | 21 ++ homeassistant/generated/bluetooth.py | 5 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/thermopro/__init__.py | 25 +++ tests/components/thermopro/conftest.py | 8 + .../components/thermopro/test_config_flow.py | 200 ++++++++++++++++++ tests/components/thermopro/test_sensor.py | 50 +++++ 16 files changed, 644 insertions(+) create mode 100644 homeassistant/components/thermopro/__init__.py create mode 100644 homeassistant/components/thermopro/config_flow.py create mode 100644 homeassistant/components/thermopro/const.py create mode 100644 homeassistant/components/thermopro/manifest.json create mode 100644 homeassistant/components/thermopro/sensor.py create mode 100644 homeassistant/components/thermopro/strings.json create mode 100644 homeassistant/components/thermopro/translations/en.json create mode 100644 tests/components/thermopro/__init__.py create mode 100644 tests/components/thermopro/conftest.py create mode 100644 tests/components/thermopro/test_config_flow.py create mode 100644 tests/components/thermopro/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 799c81bfa81..698c91a1777 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1105,6 +1105,8 @@ build.json @home-assistant/supervisor /homeassistant/components/tesla_wall_connector/ @einarhauks /tests/components/tesla_wall_connector/ @einarhauks /homeassistant/components/tfiac/ @fredrike @mellado +/homeassistant/components/thermopro/ @bdraco +/tests/components/thermopro/ @bdraco /homeassistant/components/thethingsnetwork/ @fabaff /homeassistant/components/threshold/ @fabaff /tests/components/threshold/ @fabaff diff --git a/homeassistant/components/thermopro/__init__.py b/homeassistant/components/thermopro/__init__.py new file mode 100644 index 00000000000..7093484648f --- /dev/null +++ b/homeassistant/components/thermopro/__init__.py @@ -0,0 +1,49 @@ +"""The ThermoPro Bluetooth integration.""" +from __future__ import annotations + +import logging + +from thermopro_ble import ThermoProBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up ThermoPro BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = ThermoProBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/thermopro/config_flow.py b/homeassistant/components/thermopro/config_flow.py new file mode 100644 index 00000000000..f7e03aff685 --- /dev/null +++ b/homeassistant/components/thermopro/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for thermopro ble integration.""" +from __future__ import annotations + +from typing import Any + +from thermopro_ble import ThermoProBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class ThermoProConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for thermopro.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/thermopro/const.py b/homeassistant/components/thermopro/const.py new file mode 100644 index 00000000000..343729442cf --- /dev/null +++ b/homeassistant/components/thermopro/const.py @@ -0,0 +1,3 @@ +"""Constants for the ThermoPro Bluetooth integration.""" + +DOMAIN = "thermopro" diff --git a/homeassistant/components/thermopro/manifest.json b/homeassistant/components/thermopro/manifest.json new file mode 100644 index 00000000000..912a070ccf1 --- /dev/null +++ b/homeassistant/components/thermopro/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "thermopro", + "name": "ThermoPro", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/thermopro", + "bluetooth": [{ "local_name": "TP35*", "connectable": false }], + "dependencies": ["bluetooth"], + "requirements": ["thermopro-ble==0.4.0"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/thermopro/sensor.py b/homeassistant/components/thermopro/sensor.py new file mode 100644 index 00000000000..505f620229c --- /dev/null +++ b/homeassistant/components/thermopro/sensor.py @@ -0,0 +1,148 @@ +"""Support for thermopro ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from thermopro_ble import ( + DeviceKey, + SensorDeviceClass as ThermoProSensorDeviceClass, + SensorDeviceInfo, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + ( + ThermoProSensorDeviceClass.TEMPERATURE, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{ThermoProSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (ThermoProSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{ThermoProSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + ThermoProSensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{ThermoProSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the ThermoPro BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + ThermoProBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class ThermoProBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a thermopro ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/thermopro/strings.json b/homeassistant/components/thermopro/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/thermopro/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/thermopro/translations/en.json b/homeassistant/components/thermopro/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/thermopro/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index f33f26c366a..7ceb13bce9f 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -143,6 +143,11 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", "connectable": False }, + { + "domain": "thermopro", + "local_name": "TP35*", + "connectable": False + }, { "domain": "xiaomi_ble", "connectable": False, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 59070af31c7..237090420c1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -377,6 +377,7 @@ FLOWS = { "tautulli", "tellduslive", "tesla_wall_connector", + "thermopro", "tibber", "tile", "tolo", diff --git a/requirements_all.txt b/requirements_all.txt index 2d3404483ef..01edbe8d47a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2338,6 +2338,9 @@ tesla-wall-connector==1.0.1 # homeassistant.components.tensorflow # tf-models-official==2.5.0 +# homeassistant.components.thermopro +thermopro-ble==0.4.0 + # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc284136d8f..cc4030baf39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1587,6 +1587,9 @@ tesla-powerwall==0.3.18 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 +# homeassistant.components.thermopro +thermopro-ble==0.4.0 + # homeassistant.components.todoist todoist-python==8.0.0 diff --git a/tests/components/thermopro/__init__.py b/tests/components/thermopro/__init__.py new file mode 100644 index 00000000000..a7dd5fcf9c5 --- /dev/null +++ b/tests/components/thermopro/__init__.py @@ -0,0 +1,25 @@ +"""Tests for the ThermoPro integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_THERMOPRO_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + + +TP357_SERVICE_INFO = BluetoothServiceInfo( + name="TP357 (2142)", + manufacturer_data={61890: b"\x00\x1d\x02,"}, + service_uuids=[], + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-60, + service_data={}, + source="local", +) diff --git a/tests/components/thermopro/conftest.py b/tests/components/thermopro/conftest.py new file mode 100644 index 00000000000..1a4c59ff609 --- /dev/null +++ b/tests/components/thermopro/conftest.py @@ -0,0 +1,8 @@ +"""ThermoPro session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/thermopro/test_config_flow.py b/tests/components/thermopro/test_config_flow.py new file mode 100644 index 00000000000..681f059ecb3 --- /dev/null +++ b/tests/components/thermopro/test_config_flow.py @@ -0,0 +1,200 @@ +"""Test the ThermoPro config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.thermopro.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import NOT_THERMOPRO_SERVICE_INFO, TP357_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TP357_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.thermopro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TP357 (2142) AC3D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_bluetooth_not_thermopro(hass): + """Test discovery via bluetooth not thermopro.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_THERMOPRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.thermopro.config_flow.async_discovered_service_info", + return_value=[TP357_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.thermopro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TP357 (2142) AC3D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.thermopro.config_flow.async_discovered_service_info", + return_value=[TP357_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.thermopro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.thermopro.config_flow.async_discovered_service_info", + return_value=[TP357_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TP357_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TP357_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TP357_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TP357_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.thermopro.config_flow.async_discovered_service_info", + return_value=[TP357_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.thermopro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TP357 (2142) AC3D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/thermopro/test_sensor.py b/tests/components/thermopro/test_sensor.py new file mode 100644 index 00000000000..908101faf56 --- /dev/null +++ b/tests/components/thermopro/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the ThermoPro config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.thermopro.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import TP357_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(TP357_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + temp_sensor = hass.states.get("sensor.tp357_2142_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "24.1" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "TP357 (2142) Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From c741d9d0452970c39397deca1c65766c8cb917da Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Aug 2022 15:33:05 +0200 Subject: [PATCH 623/903] Update integrations to import issue_registry from helpers (#77305) * Update integrations to import issue_registry from helpers * Update tests --- homeassistant/components/ambee/__init__.py | 10 +++++----- homeassistant/components/ambee/manifest.json | 1 - .../components/android_ip_webcam/__init__.py | 3 +-- .../components/android_ip_webcam/manifest.json | 1 - homeassistant/components/anthemav/manifest.json | 1 - homeassistant/components/anthemav/media_player.py | 2 +- homeassistant/components/demo/__init__.py | 2 +- homeassistant/components/demo/manifest.json | 2 +- .../components/deutsche_bahn/manifest.json | 1 - homeassistant/components/deutsche_bahn/sensor.py | 2 +- homeassistant/components/flunearyou/__init__.py | 2 +- homeassistant/components/flunearyou/manifest.json | 1 - homeassistant/components/google/__init__.py | 2 +- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/guardian/__init__.py | 2 +- homeassistant/components/guardian/manifest.json | 1 - .../components/homeassistant_alerts/__init__.py | 7 +++++-- .../components/homeassistant_alerts/manifest.json | 3 +-- homeassistant/components/lametric/__init__.py | 2 +- homeassistant/components/lametric/manifest.json | 2 +- homeassistant/components/lyric/__init__.py | 2 +- homeassistant/components/lyric/manifest.json | 2 +- homeassistant/components/miflora/manifest.json | 1 - homeassistant/components/miflora/sensor.py | 2 +- homeassistant/components/mitemp_bt/manifest.json | 1 - homeassistant/components/mitemp_bt/sensor.py | 2 +- homeassistant/components/nest/__init__.py | 10 +++++----- homeassistant/components/nest/manifest.json | 2 +- .../components/openalpr_local/image_processing.py | 2 +- .../components/openalpr_local/manifest.json | 1 - .../components/openexchangerates/manifest.json | 1 - .../components/openexchangerates/sensor.py | 3 +-- homeassistant/components/pushover/manifest.json | 1 - homeassistant/components/pushover/notify.py | 3 +-- homeassistant/components/radiotherm/climate.py | 2 +- homeassistant/components/radiotherm/manifest.json | 1 - homeassistant/components/repairs/__init__.py | 12 ------------ homeassistant/components/senz/__init__.py | 2 +- homeassistant/components/senz/manifest.json | 2 +- homeassistant/components/simplepush/manifest.json | 1 - homeassistant/components/simplepush/notify.py | 3 +-- homeassistant/components/skybell/__init__.py | 3 +-- homeassistant/components/skybell/manifest.json | 2 +- homeassistant/components/soundtouch/manifest.json | 1 - .../components/soundtouch/media_player.py | 2 +- homeassistant/components/spotify/__init__.py | 2 +- homeassistant/components/spotify/manifest.json | 2 +- homeassistant/components/steam_online/__init__.py | 2 +- .../components/steam_online/manifest.json | 1 - homeassistant/components/uscis/manifest.json | 1 - homeassistant/components/uscis/sensor.py | 2 +- homeassistant/components/volvooncall/__init__.py | 2 +- .../components/volvooncall/manifest.json | 1 - homeassistant/components/xbox/__init__.py | 2 +- homeassistant/components/xbox/manifest.json | 2 +- tests/components/demo/test_init.py | 2 ++ .../components/homeassistant_alerts/test_init.py | 7 +++++++ tests/components/repairs/test_init.py | 15 ++++++++------- tests/components/repairs/test_websocket_api.py | 6 +++--- 59 files changed, 69 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py index dbc503928c4..547b8720fef 100644 --- a/homeassistant/components/ambee/__init__.py +++ b/homeassistant/components/ambee/__init__.py @@ -3,15 +3,15 @@ from __future__ import annotations from ambee import AirQuality, Ambee, AmbeeAuthenticationError, Pollen -from homeassistant.components.repairs import ( - IssueSeverity, - async_create_issue, - async_delete_issue, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json index f74832100cd..3226e9de3a3 100644 --- a/homeassistant/components/ambee/manifest.json +++ b/homeassistant/components/ambee/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambee", "requirements": ["ambee==0.4.0"], - "dependencies": ["repairs"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index 12885db6375..d792b42d4bf 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -4,8 +4,6 @@ from __future__ import annotations from pydroid_ipcam import PyDroidIPCam import voluptuous as vol -from homeassistant.components.repairs.issue_handler import async_create_issue -from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -22,6 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import ( diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 29a077443c0..ded547bbf57 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -2,7 +2,6 @@ "domain": "android_ip_webcam", "name": "Android IP Webcam", "config_flow": true, - "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "requirements": ["pydroid-ipcam==2.0.0"], "codeowners": ["@engrbm87"], diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 2055ec75f27..9a88d53e75a 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -3,7 +3,6 @@ "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", "requirements": ["anthemav==1.4.1"], - "dependencies": ["repairs"], "codeowners": ["@hyralex"], "config_flow": true, "iot_class": "local_push", diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 4754a416d27..7582aa24083 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -13,7 +13,6 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -28,6 +27,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 6d0f6499001..7384a79adcc 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -11,7 +11,6 @@ from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, ) -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -21,6 +20,7 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.core import HomeAssistant from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index 2965a66e23e..df6fa494079 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -3,7 +3,7 @@ "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", "after_dependencies": ["recorder"], - "dependencies": ["conversation", "group", "repairs", "zone"], + "dependencies": ["conversation", "group", "zone"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "calculated" diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index dd69a940de0..1eeb2241db5 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -3,7 +3,6 @@ "name": "Deutsche Bahn", "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", "requirements": ["schiene==0.23"], - "dependencies": ["repairs"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["schiene"] diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index 9638fccf2cd..8a563574f46 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -7,12 +7,12 @@ import logging import schiene import voluptuous as vol -from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_OFFSET from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 75349002ec0..ecdf05bbeb1 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -9,11 +9,11 @@ from typing import Any from pyflunearyou import Client from pyflunearyou.errors import FluNearYouError -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN, LOGGER diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index fa98bf2e01e..ee69961d1b0 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -3,7 +3,6 @@ "name": "Flu Near You", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flunearyou", - "dependencies": ["repairs"], "requirements": ["pyflunearyou==2.0.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index c983868b167..7416a9d7793 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -20,7 +20,6 @@ from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, @@ -41,6 +40,7 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .api import ApiAuthImpl, get_feature_access diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index bf745a72927..d39f2093cf0 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -2,7 +2,7 @@ "domain": "google", "name": "Google Calendars", "config_flow": true, - "dependencies": ["application_credentials", "repairs"], + "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": ["gcal-sync==0.10.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index a30536f66c3..a3cc7a0031b 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -10,7 +10,6 @@ from aioguardian import Client from aioguardian.errors import GuardianError import voluptuous as vol -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( ATTR_DEVICE_ID, @@ -26,6 +25,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 24dfbad13fe..7fab487563c 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,7 +3,6 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "dependencies": ["repairs"], "requirements": ["aioguardian==2022.07.0"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 60386e3d080..006f0db54a5 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -9,11 +9,14 @@ import logging import aiohttp from awesomeversion import AwesomeVersion, AwesomeVersionStrategy -from homeassistant.components.repairs import async_create_issue, async_delete_issue -from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import __version__ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json index 7c9ddf4f905..62b729cada9 100644 --- a/homeassistant/components/homeassistant_alerts/manifest.json +++ b/homeassistant/components/homeassistant_alerts/manifest.json @@ -3,6 +3,5 @@ "name": "Home Assistant Alerts", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts", - "codeowners": ["@home-assistant/core"], - "dependencies": ["repairs"] + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index 04f02a7f8f9..8b7c8b2dca2 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -2,12 +2,12 @@ import voluptuous as vol from homeassistant.components import notify as hass_notify -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, PLATFORMS diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 578906483fa..735c05e659c 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -5,7 +5,7 @@ "requirements": ["demetriek==0.2.2"], "codeowners": ["@robbiet480", "@frenck"], "iot_class": "local_polling", - "dependencies": ["application_credentials", "repairs"], + "dependencies": ["application_credentials"], "loggers": ["demetriek"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index d1048ac8e58..8a93591b037 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -12,7 +12,6 @@ from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import async_timeout -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -24,6 +23,7 @@ from homeassistant.helpers import ( device_registry as dr, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 91b152cdf21..c0d9168f46f 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Lyric", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", - "dependencies": ["application_credentials", "repairs"], + "dependencies": ["application_credentials"], "requirements": ["aiolyric==1.0.8"], "codeowners": ["@timmo001"], "quality_scale": "silver", diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index faae8fb140e..20d695dedd0 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -3,7 +3,6 @@ "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", "requirements": [], - "dependencies": ["repairs"], "codeowners": ["@danielhiversen", "@basnijholt"], "iot_class": "local_polling" } diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 76808e9706c..764e03786f8 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -1,10 +1,10 @@ """Support for Xiaomi Mi Flora BLE plant sensor.""" from __future__ import annotations -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 3af60301802..81adfe5f3f9 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -3,7 +3,6 @@ "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", "requirements": [], - "dependencies": ["repairs"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index 74d0db7648c..a1646bed51c 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -1,10 +1,10 @@ """Support for Xiaomi Mi Temp BLE environmental sensor.""" from __future__ import annotations -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 44658558b62..aa50b999c1c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -29,11 +29,6 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.camera import Image, img_util from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.view import HomeAssistantView -from homeassistant.components.repairs import ( - IssueSeverity, - async_create_issue, - async_delete_issue, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, @@ -57,6 +52,11 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.typing import ConfigType from . import api, config_flow diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index d826272b207..72e0aed8420 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "application_credentials", "repairs"], + "dependencies": ["ffmpeg", "http", "application_credentials"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"], diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index 06688b3b297..ef8c942189f 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -14,7 +14,6 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, ) -from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -25,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.async_ import run_callback_threadsafe diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 5243aa2b282..8837d79369d 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -3,6 +3,5 @@ "name": "OpenALPR Local", "documentation": "https://www.home-assistant.io/integrations/openalpr_local", "codeowners": [], - "dependencies": ["repairs"], "iot_class": "local_push" } diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index efa67ff39e9..5b3b3ed8fdf 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -3,7 +3,6 @@ "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", "requirements": ["aioopenexchangerates==0.4.0"], - "dependencies": ["repairs"], "codeowners": ["@MartinHjelmare"], "iot_class": "cloud_polling", "config_flow": true diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 7f7681b6887..76573b351b3 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -3,8 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.repairs.issue_handler import async_create_issue -from homeassistant.components.repairs.models import IssueSeverity from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE @@ -13,6 +11,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index a13de899480..0f5ef103ce8 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -1,7 +1,6 @@ { "domain": "pushover", "name": "Pushover", - "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": ["pushover_complete==1.1.1"], "codeowners": ["@engrbm87"], diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index d9073b18a0a..16ad452fd9a 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -15,12 +15,11 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.components.repairs.issue_handler import async_create_issue -from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ...exceptions import HomeAssistantError diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index c466a7108e8..cd2bb69ad58 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -18,7 +18,6 @@ from homeassistant.components.climate.const import ( HVACAction, HVACMode, ) -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, @@ -29,6 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index 5c37b4e3cde..c6ae4e5bb06 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -3,7 +3,6 @@ "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": ["radiotherm==2.1.0"], - "dependencies": ["repairs"], "codeowners": ["@bdraco", "@vinnyfuria"], "iot_class": "local_polling", "loggers": ["radiotherm"], diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index 726102d4b08..9c26fe01a69 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -2,13 +2,6 @@ from __future__ import annotations from homeassistant.core import HomeAssistant -from homeassistant.helpers.issue_registry import ( - IssueSeverity, - async_create_issue, - async_delete_issue, - create_issue, - delete_issue, -) from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api @@ -17,13 +10,8 @@ from .issue_handler import ConfirmRepairFlow from .models import RepairsFlow __all__ = [ - "async_create_issue", - "async_delete_issue", - "create_issue", - "delete_issue", "DOMAIN", "ConfirmRepairFlow", - "IssueSeverity", "RepairsFlow", ] diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 9155c8ca036..e6ded8e0355 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -7,7 +7,6 @@ import logging from aiosenz import SENZAPI, Thermostat from httpx import RequestError -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -17,6 +16,7 @@ from homeassistant.helpers import ( config_validation as cv, httpx_client, ) +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed diff --git a/homeassistant/components/senz/manifest.json b/homeassistant/components/senz/manifest.json index 36687e46d4a..937a20d8482 100644 --- a/homeassistant/components/senz/manifest.json +++ b/homeassistant/components/senz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/senz", "requirements": ["aiosenz==1.0.0"], - "dependencies": ["application_credentials", "repairs"], + "dependencies": ["application_credentials"], "codeowners": ["@milanmeu"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index 6b4ee263ba6..7c37546485a 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -5,7 +5,6 @@ "requirements": ["simplepush==1.1.4"], "codeowners": ["@engrbm87"], "config_flow": true, - "dependencies": ["repairs"], "iot_class": "cloud_polling", "loggers": ["simplepush"] } diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 8bcf166ad25..36abf31fcb7 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -13,10 +13,9 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.components.notify.const import ATTR_DATA -from homeassistant.components.repairs.issue_handler import async_create_issue -from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import CONF_EVENT, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ATTR_EVENT, CONF_DEVICE_KEY, CONF_SALT, DOMAIN diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index d986707dfe7..35de1a5b57b 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -6,13 +6,12 @@ import asyncio from aioskybell import Skybell from aioskybell.exceptions import SkybellAuthenticationException, SkybellException -from homeassistant.components.repairs.issue_handler import async_create_issue -from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import DOMAIN diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 4365a9cf713..bfef4bc3422 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", "requirements": ["aioskybell==22.7.0"], - "dependencies": ["ffmpeg", "repairs"], + "dependencies": ["ffmpeg"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["aioskybell"] diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index 4512f3a8f9b..c1c2abd3b80 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": ["libsoundtouch==0.8"], "zeroconf": ["_soundtouch._tcp.local."], - "dependencies": ["repairs"], "codeowners": ["@kroimon"], "iot_class": "local_polling", "loggers": ["libsoundtouch"], diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 74d89404d27..6dff35df7bc 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -19,7 +19,6 @@ from homeassistant.components.media_player import ( from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -37,6 +36,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 83841a1780e..94be47bed7e 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -9,7 +9,6 @@ import aiohttp import requests from spotipy import Spotify, SpotifyException -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -19,6 +18,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 0556cad26b7..2940700d230 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/spotify", "requirements": ["spotipy==2.20.0"], "zeroconf": ["_spotify-connect._tcp.local."], - "dependencies": ["application_credentials", "repairs"], + "dependencies": ["application_credentials"], "codeowners": ["@frenck"], "config_flow": true, "quality_scale": "silver", diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index c422269a277..b1697e3b794 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -1,12 +1,12 @@ """The Steam integration.""" from __future__ import annotations -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index f2e3a35bbe7..f8aba1aee07 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/steam_online", "requirements": ["steamodd==4.21"], - "dependencies": ["repairs"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["steam"] diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index 882dc588eba..0680848f70a 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -3,7 +3,6 @@ "name": "U.S. Citizenship and Immigration Services (USCIS)", "documentation": "https://www.home-assistant.io/integrations/uscis", "requirements": ["uscisstatus==0.1.1"], - "dependencies": ["repairs"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["uscisstatus"] diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index b4719243a8b..a26bb655c58 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -7,12 +7,12 @@ import logging import uscisstatus import voluptuous as vol -from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 52890ce9d55..832a0460903 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -8,7 +8,6 @@ import voluptuous as vol from volvooncall import Connection from volvooncall.dashboard import Instrument -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_NAME, @@ -23,6 +22,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 16628d0a5d2..4865b95d56b 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -3,7 +3,6 @@ "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", "requirements": ["volvooncall==0.10.0"], - "dependencies": ["repairs"], "codeowners": ["@molobrakos"], "iot_class": "cloud_polling", "loggers": ["geopy", "hbmqtt", "volvooncall"], diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index c49fd55e8c8..443eee7a334 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -21,7 +21,6 @@ from xbox.webapi.api.provider.smartglass.models import ( ) from homeassistant.components import application_credentials -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant @@ -30,6 +29,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator diff --git a/homeassistant/components/xbox/manifest.json b/homeassistant/components/xbox/manifest.json index 8857a55d66d..5adfa54a901 100644 --- a/homeassistant/components/xbox/manifest.json +++ b/homeassistant/components/xbox/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xbox", "requirements": ["xbox-webapi==2.0.11"], - "dependencies": ["auth", "application_credentials", "repairs"], + "dependencies": ["auth", "application_credentials"], "codeowners": ["@hunterjm"], "iot_class": "cloud_polling" } diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 413badde18d..1adfe0a5834 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.demo import DOMAIN from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.statistics import list_statistic_ids +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component @@ -74,6 +75,7 @@ async def test_demo_statistics(hass, recorder_mock): async def test_issues_created(hass, hass_client, hass_ws_client): """Test issues are created and can be fixed.""" + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() await hass.async_start() diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index a0fb2e8557d..6b8cb7bf475 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -8,6 +8,7 @@ from unittest.mock import ANY, patch import pytest from homeassistant.components.homeassistant_alerts import DOMAIN, UPDATE_INTERVAL +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -28,6 +29,12 @@ Content for {filename} ) +@pytest.fixture(autouse=True) +async def setup_repairs(hass): + """Set up the repairs integration.""" + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + + @pytest.mark.parametrize( "ha_version, expected_alerts", ( diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 9071785aeea..9311b1cf024 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -6,19 +6,20 @@ from aiohttp import ClientWebSocketResponse from freezegun import freeze_time import pytest -from homeassistant.components.repairs import ( - async_create_issue, - async_delete_issue, - create_issue, - delete_issue, -) from homeassistant.components.repairs.const import DOMAIN from homeassistant.components.repairs.issue_handler import ( async_process_repairs_platforms, ) from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant -from homeassistant.helpers.issue_registry import IssueSeverity, async_ignore_issue +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, + async_ignore_issue, + create_issue, + delete_issue, +) from homeassistant.setup import async_setup_component from tests.common import mock_platform diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 508e2edeb92..0ba4c3043df 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -9,7 +9,7 @@ import pytest import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.repairs import RepairsFlow, async_create_issue +from homeassistant.components.repairs import RepairsFlow from homeassistant.components.repairs.const import DOMAIN from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant @@ -49,7 +49,7 @@ async def create_issues(hass, ws_client, issues=None): issues = DEFAULT_ISSUES for issue in issues: - async_create_issue( + issue_registry.async_create_issue( hass, issue["domain"], issue["issue_id"], @@ -463,7 +463,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> ] for issue in issues: - async_create_issue( + issue_registry.async_create_issue( hass, issue["domain"], issue["issue_id"], From f166d213909232d4e1ea9f5cf67de5e412f3cc35 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 25 Aug 2022 16:22:58 +0100 Subject: [PATCH 624/903] Fix characteristic cache clear in homekit_controller on BLE unpair (#77309) --- homeassistant/components/homekit_controller/__init__.py | 7 +------ homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 9b431b09c9e..dca626d2abd 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -20,8 +20,7 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice -from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS -from .storage import EntityMapStorage +from .const import KNOWN_DEVICES, TRIGGERS from .utils import async_get_controller _LOGGER = logging.getLogger(__name__) @@ -83,10 +82,6 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Cleanup caches before removing config entry.""" hkid = entry.data["AccessoryPairingID"] - # Remove cached type data from .storage/homekit_controller-entity-map - entity_map_storage: EntityMapStorage = hass.data[ENTITY_MAP] - entity_map_storage.async_delete_map(hkid) - controller = await async_get_controller(hass) # Remove the pairing on the device, making the device discoverable again. diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 143627fe7f0..10b3cb4cbb5 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.4.0"], + "requirements": ["aiohomekit==1.5.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 01edbe8d47a..44c957a9fba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.4.0 +aiohomekit==1.5.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc4030baf39..e0b8f21d3f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.4.0 +aiohomekit==1.5.0 # homeassistant.components.emulated_hue # homeassistant.components.http From ac9ba8f23177400ef8f5d4ccdce02a2ad79419a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Aug 2022 17:39:51 +0200 Subject: [PATCH 625/903] Improve demo test coverage (#77301) --- homeassistant/components/demo/__init__.py | 74 +++++++---------------- tests/components/demo/test_init.py | 43 ++++++++++++- tests/components/demo/test_sensor.py | 59 ++++++++++++++++++ 3 files changed, 122 insertions(+), 54 deletions(-) create mode 100644 tests/components/demo/test_sensor.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 7384a79adcc..8572ddfbbe2 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -244,10 +244,17 @@ def _generate_mean_statistics(start, end, init_value, max_diff): return statistics -def _generate_sum_statistics(start, end, init_value, max_diff): +async def _insert_sum_statistics(hass, metadata, start, end, max_diff): statistics = [] now = start - sum_ = init_value + sum_ = 0 + statistic_id = metadata["statistic_id"] + + last_stats = await get_instance(hass).async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, True + ) + if statistic_id in last_stats: + sum_ = last_stats[statistic_id][0]["sum"] or 0 while now < end: sum_ = sum_ + random() * max_diff statistics.append( @@ -258,7 +265,7 @@ def _generate_sum_statistics(start, end, init_value, max_diff): ) now = now + datetime.timedelta(hours=1) - return statistics + async_add_external_statistics(hass, metadata, statistics) async def _insert_statistics(hass: HomeAssistant) -> None: @@ -266,6 +273,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None: now = dt_util.now() yesterday = now - datetime.timedelta(days=1) yesterday_midnight = yesterday.replace(hour=0, minute=0, second=0, microsecond=0) + today_midnight = yesterday_midnight + datetime.timedelta(days=1) # Fake yesterday's temperatures metadata: StatisticMetaData = { @@ -276,98 +284,58 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "has_mean": True, "has_sum": False, } - statistics = _generate_mean_statistics( - yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), 15, 1 - ) + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) async_add_external_statistics(hass, metadata, statistics) # Add external energy consumption in kWh, ~ 12 kWh / day # This should be possible to pick for the energy dashboard - statistic_id = f"{DOMAIN}:energy_consumption_kwh" metadata = { "source": DOMAIN, "name": "Energy consumption 1", - "statistic_id": statistic_id, + "statistic_id": f"{DOMAIN}:energy_consumption_kwh", "unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, } - sum_ = 0 - last_stats = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, True - ) - if statistic_id in last_stats: - sum_ = last_stats[statistic_id][0]["sum"] or 0 - statistics = _generate_sum_statistics( - yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), sum_, 2 - ) - async_add_external_statistics(hass, metadata, statistics) + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 2) # Add external energy consumption in MWh, ~ 12 kWh / day # This should not be possible to pick for the energy dashboard - statistic_id = f"{DOMAIN}:energy_consumption_mwh" metadata = { "source": DOMAIN, "name": "Energy consumption 2", - "statistic_id": statistic_id, + "statistic_id": f"{DOMAIN}:energy_consumption_mwh", "unit_of_measurement": "MWh", "has_mean": False, "has_sum": True, } - sum_ = 0 - last_stats = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, True + await _insert_sum_statistics( + hass, metadata, yesterday_midnight, today_midnight, 0.002 ) - if statistic_id in last_stats: - sum_ = last_stats[statistic_id][0]["sum"] or 0 - statistics = _generate_sum_statistics( - yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), sum_, 0.002 - ) - async_add_external_statistics(hass, metadata, statistics) # Add external gas consumption in m³, ~6 m3/day # This should be possible to pick for the energy dashboard - statistic_id = f"{DOMAIN}:gas_consumption_m3" metadata = { "source": DOMAIN, "name": "Gas consumption 1", - "statistic_id": statistic_id, + "statistic_id": f"{DOMAIN}:gas_consumption_m3", "unit_of_measurement": "m³", "has_mean": False, "has_sum": True, } - sum_ = 0 - last_stats = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, True - ) - if statistic_id in last_stats: - sum_ = last_stats[statistic_id][0]["sum"] or 0 - statistics = _generate_sum_statistics( - yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), sum_, 1 - ) - async_add_external_statistics(hass, metadata, statistics) + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 1) # Add external gas consumption in ft³, ~180 ft3/day # This should not be possible to pick for the energy dashboard - statistic_id = f"{DOMAIN}:gas_consumption_ft3" metadata = { "source": DOMAIN, "name": "Gas consumption 2", - "statistic_id": statistic_id, + "statistic_id": f"{DOMAIN}:gas_consumption_ft3", "unit_of_measurement": "ft³", "has_mean": False, "has_sum": True, } - sum_ = 0 - last_stats = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, True - ) - if statistic_id in last_stats: - sum_ = last_stats[statistic_id][0]["sum"] or 0 - statistics = _generate_sum_statistics( - yesterday_midnight, yesterday_midnight + datetime.timedelta(days=1), sum_, 30 - ) - async_add_external_statistics(hass, metadata, statistics) + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 30) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 1adfe0a5834..5a407ba25fc 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,4 +1,5 @@ """The tests for the Demo component.""" +import datetime from http import HTTPStatus import json from unittest.mock import ANY, patch @@ -7,10 +8,15 @@ import pytest from homeassistant.components.demo import DOMAIN from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.statistics import list_statistic_ids +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, + list_statistic_ids, +) from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util from tests.components.recorder.common import async_wait_recording_done @@ -73,6 +79,41 @@ async def test_demo_statistics(hass, recorder_mock): } in statistic_ids +async def test_demo_statistics_growth(hass, recorder_mock): + """Test that the demo sum statistics adds to the previous state.""" + now = dt_util.now() + last_week = now - datetime.timedelta(days=7) + last_week_midnight = last_week.replace(hour=0, minute=0, second=0, microsecond=0) + + statistic_id = f"{DOMAIN}:energy_consumption_kwh" + metadata = { + "source": DOMAIN, + "name": "Energy consumption 1", + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + "has_mean": False, + "has_sum": True, + } + statistics = [ + { + "start": last_week_midnight, + "sum": 2**20, + } + ] + async_add_external_statistics(hass, metadata, statistics) + await async_wait_recording_done(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + await async_wait_recording_done(hass) + + statistics = await get_instance(hass).async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, False + ) + assert statistics[statistic_id][0]["sum"] > 2**20 + + async def test_issues_created(hass, hass_client, hass_ws_client): """Test issues are created and can be fixed.""" assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) diff --git a/tests/components/demo/test_sensor.py b/tests/components/demo/test_sensor.py new file mode 100644 index 00000000000..0adf94d7867 --- /dev/null +++ b/tests/components/demo/test_sensor.py @@ -0,0 +1,59 @@ +"""The tests for the demo sensor component.""" +from datetime import timedelta + +import pytest + +from homeassistant import core as ha +from homeassistant.components.demo import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import mock_restore_cache_with_extra_data + + +@pytest.mark.parametrize("entity_id, delta", (("sensor.total_energy_kwh", 0.5),)) +async def test_energy_sensor(hass: ha.HomeAssistant, entity_id, delta, freezer): + """Test energy sensors increase periodically.""" + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": DOMAIN}} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == "0" + + freezer.tick(timedelta(minutes=5, seconds=1)) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == str(delta) + + +@pytest.mark.parametrize("entity_id, delta", (("sensor.total_energy_kwh", 0.5),)) +async def test_restore_state(hass: ha.HomeAssistant, entity_id, delta, freezer): + """Test energy sensors restore state.""" + fake_state = ha.State( + entity_id, + "", + ) + fake_extra_data = { + "native_value": 2**20, + "native_unit_of_measurement": None, + } + mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) + + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": DOMAIN}} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == str(2**20) + + freezer.tick(timedelta(minutes=5, seconds=1)) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == str(2**20 + delta) From 462ec4ced320831d4fcfbbcfd7b7b3c589e5125b Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 25 Aug 2022 10:21:41 -0600 Subject: [PATCH 626/903] Add Flume DataUpdateCoordinator class (#77114) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + CODEOWNERS | 4 +- homeassistant/components/flume/const.py | 8 +++ homeassistant/components/flume/coordinator.py | 35 ++++++++++ homeassistant/components/flume/manifest.json | 2 +- homeassistant/components/flume/sensor.py | 66 ++++--------------- 6 files changed, 61 insertions(+), 55 deletions(-) create mode 100644 homeassistant/components/flume/coordinator.py diff --git a/.coveragerc b/.coveragerc index 95af2a269d1..f0ccf391927 100644 --- a/.coveragerc +++ b/.coveragerc @@ -393,6 +393,7 @@ omit = homeassistant/components/flick_electric/sensor.py homeassistant/components/flock/notify.py homeassistant/components/flume/__init__.py + homeassistant/components/flume/coordinator.py homeassistant/components/flume/sensor.py homeassistant/components/flunearyou/__init__.py homeassistant/components/flunearyou/repairs.py diff --git a/CODEOWNERS b/CODEOWNERS index 698c91a1777..aff9c981129 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -349,8 +349,8 @@ build.json @home-assistant/supervisor /tests/components/flipr/ @cnico /homeassistant/components/flo/ @dmulcahey /tests/components/flo/ @dmulcahey -/homeassistant/components/flume/ @ChrisMandich @bdraco -/tests/components/flume/ @ChrisMandich @bdraco +/homeassistant/components/flume/ @ChrisMandich @bdraco @jeeftor +/tests/components/flume/ @ChrisMandich @bdraco @jeeftor /homeassistant/components/flunearyou/ @bachya /tests/components/flunearyou/ @bachya /homeassistant/components/flux_led/ @icemanch @bdraco diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index 95236829bd9..5e095961ed8 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -1,6 +1,9 @@ """The Flume component.""" from __future__ import annotations +from datetime import timedelta +import logging + from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import Platform @@ -10,6 +13,11 @@ PLATFORMS = [Platform.SENSOR] DEFAULT_NAME = "Flume Sensor" +NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=1) +DEVICE_SCAN_INTERVAL = timedelta(minutes=1) + +_LOGGER = logging.getLogger(__package__) + FLUME_TYPE_SENSOR = 2 FLUME_QUERIES_SENSOR: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py new file mode 100644 index 00000000000..9e23141cd5e --- /dev/null +++ b/homeassistant/components/flume/coordinator.py @@ -0,0 +1,35 @@ +"""The IntelliFire integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import _LOGGER, DEVICE_SCAN_INTERVAL, DOMAIN + + +class FlumeDeviceDataUpdateCoordinator(DataUpdateCoordinator[None]): + """Data update coordinator for an individual flume device.""" + + def __init__(self, hass: HomeAssistant, flume_device) -> None: + """Initialize the Coordinator.""" + super().__init__( + hass, + name=DOMAIN, + logger=_LOGGER, + update_interval=DEVICE_SCAN_INTERVAL, + ) + + self.flume_device = flume_device + + async def _async_update_data(self) -> None: + """Get the latest data from the Flume.""" + _LOGGER.debug("Updating Flume data") + try: + await self.hass.async_add_executor_job(self.flume_device.update_force) + except Exception as ex: + raise UpdateFailed(f"Error communicating with flume API: {ex}") from ex + _LOGGER.debug( + "Flume update details: values=%s query_payload=%s", + self.flume_device.values, + self.flume_device.query_payload, + ) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 05b0a4bf19a..3b42e84275c 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -3,7 +3,7 @@ "name": "Flume", "documentation": "https://www.home-assistant.io/integrations/flume/", "requirements": ["pyflume==0.6.5"], - "codeowners": ["@ChrisMandich", "@bdraco"], + "codeowners": ["@ChrisMandich", "@bdraco", "@jeeftor"], "config_flow": true, "dhcp": [ { diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index bc98750cc9f..fc3e03325e9 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -1,6 +1,4 @@ """Sensor for displaying the number of result from Flume.""" -from datetime import timedelta -import logging from numbers import Number from pyflume import FlumeData @@ -11,14 +9,11 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( DEFAULT_NAME, + DEVICE_SCAN_INTERVAL, DOMAIN, FLUME_AUTH, FLUME_DEVICES, @@ -31,11 +26,7 @@ from .const import ( KEY_DEVICE_LOCATION_TIMEZONE, KEY_DEVICE_TYPE, ) - -_LOGGER = logging.getLogger(__name__) - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) -SCAN_INTERVAL = timedelta(minutes=1) +from .coordinator import FlumeDeviceDataUpdateCoordinator async def async_setup_entry( @@ -62,25 +53,27 @@ async def async_setup_entry( device_name = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_NAME] device_timezone = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_TIMEZONE] device_friendly_name = f"{name} {device_name}" + flume_device = FlumeData( flume_auth, device_id, device_timezone, - SCAN_INTERVAL, + scan_interval=DEVICE_SCAN_INTERVAL, update_on_init=False, http_session=http_session, ) - coordinator = _create_flume_device_coordinator(hass, flume_device) + coordinator = FlumeDeviceDataUpdateCoordinator( + hass=hass, flume_device=flume_device + ) flume_entity_list.extend( [ FlumeSensor( - coordinator, - flume_device, - device_friendly_name, - device_id, - description, + coordinator=coordinator, + description=description, + name=device_friendly_name, + device_id=device_id, ) for description in FLUME_QUERIES_SENSOR ] @@ -96,7 +89,6 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): def __init__( self, coordinator, - flume_device, name, device_id, description: SensorEntityDescription, @@ -104,7 +96,6 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): """Initialize the Flume sensor.""" super().__init__(coordinator) self.entity_description = description - self._flume_device = flume_device self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{description.key}_{device_id}" @@ -119,10 +110,10 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): def native_value(self): """Return the state of the sensor.""" sensor_key = self.entity_description.key - if sensor_key not in self._flume_device.values: + if sensor_key not in self.coordinator.flume_device.values: return None - return _format_state_value(self._flume_device.values[sensor_key]) + return _format_state_value(self.coordinator.flume_device.values[sensor_key]) async def async_added_to_hass(self) -> None: """Request an update when added.""" @@ -134,32 +125,3 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): def _format_state_value(value): return round(value, 1) if isinstance(value, Number) else None - - -def _create_flume_device_coordinator(hass, flume_device): - """Create a data coordinator for the flume device.""" - - async def _async_update_data(): - """Get the latest data from the Flume.""" - _LOGGER.debug("Updating Flume data") - try: - await hass.async_add_executor_job(flume_device.update_force) - except Exception as ex: - raise UpdateFailed(f"Error communicating with flume API: {ex}") from ex - _LOGGER.debug( - "Flume update details: %s", - { - "values": flume_device.values, - "query_payload": flume_device.query_payload, - }, - ) - - return DataUpdateCoordinator( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name=flume_device.device_id, - update_method=_async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=SCAN_INTERVAL, - ) From b563bd0ae5d7cf2dceb148975f0c1065461436a9 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 25 Aug 2022 10:32:27 -0600 Subject: [PATCH 627/903] Add support for Litter-Robot 4 (#75790) --- .../components/litterrobot/__init__.py | 6 +++++- homeassistant/components/litterrobot/button.py | 13 +++++++------ homeassistant/components/litterrobot/entity.py | 17 ++++++++++++----- homeassistant/components/litterrobot/hub.py | 15 ++++++++++----- .../components/litterrobot/manifest.json | 2 +- homeassistant/components/litterrobot/select.py | 14 +++++--------- homeassistant/components/litterrobot/sensor.py | 12 ++++++------ homeassistant/components/litterrobot/switch.py | 16 +++++++--------- homeassistant/components/litterrobot/vacuum.py | 8 +++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/conftest.py | 5 +++-- tests/components/litterrobot/test_select.py | 4 ++-- 13 files changed, 63 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index f3b150f2c1d..334773c6f86 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except LitterRobotException as ex: raise ConfigEntryNotReady from ex - if hub.account.robots: + if any(hub.litter_robots()): await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -40,6 +40,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + await hub.account.disconnect() + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 90fbecc6753..5cb65596ec6 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -1,6 +1,8 @@ """Support for Litter-Robot button.""" from __future__ import annotations +from pylitterbot import LitterRobot3 + from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -22,12 +24,11 @@ async def async_setup_entry( """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - LitterRobotResetWasteDrawerButton( - robot=robot, entity_type=TYPE_RESET_WASTE_DRAWER, hub=hub - ) - for robot in hub.account.robots - ] + LitterRobotResetWasteDrawerButton( + robot=robot, entity_type=TYPE_RESET_WASTE_DRAWER, hub=hub + ) + for robot in hub.litter_robots() + if isinstance(robot, LitterRobot3) ) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 501b71fbd06..b169e075455 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -6,7 +6,7 @@ from datetime import time import logging from typing import Any -from pylitterbot import Robot +from pylitterbot import LitterRobot from pylitterbot.exceptions import InvalidCommandException from typing_extensions import ParamSpec @@ -32,7 +32,9 @@ REFRESH_WAIT_TIME_SECONDS = 8 class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): """Generic Litter-Robot entity representing common data and methods.""" - def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: LitterRobot, entity_type: str, hub: LitterRobotHub + ) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.robot = robot @@ -52,6 +54,7 @@ class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): @property def device_info(self) -> DeviceInfo: """Return the device information for a Litter-Robot.""" + assert self.robot.serial return DeviceInfo( identifiers={(DOMAIN, self.robot.serial)}, manufacturer="Litter-Robot", @@ -63,7 +66,9 @@ class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): class LitterRobotControlEntity(LitterRobotEntity): """A Litter-Robot entity that can control the unit.""" - def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: LitterRobot, entity_type: str, hub: LitterRobotHub + ) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._refresh_callback: CALLBACK_TYPE | None = None @@ -113,7 +118,7 @@ class LitterRobotControlEntity(LitterRobotEntity): if time_str is None: return None - if (parsed_time := dt_util.parse_time(time_str)) is None: + if (parsed_time := dt_util.parse_time(time_str)) is None: # pragma: no cover return None return ( @@ -132,7 +137,9 @@ class LitterRobotConfigEntity(LitterRobotControlEntity): _attr_entity_category = EntityCategory.CONFIG - def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: LitterRobot, entity_type: str, hub: LitterRobotHub + ) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._assumed_state: bool | None = None diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index bde4c780482..40ba9e74a7a 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -1,16 +1,17 @@ """A wrapper 'hub' for the Litter-Robot API.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Generator, Mapping from datetime import timedelta import logging from typing import Any -from pylitterbot import Account +from pylitterbot import Account, LitterRobot from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -23,11 +24,10 @@ UPDATE_INTERVAL_SECONDS = 20 class LitterRobotHub: """A Litter-Robot hub wrapper class.""" - account: Account - def __init__(self, hass: HomeAssistant, data: Mapping[str, Any]) -> None: """Initialize the Litter-Robot hub.""" self._data = data + self.account = Account(websession=async_get_clientsession(hass)) async def _async_update_data() -> bool: """Update all device states from the Litter-Robot API.""" @@ -44,7 +44,6 @@ class LitterRobotHub: async def login(self, load_robots: bool = False) -> None: """Login to Litter-Robot.""" - self.account = Account() try: await self.account.connect( username=self._data[CONF_USERNAME], @@ -58,3 +57,9 @@ class LitterRobotHub: except LitterRobotException as ex: _LOGGER.error("Unable to connect to Litter-Robot API") raise ex + + def litter_robots(self) -> Generator[LitterRobot, Any, Any]: + """Get Litter-Robots from the account.""" + return ( + robot for robot in self.account.robots if isinstance(robot, LitterRobot) + ) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 104a06afc8f..9c5eb1486bf 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.7.0"], + "requirements": ["pylitterbot==2022.8.0"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling", "loggers": ["pylitterbot"] diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index c1a0718510d..403f7a8c257 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -1,8 +1,6 @@ """Support for Litter-Robot selects.""" from __future__ import annotations -from pylitterbot.robot import VALID_WAIT_TIMES - from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -24,12 +22,10 @@ async def async_setup_entry( hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - [ - LitterRobotSelect( - robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub - ) - for robot in hub.account.robots - ] + LitterRobotSelect( + robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub + ) + for robot in hub.litter_robots() ) @@ -46,7 +42,7 @@ class LitterRobotSelect(LitterRobotConfigEntity, SelectEntity): @property def options(self) -> list[str]: """Return a set of selectable options.""" - return [str(minute) for minute in VALID_WAIT_TIMES] + return [str(minute) for minute in self.robot.VALID_WAIT_TIMES] async def async_select_option(self, option: str) -> None: """Change the selected option.""" diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index b6dd2a976c3..53ed3605c68 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Any, Union, cast -from pylitterbot.robot import Robot +from pylitterbot import LitterRobot from homeassistant.components.sensor import ( SensorDeviceClass, @@ -40,7 +40,7 @@ class LitterRobotSensorEntityDescription(SensorEntityDescription): """A class that describes Litter-Robot sensor entities.""" icon_fn: Callable[[Any], str | None] = lambda _: None - should_report: Callable[[Robot], bool] = lambda _: True + should_report: Callable[[LitterRobot], bool] = lambda _: True class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): @@ -50,7 +50,7 @@ class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): def __init__( self, - robot: Robot, + robot: LitterRobot, hub: LitterRobotHub, description: LitterRobotSensorEntityDescription, ) -> None: @@ -87,13 +87,13 @@ ROBOT_SENSORS = [ name="Sleep Mode Start Time", key="sleep_mode_start_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] + should_report=lambda robot: robot.sleep_mode_enabled, ), LitterRobotSensorEntityDescription( name="Sleep Mode End Time", key="sleep_mode_end_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] + should_report=lambda robot: robot.sleep_mode_enabled, ), LitterRobotSensorEntityDescription( name="Last Seen", @@ -120,5 +120,5 @@ async def async_setup_entry( async_add_entities( LitterRobotSensorEntity(robot=robot, hub=hub, description=description) for description in ROBOT_SENSORS - for robot in hub.account.robots + for robot in hub.litter_robots() ) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 5374add1e34..69050057050 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -21,7 +21,7 @@ class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.night_light_mode_enabled # type: ignore[no-any-return] + return self.robot.night_light_mode_enabled @property def icon(self) -> str: @@ -45,7 +45,7 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.panel_lock_enabled # type: ignore[no-any-return] + return self.robot.panel_lock_enabled @property def icon(self) -> str: @@ -76,10 +76,8 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - - entities: list[SwitchEntity] = [] - for robot in hub.account.robots: - for switch_class, switch_type in ROBOT_SWITCHES: - entities.append(switch_class(robot=robot, entity_type=switch_type, hub=hub)) - - async_add_entities(entities) + async_add_entities( + switch_class(robot=robot, entity_type=switch_type, hub=hub) + for switch_class, switch_type in ROBOT_SWITCHES + for robot in hub.litter_robots() + ) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 51be573b14e..fe73ac16497 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -5,7 +5,7 @@ import logging from typing import Any from pylitterbot.enums import LitterBoxStatus -from pylitterbot.robot import VALID_WAIT_TIMES +from pylitterbot.robot.litterrobot import VALID_WAIT_TIMES import voluptuous as vol from homeassistant.components.vacuum import ( @@ -56,10 +56,8 @@ async def async_setup_entry( hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) - for robot in hub.account.robots - ] + LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) + for robot in hub.litter_robots() ) platform = entity_platform.async_get_current_platform() diff --git a/requirements_all.txt b/requirements_all.txt index 44c957a9fba..b09a621d4ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1647,7 +1647,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.7.0 +pylitterbot==2022.8.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0b8f21d3f3..6c754ebd0c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1148,7 +1148,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.7.0 +pylitterbot==2022.8.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 0e3d85dc828..d5d29e12988 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from unittest.mock import AsyncMock, MagicMock, patch -from pylitterbot import Account, Robot +from pylitterbot import Account, LitterRobot3, Robot from pylitterbot.exceptions import InvalidCommandException import pytest @@ -23,7 +23,7 @@ def create_mock_robot( if not robot_data: robot_data = {} - robot = Robot(data={**ROBOT_DATA, **robot_data}) + robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}) robot.start_cleaning = AsyncMock(side_effect=side_effect) robot.set_power_status = AsyncMock(side_effect=side_effect) robot.reset_waste_drawer = AsyncMock(side_effect=side_effect) @@ -31,6 +31,7 @@ def create_mock_robot( robot.set_night_light = AsyncMock(side_effect=side_effect) robot.set_panel_lockout = AsyncMock(side_effect=side_effect) robot.set_wait_time = AsyncMock(side_effect=side_effect) + robot.refresh = AsyncMock(side_effect=side_effect) return robot diff --git a/tests/components/litterrobot/test_select.py b/tests/components/litterrobot/test_select.py index e3e9782423e..eda59216718 100644 --- a/tests/components/litterrobot/test_select.py +++ b/tests/components/litterrobot/test_select.py @@ -1,7 +1,7 @@ """Test the Litter-Robot select entity.""" from datetime import timedelta -from pylitterbot.robot import VALID_WAIT_TIMES +from pylitterbot import LitterRobot3 import pytest from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS @@ -38,7 +38,7 @@ async def test_wait_time_select(hass: HomeAssistant, mock_account): data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID} count = 0 - for wait_time in VALID_WAIT_TIMES: + for wait_time in LitterRobot3.VALID_WAIT_TIMES: count += 1 data[ATTR_OPTION] = wait_time From 7fbb9c189f17a0106067b3ecee9a26a3c33748c0 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 25 Aug 2022 14:09:38 -0400 Subject: [PATCH 628/903] Bump version of pyunifiprotect to 4.1.15 (#77320) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 6ff2e8bb91d..2d3b664382a 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.1.4", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.1.5", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index b09a621d4ed..a25550c7620 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2019,7 +2019,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.4 +pyunifiprotect==4.1.5 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c754ebd0c1..45012a5c710 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1382,7 +1382,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.4 +pyunifiprotect==4.1.5 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 8c24d5810cab16dba24bd1ab39a5c83c6f64fd64 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 25 Aug 2022 12:31:04 -0600 Subject: [PATCH 629/903] Remove deprecated reset_waste_drawer and set_wait_time services from litterrobot (#77052) --- .../components/litterrobot/services.yaml | 26 ------------ .../components/litterrobot/vacuum.py | 36 ---------------- tests/components/litterrobot/test_vacuum.py | 42 ++----------------- 3 files changed, 3 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/litterrobot/services.yaml b/homeassistant/components/litterrobot/services.yaml index 0071c525567..164445e375f 100644 --- a/homeassistant/components/litterrobot/services.yaml +++ b/homeassistant/components/litterrobot/services.yaml @@ -1,12 +1,5 @@ # Describes the format for available Litter-Robot services -reset_waste_drawer: - name: Reset waste drawer - description: Reset the waste drawer level. - target: - entity: - integration: litterrobot - set_sleep_mode: name: Set sleep mode description: Set the sleep mode and start time. @@ -27,22 +20,3 @@ set_sleep_mode: example: '"22:30:00"' selector: time: - -set_wait_time: - name: Set wait time - description: Set the wait time, in minutes, between when your cat uses the Litter-Robot and when the unit cycles automatically. - target: - entity: - integration: litterrobot - fields: - minutes: - name: Minutes - description: Minutes to wait. - required: true - default: 7 - selector: - select: - options: - - "3" - - "7" - - "15" diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index fe73ac16497..11a437e893c 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -5,7 +5,6 @@ import logging from typing import Any from pylitterbot.enums import LitterBoxStatus -from pylitterbot.robot.litterrobot import VALID_WAIT_TIMES import voluptuous as vol from homeassistant.components.vacuum import ( @@ -30,9 +29,7 @@ _LOGGER = logging.getLogger(__name__) TYPE_LITTER_BOX = "Litter Box" -SERVICE_RESET_WASTE_DRAWER = "reset_waste_drawer" SERVICE_SET_SLEEP_MODE = "set_sleep_mode" -SERVICE_SET_WAIT_TIME = "set_wait_time" LITTER_BOX_STATUS_STATE_MAP = { LitterBoxStatus.CLEAN_CYCLE: STATE_CLEANING, @@ -61,11 +58,6 @@ async def async_setup_entry( ) platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - SERVICE_RESET_WASTE_DRAWER, - {}, - "async_reset_waste_drawer", - ) platform.async_register_entity_service( SERVICE_SET_SLEEP_MODE, { @@ -74,11 +66,6 @@ async def async_setup_entry( }, "async_set_sleep_mode", ) - platform.async_register_entity_service( - SERVICE_SET_WAIT_TIME, - {vol.Required("minutes"): vol.All(vol.Coerce(int), vol.In(VALID_WAIT_TIMES))}, - "async_set_wait_time", - ) class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): @@ -116,18 +103,6 @@ class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): """Start a clean cycle.""" await self.perform_action_and_refresh(self.robot.start_cleaning) - async def async_reset_waste_drawer(self) -> None: - """Reset the waste drawer level.""" - # The Litter-Robot reset waste drawer service has been replaced by a - # dedicated button entity and marked as deprecated - _LOGGER.warning( - "The 'litterrobot.reset_waste_drawer' service is deprecated and " - "replaced by a dedicated reset waste drawer button entity; Please " - "use that entity to reset the waste drawer instead" - ) - await self.robot.reset_waste_drawer() - self.coordinator.async_set_updated_data(True) - async def async_set_sleep_mode( self, enabled: bool, start_time: str | None = None ) -> None: @@ -138,17 +113,6 @@ class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): self.parse_time_at_default_timezone(start_time), ) - async def async_set_wait_time(self, minutes: int) -> None: - """Set the wait time.""" - # The Litter-Robot set wait time service has been replaced by a - # dedicated select entity and marked as deprecated - _LOGGER.warning( - "The 'litterrobot.set_wait_time' service is deprecated and " - "replaced by a dedicated set wait time select entity; Please " - "use that entity to set the wait time instead" - ) - await self.perform_action_and_refresh(self.robot.set_wait_time, minutes) - @property def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 89f8f077b55..02667bb8310 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -6,15 +6,10 @@ from typing import Any from unittest.mock import MagicMock import pytest -from voluptuous.error import MultipleInvalid from homeassistant.components.litterrobot import DOMAIN from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS -from homeassistant.components.litterrobot.vacuum import ( - SERVICE_RESET_WASTE_DRAWER, - SERVICE_SET_SLEEP_MODE, - SERVICE_SET_WAIT_TIME, -) +from homeassistant.components.litterrobot.vacuum import SERVICE_SET_SLEEP_MODE from homeassistant.components.vacuum import ( ATTR_STATUS, DOMAIN as PLATFORM_DOMAIN, @@ -34,16 +29,14 @@ from .conftest import setup_integration from tests.common import async_fire_time_changed COMPONENT_SERVICE_DOMAIN = { - SERVICE_RESET_WASTE_DRAWER: DOMAIN, SERVICE_SET_SLEEP_MODE: DOMAIN, - SERVICE_SET_WAIT_TIME: DOMAIN, } async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: """Tests the vacuum entity was set up.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) - assert hass.services.has_service(DOMAIN, SERVICE_RESET_WASTE_DRAWER) + assert hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum @@ -68,7 +61,7 @@ async def test_no_robots( """Tests the vacuum entity was set up.""" await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) - assert not hass.services.has_service(DOMAIN, SERVICE_RESET_WASTE_DRAWER) + assert not hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) async def test_vacuum_with_error( @@ -88,7 +81,6 @@ async def test_vacuum_with_error( (SERVICE_START, "start_cleaning", None), (SERVICE_TURN_OFF, "set_power_status", None), (SERVICE_TURN_ON, "set_power_status", None), - (SERVICE_RESET_WASTE_DRAWER, "reset_waste_drawer", {"deprecated": True}), ( SERVICE_SET_SLEEP_MODE, "set_sleep_mode", @@ -96,16 +88,6 @@ async def test_vacuum_with_error( ), (SERVICE_SET_SLEEP_MODE, "set_sleep_mode", {"data": {"enabled": True}}), (SERVICE_SET_SLEEP_MODE, "set_sleep_mode", {"data": {"enabled": False}}), - ( - SERVICE_SET_WAIT_TIME, - "set_wait_time", - {"data": {"minutes": 3}, "deprecated": True}, - ), - ( - SERVICE_SET_WAIT_TIME, - "set_wait_time", - {"data": {"minutes": "15"}, "deprecated": True}, - ), ], ) async def test_commands( @@ -137,21 +119,3 @@ async def test_commands( async_fire_time_changed(hass, future) getattr(mock_account.robots[0], command).assert_called_once() assert (f"'{DOMAIN}.{service}' service is deprecated" in caplog.text) is deprecated - - -async def test_invalid_wait_time(hass: HomeAssistant, mock_account: MagicMock) -> None: - """Test an attempt to send an invalid wait time to the vacuum.""" - await setup_integration(hass, mock_account, PLATFORM_DOMAIN) - - vacuum = hass.states.get(VACUUM_ENTITY_ID) - assert vacuum - assert vacuum.state == STATE_DOCKED - - with pytest.raises(MultipleInvalid): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_WAIT_TIME, - {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, "minutes": 10}, - blocking=True, - ) - assert not mock_account.robots[0].set_wait_time.called From edb4e4de3595380b066cbd94ee2ce87f04665996 Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 25 Aug 2022 13:45:35 -0600 Subject: [PATCH 630/903] Refactor Flume to use base entity class (#77115) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/flume/entity.py | 41 +++++++++++++++++++++ homeassistant/components/flume/sensor.py | 45 ++++++------------------ 3 files changed, 52 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/flume/entity.py diff --git a/.coveragerc b/.coveragerc index f0ccf391927..af27bb86d66 100644 --- a/.coveragerc +++ b/.coveragerc @@ -394,6 +394,7 @@ omit = homeassistant/components/flock/notify.py homeassistant/components/flume/__init__.py homeassistant/components/flume/coordinator.py + homeassistant/components/flume/entity.py homeassistant/components/flume/sensor.py homeassistant/components/flunearyou/__init__.py homeassistant/components/flunearyou/repairs.py diff --git a/homeassistant/components/flume/entity.py b/homeassistant/components/flume/entity.py new file mode 100644 index 00000000000..b36ecd28cf8 --- /dev/null +++ b/homeassistant/components/flume/entity.py @@ -0,0 +1,41 @@ +"""Platform for shared base classes for sensors.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import FlumeDeviceDataUpdateCoordinator + + +class FlumeEntity(CoordinatorEntity[FlumeDeviceDataUpdateCoordinator]): + """Base entity class.""" + + _attr_attribution = "Data provided by Flume API" + _attr_has_entity_name = True + + def __init__( + self, + coordinator: FlumeDeviceDataUpdateCoordinator, + description: EntityDescription, + device_id: str, + ) -> None: + """Class initializer.""" + super().__init__(coordinator) + self.entity_description = description + self.device_id = device_id + self._attr_unique_id = f"{description.key}_{device_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + manufacturer="Flume, Inc.", + model="Flume Smart Water Monitor", + name=f"Flume {device_id}", + configuration_url="https://portal.flumewater.com", + ) + + async def async_added_to_hass(self): + """Request an update when added.""" + await super().async_added_to_hass() + # We do not ask for an update with async_add_entities() + # because it will update disabled entities + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index fc3e03325e9..b9b5f819520 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -5,14 +5,10 @@ from pyflume import FlumeData from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - DEFAULT_NAME, DEVICE_SCAN_INTERVAL, DOMAIN, FLUME_AUTH, @@ -22,11 +18,11 @@ from .const import ( FLUME_TYPE_SENSOR, KEY_DEVICE_ID, KEY_DEVICE_LOCATION, - KEY_DEVICE_LOCATION_NAME, KEY_DEVICE_LOCATION_TIMEZONE, KEY_DEVICE_TYPE, ) from .coordinator import FlumeDeviceDataUpdateCoordinator +from .entity import FlumeEntity async def async_setup_entry( @@ -35,24 +31,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Flume sensor.""" + flume_domain_data = hass.data[DOMAIN][config_entry.entry_id] flume_auth = flume_domain_data[FLUME_AUTH] http_session = flume_domain_data[FLUME_HTTP_SESSION] flume_devices = flume_domain_data[FLUME_DEVICES] - config = config_entry.data - name = config.get(CONF_NAME, DEFAULT_NAME) - flume_entity_list = [] for device in flume_devices.device_list: if device[KEY_DEVICE_TYPE] != FLUME_TYPE_SENSOR: continue device_id = device[KEY_DEVICE_ID] - device_name = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_NAME] device_timezone = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_TIMEZONE] - device_friendly_name = f"{name} {device_name}" flume_device = FlumeData( flume_auth, @@ -72,7 +64,6 @@ async def async_setup_entry( FlumeSensor( coordinator=coordinator, description=description, - name=device_friendly_name, device_id=device_id, ) for description in FLUME_QUERIES_SENSOR @@ -83,28 +74,19 @@ async def async_setup_entry( async_add_entities(flume_entity_list) -class FlumeSensor(CoordinatorEntity, SensorEntity): +class FlumeSensor(FlumeEntity, SensorEntity): """Representation of the Flume sensor.""" + coordinator: FlumeDeviceDataUpdateCoordinator + def __init__( self, - coordinator, - name, - device_id, + coordinator: FlumeDeviceDataUpdateCoordinator, + device_id: str, description: SensorEntityDescription, - ): - """Initialize the Flume sensor.""" - super().__init__(coordinator) - self.entity_description = description - - self._attr_name = f"{name} {description.name}" - self._attr_unique_id = f"{description.key}_{device_id}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device_id)}, - manufacturer="Flume, Inc.", - model="Flume Smart Water Monitor", - name=self.name, - ) + ) -> None: + """Inlitializer function with type hints.""" + super().__init__(coordinator, description, device_id) @property def native_value(self): @@ -115,13 +97,6 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): return _format_state_value(self.coordinator.flume_device.values[sensor_key]) - async def async_added_to_hass(self) -> None: - """Request an update when added.""" - await super().async_added_to_hass() - # We do not ask for an update with async_add_entities() - # because it will update disabled entities - await self.coordinator.async_request_refresh() - def _format_state_value(value): return round(value, 1) if isinstance(value, Number) else None From a09e9040f3a1e88a7e4cb82cf859898c70fe4b7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Aug 2022 15:22:09 -0500 Subject: [PATCH 631/903] Bump aiohomekit to 1.5.1 (#77323) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 10b3cb4cbb5..e3526dd870a 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.5.0"], + "requirements": ["aiohomekit==1.5.1"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index a25550c7620..4fdb96947d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.5.0 +aiohomekit==1.5.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45012a5c710..67c8b74c0fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.5.0 +aiohomekit==1.5.1 # homeassistant.components.emulated_hue # homeassistant.components.http From 525afb729cf998723b21ac40bd3042ed1b33de62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Aug 2022 17:45:27 -0400 Subject: [PATCH 632/903] Disable some upnp entities by default (#77330) --- homeassistant/components/upnp/binary_sensor.py | 4 +++- homeassistant/components/upnp/sensor.py | 10 ++++++++++ tests/components/upnp/conftest.py | 10 +++++++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index d49bbddf996..7da7f187882 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import UpnpBinarySensorEntityDescription, UpnpDataUpdateCoordinator, UpnpEntity @@ -16,6 +17,8 @@ BINARYSENSOR_ENTITY_DESCRIPTIONS: tuple[UpnpBinarySensorEntityDescription, ...] UpnpBinarySensorEntityDescription( key=WAN_STATUS, name="wan status", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -44,7 +47,6 @@ class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity): """Class for UPnP/IGD binary sensors.""" entity_description: UpnpBinarySensorEntityDescription - _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY def __init__( self, diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 908a5b53940..53a918ba053 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND, TIME_SECONDS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import UpnpDataUpdateCoordinator, UpnpEntity, UpnpSensorEntityDescription @@ -31,6 +32,7 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( icon="mdi:server-network", native_unit_of_measurement=DATA_BYTES, format="d", + entity_registry_enabled_default=False, ), UpnpSensorEntityDescription( key=BYTES_SENT, @@ -38,6 +40,7 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( icon="mdi:server-network", native_unit_of_measurement=DATA_BYTES, format="d", + entity_registry_enabled_default=False, ), UpnpSensorEntityDescription( key=PACKETS_RECEIVED, @@ -45,6 +48,7 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( icon="mdi:server-network", native_unit_of_measurement=DATA_PACKETS, format="d", + entity_registry_enabled_default=False, ), UpnpSensorEntityDescription( key=PACKETS_SENT, @@ -52,6 +56,7 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( icon="mdi:server-network", native_unit_of_measurement=DATA_PACKETS, format="d", + entity_registry_enabled_default=False, ), UpnpSensorEntityDescription( key=ROUTER_IP, @@ -65,11 +70,14 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, format="d", + entity_category=EntityCategory.DIAGNOSTIC, ), UpnpSensorEntityDescription( key=WAN_STATUS, name="wan status", icon="mdi:server-network", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, ), ) @@ -97,6 +105,7 @@ DERIVED_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, format=".1f", + entity_registry_enabled_default=False, ), UpnpSensorEntityDescription( key=PACKETS_SENT, @@ -105,6 +114,7 @@ DERIVED_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, format=".1f", + entity_registry_enabled_default=False, ), ) diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 0d3e869db35..e7cd24d0c7c 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -1,7 +1,7 @@ """Configuration for SSDP tests.""" from __future__ import annotations -from unittest.mock import AsyncMock, MagicMock, create_autospec, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch from urllib.parse import urlparse from async_upnp_client.client import UpnpDevice @@ -184,7 +184,11 @@ async def mock_config_entry( # Load config_entry. entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + with patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + PropertyMock(return_value=True), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() yield entry From 29b74f10c25d167fd4b0393563ac6623ce1be760 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Aug 2022 17:05:43 -0500 Subject: [PATCH 633/903] Bump govee-ble to 0.16.1 (#77311) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index de76435f5d4..d3aaf6066d4 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -43,7 +43,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.16.0"], + "requirements": ["govee-ble==0.16.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 4fdb96947d5..f30a7e0da3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.16.0 +govee-ble==0.16.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67c8b74c0fb..d9e0b30ca99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -564,7 +564,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.16.0 +govee-ble==0.16.1 # homeassistant.components.gree greeclimate==1.3.0 From 0e10e6f4cd2e5bdb84a3e25eb47fe7ed644d93bb Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 25 Aug 2022 18:15:56 -0400 Subject: [PATCH 634/903] Bump version of pyunifiprotect to 4.1.7 (#77334) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 2d3b664382a..41b62e3631b 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.1.5", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.1.7", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index f30a7e0da3a..ec53475282c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2019,7 +2019,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.5 +pyunifiprotect==4.1.7 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9e0b30ca99..27c96bf2599 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1382,7 +1382,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.5 +pyunifiprotect==4.1.7 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From bda9cb59d206f8a9c3af3c64de555816a874ce21 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 26 Aug 2022 00:20:29 +0200 Subject: [PATCH 635/903] Clean up double spotify persistent notification for re-auth (#77307) --- homeassistant/components/spotify/config_flow.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index 013063308bb..bbfb92db091 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -7,7 +7,6 @@ from typing import Any from spotipy import Spotify -from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow @@ -63,14 +62,6 @@ class SpotifyFlowHandler( self.context["entry_id"] ) - persistent_notification.async_create( - self.hass, - f"Spotify integration for account {entry_data['id']} needs to be " - "re-authenticated. Please go to the integrations page to re-configure it.", - "Spotify re-authentication", - "spotify_reauth", - ) - return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -87,7 +78,6 @@ class SpotifyFlowHandler( errors={}, ) - persistent_notification.async_dismiss(self.hass, "spotify_reauth") return await self.async_step_pick_implementation( user_input={"implementation": self.reauth_entry.data["auth_implementation"]} ) From 5c0fc0c0021ba7d13409e8a1218af59bacf76081 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 25 Aug 2022 19:54:52 -0400 Subject: [PATCH 636/903] Add adopt/unadopt flows for UniFi Protect devices (#76524) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/button.py | 74 +++++++++++++++++- .../components/unifiprotect/const.py | 1 + homeassistant/components/unifiprotect/data.py | 69 ++++++++++------- .../components/unifiprotect/entity.py | 63 +++++++++++---- .../components/unifiprotect/light.py | 3 - homeassistant/components/unifiprotect/lock.py | 3 - .../components/unifiprotect/media_player.py | 3 - .../components/unifiprotect/models.py | 1 + .../components/unifiprotect/switch.py | 1 + tests/components/unifiprotect/test_button.py | 76 +++++++++++++++++-- tests/components/unifiprotect/test_migrate.py | 4 +- tests/components/unifiprotect/utils.py | 7 ++ 12 files changed, 246 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 9440e46b936..823a050ef09 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -2,9 +2,10 @@ from __future__ import annotations from dataclasses import dataclass +import logging from typing import Final -from pyunifiprotect.data import ProtectAdoptableDeviceModel +from pyunifiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId from homeassistant.components.button import ( ButtonDeviceClass, @@ -12,16 +13,20 @@ from homeassistant.components.button import ( ButtonEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, DOMAIN +from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd +_LOGGER = logging.getLogger(__name__) + @dataclass class ProtectButtonEntityDescription( @@ -33,6 +38,7 @@ class ProtectButtonEntityDescription( DEVICE_CLASS_CHIME_BUTTON: Final = "unifiprotect__chime_button" +KEY_ADOPT = "adopt" ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( @@ -44,6 +50,21 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ufp_press="reboot", ufp_perm=PermRequired.WRITE, ), + ProtectButtonEntityDescription( + key="unadopt", + entity_registry_enabled_default=False, + name="Unadopt Device", + icon="mdi:delete", + ufp_press="unadopt", + ufp_perm=PermRequired.DELETE, + ), +) + +ADOPT_BUTTON = ProtectButtonEntityDescription[ProtectAdoptableDeviceModel]( + key=KEY_ADOPT, + name="Adopt Device", + icon="mdi:plus-circle", + ufp_press="adopt", ) SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( @@ -73,6 +94,20 @@ CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ) +@callback +def _async_remove_adopt_button( + hass: HomeAssistant, device: ProtectAdoptableDeviceModel +) -> None: + + entity_registry = er.async_get(hass) + if device.is_adopted_by_us and ( + entity_id := entity_registry.async_get_entity_id( + Platform.BUTTON, DOMAIN, f"{device.mac}_adopt" + ) + ): + entity_registry.async_remove(entity_id) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -86,25 +121,49 @@ async def async_setup_entry( data, ProtectButton, all_descs=ALL_DEVICE_BUTTONS, + unadopted_descs=[ADOPT_BUTTON], chime_descs=CHIME_BUTTONS, sense_descs=SENSOR_BUTTONS, ufp_device=device, ) async_add_entities(entities) + _async_remove_adopt_button(hass, device) + + async def _add_unadopted_device(device: ProtectAdoptableDeviceModel) -> None: + if not device.can_adopt or not device.can_create(data.api.bootstrap.auth_user): + _LOGGER.debug("Device is not adoptable: %s", device.id) + return + + entities = async_all_device_entities( + data, + ProtectButton, + unadopted_descs=[ADOPT_BUTTON], + ufp_device=device, + ) + async_add_entities(entities) entry.async_on_unload( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) + entry.async_on_unload( + async_dispatcher_connect( + hass, _ufpd(entry, DISPATCH_ADD), _add_unadopted_device + ) + ) entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectButton, all_descs=ALL_DEVICE_BUTTONS, + unadopted_descs=[ADOPT_BUTTON], chime_descs=CHIME_BUTTONS, sense_descs=SENSOR_BUTTONS, ) async_add_entities(entities) + for device in data.get_by_types(DEVICES_THAT_ADOPT): + _async_remove_adopt_button(hass, device) + class ProtectButton(ProtectDeviceEntity, ButtonEntity): """A Ubiquiti UniFi Protect Reboot button.""" @@ -121,6 +180,15 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity): super().__init__(data, device, description) self._attr_name = f"{self.device.display_name} {self.entity_description.name}" + @callback + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) + + if self.entity_description.key == KEY_ADOPT: + self._attr_available = self.device.can_adopt and self.device.can_create( + self.data.api.bootstrap.auth_user + ) + async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 9710279a7c4..93a0fa5ff74 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -63,5 +63,6 @@ PLATFORMS = [ Platform.SWITCH, ] +DISPATCH_ADD = "add_device" DISPATCH_ADOPT = "adopt_device" DISPATCH_CHANNELS = "new_camera_channels" diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 74ef6ab37f8..d95668ea29d 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -29,6 +29,7 @@ from .const import ( CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA, DEVICES_THAT_ADOPT, + DISPATCH_ADD, DISPATCH_ADOPT, DISPATCH_CHANNELS, DOMAIN, @@ -156,36 +157,55 @@ class ProtectData: self._pending_camera_ids.add(camera_id) + @callback + def _async_add_device(self, device: ProtectAdoptableDeviceModel) -> None: + if device.is_adopted_by_us: + _LOGGER.debug("Device adopted: %s", device.id) + async_dispatcher_send( + self._hass, _ufpd(self._entry, DISPATCH_ADOPT), device + ) + else: + _LOGGER.debug("New device detected: %s", device.id) + async_dispatcher_send(self._hass, _ufpd(self._entry, DISPATCH_ADD), device) + + @callback + def _async_update_device( + self, device: ProtectAdoptableDeviceModel | NVR, changed_data: dict[str, Any] + ) -> None: + self._async_signal_device_update(device) + if ( + device.model == ModelType.CAMERA + and device.id in self._pending_camera_ids + and "channels" in changed_data + ): + self._pending_camera_ids.remove(device.id) + async_dispatcher_send( + self._hass, _ufpd(self._entry, DISPATCH_CHANNELS), device + ) + + # trigger update for all Cameras with LCD screens when NVR Doorbell settings updates + if "doorbell_settings" in changed_data: + _LOGGER.debug( + "Doorbell messages updated. Updating devices with LCD screens" + ) + self.api.bootstrap.nvr.update_all_messages() + for camera in self.api.bootstrap.cameras.values(): + if camera.feature_flags.has_lcd_screen: + self._async_signal_device_update(camera) + @callback def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: # removed packets are not processed yet - if message.new_obj is None or not getattr( - message.new_obj, "is_adopted_by_us", True - ): + if message.new_obj is None: return obj = message.new_obj if isinstance(obj, (ProtectAdoptableDeviceModel, NVR)): - self._async_signal_device_update(obj) - if ( - obj.model == ModelType.CAMERA - and obj.id in self._pending_camera_ids - and "channels" in message.changed_data - ): - self._pending_camera_ids.remove(obj.id) - async_dispatcher_send( - self._hass, _ufpd(self._entry, DISPATCH_CHANNELS), obj - ) + if message.old_obj is None and isinstance(obj, ProtectAdoptableDeviceModel): + self._async_add_device(obj) + elif getattr(obj, "is_adopted_by_us", True): + self._async_update_device(obj, message.changed_data) - # trigger update for all Cameras with LCD screens when NVR Doorbell settings updates - if "doorbell_settings" in message.changed_data: - _LOGGER.debug( - "Doorbell messages updated. Updating devices with LCD screens" - ) - self.api.bootstrap.nvr.update_all_messages() - for camera in self.api.bootstrap.cameras.values(): - if camera.feature_flags.has_lcd_screen: - self._async_signal_device_update(camera) # trigger updates for camera that the event references elif isinstance(obj, Event): if obj.type == EventType.DEVICE_ADOPTED: @@ -194,10 +214,7 @@ class ProtectData: obj.metadata.device_id ) if device is not None: - _LOGGER.debug("New device detected: %s", device.id) - async_dispatcher_send( - self._hass, _ufpd(self._entry, DISPATCH_ADOPT), device - ) + self._async_add_device(device) elif obj.camera is not None: self._async_signal_device_update(obj.camera) elif obj.light is not None: diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index e68e5cfb81d..986dff13dc0 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -38,9 +38,10 @@ def _async_device_entities( klass: type[ProtectDeviceEntity], model_type: ModelType, descs: Sequence[ProtectRequiredKeysMixin], + unadopted_descs: Sequence[ProtectRequiredKeysMixin], ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: - if len(descs) == 0: + if len(descs) + len(unadopted_descs) == 0: return [] entities: list[ProtectDeviceEntity] = [] @@ -48,17 +49,36 @@ def _async_device_entities( [ufp_device] if ufp_device is not None else data.get_by_types({model_type}) ) for device in devices: + assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime)) if not device.is_adopted_by_us: + for description in unadopted_descs: + entities.append( + klass( + data, + device=device, + description=description, + ) + ) + _LOGGER.debug( + "Adding %s entity %s for %s", + klass.__name__, + description.name, + device.display_name, + ) continue - assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime)) + can_write = device.can_write(data.api.bootstrap.auth_user) for description in descs: if description.ufp_perm is not None: - can_write = device.can_write(data.api.bootstrap.auth_user) if description.ufp_perm == PermRequired.WRITE and not can_write: continue if description.ufp_perm == PermRequired.NO_WRITE and can_write: continue + if ( + description.ufp_perm == PermRequired.DELETE + and not device.can_delete(data.api.bootstrap.auth_user) + ): + continue if description.ufp_required_field: required_field = get_nested_attr(device, description.ufp_required_field) @@ -93,10 +113,12 @@ def async_all_device_entities( lock_descs: Sequence[ProtectRequiredKeysMixin] | None = None, chime_descs: Sequence[ProtectRequiredKeysMixin] | None = None, all_descs: Sequence[ProtectRequiredKeysMixin] | None = None, + unadopted_descs: Sequence[ProtectRequiredKeysMixin] | None = None, ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: """Generate a list of all the device entities.""" all_descs = list(all_descs or []) + unadopted_descs = list(unadopted_descs or []) camera_descs = list(camera_descs or []) + all_descs light_descs = list(light_descs or []) + all_descs sense_descs = list(sense_descs or []) + all_descs @@ -106,12 +128,24 @@ def async_all_device_entities( if ufp_device is None: return ( - _async_device_entities(data, klass, ModelType.CAMERA, camera_descs) - + _async_device_entities(data, klass, ModelType.LIGHT, light_descs) - + _async_device_entities(data, klass, ModelType.SENSOR, sense_descs) - + _async_device_entities(data, klass, ModelType.VIEWPORT, viewer_descs) - + _async_device_entities(data, klass, ModelType.DOORLOCK, lock_descs) - + _async_device_entities(data, klass, ModelType.CHIME, chime_descs) + _async_device_entities( + data, klass, ModelType.CAMERA, camera_descs, unadopted_descs + ) + + _async_device_entities( + data, klass, ModelType.LIGHT, light_descs, unadopted_descs + ) + + _async_device_entities( + data, klass, ModelType.SENSOR, sense_descs, unadopted_descs + ) + + _async_device_entities( + data, klass, ModelType.VIEWPORT, viewer_descs, unadopted_descs + ) + + _async_device_entities( + data, klass, ModelType.DOORLOCK, lock_descs, unadopted_descs + ) + + _async_device_entities( + data, klass, ModelType.CHIME, chime_descs, unadopted_descs + ) ) descs = [] @@ -128,9 +162,11 @@ def async_all_device_entities( elif ufp_device.model == ModelType.CHIME: descs = chime_descs - if len(descs) == 0 or ufp_device.model is None: + if len(descs) + len(unadopted_descs) == 0 or ufp_device.model is None: return [] - return _async_device_entities(data, klass, ufp_device.model, descs, ufp_device) + return _async_device_entities( + data, klass, ufp_device.model, descs, unadopted_descs, ufp_device + ) class ProtectDeviceEntity(Entity): @@ -190,8 +226,9 @@ class ProtectDeviceEntity(Entity): assert isinstance(device, ProtectAdoptableDeviceModel) self.device = device - is_connected = ( - self.data.last_update_success and self.device.state == StateType.CONNECTED + is_connected = self.data.last_update_success and ( + self.device.state == StateType.CONNECTED + or (not self.device.is_adopted_by_us and self.device.can_adopt) ) if ( hasattr(self, "entity_description") diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index 588b99b38d7..817e0ba1d6b 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -34,9 +34,6 @@ async def async_setup_entry( data: ProtectData = hass.data[DOMAIN][entry.entry_id] async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - if not device.is_adopted_by_us: - return - if device.model == ModelType.LIGHT and device.can_write( data.api.bootstrap.auth_user ): diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 0a203308d1e..6a33289234e 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -34,9 +34,6 @@ async def async_setup_entry( data: ProtectData = hass.data[DOMAIN][entry.entry_id] async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - if not device.is_adopted_by_us: - return - if isinstance(device, Doorlock): async_add_entities([ProtectLock(data, device)]) diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index d4046e4b8b7..f426d878ae2 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -43,9 +43,6 @@ async def async_setup_entry( data: ProtectData = hass.data[DOMAIN][entry.entry_id] async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - if not device.is_adopted_by_us: - return - if isinstance(device, Camera) and device.feature_flags.has_speaker: async_add_entities([ProtectMediaPlayer(data, device)]) diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index dee2006b429..adab5c032e1 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -23,6 +23,7 @@ class PermRequired(int, Enum): NO_WRITE = 1 WRITE = 2 + DELETE = 3 @dataclass diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index e0ddddd4c53..fa53cc3b87b 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -320,6 +320,7 @@ async def async_setup_entry( data, ProtectPrivacyModeSwitch, camera_descs=[PRIVACY_MODE_SWITCH], + ufp_device=device, ) async_add_entities(entities) diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index a46d74e0b8e..9db7a46dda3 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -2,9 +2,9 @@ # pylint: disable=protected-access from __future__ import annotations -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data.devices import Chime +from pyunifiprotect.data.devices import Camera, Chime, Doorlock from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform @@ -27,11 +27,11 @@ async def test_button_chime_remove( """Test removing and re-adding a light device.""" await init_entry(hass, ufp, [chime]) - assert_entity_counts(hass, Platform.BUTTON, 3, 2) + assert_entity_counts(hass, Platform.BUTTON, 4, 2) await remove_entities(hass, [chime]) assert_entity_counts(hass, Platform.BUTTON, 0, 0) await adopt_devices(hass, ufp, [chime]) - assert_entity_counts(hass, Platform.BUTTON, 3, 2) + assert_entity_counts(hass, Platform.BUTTON, 4, 2) async def test_reboot_button( @@ -42,7 +42,7 @@ async def test_reboot_button( """Test button entity.""" await init_entry(hass, ufp, [chime]) - assert_entity_counts(hass, Platform.BUTTON, 3, 2) + assert_entity_counts(hass, Platform.BUTTON, 4, 2) ufp.api.reboot_device = AsyncMock() @@ -74,7 +74,7 @@ async def test_chime_button( """Test button entity.""" await init_entry(hass, ufp, [chime]) - assert_entity_counts(hass, Platform.BUTTON, 3, 2) + assert_entity_counts(hass, Platform.BUTTON, 4, 2) ufp.api.play_speaker = AsyncMock() @@ -95,3 +95,67 @@ async def test_chime_button( "button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) ufp.api.play_speaker.assert_called_once() + + +async def test_adopt_button( + hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock, doorbell: Camera +): + """Test button entity.""" + + doorlock._api = ufp.api + doorlock.is_adopted = False + doorlock.can_adopt = True + + await init_entry(hass, ufp, []) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.old_obj = None + mock_msg.new_obj = doorlock + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + + assert_entity_counts(hass, Platform.BUTTON, 1, 1) + + ufp.api.adopt_device = AsyncMock() + + unique_id = f"{doorlock.mac}_adopt" + entity_id = "button.test_lock_adopt_device" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert not entity.disabled + assert entity.unique_id == unique_id + + state = hass.states.get(entity_id) + assert state + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + await hass.services.async_call( + "button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + ufp.api.adopt_device.assert_called_once() + + +async def test_adopt_button_removed( + hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock, doorbell: Camera +): + """Test button entity.""" + + entity_id = "button.test_lock_adopt_device" + entity_registry = er.async_get(hass) + + doorlock._api = ufp.api + doorlock.is_adopted = False + doorlock.can_adopt = True + + await init_entry(hass, ufp, [doorlock]) + assert_entity_counts(hass, Platform.BUTTON, 1, 1) + entity = entity_registry.async_get(entity_id) + assert entity + + await adopt_devices(hass, ufp, [doorlock], fully_adopt=True) + assert_entity_counts(hass, Platform.BUTTON, 2, 0) + entity = entity_registry.async_get(entity_id) + assert entity is None diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index 64c8384d400..20e4ec61ca0 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -56,7 +56,7 @@ async def test_migrate_reboot_button( for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id): if entity.domain == Platform.BUTTON.value: buttons.append(entity) - assert len(buttons) == 2 + assert len(buttons) == 4 assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None @@ -135,7 +135,7 @@ async def test_migrate_reboot_button_no_device( for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id): if entity.domain == Platform.BUTTON.value: buttons.append(entity) - assert len(buttons) == 2 + assert len(buttons) == 3 entity = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}") assert entity is not None diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 8d54cbddea6..0b5d29ba12a 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -89,6 +89,7 @@ def assert_entity_counts( e for e in entity_registry.entities if split_entity_id(e)[0] == platform.value ] + print(len(entities), total) assert len(entities) == total assert len(hass.states.async_all(platform.value)) == enabled @@ -203,10 +204,16 @@ async def adopt_devices( hass: HomeAssistant, ufp: MockUFPFixture, ufp_devices: list[ProtectAdoptableDeviceModel], + fully_adopt: bool = False, ): """Emit WS to re-adopt give Protect devices.""" for ufp_device in ufp_devices: + if fully_adopt: + ufp_device.is_adopted = True + ufp_device.is_adopted_by_other = False + ufp_device.can_adopt = False + mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = Event( From d7724235ff7c928d2da81197ffb0b05c9fbf02b1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Aug 2022 00:27:16 +0000 Subject: [PATCH 637/903] [ci skip] Translation update --- .../android_ip_webcam/translations/pl.json | 3 +- .../components/awair/translations/pl.json | 6 ++++ .../components/bluetooth/translations/el.json | 6 ++-- .../components/bluetooth/translations/et.json | 6 ++-- .../components/bluetooth/translations/fr.json | 10 +++++++ .../components/bluetooth/translations/no.json | 6 ++-- .../components/bluetooth/translations/pl.json | 2 +- .../bluetooth/translations/pt-BR.json | 6 ++-- .../bluetooth/translations/zh-Hant.json | 6 ++-- .../fully_kiosk/translations/pl.json | 20 +++++++++++++ .../huawei_lte/translations/bg.json | 3 +- .../components/icloud/translations/bg.json | 1 + .../lacrosse_view/translations/pl.json | 3 +- .../components/lametric/translations/pl.json | 24 ++++++++++++++++ .../landisgyr_heat_meter/translations/pl.json | 18 ++++++++++++ .../components/mqtt/translations/de.json | 6 ++++ .../components/mqtt/translations/en.json | 14 +++++----- .../components/mqtt/translations/es.json | 6 ++++ .../components/mqtt/translations/id.json | 6 ++++ .../components/mqtt/translations/it.json | 6 ++++ .../components/mqtt/translations/pt-BR.json | 6 ++++ .../components/mqtt/translations/zh-Hant.json | 6 ++++ .../nightscout/translations/bg.json | 3 +- .../components/pushover/translations/pl.json | 26 +++++++++++++++++ .../components/qingping/translations/pl.json | 22 +++++++++++++++ .../components/risco/translations/el.json | 18 ++++++++++++ .../components/risco/translations/no.json | 18 ++++++++++++ .../components/risco/translations/pl.json | 14 ++++++++++ .../components/risco/translations/pt-BR.json | 18 ++++++++++++ .../components/skybell/translations/de.json | 7 +++++ .../components/skybell/translations/en.json | 14 +++++----- .../components/skybell/translations/es.json | 7 +++++ .../components/skybell/translations/fr.json | 7 +++++ .../components/skybell/translations/id.json | 7 +++++ .../skybell/translations/pt-BR.json | 7 +++++ .../skybell/translations/zh-Hant.json | 7 +++++ .../components/thermopro/translations/de.json | 21 ++++++++++++++ .../components/thermopro/translations/es.json | 21 ++++++++++++++ .../components/thermopro/translations/fr.json | 21 ++++++++++++++ .../components/thermopro/translations/id.json | 21 ++++++++++++++ .../thermopro/translations/pt-BR.json | 21 ++++++++++++++ .../thermopro/translations/zh-Hant.json | 21 ++++++++++++++ .../components/unifi/translations/bg.json | 3 +- .../volvooncall/translations/it.json | 1 + .../volvooncall/translations/pl.json | 17 +++++++++++ .../volvooncall/translations/pt-BR.json | 28 +++++++++++++++++++ .../yalexs_ble/translations/pl.json | 1 + 47 files changed, 491 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/fully_kiosk/translations/pl.json create mode 100644 homeassistant/components/lametric/translations/pl.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/pl.json create mode 100644 homeassistant/components/pushover/translations/pl.json create mode 100644 homeassistant/components/qingping/translations/pl.json create mode 100644 homeassistant/components/thermopro/translations/de.json create mode 100644 homeassistant/components/thermopro/translations/es.json create mode 100644 homeassistant/components/thermopro/translations/fr.json create mode 100644 homeassistant/components/thermopro/translations/id.json create mode 100644 homeassistant/components/thermopro/translations/pt-BR.json create mode 100644 homeassistant/components/thermopro/translations/zh-Hant.json create mode 100644 homeassistant/components/volvooncall/translations/pl.json create mode 100644 homeassistant/components/volvooncall/translations/pt-BR.json diff --git a/homeassistant/components/android_ip_webcam/translations/pl.json b/homeassistant/components/android_ip_webcam/translations/pl.json index 8698af51c4b..7a879ebefec 100644 --- a/homeassistant/components/android_ip_webcam/translations/pl.json +++ b/homeassistant/components/android_ip_webcam/translations/pl.json @@ -4,7 +4,8 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { "user": { diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index 840612eff40..7bd01486f88 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -31,6 +31,12 @@ }, "description": "Lokalny interfejs API Awair musi by\u0107 w\u0142\u0105czony, wykonuj\u0105c nast\u0119puj\u0105ce czynno\u015bci: {url}" }, + "local_pick": { + "data": { + "device": "Urz\u0105dzenie", + "host": "Adres IP" + } + }, "reauth": { "data": { "access_token": "Token dost\u0119pu", diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index e81460bbdec..1d6685d307e 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "\u039f \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2 Bluetooth \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7" - } + "adapter": "\u039f \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2 Bluetooth \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", + "passive": "\u03a0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7" + }, + "description": "\u0397 \u03c0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af BlueZ 5.63 \u03ae \u03bc\u03b5\u03c4\u03b1\u03b3\u03b5\u03bd\u03ad\u03c3\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03bc\u03b5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c4\u03b9\u03c2 \u03c0\u03b5\u03b9\u03c1\u03b1\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b5\u03c2." } } } diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json index e1fdd22a7fb..d8c6eb7bdf2 100644 --- a/homeassistant/components/bluetooth/translations/et.json +++ b/homeassistant/components/bluetooth/translations/et.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "Sk\u00e4nnimiseks kasutatav Bluetoothi adapter" - } + "adapter": "Sk\u00e4nnimiseks kasutatav Bluetoothi adapter", + "passive": "Passiivne kuulamine" + }, + "description": "Passiivseks kuulamiseks on vaja BlueZ 5.63 v\u00f5i uuemat versiooni koos lubatud eksperimentaalsete funktsioonidega." } } } diff --git a/homeassistant/components/bluetooth/translations/fr.json b/homeassistant/components/bluetooth/translations/fr.json index da430eedb0e..2eb71ec1fe8 100644 --- a/homeassistant/components/bluetooth/translations/fr.json +++ b/homeassistant/components/bluetooth/translations/fr.json @@ -24,5 +24,15 @@ "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" } } + }, + "options": { + "step": { + "init": { + "data": { + "passive": "\u00c9coute passive" + }, + "description": "L'\u00e9coute passive n\u00e9cessite BlueZ version 5.63 ou ult\u00e9rieure avec les fonctions exp\u00e9rimentales activ\u00e9es." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/no.json b/homeassistant/components/bluetooth/translations/no.json index 8e82e77216c..761e3fd0a84 100644 --- a/homeassistant/components/bluetooth/translations/no.json +++ b/homeassistant/components/bluetooth/translations/no.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "Bluetooth-adapteren som skal brukes til skanning" - } + "adapter": "Bluetooth-adapteren som skal brukes til skanning", + "passive": "Passiv lytting" + }, + "description": "Passiv lytting krever BlueZ 5.63 eller nyere med eksperimentelle funksjoner aktivert." } } } diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index f7e6fe060af..0c7c8f828b5 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "no_adapters": "Nie znaleziono adapter\u00f3w Bluetooth" + "no_adapters": "Nie znaleziono nieskonfigurowanych adapter\u00f3w Bluetooth" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index ec69310dc40..daa1f2bc091 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "O adaptador Bluetooth a ser usado para escaneamento" - } + "adapter": "O adaptador Bluetooth a ser usado para escaneamento", + "passive": "Escuta passiva" + }, + "description": "A escuta passiva requer BlueZ 5.63 ou posterior com recursos experimentais ativados." } } } diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index 8dea681a178..ca363ff0e30 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u7259\u50b3\u8f38\u5668" - } + "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u7259\u50b3\u8f38\u5668", + "passive": "\u88ab\u52d5\u76e3\u807d" + }, + "description": "\u88ab\u52d5\u76e3\u807d\u9700\u8981 BlueZ 5.63 \u6216\u66f4\u65b0\u7248\u672c\u3001\u4e26\u958b\u555f\u5be6\u9a57\u529f\u80fd\u3002" } } } diff --git a/homeassistant/components/fully_kiosk/translations/pl.json b/homeassistant/components/fully_kiosk/translations/pl.json new file mode 100644 index 00000000000..905cdeb6149 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/pl.json @@ -0,0 +1,20 @@ +{ + "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", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 7e67477f000..feb010b214f 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -9,7 +9,8 @@ "incorrect_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0430\u0434\u0440\u0435\u0441", "login_attempts_exceeded": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0438\u0442\u0435 \u043e\u043f\u0438\u0442\u0438 \u0437\u0430 \u0432\u043b\u0438\u0437\u0430\u043d\u0435 \u0441\u0430 \u043d\u0430\u0434\u0432\u0438\u0448\u0435\u043d\u0438. \u041c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e", - "response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + "response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index 4ee0a45d19d..bd81093beb0 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -4,6 +4,7 @@ "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" }, "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "send_verification_code": "\u041a\u043e\u0434\u044a\u0442 \u0437\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043d\u0435 \u0431\u0435 \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d", "validate_verification_code": "\u041f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0430\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043a\u043e\u0434\u0430 \u0437\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e" }, diff --git a/homeassistant/components/lacrosse_view/translations/pl.json b/homeassistant/components/lacrosse_view/translations/pl.json index b7fbbe50779..209ea64c9af 100644 --- a/homeassistant/components/lacrosse_view/translations/pl.json +++ b/homeassistant/components/lacrosse_view/translations/pl.json @@ -1,7 +1,8 @@ { "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": { "invalid_auth": "Niepoprawne uwierzytelnienie", diff --git a/homeassistant/components/lametric/translations/pl.json b/homeassistant/components/lametric/translations/pl.json new file mode 100644 index 00000000000..77e50661cae --- /dev/null +++ b/homeassistant/components/lametric/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "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})" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "manual_entry": { + "data": { + "api_key": "Klucz API", + "host": "Nazwa hosta lub adres IP" + } + }, + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/pl.json b/homeassistant/components/landisgyr_heat_meter/translations/pl.json new file mode 100644 index 00000000000..8dcc31a5a11 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/pl.json @@ -0,0 +1,18 @@ +{ + "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": { + "setup_serial_manual_path": { + "data": { + "device": "\u015acie\u017cka urz\u0105dzenia USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 3a71d7bc547..ad92ed995d6 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" dreifach geklickt" } }, + "issues": { + "deprecated_yaml": { + "description": "Manuell konfigurierte MQTT-{platform}(en) gefunden unter Plattformschl\u00fcssel `{platform}`. \n\nBitte verschiebe die Konfiguration auf den \u201emqtt\u201c-Integrationsschl\u00fcssel und starte Home Assistant neu, um dieses Problem zu beheben. Weitere Informationen findest du in der [Dokumentation]({more_info_url}).", + "title": "Deine manuell konfigurierte(n) MQTT-{platform}(en) erfordert Aufmerksamkeit" + } + }, "options": { "error": { "bad_birth": "Ung\u00fcltiges Birth Thema.", diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 724387dc923..f495c4eea2b 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -1,11 +1,5 @@ { - "issues": { - "deprecated_yaml": { - "title": "Your manually configured MQTT {platform}(s) needs attention", - "description": "Manually configured MQTT {platform}(s) found under platform key `{platform}`.\n\nPlease move the configuration to the `mqtt` integration key and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information." - } - }, - "config": { + "config": { "abort": { "already_configured": "Service is already configured", "single_instance_allowed": "Already configured. Only a single configuration possible." @@ -55,6 +49,12 @@ "button_triple_press": "\"{subtype}\" triple clicked" } }, + "issues": { + "deprecated_yaml": { + "description": "Manually configured MQTT {platform}(s) found under platform key `{platform}`.\n\nPlease move the configuration to the `mqtt` integration key and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information.", + "title": "Your manually configured MQTT {platform}(s) needs attention" + } + }, "options": { "error": { "bad_birth": "Invalid birth topic.", diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 016cb320cfd..e87f078c5b8 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" triple pulsaci\u00f3n" } }, + "issues": { + "deprecated_yaml": { + "description": "Configuraci\u00f3n manual MQTT de {platform}(s) encontrada en la clave de la plataforma `{platform}`.\n\nPor favor, mueve la configuraci\u00f3n a la clave `mqtt` de la integraci\u00f3n y reinicia Home Assistant para solucionar este problema. Consulta la [documentaci\u00f3n]({more_info_url}), para obtener m\u00e1s informaci\u00f3n.", + "title": "Tu configuraci\u00f3n manual MQTT de {platform}(s) necesita atenci\u00f3n" + } + }, "options": { "error": { "bad_birth": "Tema de nacimiento no v\u00e1lido.", diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 308c9f31e8c..8a45f155815 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" diklik tiga kali" } }, + "issues": { + "deprecated_yaml": { + "description": "MQTT {platform}(s) yang dikonfigurasi secara manual ditemukan di bawah kunci platform `{platform}`.\n\nPindahkan konfigurasi ke kunci integrasi `mqtt` dan mulai ulang Home Assistant untuk memperbaiki masalah ini. Lihat [dokumentasi]({more_info_url}), untuk informasi lebih lanjut.", + "title": "Entitas MQTT {platform} yang dikonfigurasi secara manual membutuhkan perhatian" + } + }, "options": { "error": { "bad_birth": "Topik birth tidak valid", diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 6317fdf61d5..a94dd83a55d 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" cliccato tre volte" } }, + "issues": { + "deprecated_yaml": { + "description": "{platform} MQTT configurata manualmente trovata sotto la voce della piattaforma `{platform}`. \n\nSposta la configurazione sulla voce di integrazione `mqtt` e riavvia Home Assistant per risolvere questo problema. Consulta la [documentazione]({more_info_url}), per ulteriori informazioni.", + "title": "La/e tua/e {platform} MQTT configurata/e manualmente ha/hanno bisogno di attenzione" + } + }, "options": { "error": { "bad_birth": "Argomento birth non valido.", diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index 2e1bb5bebfd..51860685270 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" triplo clicado" } }, + "issues": { + "deprecated_yaml": { + "description": "MQTT configurado manualmente {platform}(s) encontrado na chave de plataforma ` {platform} `. \n\n Por favor, mova a configura\u00e7\u00e3o para a chave de integra\u00e7\u00e3o `mqtt` e reinicie o Home Assistant para corrigir este problema. Consulte a [documenta\u00e7\u00e3o]( {more_info_url} ), para mais informa\u00e7\u00f5es.", + "title": "Sua(s) {platform}(s) MQTT configurada(s) manualmente precisa(m) de aten\u00e7\u00e3o" + } + }, "options": { "error": { "bad_birth": "T\u00f3pico \u00b4Birth message\u00b4 inv\u00e1lido", diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index b34b4813499..ce101d389e8 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" \u4e09\u9023\u64ca" } }, + "issues": { + "deprecated_yaml": { + "description": "\u65bc\u5e73\u53f0\u9375 `{platform}` \u5167\u627e\u5230\u624b\u52d5\u8a2d\u5b9a\u4e4b MQTT {platform}\u3002\n\n\u8acb\u5c07\u8a2d\u5b9a\u79fb\u52d5\u81f3 `mqtt` \u6574\u5408\u9375\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002\u8acb\u53c3\u95b1 [\u6587\u4ef6]({more_info_url}) \u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a\u3002", + "title": "\u624b\u52d5\u8a2d\u5b9a\u4e4b MQTT {platform} \u6709\u4e9b\u554f\u984c\u9700\u8981\u8655\u7406" + } + }, "options": { "error": { "bad_birth": "Birth \u4e3b\u984c\u7121\u6548\u3002", diff --git a/homeassistant/components/nightscout/translations/bg.json b/homeassistant/components/nightscout/translations/bg.json index 6685ae35a2f..5b8d305726e 100644 --- a/homeassistant/components/nightscout/translations/bg.json +++ b/homeassistant/components/nightscout/translations/bg.json @@ -2,7 +2,8 @@ "config": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { diff --git a/homeassistant/components/pushover/translations/pl.json b/homeassistant/components/pushover/translations/pl.json new file mode 100644 index 00000000000..01f297f75cc --- /dev/null +++ b/homeassistant/components/pushover/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + }, + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "api_key": "Klucz API", + "name": "Nazwa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/pl.json b/homeassistant/components/qingping/translations/pl.json new file mode 100644 index 00000000000..4715905a2e9 --- /dev/null +++ b/homeassistant/components/qingping/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/el.json b/homeassistant/components/risco/translations/el.json index b1c6ddb668e..4d68d564184 100644 --- a/homeassistant/components/risco/translations/el.json +++ b/homeassistant/components/risco/translations/el.json @@ -9,11 +9,29 @@ "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "cloud": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + }, + "local": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "port": "\u0398\u03cd\u03c1\u03b1" + } + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "menu_options": { + "cloud": "Risco Cloud (\u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9)", + "local": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03cc \u03a0\u03ac\u03bd\u03b5\u03bb Risco (\u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2)" } } } diff --git a/homeassistant/components/risco/translations/no.json b/homeassistant/components/risco/translations/no.json index 758c5c68bba..177a092f4f3 100644 --- a/homeassistant/components/risco/translations/no.json +++ b/homeassistant/components/risco/translations/no.json @@ -9,11 +9,29 @@ "unknown": "Uventet feil" }, "step": { + "cloud": { + "data": { + "password": "Passord", + "pin": "PIN kode", + "username": "Brukernavn" + } + }, + "local": { + "data": { + "host": "Vert", + "pin": "PIN kode", + "port": "Port" + } + }, "user": { "data": { "password": "Passord", "pin": "PIN kode", "username": "Brukernavn" + }, + "menu_options": { + "cloud": "Risco Cloud (anbefalt)", + "local": "Lokalt Risco-panel (avansert)" } } } diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json index b39c2cde23b..e5523667231 100644 --- a/homeassistant/components/risco/translations/pl.json +++ b/homeassistant/components/risco/translations/pl.json @@ -9,6 +9,20 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "cloud": { + "data": { + "password": "Has\u0142o", + "pin": "Kod PIN", + "username": "Nazwa u\u017cytkownika" + } + }, + "local": { + "data": { + "host": "Nazwa hosta lub adres IP", + "pin": "Kod PIN", + "port": "Port" + } + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/risco/translations/pt-BR.json b/homeassistant/components/risco/translations/pt-BR.json index 53659ab672a..b312b29c255 100644 --- a/homeassistant/components/risco/translations/pt-BR.json +++ b/homeassistant/components/risco/translations/pt-BR.json @@ -9,11 +9,29 @@ "unknown": "Erro inesperado" }, "step": { + "cloud": { + "data": { + "password": "Senha", + "pin": "C\u00f3digo PIN", + "username": "Nome de usu\u00e1rio" + } + }, + "local": { + "data": { + "host": "Host", + "pin": "C\u00f3digo PIN", + "port": "Porta" + } + }, "user": { "data": { "password": "Senha", "pin": "C\u00f3digo PIN", "username": "Usu\u00e1rio" + }, + "menu_options": { + "cloud": "Risco Cloud (recomendado)", + "local": "Painel de Risco Local (avan\u00e7ado)" } } } diff --git a/homeassistant/components/skybell/translations/de.json b/homeassistant/components/skybell/translations/de.json index 2705eab7086..d86aa22f4b7 100644 --- a/homeassistant/components/skybell/translations/de.json +++ b/homeassistant/components/skybell/translations/de.json @@ -10,6 +10,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Bitte aktualisiere dein Passwort f\u00fcr {email}", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "email": "E-Mail", diff --git a/homeassistant/components/skybell/translations/en.json b/homeassistant/components/skybell/translations/en.json index 0a7be932056..dd8f59b5ad6 100644 --- a/homeassistant/components/skybell/translations/en.json +++ b/homeassistant/components/skybell/translations/en.json @@ -10,18 +10,18 @@ "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please update your password for {email}", + "title": "Reauthenticate Integration" + }, "user": { "data": { "email": "Email", "password": "Password" } - }, - "reauth_confirm": { - "description": "Please update your password for {email}", - "title": "Reauthenticate Integration", - "data": { - "password": "Password" - } } } }, diff --git a/homeassistant/components/skybell/translations/es.json b/homeassistant/components/skybell/translations/es.json index 18cb57a194b..a86c086c788 100644 --- a/homeassistant/components/skybell/translations/es.json +++ b/homeassistant/components/skybell/translations/es.json @@ -10,6 +10,13 @@ "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Por favor, actualiza tu contrase\u00f1a para {email}", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "email": "Correo electr\u00f3nico", diff --git a/homeassistant/components/skybell/translations/fr.json b/homeassistant/components/skybell/translations/fr.json index ecb6a0d773d..aaa376d031d 100644 --- a/homeassistant/components/skybell/translations/fr.json +++ b/homeassistant/components/skybell/translations/fr.json @@ -10,6 +10,13 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Veuillez mettre \u00e0 jour votre mot de passe pour {email}", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "email": "Courriel", diff --git a/homeassistant/components/skybell/translations/id.json b/homeassistant/components/skybell/translations/id.json index 8e7aa4a5a87..765c9cac274 100644 --- a/homeassistant/components/skybell/translations/id.json +++ b/homeassistant/components/skybell/translations/id.json @@ -10,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Perbarui kata sandi Anda untuk {email}", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/skybell/translations/pt-BR.json b/homeassistant/components/skybell/translations/pt-BR.json index 6ebac606d58..c5100302fb4 100644 --- a/homeassistant/components/skybell/translations/pt-BR.json +++ b/homeassistant/components/skybell/translations/pt-BR.json @@ -10,6 +10,13 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Por favor, atualize sua senha para {email}", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/skybell/translations/zh-Hant.json b/homeassistant/components/skybell/translations/zh-Hant.json index faae37d9b31..365a4f8fd18 100644 --- a/homeassistant/components/skybell/translations/zh-Hant.json +++ b/homeassistant/components/skybell/translations/zh-Hant.json @@ -10,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u66f4\u65b0 {email} \u5bc6\u78bc", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "email": "\u96fb\u5b50\u90f5\u4ef6", diff --git a/homeassistant/components/thermopro/translations/de.json b/homeassistant/components/thermopro/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/thermopro/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/es.json b/homeassistant/components/thermopro/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/thermopro/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/fr.json b/homeassistant/components/thermopro/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/thermopro/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/id.json b/homeassistant/components/thermopro/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/thermopro/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/pt-BR.json b/homeassistant/components/thermopro/translations/pt-BR.json new file mode 100644 index 00000000000..e600b7b6bcf --- /dev/null +++ b/homeassistant/components/thermopro/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/zh-Hant.json b/homeassistant/components/thermopro/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/thermopro/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\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" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/bg.json b/homeassistant/components/unifi/translations/bg.json index c1bf205abfe..ebe6a494d6c 100644 --- a/homeassistant/components/unifi/translations/bg.json +++ b/homeassistant/components/unifi/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0421\u0430\u0439\u0442\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0421\u0430\u0439\u0442\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "faulty_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438", diff --git a/homeassistant/components/volvooncall/translations/it.json b/homeassistant/components/volvooncall/translations/it.json index c933ff732b1..84075dc8756 100644 --- a/homeassistant/components/volvooncall/translations/it.json +++ b/homeassistant/components/volvooncall/translations/it.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "mutable": "Consenti da remoto l'avvio / il blocco / ecc.", "password": "Password", "region": "Regione", "scandinavian_miles": "Usa le miglia scandinave", diff --git a/homeassistant/components/volvooncall/translations/pl.json b/homeassistant/components/volvooncall/translations/pl.json new file mode 100644 index 00000000000..fc0e5482c42 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "region": "Region", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/pt-BR.json b/homeassistant/components/volvooncall/translations/pt-BR.json new file mode 100644 index 00000000000..f6e4849b602 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "mutable": "Permitir partida remota / bloqueio / etc.", + "password": "Senha", + "region": "Regi\u00e3o", + "scandinavian_miles": "Usar milhas escandinavas", + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o da plataforma Volvo On Call usando YAML est\u00e1 sendo removida em uma vers\u00e3o futura do Home Assistant. \n\n Sua configura\u00e7\u00e3o existente foi importada para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de Volvo On Call est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/pl.json b/homeassistant/components/yalexs_ble/translations/pl.json index b178db81e8d..2d32834337c 100644 --- a/homeassistant/components/yalexs_ble/translations/pl.json +++ b/homeassistant/components/yalexs_ble/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", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "no_unconfigured_devices": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144." }, From 5f0cca9b264fcab86bfc929e824e84c8789d56ba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 26 Aug 2022 02:56:26 +0200 Subject: [PATCH 638/903] Raise repairs issue if automation calls unknown service (#76949) --- .../components/automation/__init__.py | 19 +++++++++++++++++++ .../components/automation/manifest.json | 2 +- .../components/automation/strings.json | 13 +++++++++++++ .../automation/translations/en.json | 13 +++++++++++++ tests/components/automation/test_init.py | 13 ++++++++++++- tests/components/mobile_app/conftest.py | 1 + tests/components/mqtt/test_device_trigger.py | 1 + 7 files changed, 60 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e177b76faf3..b653d1649d6 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,6 +9,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import blueprint +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -43,6 +44,7 @@ from homeassistant.exceptions import ( ConditionErrorContainer, ConditionErrorIndex, HomeAssistantError, + ServiceNotFound, TemplateError, ) from homeassistant.helpers import condition, extract_domain_configs @@ -525,6 +527,23 @@ class AutomationEntity(ToggleEntity, RestoreEntity): await self.action_script.async_run( variables, trigger_context, started_action ) + except ServiceNotFound as err: + async_create_issue( + self.hass, + DOMAIN, + f"{self.entity_id}_service_not_found_{err.domain}.{err.service}", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.ERROR, + translation_key="service_not_found", + translation_placeholders={ + "service": f"{err.domain}.{err.service}", + "entity_id": self.entity_id, + "name": self.name or self.entity_id, + "edit": f"/config/automation/edit/{self.unique_id}", + }, + ) + automation_trace.set_error(err) except (vol.Invalid, HomeAssistantError) as err: self._logger.error( "Error while executing automation %s: %s", diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 9dd0130ee2f..6d1e4ee6027 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -2,7 +2,7 @@ "domain": "automation", "name": "Automation", "documentation": "https://www.home-assistant.io/integrations/automation", - "dependencies": ["blueprint", "trace"], + "dependencies": ["blueprint", "repairs", "trace"], "after_dependencies": ["device_automation", "webhook"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/automation/strings.json b/homeassistant/components/automation/strings.json index adcc505b145..ea03868e639 100644 --- a/homeassistant/components/automation/strings.json +++ b/homeassistant/components/automation/strings.json @@ -5,5 +5,18 @@ "off": "[%key:common::state::off%]", "on": "[%key:common::state::on%]" } + }, + "issues": { + "service_not_found": { + "title": "{name} uses an unknown service", + "fix_flow": { + "step": { + "confirm": { + "title": "{name} uses an unknown service", + "description": "The automation \"{name}\" (`{entity_id}`) has an action that calls an unknown service: `{service}`.\n\nThis error prevents the automation from running correctly. Maybe this service is no longer available, or perhaps a typo caused it.\n\nTo fix this error, [edit the automation]({edit}) and remove the action that calls this service.\n\nClick on SUBMIT below to confirm you have fixed this automation." + } + } + } + } } } diff --git a/homeassistant/components/automation/translations/en.json b/homeassistant/components/automation/translations/en.json index e5dabcf3bce..e7f60221a01 100644 --- a/homeassistant/components/automation/translations/en.json +++ b/homeassistant/components/automation/translations/en.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "The automation \"{name}\" (`{entity_id}`) has an action that calls an unknown service: `{service}`.\n\nThis error prevents the automation from running correctly. Maybe this service is no longer available, or perhaps a typo caused it.\n\nTo fix this error, [edit the automation]({edit}) and remove the action that calls this service.\n\nClick on SUBMIT below to confirm you have fixed this automation.", + "title": "{name} uses an unknown service" + } + } + }, + "title": "{name} uses an unknown service" + } + }, "state": { "_": { "off": "Off", diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index cef553653de..2c0e5bd5b6c 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,9 +1,11 @@ """The tests for the automation component.""" import asyncio +from collections.abc import Awaitable, Callable from datetime import timedelta import logging from unittest.mock import Mock, patch +from aiohttp import ClientWebSocketResponse import pytest import homeassistant.components.automation as automation @@ -53,6 +55,7 @@ from tests.common import ( mock_restore_cache, ) from tests.components.logbook.common import MockRow, mock_humanify +from tests.components.repairs import get_repairs @pytest.fixture @@ -983,7 +986,11 @@ async def test_automation_bad_trigger(hass, caplog): assert "Integration 'automation' does not provide trigger support." in caplog.text -async def test_automation_with_error_in_script(hass, caplog): +async def test_automation_with_error_in_script( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: """Test automation with an error in script.""" assert await async_setup_component( hass, @@ -1002,6 +1009,10 @@ async def test_automation_with_error_in_script(hass, caplog): assert "Service not found" in caplog.text assert "Traceback" not in caplog.text + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "automation.hello_service_not_found_test.automation" + async def test_automation_with_error_in_script_2(hass, caplog): """Test automation with an error in script.""" diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 18f425d13c0..0455d4a8ea4 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -75,5 +75,6 @@ async def authed_api_client(hass, hass_client): @pytest.fixture(autouse=True) async def setup_ws(hass): """Configure the websocket_api component.""" + assert await async_setup_component(hass, "repairs", {}) assert await async_setup_component(hass, "websocket_api", {}) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index b49276b8f85..8363ca34fd7 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -696,6 +696,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( ): """Test triggers not firing after removal.""" assert await async_setup_component(hass, "config", {}) + assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await mqtt_mock_entry_no_yaml_config() From 47848b7cf81ac6d1c61065651145bb9018d8aab5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Aug 2022 21:52:33 -0500 Subject: [PATCH 639/903] Fix IssueSeverity import (#77338) --- homeassistant/components/automation/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index b653d1649d6..154c443e799 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,7 +9,6 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import blueprint -from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -54,6 +53,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.integration_platform import ( async_process_integration_platform_for_component, ) +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import ( ATTR_CUR, From 655864344882af5a821a044de2493a3acb1c0645 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 25 Aug 2022 23:05:18 -0400 Subject: [PATCH 640/903] Handle remove packets for UniFi Protect (#77337) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/button.py | 5 ++-- homeassistant/components/unifiprotect/data.py | 16 ++++++++++- .../components/unifiprotect/entity.py | 4 +-- .../unifiprotect/test_binary_sensor.py | 6 ++--- tests/components/unifiprotect/test_button.py | 2 +- tests/components/unifiprotect/test_camera.py | 4 +-- tests/components/unifiprotect/test_light.py | 2 +- tests/components/unifiprotect/test_lock.py | 2 +- .../unifiprotect/test_media_player.py | 2 +- tests/components/unifiprotect/test_number.py | 6 ++--- tests/components/unifiprotect/test_select.py | 6 ++--- tests/components/unifiprotect/test_sensor.py | 4 +-- tests/components/unifiprotect/test_switch.py | 4 +-- tests/components/unifiprotect/utils.py | 27 +++++++++---------- 14 files changed, 52 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 823a050ef09..5b8ea4d0c4e 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -129,7 +129,8 @@ async def async_setup_entry( async_add_entities(entities) _async_remove_adopt_button(hass, device) - async def _add_unadopted_device(device: ProtectAdoptableDeviceModel) -> None: + @callback + def _async_add_unadopted_device(device: ProtectAdoptableDeviceModel) -> None: if not device.can_adopt or not device.can_create(data.api.bootstrap.auth_user): _LOGGER.debug("Device is not adoptable: %s", device.id) return @@ -147,7 +148,7 @@ async def async_setup_entry( ) entry.async_on_unload( async_dispatcher_connect( - hass, _ufpd(entry, DISPATCH_ADD), _add_unadopted_device + hass, _ufpd(entry, DISPATCH_ADD), _async_add_unadopted_device ) ) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index d95668ea29d..cb37897c9a8 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -21,6 +21,7 @@ from pyunifiprotect.exceptions import ClientError, NotAuthorized from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -168,6 +169,18 @@ class ProtectData: _LOGGER.debug("New device detected: %s", device.id) async_dispatcher_send(self._hass, _ufpd(self._entry, DISPATCH_ADD), device) + @callback + def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None: + registry = dr.async_get(self._hass) + device_entry = registry.async_get_device( + identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} + ) + if device_entry: + _LOGGER.debug("Device removed: %s", device.id) + registry.async_update_device( + device_entry.id, remove_config_entry_id=self._entry.entry_id + ) + @callback def _async_update_device( self, device: ProtectAdoptableDeviceModel | NVR, changed_data: dict[str, Any] @@ -195,8 +208,9 @@ class ProtectData: @callback def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: - # removed packets are not processed yet if message.new_obj is None: + if isinstance(message.old_obj, ProtectAdoptableDeviceModel): + self._async_remove_device(message.old_obj) return obj = message.new_obj diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 986dff13dc0..23af21e825e 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -41,7 +41,7 @@ def _async_device_entities( unadopted_descs: Sequence[ProtectRequiredKeysMixin], ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: - if len(descs) + len(unadopted_descs) == 0: + if not descs and not unadopted_descs: return [] entities: list[ProtectDeviceEntity] = [] @@ -162,7 +162,7 @@ def async_all_device_entities( elif ufp_device.model == ModelType.CHIME: descs = chime_descs - if len(descs) + len(unadopted_descs) == 0 or ufp_device.model is None: + if not descs and not unadopted_descs or ufp_device.model is None: return [] return _async_device_entities( data, klass, ufp_device.model, descs, unadopted_descs, ufp_device diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 640bf81ec49..b2eec518d40 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -51,7 +51,7 @@ async def test_binary_sensor_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) - await remove_entities(hass, [doorbell, unadopted_camera]) + await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) @@ -65,7 +65,7 @@ async def test_binary_sensor_light_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) - await remove_entities(hass, [light]) + await remove_entities(hass, ufp, [light]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) await adopt_devices(hass, ufp, [light]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) @@ -79,7 +79,7 @@ async def test_binary_sensor_sensor_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [sensor_all]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 4, 4) - await remove_entities(hass, [sensor_all]) + await remove_entities(hass, ufp, [sensor_all]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) await adopt_devices(hass, ufp, [sensor_all]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 4, 4) diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index 9db7a46dda3..ac158b8121e 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -28,7 +28,7 @@ async def test_button_chime_remove( await init_entry(hass, ufp, [chime]) assert_entity_counts(hass, Platform.BUTTON, 4, 2) - await remove_entities(hass, [chime]) + await remove_entities(hass, ufp, [chime]) assert_entity_counts(hass, Platform.BUTTON, 0, 0) await adopt_devices(hass, ufp, [chime]) assert_entity_counts(hass, Platform.BUTTON, 4, 2) diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 2b103e8d714..455a69dd152 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -279,7 +279,7 @@ async def test_adopt(hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCa await init_entry(hass, ufp, [camera1]) assert_entity_counts(hass, Platform.CAMERA, 0, 0) - await remove_entities(hass, [camera1]) + await remove_entities(hass, ufp, [camera1]) assert_entity_counts(hass, Platform.CAMERA, 0, 0) camera1.channels = [] await adopt_devices(hass, ufp, [camera1]) @@ -296,7 +296,7 @@ async def test_adopt(hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCa await hass.async_block_till_done() assert_entity_counts(hass, Platform.CAMERA, 2, 1) - await remove_entities(hass, [camera1]) + await remove_entities(hass, ufp, [camera1]) assert_entity_counts(hass, Platform.CAMERA, 0, 0) await adopt_devices(hass, ufp, [camera1]) assert_entity_counts(hass, Platform.CAMERA, 2, 1) diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index 40f2191828e..401d222db8a 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -33,7 +33,7 @@ async def test_light_remove(hass: HomeAssistant, ufp: MockUFPFixture, light: Lig await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.LIGHT, 1, 1) - await remove_entities(hass, [light]) + await remove_entities(hass, ufp, [light]) assert_entity_counts(hass, Platform.LIGHT, 0, 0) await adopt_devices(hass, ufp, [light]) assert_entity_counts(hass, Platform.LIGHT, 1, 1) diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index d6534e93845..dcbd7537100 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -37,7 +37,7 @@ async def test_lock_remove( await init_entry(hass, ufp, [doorlock]) assert_entity_counts(hass, Platform.LOCK, 1, 1) - await remove_entities(hass, [doorlock]) + await remove_entities(hass, ufp, [doorlock]) assert_entity_counts(hass, Platform.LOCK, 0, 0) await adopt_devices(hass, ufp, [doorlock]) assert_entity_counts(hass, Platform.LOCK, 1, 1) diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index ade84e2d51c..c50409a7848 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -41,7 +41,7 @@ async def test_media_player_camera_remove( await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) - await remove_entities(hass, [doorbell]) + await remove_entities(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.MEDIA_PLAYER, 0, 0) await adopt_devices(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 51e9dfc85a2..b0d1b764999 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -36,7 +36,7 @@ async def test_number_sensor_camera_remove( await init_entry(hass, ufp, [camera, unadopted_camera]) assert_entity_counts(hass, Platform.NUMBER, 3, 3) - await remove_entities(hass, [camera, unadopted_camera]) + await remove_entities(hass, ufp, [camera, unadopted_camera]) assert_entity_counts(hass, Platform.NUMBER, 0, 0) await adopt_devices(hass, ufp, [camera, unadopted_camera]) assert_entity_counts(hass, Platform.NUMBER, 3, 3) @@ -49,7 +49,7 @@ async def test_number_sensor_light_remove( await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.NUMBER, 2, 2) - await remove_entities(hass, [light]) + await remove_entities(hass, ufp, [light]) assert_entity_counts(hass, Platform.NUMBER, 0, 0) await adopt_devices(hass, ufp, [light]) assert_entity_counts(hass, Platform.NUMBER, 2, 2) @@ -62,7 +62,7 @@ async def test_number_lock_remove( await init_entry(hass, ufp, [doorlock]) assert_entity_counts(hass, Platform.NUMBER, 1, 1) - await remove_entities(hass, [doorlock]) + await remove_entities(hass, ufp, [doorlock]) assert_entity_counts(hass, Platform.NUMBER, 0, 0) await adopt_devices(hass, ufp, [doorlock]) assert_entity_counts(hass, Platform.NUMBER, 1, 1) diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index 46bc70f61f6..0cc8308f0f2 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -57,7 +57,7 @@ async def test_select_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SELECT, 4, 4) - await remove_entities(hass, [doorbell, unadopted_camera]) + await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SELECT, 0, 0) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SELECT, 4, 4) @@ -71,7 +71,7 @@ async def test_select_light_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.SELECT, 2, 2) - await remove_entities(hass, [light]) + await remove_entities(hass, ufp, [light]) assert_entity_counts(hass, Platform.SELECT, 0, 0) await adopt_devices(hass, ufp, [light]) assert_entity_counts(hass, Platform.SELECT, 2, 2) @@ -85,7 +85,7 @@ async def test_select_viewer_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [viewer]) assert_entity_counts(hass, Platform.SELECT, 1, 1) - await remove_entities(hass, [viewer]) + await remove_entities(hass, ufp, [viewer]) assert_entity_counts(hass, Platform.SELECT, 0, 0) await adopt_devices(hass, ufp, [viewer]) assert_entity_counts(hass, Platform.SELECT, 1, 1) diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index fcad6ce2725..a712c112b6d 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -63,7 +63,7 @@ async def test_sensor_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SENSOR, 25, 13) - await remove_entities(hass, [doorbell, unadopted_camera]) + await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SENSOR, 12, 9) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SENSOR, 25, 13) @@ -77,7 +77,7 @@ async def test_sensor_sensor_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [sensor_all]) assert_entity_counts(hass, Platform.SENSOR, 22, 14) - await remove_entities(hass, [sensor_all]) + await remove_entities(hass, ufp, [sensor_all]) assert_entity_counts(hass, Platform.SENSOR, 12, 9) await adopt_devices(hass, ufp, [sensor_all]) assert_entity_counts(hass, Platform.SENSOR, 22, 14) diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 6fa718c4952..7bf9e3f8f83 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -50,7 +50,7 @@ async def test_switch_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SWITCH, 13, 12) - await remove_entities(hass, [doorbell, unadopted_camera]) + await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SWITCH, 0, 0) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SWITCH, 13, 12) @@ -64,7 +64,7 @@ async def test_switch_light_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.SWITCH, 2, 1) - await remove_entities(hass, [light]) + await remove_entities(hass, ufp, [light]) assert_entity_counts(hass, Platform.SWITCH, 0, 0) await adopt_devices(hass, ufp, [light]) assert_entity_counts(hass, Platform.SWITCH, 2, 1) diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 0b5d29ba12a..bee479b8e2b 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -23,7 +23,7 @@ from pyunifiprotect.test_util.anonymize import random_hex from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityDescription import homeassistant.util.dt as dt_util @@ -89,7 +89,6 @@ def assert_entity_counts( e for e in entity_registry.entities if split_entity_id(e)[0] == platform.value ] - print(len(entities), total) assert len(entities) == total assert len(hass.states.async_all(platform.value)) == enabled @@ -176,28 +175,25 @@ async def init_entry( async def remove_entities( hass: HomeAssistant, + ufp: MockUFPFixture, ufp_devices: list[ProtectAdoptableDeviceModel], ) -> None: """Remove all entities for given Protect devices.""" - entity_registry = er.async_get(hass) - device_registry = dr.async_get(hass) - for ufp_device in ufp_devices: if not ufp_device.is_adopted_by_us: continue - name = ufp_device.display_name.replace(" ", "_").lower() - entity = entity_registry.async_get(f"{Platform.SENSOR}.{name}_uptime") - assert entity is not None + devices = getattr(ufp.api.bootstrap, f"{ufp_device.model.value}s") + del devices[ufp_device.id] - device_id = entity.device_id - for reg in list(entity_registry.entities.values()): - if reg.device_id == device_id: - entity_registry.async_remove(reg.entity_id) - device_registry.async_remove_device(device_id) + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.old_obj = ufp_device + mock_msg.new_obj = None + ufp.ws_msg(mock_msg) - await hass.async_block_till_done() + await time_changed(hass, 30) async def adopt_devices( @@ -214,6 +210,9 @@ async def adopt_devices( ufp_device.is_adopted_by_other = False ufp_device.can_adopt = False + devices = getattr(ufp.api.bootstrap, f"{ufp_device.model.value}s") + devices[ufp_device.id] = ufp_device + mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = Event( From 120c76524dea0e3784d9142a9a445b764b6253da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Aug 2022 02:33:35 -0500 Subject: [PATCH 641/903] Fix incorrect key update for Gen2 locks with yalexs_ble (#77335) --- .../components/yalexs_ble/config_flow.py | 5 +- .../components/yalexs_ble/test_config_flow.py | 52 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yalexs_ble/config_flow.py b/homeassistant/components/yalexs_ble/config_flow.py index c7213eefbe9..5fee6f62848 100644 --- a/homeassistant/components/yalexs_ble/config_flow.py +++ b/homeassistant/components/yalexs_ble/config_flow.py @@ -98,7 +98,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): new_data = {CONF_KEY: lock_cfg.key, CONF_SLOT: lock_cfg.slot} self._abort_if_unique_id_configured(updates=new_data) for entry in self._async_current_entries(): - if entry.data.get(CONF_LOCAL_NAME) == lock_cfg.local_name: + if ( + local_name_is_unique(lock_cfg.local_name) + and entry.data.get(CONF_LOCAL_NAME) == lock_cfg.local_name + ): if hass.config_entries.async_update_entry( entry, data={**entry.data, **new_data} ): diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index 6ea1b4e8a63..64a4e93eae2 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -621,6 +621,58 @@ async def test_integration_discovery_updates_key_without_unique_local_name( assert entry.data[CONF_SLOT] == 66 +async def test_integration_discovery_updates_key_duplicate_local_name( + hass: HomeAssistant, +) -> None: + """Test integration discovery updates the key with duplicate local names.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LOCAL_NAME: "Aug", + CONF_ADDRESS: OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "5fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 11, + }, + unique_id=OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + ) + entry.add_to_hass(hass) + entry2 = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LOCAL_NAME: "Aug", + CONF_ADDRESS: "CC:DD:CC:DD:CC:DD", + CONF_KEY: "5fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 11, + }, + unique_id="CC:DD:CC:DD:CC:DD", + ) + entry2.add_to_hass(hass) + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" + assert entry.data[CONF_SLOT] == 66 + + assert entry2.data[CONF_KEY] == "5fd51b8621c6a139eaffbedcb846b60f" + assert entry2.data[CONF_SLOT] == 11 + + async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_address( hass: HomeAssistant, ) -> None: From a46c25d2c8244ffcda1c8f521cf61c03ebdec0e7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:15:33 +0200 Subject: [PATCH 642/903] Use _attr_should_poll in components [a-g] (#77268) --- homeassistant/components/counter/__init__.py | 7 ++----- homeassistant/components/edl21/sensor.py | 7 ++----- homeassistant/components/elkm1/__init__.py | 6 +----- homeassistant/components/envisalink/__init__.py | 7 ++----- homeassistant/components/esphome/__init__.py | 7 ++----- homeassistant/components/ffmpeg/__init__.py | 7 ++----- homeassistant/components/fibaro/__init__.py | 7 ++----- homeassistant/components/fireservicerota/sensor.py | 7 ++----- homeassistant/components/fireservicerota/switch.py | 7 ++----- homeassistant/components/firmata/entity.py | 7 ++----- .../components/forked_daapd/media_player.py | 14 ++++---------- homeassistant/components/freebox/device_tracker.py | 7 ++----- homeassistant/components/gdacs/sensor.py | 7 ++----- .../components/generic_hygrostat/humidifier.py | 7 ++----- .../components/generic_thermostat/climate.py | 7 ++----- homeassistant/components/geniushub/__init__.py | 7 ++----- homeassistant/components/geonetnz_quakes/sensor.py | 7 ++----- .../components/geonetnz_volcano/sensor.py | 7 ++----- 18 files changed, 37 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index d035d658206..a16d891af54 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -173,6 +173,8 @@ class CounterStorageCollection(collection.StorageCollection): class Counter(RestoreEntity): """Representation of a counter.""" + _attr_should_poll: bool = False + def __init__(self, config: dict) -> None: """Initialize a counter.""" self._config: dict = config @@ -187,11 +189,6 @@ class Counter(RestoreEntity): counter.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) return counter - @property - def should_poll(self) -> bool: - """If entity should be polled.""" - return False - @property def name(self) -> str | None: """Return name of the counter.""" diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index fe3e52548c5..cfdbbd01a2e 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -368,6 +368,8 @@ class EDL21: class EDL21Entity(SensorEntity): """Entity reading values from EDL21 telegram.""" + _attr_should_poll = False + def __init__(self, electricity_id, obis, name, entity_description, telegram): """Initialize an EDL21Entity.""" self._electricity_id = electricity_id @@ -416,11 +418,6 @@ class EDL21Entity(SensorEntity): if self._async_remove_dispatcher: self._async_remove_dispatcher() - @property - def should_poll(self) -> bool: - """Do not poll.""" - return False - @property def unique_id(self) -> str: """Return a unique ID.""" diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 2ce0e726fc4..7833bfd66b3 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -444,6 +444,7 @@ class ElkEntity(Entity): """Base class for all Elk entities.""" _attr_has_entity_name = True + _attr_should_poll = False def __init__(self, element: Element, elk: Elk, elk_data: dict[str, Any]) -> None: """Initialize the base of all Elk devices.""" @@ -472,11 +473,6 @@ class ElkEntity(Entity): """Return unique id of the element.""" return self._unique_id - @property - def should_poll(self) -> bool: - """Don't poll this device.""" - return False - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the default attributes of the element.""" diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index aa276af492c..55ad58a030d 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -248,6 +248,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class EnvisalinkDevice(Entity): """Representation of an Envisalink device.""" + _attr_should_poll = False + def __init__(self, name, info, controller): """Initialize the device.""" self._controller = controller @@ -258,8 +260,3 @@ class EnvisalinkDevice(Entity): def name(self): """Return the name of the device.""" return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 9df1d1af7d9..ef8808288f3 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -675,6 +675,8 @@ ENTITY_CATEGORIES: EsphomeEnumMapper[ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" + _attr_should_poll = False + def __init__( self, entry_data: RuntimeEntryData, @@ -804,11 +806,6 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): return cast(str, ICON_SCHEMA(self._static_info.icon)) - @property - def should_poll(self) -> bool: - """Disable polling.""" - return False - @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 31650598371..a98766c78c6 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -167,6 +167,8 @@ class FFmpegManager: class FFmpegBase(Entity): """Interface object for FFmpeg.""" + _attr_should_poll = False + def __init__(self, initial_state=True): """Initialize ffmpeg base object.""" self.ffmpeg = None @@ -201,11 +203,6 @@ class FFmpegBase(Entity): """Return True if entity is available.""" return self.ffmpeg.is_running - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg process. diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index ece4d38d726..7ab83d796f2 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -540,6 +540,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class FibaroDevice(Entity): """Representation of a Fibaro device entity.""" + _attr_should_poll = False + def __init__(self, fibaro_device): """Initialize the device.""" self.fibaro_device = fibaro_device @@ -634,11 +636,6 @@ class FibaroDevice(Entity): return True return False - @property - def should_poll(self): - """Get polling requirement from fibaro device.""" - return False - @property def extra_state_attributes(self): """Return the state attributes of the device.""" diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 66878b73145..36455da9fb7 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -26,6 +26,8 @@ async def async_setup_entry( class IncidentsSensor(RestoreEntity, SensorEntity): """Representation of FireServiceRota incidents sensor.""" + _attr_should_poll = False + def __init__(self, client): """Initialize.""" self._client = client @@ -60,11 +62,6 @@ class IncidentsSensor(RestoreEntity, SensorEntity): """Return the unique ID of the sensor.""" return self._unique_id - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @property def extra_state_attributes(self) -> dict[str, Any]: """Return available attributes for sensor.""" diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index 583125873d0..49c6d577b30 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -27,6 +27,8 @@ async def async_setup_entry( class ResponseSwitch(SwitchEntity): """Representation of an FireServiceRota switch.""" + _attr_should_poll = False + def __init__(self, coordinator, client, entry): """Initialize.""" self._coordinator = coordinator @@ -63,11 +65,6 @@ class ResponseSwitch(SwitchEntity): """Return the unique ID for this switch.""" return self._unique_id - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @property def available(self) -> bool: """Return if switch is available.""" diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py index 0e66656421b..33e23f8d401 100644 --- a/homeassistant/components/firmata/entity.py +++ b/homeassistant/components/firmata/entity.py @@ -31,6 +31,8 @@ class FirmataEntity: class FirmataPinEntity(FirmataEntity): """Representation of a Firmata pin entity.""" + _attr_should_poll = False + def __init__( self, api: FirmataBoardPin, @@ -50,11 +52,6 @@ class FirmataPinEntity(FirmataEntity): """Get the name of the pin.""" return self._name - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @property def unique_id(self) -> str: """Return a unique identifier for this device.""" diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 81a7a65cf36..6c1a772fb4d 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -130,6 +130,8 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: class ForkedDaapdZone(MediaPlayerEntity): """Representation of a forked-daapd output.""" + _attr_should_poll = False + def __init__(self, api, output, entry_id): """Initialize the ForkedDaapd Zone.""" self._api = api @@ -164,11 +166,6 @@ class ForkedDaapdZone(MediaPlayerEntity): """Return unique ID.""" return f"{self._entry_id}-{self._output_id}" - @property - def should_poll(self) -> bool: - """Entity pushes its state to HA.""" - return False - async def async_toggle(self) -> None: """Toggle the power on the zone.""" if self.state == STATE_OFF: @@ -235,6 +232,8 @@ class ForkedDaapdZone(MediaPlayerEntity): class ForkedDaapdMaster(MediaPlayerEntity): """Representation of the main forked-daapd device.""" + _attr_should_poll = False + def __init__( self, clientsession, api, ip_address, api_port, api_password, config_entry ): @@ -412,11 +411,6 @@ class ForkedDaapdMaster(MediaPlayerEntity): """Return unique ID.""" return self._config_entry.entry_id - @property - def should_poll(self) -> bool: - """Entity pushes its state to HA.""" - return False - @property def available(self) -> bool: """Return whether the master is available.""" diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 8b5ce4fd6e8..bd71588aea0 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -55,6 +55,8 @@ def add_entities( class FreeboxDevice(ScannerEntity): """Representation of a Freebox device.""" + _attr_should_poll = False + def __init__(self, router: FreeboxRouter, device: dict[str, Any]) -> None: """Initialize a Freebox device.""" self._router = router @@ -112,11 +114,6 @@ class FreeboxDevice(ScannerEntity): """Return the attributes.""" return self._attrs - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @callback def async_on_demand_update(self): """Update state.""" diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index 0f3b19c8ef7..c5d8e76b289 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -41,6 +41,8 @@ async def async_setup_entry( class GdacsSensor(SensorEntity): """This is a status sensor for the GDACS integration.""" + _attr_should_poll = False + def __init__(self, config_entry_id, config_unique_id, config_title, manager): """Initialize entity.""" self._config_entry_id = config_entry_id @@ -79,11 +81,6 @@ class GdacsSensor(SensorEntity): _LOGGER.debug("Received status update for %s", self._config_entry_id) self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GDACS status sensor.""" - return False - async def async_update(self): """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._config_entry_id) diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index 8072c76ea07..07d2d2a36f0 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -112,6 +112,8 @@ async def async_setup_platform( class GenericHygrostat(HumidifierEntity, RestoreEntity): """Representation of a Generic Hygrostat device.""" + _attr_should_poll = False + def __init__( self, name, @@ -218,11 +220,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): return {ATTR_SAVED_HUMIDITY: self._saved_target_humidity} return None - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name of the hygrostat.""" diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 9e6df475d39..c2eecf413f7 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -165,6 +165,8 @@ async def async_setup_platform( class GenericThermostat(ClimateEntity, RestoreEntity): """Representation of a Generic Thermostat device.""" + _attr_should_poll = False + def __init__( self, name, @@ -300,11 +302,6 @@ class GenericThermostat(ClimateEntity, RestoreEntity): if not self._hvac_mode: self._hvac_mode = HVACMode.OFF - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name of the thermostat.""" diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 3c5cc22af81..fc04f314812 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -221,6 +221,8 @@ class GeniusBroker: class GeniusEntity(Entity): """Base for all Genius Hub entities.""" + _attr_should_poll = False + def __init__(self) -> None: """Initialize the entity.""" self._unique_id: str | None = None @@ -238,11 +240,6 @@ class GeniusEntity(Entity): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self) -> bool: - """Return False as geniushub entities should not be polled.""" - return False - class GeniusDevice(GeniusEntity): """Base for all Genius Hub devices.""" diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py index bd99eb5a59f..357c86a3b8f 100644 --- a/homeassistant/components/geonetnz_quakes/sensor.py +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -42,6 +42,8 @@ async def async_setup_entry( class GeonetnzQuakesSensor(SensorEntity): """This is a status sensor for the GeoNet NZ Quakes integration.""" + _attr_should_poll = False + def __init__(self, config_entry_id, config_unique_id, config_title, manager): """Initialize entity.""" self._config_entry_id = config_entry_id @@ -80,11 +82,6 @@ class GeonetnzQuakesSensor(SensorEntity): _LOGGER.debug("Received status update for %s", self._config_entry_id) self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GeoNet NZ Quakes status sensor.""" - return False - async def async_update(self): """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._config_entry_id) diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index a1d012d8d41..e11a9394579 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -61,6 +61,8 @@ async def async_setup_entry( class GeonetnzVolcanoSensor(SensorEntity): """This represents an external event with GeoNet NZ Volcano feed data.""" + _attr_should_poll = False + def __init__(self, config_entry_id, feed_manager, external_id, unit_system): """Initialize entity with data from feed entry.""" self._config_entry_id = config_entry_id @@ -97,11 +99,6 @@ class GeonetnzVolcanoSensor(SensorEntity): """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GeoNet NZ Volcano feed location events.""" - return False - async def async_update(self): """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) From 2a8109304f42b3d93cb171cfc6ac7265a22623ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:20:38 +0200 Subject: [PATCH 643/903] Use _attr_should_poll in components [h-i] (#77270) --- homeassistant/components/heos/media_player.py | 7 ++----- .../components/hikvision/binary_sensor.py | 7 ++----- homeassistant/components/hlk_sw16/__init__.py | 7 ++----- .../components/home_connect/entity.py | 7 ++----- homeassistant/components/homematic/entity.py | 13 +++--------- .../homematicip_cloud/alarm_control_panel.py | 6 +----- .../homematicip_cloud/generic_entity.py | 7 ++----- .../components/huawei_lte/__init__.py | 6 +----- homeassistant/components/hyperion/light.py | 6 +----- homeassistant/components/hyperion/switch.py | 6 +----- .../components/iaqualink/__init__.py | 11 ++-------- homeassistant/components/icloud/sensor.py | 6 +----- homeassistant/components/ihc/ihcdevice.py | 7 ++----- .../components/incomfort/__init__.py | 7 ++----- .../components/input_datetime/__init__.py | 7 ++----- .../components/input_number/__init__.py | 7 ++----- .../components/input_text/__init__.py | 7 ++----- .../components/insteon/insteon_entity.py | 7 ++----- .../components/intesishome/climate.py | 7 ++----- homeassistant/components/izone/climate.py | 20 ++++--------------- 20 files changed, 38 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index ad9225d9b21..765fe2f79c5 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -117,6 +117,8 @@ def log_command_error( class HeosMediaPlayer(MediaPlayerEntity): """The HEOS player.""" + _attr_should_poll = False + def __init__(self, player): """Initialize.""" self._media_position_updated_at = None @@ -402,11 +404,6 @@ class HeosMediaPlayer(MediaPlayerEntity): """Return the name of the device.""" return self._player.name - @property - def should_poll(self) -> bool: - """No polling needed for this device.""" - return False - @property def shuffle(self) -> bool: """Boolean if shuffle is enabled.""" diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index 011f4ca05d1..e7f3c49fb08 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -199,6 +199,8 @@ class HikvisionData: class HikvisionBinarySensor(BinarySensorEntity): """Representation of a Hikvision binary sensor.""" + _attr_should_poll = False + def __init__(self, hass, sensor, channel, cam, delay): """Initialize the binary_sensor.""" self._hass = hass @@ -255,11 +257,6 @@ class HikvisionBinarySensor(BinarySensorEntity): # Sensor must be unknown to us, add as generic return None - @property - def should_poll(self): - """No polling needed.""" - return False - @property def extra_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index c695f5524c3..f80972da613 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -138,6 +138,8 @@ class SW16Device(Entity): Contains the common logic for HLK-SW16 entities. """ + _attr_should_poll = False + def __init__(self, device_port, entry_id, client): """Initialize the device.""" # HLK-SW16 specific attributes for every component type @@ -159,11 +161,6 @@ class SW16Device(Entity): self._is_on = event self.async_write_ha_state() - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return a name for the device.""" diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index b27988f997d..3c2ac52929a 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -15,6 +15,8 @@ _LOGGER = logging.getLogger(__name__) class HomeConnectEntity(Entity): """Generic Home Connect entity (base class).""" + _attr_should_poll = False + def __init__(self, device: HomeConnectDevice, desc: str) -> None: """Initialize the entity.""" self.device = device @@ -35,11 +37,6 @@ class HomeConnectEntity(Entity): if ha_id == self.device.appliance.haId: self.async_entity_update() - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the node (used for Entity_ID).""" diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index fee68caf7ed..700ef5cdc94 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -35,6 +35,7 @@ class HMDevice(Entity): _homematic: HMConnection _hmdevice: HMGeneric + _attr_should_poll = False def __init__( self, @@ -69,11 +70,6 @@ class HMDevice(Entity): """Return unique ID. HomeMatic entity IDs are unique by default.""" return self._unique_id.replace(" ", "_") - @property - def should_poll(self): - """Return false. HomeMatic states are pushed by the XML-RPC Server.""" - return False - @property def name(self): """Return the name of the device.""" @@ -213,6 +209,8 @@ class HMDevice(Entity): class HMHub(Entity): """The HomeMatic hub. (CCU2/HomeGear).""" + _attr_should_poll = False + def __init__(self, hass, homematic, name): """Initialize HomeMatic hub.""" self.hass = hass @@ -234,11 +232,6 @@ class HMHub(Entity): """Return the name of the device.""" return self._name - @property - def should_poll(self): - """Return false. HomeMatic Hub object updates variables.""" - return False - @property def state(self): """Return the state of the entity.""" diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 3b6ae684d07..85d241051c1 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -41,6 +41,7 @@ async def async_setup_entry( class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): """Representation of the HomematicIP alarm control panel.""" + _attr_should_poll = False _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY @@ -120,11 +121,6 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): name = f"{self._home.name} {name}" return name - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @property def available(self) -> bool: """Return if alarm control panel is available.""" diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 8bcea5d1435..a5296675292 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -72,6 +72,8 @@ GROUP_ATTRIBUTES = { class HomematicipGenericEntity(Entity): """Representation of the HomematicIP generic entity.""" + _attr_should_poll = False + def __init__( self, hap: HomematicipHAP, @@ -201,11 +203,6 @@ class HomematicipGenericEntity(Entity): return name - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @property def available(self) -> bool: """Return if entity is available.""" diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 565286c4505..5740cf99f53 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -588,6 +588,7 @@ class HuaweiLteBaseEntity(Entity): _available: bool = field(default=True, init=False) _unsub_handlers: list[Callable] = field(default_factory=list, init=False) _attr_has_entity_name: bool = field(default=True, init=False) + _attr_should_poll = False @property def _device_unique_id(self) -> str: @@ -604,11 +605,6 @@ class HuaweiLteBaseEntity(Entity): """Return whether the entity is available.""" return self._available - @property - def should_poll(self) -> bool: - """Huawei LTE entities report their state without polling.""" - return False - async def async_update(self) -> None: """Update state.""" raise NotImplementedError diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index fc4ec4d023e..49177ac94c6 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -125,6 +125,7 @@ class HyperionBaseLight(LightEntity): """A Hyperion light base class.""" _attr_color_mode = ColorMode.HS + _attr_should_poll = False _attr_supported_color_modes = {ColorMode.HS} _attr_supported_features = LightEntityFeature.EFFECT @@ -178,11 +179,6 @@ class HyperionBaseLight(LightEntity): """Whether or not the entity is enabled by default.""" return True - @property - def should_poll(self) -> bool: - """Return whether or not this entity should be polled.""" - return False - @property def name(self) -> str: """Return the name of the light.""" diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index bf4958d845c..523aa3e31b6 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -127,6 +127,7 @@ class HyperionComponentSwitch(SwitchEntity): """ComponentBinarySwitch switch class.""" _attr_entity_category = EntityCategory.CONFIG + _attr_should_poll = False def __init__( self, @@ -149,11 +150,6 @@ class HyperionComponentSwitch(SwitchEntity): f"{KEY_COMPONENTS}-{KEY_UPDATE}": self._update_components } - @property - def should_poll(self) -> bool: - """Return whether or not this entity should be polled.""" - return False - @property def entity_registry_enabled_default(self) -> bool: """Whether or not the entity is enabled by default.""" diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 844313a4aed..61f27e8970c 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -194,6 +194,8 @@ class AqualinkEntity(Entity): class. """ + _attr_should_poll = False + def __init__(self, dev: AqualinkDevice) -> None: """Initialize the entity.""" self.dev = dev @@ -204,15 +206,6 @@ class AqualinkEntity(Entity): async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) ) - @property - def should_poll(self) -> bool: - """Return False as entities shouldn't be polled. - - Entities are checked periodically as the integration runs periodic - updates on a timer. - """ - return False - @property def unique_id(self) -> str: """Return a unique identifier for this entity.""" diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 3feb30f078f..e747d898dea 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -56,6 +56,7 @@ class IcloudDeviceBatterySensor(SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE + _attr_should_poll = False def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None: """Initialize the battery sensor.""" @@ -102,11 +103,6 @@ class IcloudDeviceBatterySensor(SensorEntity): name=self._device.name, ) - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 31887c51397..0c077f8698e 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -18,6 +18,8 @@ class IHCDevice(Entity): Derived classes must implement the on_ihc_change method """ + _attr_should_poll = False + def __init__( self, ihc_controller: IHCController, @@ -56,11 +58,6 @@ class IHCDevice(Entity): _LOGGER.debug("Adding IHC entity notify event: %s", self.ihc_id) self.ihc_controller.add_notify_event(self.ihc_id, self.on_ihc_change, True) - @property - def should_poll(self) -> bool: - """No polling needed for IHC devices.""" - return False - @property def name(self): """Return the device name.""" diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index 9b2f0d88b3f..fe3d37e1440 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -91,6 +91,8 @@ class IncomfortEntity(Entity): class IncomfortChild(IncomfortEntity): """Base class for all InComfort entities (excluding the boiler).""" + _attr_should_poll = False + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh)) @@ -98,8 +100,3 @@ class IncomfortChild(IncomfortEntity): @callback def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) - - @property - def should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 2a64c8d3b89..bda5572081c 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -234,6 +234,8 @@ class DateTimeStorageCollection(collection.StorageCollection): class InputDatetime(RestoreEntity): """Representation of a datetime input.""" + _attr_should_poll = False + def __init__(self, config: dict) -> None: """Initialize a select input.""" self._config = config @@ -303,11 +305,6 @@ class InputDatetime(RestoreEntity): tzinfo=dt_util.DEFAULT_TIME_ZONE ) - @property - def should_poll(self): - """If entity should be polled.""" - return False - @property def name(self): """Return the name of the select input.""" diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 8e922687e59..ff01bd124b6 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -205,6 +205,8 @@ class NumberStorageCollection(collection.StorageCollection): class InputNumber(RestoreEntity): """Representation of a slider.""" + _attr_should_poll = False + def __init__(self, config: dict) -> None: """Initialize an input number.""" self._config = config @@ -219,11 +221,6 @@ class InputNumber(RestoreEntity): input_num.editable = False return input_num - @property - def should_poll(self): - """If entity should be polled.""" - return False - @property def _minimum(self) -> float: """Return minimum allowed value.""" diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index f1e0b45afec..38d74f57931 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -198,6 +198,8 @@ class InputTextStorageCollection(collection.StorageCollection): class InputText(RestoreEntity): """Represent a text box.""" + _attr_should_poll = False + def __init__(self, config: dict) -> None: """Initialize a text input.""" self._config = config @@ -212,11 +214,6 @@ class InputText(RestoreEntity): input_text.editable = False return input_text - @property - def should_poll(self): - """If entity should be polled.""" - return False - @property def name(self): """Return the name of the text input entity.""" diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 67d30ba8cad..d1ba7c5f829 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -28,6 +28,8 @@ _LOGGER = logging.getLogger(__name__) class InsteonEntity(Entity): """INSTEON abstract base entity.""" + _attr_should_poll = False + def __init__(self, device, group): """Initialize the INSTEON binary sensor.""" self._insteon_device_group = device.groups[group] @@ -37,11 +39,6 @@ class InsteonEntity(Entity): """Return the hash of the Insteon Entity.""" return hash(self._insteon_device) - @property - def should_poll(self): - """No polling needed.""" - return False - @property def address(self): """Return the address of the node.""" diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 050bed8c721..61b171ea8cc 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -143,6 +143,8 @@ async def async_setup_platform( class IntesisAC(ClimateEntity): """Represents an Intesishome air conditioning device.""" + _attr_should_poll = False + def __init__(self, ih_device_id, ih_device, controller): """Initialize the thermostat.""" self._controller = controller @@ -410,11 +412,6 @@ class IntesisAC(ClimateEntity): """Return the maximum temperature for the current mode of operation.""" return self._max_temp - @property - def should_poll(self): - """Poll for updates if pyIntesisHome doesn't have a socket open.""" - return False - @property def fan_mode(self): """Return whether the fan is on.""" diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 58834f995dd..df7b8af4fa3 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -126,6 +126,8 @@ def _return_on_connection_error(ret=None): class ControllerDevice(ClimateEntity): """Representation of iZone Controller.""" + _attr_should_poll = False + def __init__(self, controller: Controller) -> None: """Initialise ControllerDevice.""" self._controller = controller @@ -250,14 +252,6 @@ class ControllerDevice(ClimateEntity): """Return the name of the entity.""" return f"iZone Controller {self._controller.device_uid}" - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return False - @property def temperature_unit(self) -> str: """Return the unit of measurement which this thermostat uses.""" @@ -448,6 +442,8 @@ class ControllerDevice(ClimateEntity): class ZoneDevice(ClimateEntity): """Representation of iZone Zone.""" + _attr_should_poll = False + def __init__(self, controller: ControllerDevice, zone: Zone) -> None: """Initialise ZoneDevice.""" self._controller = controller @@ -525,14 +521,6 @@ class ZoneDevice(ClimateEntity): """Return the name of the entity.""" return self._name - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return False - @property # type: ignore[misc] @_return_on_connection_error(0) def supported_features(self): From f9a46cc79f82e8a493337db3efb5e6aa19cc24d7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:21:13 +0200 Subject: [PATCH 644/903] Use _attr_should_poll in econet (#77262) --- homeassistant/components/econet/__init__.py | 10 ++-------- homeassistant/components/econet/water_heater.py | 14 +++----------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index 728222dcda7..981d6cbad23 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -111,6 +111,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class EcoNetEntity(Entity): """Define a base EcoNet entity.""" + _attr_should_poll = False + def __init__(self, econet): """Initialize.""" self._econet = econet @@ -155,11 +157,3 @@ class EcoNetEntity(Entity): def temperature_unit(self): """Return the unit of measurement.""" return TEMP_FAHRENHEIT - - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return False diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 50f080217b4..165dc49e205 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -62,7 +62,7 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): """Initialize.""" super().__init__(water_heater) self._running = water_heater.running - self._poll = True + self._attr_should_poll = True # Override False default from EcoNetEntity self.water_heater = water_heater self.econet_state_to_ha = {} self.ha_state_to_econet = {} @@ -72,7 +72,7 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): """Update was pushed from the ecoent API.""" if self._running != self.water_heater.running: # Water heater running state has changed so check usage on next update - self._poll = True + self._attr_should_poll = True self._running = self.water_heater.running self.async_write_ha_state() @@ -149,20 +149,12 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): """Return the maximum temperature.""" return self.water_heater.set_point_limits[1] - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return self._poll - async def async_update(self) -> None: """Get the latest energy usage.""" await self.water_heater.get_energy_usage() await self.water_heater.get_water_usage() self.async_write_ha_state() - self._poll = False + self._attr_should_poll = False def turn_away_mode_on(self) -> None: """Turn away mode on.""" From c438c26df3372a33daba3ffdaff937d1f7cfbe69 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:25:33 +0200 Subject: [PATCH 645/903] Improve WLED typing (#77200) --- homeassistant/components/wled/coordinator.py | 7 +++---- homeassistant/components/wled/diagnostics.py | 2 +- homeassistant/components/wled/helpers.py | 16 ++++++++++++++-- homeassistant/components/wled/light.py | 2 +- homeassistant/components/wled/number.py | 4 ++-- homeassistant/components/wled/select.py | 4 ++-- homeassistant/components/wled/switch.py | 4 ++-- 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py index 81017779fbb..ea3be9c3771 100644 --- a/homeassistant/components/wled/coordinator.py +++ b/homeassistant/components/wled/coordinator.py @@ -2,13 +2,12 @@ from __future__ import annotations import asyncio -from collections.abc import Callable from wled import WLED, Device as WLEDDevice, WLEDConnectionClosed, WLEDError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -38,7 +37,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT ) self.wled = WLED(entry.data[CONF_HOST], session=async_get_clientsession(hass)) - self.unsub: Callable | None = None + self.unsub: CALLBACK_TYPE | None = None super().__init__( hass, @@ -85,7 +84,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): self.unsub() self.unsub = None - async def close_websocket(_) -> None: + async def close_websocket(_: Event) -> None: """Close WebSocket connection.""" self.unsub = None await self.wled.disconnect() diff --git a/homeassistant/components/wled/diagnostics.py b/homeassistant/components/wled/diagnostics.py index c2820a7a13a..d0b3de5eb6b 100644 --- a/homeassistant/components/wled/diagnostics.py +++ b/homeassistant/components/wled/diagnostics.py @@ -17,7 +17,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - data = { + data: dict[str, Any] = { "info": async_redact_data(coordinator.data.info.__dict__, "wifi"), "state": coordinator.data.state.__dict__, "effects": { diff --git a/homeassistant/components/wled/helpers.py b/homeassistant/components/wled/helpers.py index 77e288bb34d..32503383b07 100644 --- a/homeassistant/components/wled/helpers.py +++ b/homeassistant/components/wled/helpers.py @@ -1,18 +1,30 @@ """Helpers for WLED.""" +from __future__ import annotations +from collections.abc import Callable, Coroutine +from typing import Any, TypeVar + +from typing_extensions import Concatenate, ParamSpec from wled import WLEDConnectionError, WLEDError from homeassistant.exceptions import HomeAssistantError +from .models import WLEDEntity -def wled_exception_handler(func): +_WLEDEntityT = TypeVar("_WLEDEntityT", bound=WLEDEntity) +_P = ParamSpec("_P") + + +def wled_exception_handler( + func: Callable[Concatenate[_WLEDEntityT, _P], Coroutine[Any, Any, Any]] +) -> Callable[Concatenate[_WLEDEntityT, _P], Coroutine[Any, Any, None]]: """Decorate WLED calls to handle WLED exceptions. A decorator that wraps the passed in function, catches WLED errors, and handles the availability of the device in the data coordinator. """ - async def handler(self, *args, **kwargs): + async def handler(self: _WLEDEntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: try: await func(self, *args, **kwargs) self.coordinator.async_update_listeners() diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 98be359628e..74af6cc0793 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -253,7 +253,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): def async_update_segments( coordinator: WLEDDataUpdateCoordinator, current_ids: set[int], - async_add_entities, + async_add_entities: AddEntitiesCallback, ) -> None: """Update segments.""" segment_ids = {light.segment_id for light in coordinator.data.state.segments} diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index d6032791e5b..33b27777c7e 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -115,12 +115,12 @@ class WLEDNumber(WLEDEntity, NumberEntity): def async_update_segments( coordinator: WLEDDataUpdateCoordinator, current_ids: set[int], - async_add_entities, + async_add_entities: AddEntitiesCallback, ) -> None: """Update segments.""" segment_ids = {segment.segment_id for segment in coordinator.data.state.segments} - new_entities = [] + new_entities: list[WLEDNumber] = [] # Process new segments, add them to Home Assistant for segment_id in segment_ids - current_ids: diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index c3980f9f9c7..5b0de05370c 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -183,12 +183,12 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity): def async_update_segments( coordinator: WLEDDataUpdateCoordinator, current_ids: set[int], - async_add_entities, + async_add_entities: AddEntitiesCallback, ) -> None: """Update segments.""" segment_ids = {segment.segment_id for segment in coordinator.data.state.segments} - new_entities = [] + new_entities: list[WLEDPaletteSelect] = [] # Process new segments, add them to Home Assistant for segment_id in segment_ids - current_ids: diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 7d0d9ee24fb..20b5a618726 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -203,12 +203,12 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity): def async_update_segments( coordinator: WLEDDataUpdateCoordinator, current_ids: set[int], - async_add_entities, + async_add_entities: AddEntitiesCallback, ) -> None: """Update segments.""" segment_ids = {segment.segment_id for segment in coordinator.data.state.segments} - new_entities = [] + new_entities: list[WLEDReverseSwitch] = [] # Process new segments, add them to Home Assistant for segment_id in segment_ids - current_ids: From 3031caafed9811e0b3da146c2ee5a8a7f0080b5e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:30:51 +0200 Subject: [PATCH 646/903] Improve type hint in flic binary sensor entity (#77161) --- .../components/flic/binary_sensor.py | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index b0cac4ee1b5..81a23a9eeb5 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -117,10 +117,16 @@ def start_scanning(config, add_entities, client): client.add_scan_wizard(scan_wizard) -def setup_button(hass, config, add_entities, client, address): +def setup_button( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + client, + address, +) -> None: """Set up a single button device.""" - timeout = config.get(CONF_TIMEOUT) - ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES) + timeout: int = config[CONF_TIMEOUT] + ignored_click_types: list[str] | None = config.get(CONF_IGNORED_CLICK_TYPES) button = FlicButton(hass, client, address, timeout, ignored_click_types) _LOGGER.info("Connected to button %s", address) @@ -130,14 +136,25 @@ def setup_button(hass, config, add_entities, client, address): class FlicButton(BinarySensorEntity): """Representation of a flic button.""" - def __init__(self, hass, client, address, timeout, ignored_click_types): + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + client: pyflic.FlicClient, + address: str, + timeout: int, + ignored_click_types: list[str] | None, + ) -> None: """Initialize the flic button.""" + self._attr_extra_state_attributes = {"address": address} + self._attr_name = f"flic_{address.replace(':', '')}" self._attr_unique_id = format_mac(address) self._hass = hass self._address = address self._timeout = timeout - self._is_down = False + self._attr_is_on = True self._ignored_click_types = ignored_click_types or [] self._hass_click_types = { pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE, @@ -149,7 +166,7 @@ class FlicButton(BinarySensorEntity): self._channel = self._create_channel() client.add_connection_channel(self._channel) - def _create_channel(self): + def _create_channel(self) -> pyflic.ButtonConnectionChannel: """Create a new connection channel to the button.""" channel = pyflic.ButtonConnectionChannel(self._address) channel.on_button_up_or_down = self._on_up_down @@ -170,31 +187,6 @@ class FlicButton(BinarySensorEntity): return channel - @property - def name(self): - """Return the name of the device.""" - return f"flic_{self.address.replace(':', '')}" - - @property - def address(self): - """Return the bluetooth address of the device.""" - return self._address - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._is_down - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def extra_state_attributes(self): - """Return device specific state attributes.""" - return {"address": self.address} - def _queued_event_check(self, click_type, time_diff): """Generate a log message and returns true if timeout exceeded.""" time_string = f"{time_diff:d} {'second' if time_diff == 1 else 'seconds'}" @@ -203,14 +195,14 @@ class FlicButton(BinarySensorEntity): _LOGGER.warning( "Queued %s dropped for %s. Time in queue was %s", click_type, - self.address, + self._address, time_string, ) return True _LOGGER.info( "Queued %s allowed for %s. Time in queue was %s", click_type, - self.address, + self._address, time_string, ) return False @@ -220,7 +212,7 @@ class FlicButton(BinarySensorEntity): if was_queued and self._queued_event_check(click_type, time_diff): return - self._is_down = click_type == pyflic.ClickType.ButtonDown + self._attr_is_on = click_type != pyflic.ClickType.ButtonDown self.schedule_update_ha_state() def _on_click(self, channel, click_type, was_queued, time_diff): @@ -238,7 +230,7 @@ class FlicButton(BinarySensorEntity): EVENT_NAME, { EVENT_DATA_NAME: self.name, - EVENT_DATA_ADDRESS: self.address, + EVENT_DATA_ADDRESS: self._address, EVENT_DATA_QUEUED_TIME: time_diff, EVENT_DATA_TYPE: hass_click_type, }, @@ -248,5 +240,5 @@ class FlicButton(BinarySensorEntity): """Remove device, if button disconnects.""" if connection_status == pyflic.ConnectionStatus.Disconnected: _LOGGER.warning( - "Button (%s) disconnected. Reason: %s", self.address, disconnect_reason + "Button (%s) disconnected. Reason: %s", self._address, disconnect_reason ) From 8896229ea641a558161d8caed796895e9a78f457 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:33:17 +0200 Subject: [PATCH 647/903] Improve type hint in foobot sensor entity (#77164) --- homeassistant/components/foobot/sensor.py | 43 ++++++++++------------- tests/components/foobot/test_sensor.py | 8 ++--- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index e1d3c8de8d2..82a48c810c3 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging +from typing import Any import aiohttp from foobot_async import FoobotClient @@ -16,7 +17,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( ATTR_TEMPERATURE, - ATTR_TIME, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, @@ -24,14 +24,12 @@ from homeassistant.const import ( CONF_USERNAME, PERCENTAGE, TEMP_CELSIUS, - TIME_SECONDS, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle @@ -45,11 +43,6 @@ ATTR_VOLATILE_ORGANIC_COMPOUNDS = "VOC" ATTR_FOOBOT_INDEX = "index" SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="time", - name=ATTR_TIME, - native_unit_of_measurement=TIME_SECONDS, - ), SensorEntityDescription( key="pm", name=ATTR_PM2_5, @@ -105,15 +98,15 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the devices associated with the account.""" - token = config.get(CONF_TOKEN) - username = config.get(CONF_USERNAME) + token: str = config[CONF_TOKEN] + username: str = config[CONF_USERNAME] client = FoobotClient( token, username, async_get_clientsession(hass), timeout=TIMEOUT ) - entities = [] + entities: list[FoobotSensor] = [] try: - devices = await client.get_devices() + devices: list[dict[str, Any]] = await client.get_devices() _LOGGER.debug("The following devices were found: %s", devices) for device in devices: foobot_data = FoobotData(client, device["uuid"]) @@ -121,7 +114,6 @@ async def async_setup_platform( [ FoobotSensor(foobot_data, device, description) for description in SENSOR_TYPES - if description.key != "time" ] ) except ( @@ -141,7 +133,12 @@ async def async_setup_platform( class FoobotSensor(SensorEntity): """Implementation of a Foobot sensor.""" - def __init__(self, data, device, description: SensorEntityDescription): + def __init__( + self, + data: FoobotData, + device: dict[str, Any], + description: SensorEntityDescription, + ) -> None: """Initialize the sensor.""" self.entity_description = description self.foobot_data = data @@ -150,34 +147,30 @@ class FoobotSensor(SensorEntity): self._attr_unique_id = f"{device['uuid']}_{description.key}" @property - def native_value(self): + def native_value(self) -> float | None: """Return the state of the device.""" - try: - data = self.foobot_data.data[self.entity_description.key] - except (KeyError, TypeError): - data = None - return data + return self.foobot_data.data.get(self.entity_description.key) async def async_update(self) -> None: """Get the latest data.""" await self.foobot_data.async_update() -class FoobotData(Entity): +class FoobotData: """Get data from Foobot API.""" - def __init__(self, client, uuid): + def __init__(self, client: FoobotClient, uuid: str) -> None: """Initialize the data object.""" self._client = client self._uuid = uuid - self.data = {} + self.data: dict[str, float] = {} @Throttle(SCAN_INTERVAL) - async def async_update(self): + async def async_update(self) -> bool: """Get the data from Foobot API.""" interval = SCAN_INTERVAL.total_seconds() try: - response = await self._client.get_last_data( + response: list[dict[str, Any]] = await self._client.get_last_data( self._uuid, interval, interval + 1 ) except ( diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 7ffa3987110..93908715888 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -64,9 +64,7 @@ async def test_setup_timeout_error(hass, aioclient_mock): re.compile("api.foobot.io/v2/owner/.*"), exc=asyncio.TimeoutError() ) with pytest.raises(PlatformNotReady): - await foobot.async_setup_platform( - hass, {"sensor": VALID_CONFIG}, fake_async_add_entities - ) + await foobot.async_setup_platform(hass, VALID_CONFIG, fake_async_add_entities) async def test_setup_permanent_error(hass, aioclient_mock): @@ -77,7 +75,7 @@ async def test_setup_permanent_error(hass, aioclient_mock): for error in errors: aioclient_mock.get(re.compile("api.foobot.io/v2/owner/.*"), status=error) result = await foobot.async_setup_platform( - hass, {"sensor": VALID_CONFIG}, fake_async_add_entities + hass, VALID_CONFIG, fake_async_add_entities ) assert result is None @@ -91,5 +89,5 @@ async def test_setup_temporary_error(hass, aioclient_mock): aioclient_mock.get(re.compile("api.foobot.io/v2/owner/.*"), status=error) with pytest.raises(PlatformNotReady): await foobot.async_setup_platform( - hass, {"sensor": VALID_CONFIG}, fake_async_add_entities + hass, VALID_CONFIG, fake_async_add_entities ) From 8f9ff0f88ec7e7d727473d09cfd4b450d1afdb84 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:48:12 +0200 Subject: [PATCH 648/903] Improve type hint in freedompro entities (#77170) --- .../components/freedompro/binary_sensor.py | 11 +++++-- .../components/freedompro/climate.py | 29 ++++++++++++------- homeassistant/components/freedompro/cover.py | 15 +++++++--- homeassistant/components/freedompro/fan.py | 27 ++++++++--------- homeassistant/components/freedompro/lock.py | 27 ++++++++++------- homeassistant/components/freedompro/sensor.py | 13 ++++++--- homeassistant/components/freedompro/switch.py | 28 +++++++++++------- 7 files changed, 93 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index f67b9c78299..3a33c5a2a2c 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -1,4 +1,6 @@ """Support for Freedompro binary_sensor.""" +from typing import Any + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -9,6 +11,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FreedomproDataUpdateCoordinator from .const import DOMAIN DEVICE_CLASS_MAP = { @@ -32,7 +35,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Freedompro binary_sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( Device(device, coordinator) for device in coordinator.data @@ -43,7 +46,9 @@ async def async_setup_entry( class Device(CoordinatorEntity, BinarySensorEntity): """Representation of an Freedompro binary_sensor.""" - def __init__(self, device, coordinator): + def __init__( + self, device: dict[str, Any], coordinator: FreedomproDataUpdateCoordinator + ) -> None: """Initialize the Freedompro binary_sensor.""" super().__init__(coordinator) self._attr_name = device["name"] @@ -51,7 +56,7 @@ class Device(CoordinatorEntity, BinarySensorEntity): self._type = device["type"] self._attr_device_info = DeviceInfo( identifiers={ - (DOMAIN, self.unique_id), + (DOMAIN, device["uid"]), }, manufacturer="Freedompro", model=device["type"], diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py index bcd7cd109ba..7076e48c29c 100644 --- a/homeassistant/components/freedompro/climate.py +++ b/homeassistant/components/freedompro/climate.py @@ -3,7 +3,9 @@ from __future__ import annotations import json import logging +from typing import Any +from aiohttp.client import ClientSession from pyfreedompro import put_state from homeassistant.components.climate import ClimateEntity @@ -20,6 +22,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FreedomproDataUpdateCoordinator from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -43,8 +46,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Freedompro climate.""" - api_key = entry.data[CONF_API_KEY] - coordinator = hass.data[DOMAIN][entry.entry_id] + api_key: str = entry.data[CONF_API_KEY] + coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( Device( aiohttp_client.async_get_clientsession(hass), api_key, device, coordinator @@ -54,13 +57,19 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, ClimateEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], ClimateEntity): """Representation of an Freedompro climate.""" _attr_hvac_modes = SUPPORTED_HVAC_MODES _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, session, api_key, device, coordinator): + def __init__( + self, + session: ClientSession, + api_key: str, + device: dict[str, Any], + coordinator: FreedomproDataUpdateCoordinator, + ) -> None: """Initialize the Freedompro climate.""" super().__init__(coordinator) self._session = session @@ -70,7 +79,7 @@ class Device(CoordinatorEntity, ClimateEntity): self._characteristics = device["characteristics"] self._attr_device_info = DeviceInfo( identifiers={ - (DOMAIN, self.unique_id), + (DOMAIN, device["uid"]), }, manufacturer="Freedompro", model=device["type"], @@ -107,23 +116,22 @@ class Device(CoordinatorEntity, ClimateEntity): await super().async_added_to_hass() self._handle_coordinator_update() - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Async function to set mode to climate.""" if hvac_mode not in SUPPORTED_HVAC_MODES: raise ValueError(f"Got unsupported hvac_mode {hvac_mode}") payload = {} payload["heatingCoolingState"] = HVAC_INVERT_MAP[hvac_mode] - payload = json.dumps(payload) await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Async function to set temperature to climate.""" payload = {} if ATTR_HVAC_MODE in kwargs: @@ -137,11 +145,10 @@ class Device(CoordinatorEntity, ClimateEntity): payload["heatingCoolingState"] = HVAC_INVERT_MAP[kwargs[ATTR_HVAC_MODE]] if ATTR_TEMPERATURE in kwargs: payload["targetTemperature"] = kwargs[ATTR_TEMPERATURE] - payload = json.dumps(payload) await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index ebb8a98b4b1..265e06802b5 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FreedomproDataUpdateCoordinator from .const import DOMAIN DEVICE_CLASS_MAP = { @@ -35,8 +36,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Freedompro cover.""" - api_key = entry.data[CONF_API_KEY] - coordinator = hass.data[DOMAIN][entry.entry_id] + api_key: str = entry.data[CONF_API_KEY] + coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data @@ -47,7 +48,13 @@ async def async_setup_entry( class Device(CoordinatorEntity, CoverEntity): """Representation of an Freedompro cover.""" - def __init__(self, hass, api_key, device, coordinator): + def __init__( + self, + hass: HomeAssistant, + api_key: str, + device: dict[str, Any], + coordinator: FreedomproDataUpdateCoordinator, + ) -> None: """Initialize the Freedompro cover.""" super().__init__(coordinator) self._session = aiohttp_client.async_get_clientsession(hass) @@ -56,7 +63,7 @@ class Device(CoordinatorEntity, CoverEntity): self._attr_unique_id = device["uid"] self._attr_device_info = DeviceInfo( identifiers={ - (DOMAIN, self.unique_id), + (DOMAIN, device["uid"]), }, manufacturer="Freedompro", model=device["type"], diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index 443a1375f24..036c6c91471 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -15,6 +15,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FreedomproDataUpdateCoordinator from .const import DOMAIN @@ -22,8 +23,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Freedompro fan.""" - api_key = entry.data[CONF_API_KEY] - coordinator = hass.data[DOMAIN][entry.entry_id] + api_key: str = entry.data[CONF_API_KEY] + coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( FreedomproFan(hass, api_key, device, coordinator) for device in coordinator.data @@ -31,10 +32,16 @@ async def async_setup_entry( ) -class FreedomproFan(CoordinatorEntity, FanEntity): +class FreedomproFan(CoordinatorEntity[FreedomproDataUpdateCoordinator], FanEntity): """Representation of an Freedompro fan.""" - def __init__(self, hass, api_key, device, coordinator): + def __init__( + self, + hass: HomeAssistant, + api_key: str, + device: dict[str, Any], + coordinator: FreedomproDataUpdateCoordinator, + ) -> None: """Initialize the Freedompro fan.""" super().__init__(coordinator) self._session = aiohttp_client.async_get_clientsession(hass) @@ -44,7 +51,7 @@ class FreedomproFan(CoordinatorEntity, FanEntity): self._characteristics = device["characteristics"] self._attr_device_info = DeviceInfo( identifiers={ - (DOMAIN, self.unique_id), + (DOMAIN, device["uid"]), }, manufacturer="Freedompro", model=device["type"], @@ -60,11 +67,6 @@ class FreedomproFan(CoordinatorEntity, FanEntity): """Return True if entity is on.""" return self._attr_is_on - @property - def percentage(self) -> int | None: - """Return the current speed percentage.""" - return self._attr_percentage - @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -117,12 +119,11 @@ class FreedomproFan(CoordinatorEntity, FanEntity): async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" - rotation_speed = {"rotationSpeed": percentage} - payload = json.dumps(rotation_speed) + payload = {"rotationSpeed": percentage} await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/freedompro/lock.py b/homeassistant/components/freedompro/lock.py index 237ad50c053..d803354c255 100644 --- a/homeassistant/components/freedompro/lock.py +++ b/homeassistant/components/freedompro/lock.py @@ -13,6 +13,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FreedomproDataUpdateCoordinator from .const import DOMAIN @@ -20,8 +21,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Freedompro lock.""" - api_key = entry.data[CONF_API_KEY] - coordinator = hass.data[DOMAIN][entry.entry_id] + api_key: str = entry.data[CONF_API_KEY] + coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data @@ -29,10 +30,16 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, LockEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], LockEntity): """Representation of an Freedompro lock.""" - def __init__(self, hass, api_key, device, coordinator): + def __init__( + self, + hass: HomeAssistant, + api_key: str, + device: dict[str, Any], + coordinator: FreedomproDataUpdateCoordinator, + ) -> None: """Initialize the Freedompro lock.""" super().__init__(coordinator) self._hass = hass @@ -44,7 +51,7 @@ class Device(CoordinatorEntity, LockEntity): self._characteristics = device["characteristics"] self._attr_device_info = DeviceInfo( identifiers={ - (DOMAIN, self.unique_id), + (DOMAIN, device["uid"]), }, manufacturer="Freedompro", model=self._type, @@ -78,24 +85,22 @@ class Device(CoordinatorEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Async function to lock the lock.""" - payload_dict = {"lock": 1} - payload = json.dumps(payload_dict) + payload = {"lock": 1} await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() async def async_unlock(self, **kwargs: Any) -> None: """Async function to unlock the lock.""" - payload_dict = {"lock": 0} - payload = json.dumps(payload_dict) + payload = {"lock": 0} await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index 28ac82434d5..c5dc2a26bd0 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -1,4 +1,6 @@ """Support for Freedompro sensor.""" +from typing import Any + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -11,6 +13,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FreedomproDataUpdateCoordinator from .const import DOMAIN DEVICE_CLASS_MAP = { @@ -40,7 +43,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Freedompro sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( Device(device, coordinator) for device in coordinator.data @@ -48,10 +51,12 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, SensorEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], SensorEntity): """Representation of an Freedompro sensor.""" - def __init__(self, device, coordinator): + def __init__( + self, device: dict[str, Any], coordinator: FreedomproDataUpdateCoordinator + ) -> None: """Initialize the Freedompro sensor.""" super().__init__(coordinator) self._attr_name = device["name"] @@ -59,7 +64,7 @@ class Device(CoordinatorEntity, SensorEntity): self._type = device["type"] self._attr_device_info = DeviceInfo( identifiers={ - (DOMAIN, self.unique_id), + (DOMAIN, device["uid"]), }, manufacturer="Freedompro", model=device["type"], diff --git a/homeassistant/components/freedompro/switch.py b/homeassistant/components/freedompro/switch.py index ad5de21a7ab..4a7ed80de1e 100644 --- a/homeassistant/components/freedompro/switch.py +++ b/homeassistant/components/freedompro/switch.py @@ -1,5 +1,6 @@ """Support for Freedompro switch.""" import json +from typing import Any from pyfreedompro import put_state @@ -12,6 +13,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FreedomproDataUpdateCoordinator from .const import DOMAIN @@ -19,8 +21,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Freedompro switch.""" - api_key = entry.data[CONF_API_KEY] - coordinator = hass.data[DOMAIN][entry.entry_id] + api_key: str = entry.data[CONF_API_KEY] + coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data @@ -28,10 +30,16 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, SwitchEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], SwitchEntity): """Representation of an Freedompro switch.""" - def __init__(self, hass, api_key, device, coordinator): + def __init__( + self, + hass: HomeAssistant, + api_key: str, + device: dict[str, Any], + coordinator: FreedomproDataUpdateCoordinator, + ) -> None: """Initialize the Freedompro switch.""" super().__init__(coordinator) self._session = aiohttp_client.async_get_clientsession(hass) @@ -40,7 +48,7 @@ class Device(CoordinatorEntity, SwitchEntity): self._attr_unique_id = device["uid"] self._attr_device_info = DeviceInfo( identifiers={ - (DOMAIN, self.unique_id), + (DOMAIN, device["uid"]), }, manufacturer="Freedompro", model=device["type"], @@ -70,26 +78,24 @@ class Device(CoordinatorEntity, SwitchEntity): await super().async_added_to_hass() self._handle_coordinator_update() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Async function to set on to switch.""" payload = {"on": True} - payload = json.dumps(payload) await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Async function to set off to switch.""" payload = {"on": False} - payload = json.dumps(payload) await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() From 6c4290e418d4064c6cf80f4a3e32fa503627f3cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:48:31 +0200 Subject: [PATCH 649/903] Improve type hint in acmeda base entity (#77171) --- homeassistant/components/acmeda/base.py | 29 ++++++++++++++----------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index e9ffb94c6c6..dd1ab78345f 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -1,4 +1,6 @@ """Base class for Acmeda Roller Blinds.""" +from __future__ import annotations + import aiopulse from homeassistant.core import callback @@ -11,11 +13,13 @@ from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER class AcmedaBase(entity.Entity): """Base representation of an Acmeda roller.""" + _attr_should_poll = False + def __init__(self, roller: aiopulse.Roller) -> None: """Initialize the roller.""" self.roller = roller - async def async_remove_and_unregister(self): + async def async_remove_and_unregister(self) -> None: """Unregister from entity and device registry and call entity remove function.""" LOGGER.error("Removing %s %s", self.__class__.__name__, self.unique_id) @@ -25,14 +29,18 @@ class AcmedaBase(entity.Entity): dev_registry = dr.async_get(self.hass) device = dev_registry.async_get_device(identifiers={(DOMAIN, self.unique_id)}) - if device is not None: + if ( + device is not None + and self.registry_entry is not None + and self.registry_entry.config_entry_id is not None + ): dev_registry.async_update_device( device.id, remove_config_entry_id=self.registry_entry.config_entry_id ) await self.async_remove(force_remove=True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Entity has been added to hass.""" self.roller.callback_subscribe(self.notify_update) @@ -44,33 +52,28 @@ class AcmedaBase(entity.Entity): ) ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" self.roller.callback_unsubscribe(self.notify_update) @callback - def notify_update(self): + def notify_update(self) -> None: """Write updated device state information.""" LOGGER.debug("Device update notification received: %s", self.name) self.async_write_ha_state() @property - def should_poll(self): - """Report that Acmeda entities do not need polling.""" - return False - - @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of this roller.""" return self.roller.id @property - def device_id(self): + def device_id(self) -> str: """Return the ID of this roller.""" return self.roller.id @property - def name(self): + def name(self) -> str | None: """Return the name of roller.""" return self.roller.name From 563c956539843a4efed15db90429b0b20673066c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:50:47 +0200 Subject: [PATCH 650/903] Improve type hint in everlights light entity (#77139) --- homeassistant/components/everlights/light.py | 80 +++++++------------- 1 file changed, 27 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 13016ba6fe0..3ef4627c089 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -79,63 +79,35 @@ class EverLightsLight(LightEntity): _attr_supported_color_modes = {ColorMode.HS} _attr_supported_features = LightEntityFeature.EFFECT - def __init__(self, api, channel, status, effects): + def __init__( + self, + api: pyeverlights.EverLights, + channel: int, + status: dict[str, Any], + effects, + ) -> None: """Initialize the light.""" self._api = api self._channel = channel self._status = status - self._effects = effects + self._attr_effect_list = effects self._mac = status["mac"] self._error_reported = False - self._hs_color = [255, 255] - self._brightness = 255 - self._effect = None - self._available = True + self._attr_hs_color = (255, 255) + self._attr_brightness = 255 + + self._attr_name = f"EverLights {self._mac} Zone {self._channel}" + self._attr_unique_id = f"{self._mac}-{self._channel}" @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._mac}-{self._channel}" - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - @property - def name(self): - """Return the name of the device.""" - return f"EverLights {self._mac} Zone {self._channel}" - - @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._status[f"ch{self._channel}Active"] == 1 - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def hs_color(self): - """Return the color property.""" - return self._hs_color - - @property - def effect(self): - """Return the effect property.""" - return self._effect - - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._effects - - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - hs_color = kwargs.get(ATTR_HS_COLOR, self._hs_color) - brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) + hs_color = kwargs.get(ATTR_HS_COLOR, self._attr_hs_color) + brightness = kwargs.get(ATTR_BRIGHTNESS, self._attr_brightness) effect = kwargs.get(ATTR_EFFECT) if effect is not None: @@ -147,14 +119,16 @@ class EverLightsLight(LightEntity): brightness = hsv[2] / 100 * 255 else: - rgb = color_util.color_hsv_to_RGB(*hs_color, brightness / 255 * 100) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100 + ) colors = [color_rgb_to_int(*rgb)] await self._api.set_pattern(self._channel, colors) - self._hs_color = hs_color - self._brightness = brightness - self._effect = effect + self._attr_hs_color = hs_color + self._attr_brightness = brightness + self._attr_effect = effect async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" @@ -165,10 +139,10 @@ class EverLightsLight(LightEntity): try: self._status = await self._api.get_status() except pyeverlights.ConnectionError: - if self._available: + if self.available: _LOGGER.warning("EverLights control box connection lost") - self._available = False + self._attr_available = False else: - if not self._available: + if not self.available: _LOGGER.warning("EverLights control box connection restored") - self._available = True + self._attr_available = True From 79bdc02829f9ca71d83983425af074f3696735cd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:52:05 +0200 Subject: [PATCH 651/903] Improve esphome state property decorator typing (#77152) --- homeassistant/components/esphome/__init__.py | 15 +++++++-------- homeassistant/components/esphome/climate.py | 13 +++++++++---- homeassistant/components/esphome/cover.py | 9 +++++---- homeassistant/components/esphome/fan.py | 8 ++++---- homeassistant/components/esphome/light.py | 12 ++++++++---- homeassistant/components/esphome/lock.py | 8 ++++---- homeassistant/components/esphome/media_player.py | 7 +++---- homeassistant/components/esphome/number.py | 5 +---- homeassistant/components/esphome/select.py | 5 +---- homeassistant/components/esphome/sensor.py | 6 ++---- homeassistant/components/esphome/switch.py | 5 +---- 11 files changed, 45 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index ef8808288f3..2a885ed90ec 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -60,6 +60,7 @@ from .entry_data import RuntimeEntryData DOMAIN = "esphome" CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) +_R = TypeVar("_R") _DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") STORAGE_VERSION = 1 @@ -599,20 +600,18 @@ async def platform_async_setup_entry( ) -_PropT = TypeVar("_PropT", bound=Callable[..., Any]) - - -def esphome_state_property(func: _PropT) -> _PropT: +def esphome_state_property( + func: Callable[[_EntityT], _R] +) -> Callable[[_EntityT], _R | None]: """Wrap a state property of an esphome entity. This checks if the state object in the entity is set, and prevents writing NAN values to the Home Assistant state machine. """ - @property # type: ignore[misc] @functools.wraps(func) - def _wrapper(self): # type: ignore[no-untyped-def] - # pylint: disable=protected-access + def _wrapper(self: _EntityT) -> _R | None: + # pylint: disable-next=protected-access if not self._has_state: return None val = func(self) @@ -622,7 +621,7 @@ def esphome_state_property(func: _PropT) -> _PropT: return None return val - return cast(_PropT, _wrapper) + return _wrapper _EnumT = TypeVar("_EnumT", bound=APIIntEnum) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 3c0a80e780f..1552bd3775b 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -133,10 +133,6 @@ _PRESETS: EsphomeEnumMapper[ClimatePreset, str] = EsphomeEnumMapper( ) -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEntity): """A climate implementation for ESPHome.""" @@ -219,11 +215,13 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti features |= ClimateEntityFeature.SWING_MODE return features + @property # type: ignore[misc] @esphome_state_property def hvac_mode(self) -> str | None: """Return current operation ie. heat, cool, idle.""" return _CLIMATE_MODES.from_esphome(self._state.mode) + @property # type: ignore[misc] @esphome_state_property def hvac_action(self) -> str | None: """Return current action.""" @@ -232,6 +230,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti return None return _CLIMATE_ACTIONS.from_esphome(self._state.action) + @property # type: ignore[misc] @esphome_state_property def fan_mode(self) -> str | None: """Return current fan setting.""" @@ -239,6 +238,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti self._state.fan_mode ) + @property # type: ignore[misc] @esphome_state_property def preset_mode(self) -> str | None: """Return current preset mode.""" @@ -246,26 +246,31 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti self._state.preset_compat(self._api_version) ) + @property # type: ignore[misc] @esphome_state_property def swing_mode(self) -> str | None: """Return current swing mode.""" return _SWING_MODES.from_esphome(self._state.swing_mode) + @property # type: ignore[misc] @esphome_state_property def current_temperature(self) -> float | None: """Return the current temperature.""" return self._state.current_temperature + @property # type: ignore[misc] @esphome_state_property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._state.target_temperature + @property # type: ignore[misc] @esphome_state_property def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return self._state.target_temperature_low + @property # type: ignore[misc] @esphome_state_property def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 97ae22dcccc..bf179ff25a9 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -34,10 +34,6 @@ async def async_setup_entry( ) -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """A cover implementation for ESPHome.""" @@ -69,22 +65,26 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """Return true if we do optimistic updates.""" return self._static_info.assumed_state + @property # type: ignore[misc] @esphome_state_property def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" # Check closed state with api version due to a protocol change return self._state.is_closed(self._api_version) + @property # type: ignore[misc] @esphome_state_property def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._state.current_operation == CoverOperation.IS_OPENING + @property # type: ignore[misc] @esphome_state_property def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._state.current_operation == CoverOperation.IS_CLOSING + @property # type: ignore[misc] @esphome_state_property def current_cover_position(self) -> int | None: """Return current position of cover. 0 is closed, 100 is open.""" @@ -92,6 +92,7 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): return None return round(self._state.position * 100.0) + @property # type: ignore[misc] @esphome_state_property def current_cover_tilt_position(self) -> int | None: """Return current position of cover tilt. 0 is closed, 100 is open.""" diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 41d7e418673..207b9d3f9f8 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -55,10 +55,6 @@ _FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection, str] = EsphomeEnumMapper( ) -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): """A fan implementation for ESPHome.""" @@ -116,11 +112,13 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): key=self._static_info.key, direction=_FAN_DIRECTIONS.from_hass(direction) ) + @property # type: ignore[misc] @esphome_state_property def is_on(self) -> bool | None: """Return true if the entity is on.""" return self._state.state + @property # type: ignore[misc] @esphome_state_property def percentage(self) -> int | None: """Return the current speed percentage.""" @@ -143,6 +141,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): return len(ORDERED_NAMED_FAN_SPEEDS) return self._static_info.supported_speed_levels + @property # type: ignore[misc] @esphome_state_property def oscillating(self) -> bool | None: """Return the oscillation state.""" @@ -150,6 +149,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): return None return self._state.oscillating + @property # type: ignore[misc] @esphome_state_property def current_direction(self) -> str | None: """Return the current fan direction.""" diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 3eb45d63cac..2f536e82b47 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -122,10 +122,6 @@ def _filter_color_modes( return [mode for mode in supported if mode & features] -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """A light implementation for ESPHome.""" @@ -134,6 +130,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """Return whether the client supports the new color mode system natively.""" return self._api_version >= APIVersion(1, 6) + @property # type: ignore[misc] @esphome_state_property def is_on(self) -> bool | None: """Return true if the light is on.""" @@ -263,11 +260,13 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): data["transition_length"] = kwargs[ATTR_TRANSITION] await self._client.light_command(**data) + @property # type: ignore[misc] @esphome_state_property def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" return round(self._state.brightness * 255) + @property # type: ignore[misc] @esphome_state_property def color_mode(self) -> str | None: """Return the color mode of the light.""" @@ -278,6 +277,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): return _color_mode_to_ha(self._state.color_mode) + @property # type: ignore[misc] @esphome_state_property def rgb_color(self) -> tuple[int, int, int] | None: """Return the rgb color value [int, int, int].""" @@ -294,6 +294,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): round(self._state.blue * self._state.color_brightness * 255), ) + @property # type: ignore[misc] @esphome_state_property def rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the rgbw color value [int, int, int, int].""" @@ -301,6 +302,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): rgb = cast("tuple[int, int, int]", self.rgb_color) return (*rgb, white) + @property # type: ignore[misc] @esphome_state_property def rgbww_color(self) -> tuple[int, int, int, int, int] | None: """Return the rgbww color value [int, int, int, int, int].""" @@ -328,11 +330,13 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): round(self._state.warm_white * 255), ) + @property # type: ignore[misc] @esphome_state_property def color_temp(self) -> float | None: # type: ignore[override] """Return the CT color value in mireds.""" return self._state.color_temperature + @property # type: ignore[misc] @esphome_state_property def effect(self) -> str | None: """Return the current effect.""" diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index a85411b5744..62c7c6de0dd 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -29,10 +29,6 @@ async def async_setup_entry( ) -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): """A lock implementation for ESPHome.""" @@ -53,21 +49,25 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): return self._static_info.code_format return None + @property # type: ignore[misc] @esphome_state_property def is_locked(self) -> bool | None: """Return true if the lock is locked.""" return self._state.state == LockState.LOCKED + @property # type: ignore[misc] @esphome_state_property def is_locking(self) -> bool | None: """Return true if the lock is locking.""" return self._state.state == LockState.LOCKING + @property # type: ignore[misc] @esphome_state_property def is_unlocking(self) -> bool | None: """Return true if the lock is unlocking.""" return self._state.state == LockState.UNLOCKING + @property # type: ignore[misc] @esphome_state_property def is_jammed(self) -> bool | None: """Return true if the lock is jammed (incomplete locking).""" diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index d7ce73976e7..17635157754 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -59,10 +59,6 @@ _STATES: EsphomeEnumMapper[MediaPlayerState, str] = EsphomeEnumMapper( ) -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - class EsphomeMediaPlayer( EsphomeEntity[MediaPlayerInfo, MediaPlayerEntityState], MediaPlayerEntity ): @@ -70,16 +66,19 @@ class EsphomeMediaPlayer( _attr_device_class = MediaPlayerDeviceClass.SPEAKER + @property # type: ignore[misc] @esphome_state_property def state(self) -> str | None: """Return current state.""" return _STATES.from_esphome(self._state.state) + @property # type: ignore[misc] @esphome_state_property def is_volume_muted(self) -> bool: """Return true if volume is muted.""" return self._state.muted + @property # type: ignore[misc] @esphome_state_property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index bbca463a908..ed721f2db5e 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -44,10 +44,6 @@ NUMBER_MODES: EsphomeEnumMapper[EsphomeNumberMode, NumberMode] = EsphomeEnumMapp ) -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): """A number implementation for esphome.""" @@ -78,6 +74,7 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): return NUMBER_MODES.from_esphome(self._static_info.mode) return NumberMode.AUTO + @property # type: ignore[misc] @esphome_state_property def native_value(self) -> float | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index f3bfcb982ea..190fca52889 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -28,10 +28,6 @@ async def async_setup_entry( ) -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity): """A select implementation for esphome.""" @@ -40,6 +36,7 @@ class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity): """Return a set of selectable options.""" return self._static_info.options + @property # type: ignore[misc] @esphome_state_property def current_option(self) -> str | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 897ab86b18a..f7f137f4592 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -56,10 +56,6 @@ async def async_setup_entry( ) -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - _STATE_CLASSES: EsphomeEnumMapper[ EsphomeSensorStateClass, SensorStateClass | None ] = EsphomeEnumMapper( @@ -80,6 +76,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): """Return if this sensor should force a state update.""" return self._static_info.force_update + @property # type: ignore[misc] @esphome_state_property def native_value(self) -> datetime | str | None: """Return the state of the entity.""" @@ -124,6 +121,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity): """A text sensor implementation for ESPHome.""" + @property # type: ignore[misc] @esphome_state_property def native_value(self) -> str | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index fbb22bb7397..2970edf7af0 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -28,10 +28,6 @@ async def async_setup_entry( ) -# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property -# pylint: disable=invalid-overridden-method - - class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): """A switch implementation for ESPHome.""" @@ -40,6 +36,7 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): """Return true if we do optimistic updates.""" return self._static_info.assumed_state + @property # type: ignore[misc] @esphome_state_property def is_on(self) -> bool | None: """Return true if the switch is on.""" From c8400261983412abd87b2192c613c2829778cfde Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:52:57 +0200 Subject: [PATCH 652/903] Improve type hint in fibaro climate entity (#77153) --- homeassistant/components/fibaro/climate.py | 83 +++++++--------------- 1 file changed, 25 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index a2d322528db..94dd599698d 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -121,15 +121,12 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): def __init__(self, fibaro_device): """Initialize the Fibaro device.""" super().__init__(fibaro_device) - self._temp_sensor_device = None - self._target_temp_device = None - self._op_mode_device = None - self._fan_mode_device = None - self._support_flags = 0 + self._temp_sensor_device: FibaroDevice | None = None + self._target_temp_device: FibaroDevice | None = None + self._op_mode_device: FibaroDevice | None = None + self._fan_mode_device: FibaroDevice | None = None + self._attr_supported_features = 0 self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - self._hvac_support = [] - self._preset_support = [] - self._fan_support = [] siblings = fibaro_device.fibaro_controller.get_siblings(fibaro_device) _LOGGER.debug("%s siblings: %s", fibaro_device.ha_id, siblings) @@ -158,34 +155,38 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): or "setHeatingThermostatSetpoint" in device.actions ): self._target_temp_device = FibaroDevice(device) - self._support_flags |= ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE tempunit = device.properties.unit if "setMode" in device.actions or "setOperatingMode" in device.actions: self._op_mode_device = FibaroDevice(device) - self._support_flags |= ClimateEntityFeature.PRESET_MODE + self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE if "setFanMode" in device.actions: self._fan_mode_device = FibaroDevice(device) - self._support_flags |= ClimateEntityFeature.FAN_MODE + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE if tempunit == "F": - self._unit_of_temp = TEMP_FAHRENHEIT + self._attr_temperature_unit = TEMP_FAHRENHEIT else: - self._unit_of_temp = TEMP_CELSIUS + self._attr_temperature_unit = TEMP_CELSIUS if self._fan_mode_device: fan_modes = ( self._fan_mode_device.fibaro_device.properties.supportedModes.split(",") ) + self._attr_fan_modes = [] for mode in fan_modes: mode = int(mode) if mode not in FANMODES: _LOGGER.warning("%d unknown fan mode", mode) continue - self._fan_support.append(FANMODES[int(mode)]) + self._attr_fan_modes.append(FANMODES[int(mode)]) + self._attr_hvac_modes = [HVACMode.AUTO] # default if self._op_mode_device: + self._attr_preset_modes = [] + self._attr_hvac_modes = [] prop = self._op_mode_device.fibaro_device.properties if "supportedOperatingModes" in prop: op_modes = prop.supportedOperatingModes.split(",") @@ -195,10 +196,10 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): mode = int(mode) if mode in OPMODES_HVAC: mode_ha = OPMODES_HVAC[mode] - if mode_ha not in self._hvac_support: - self._hvac_support.append(mode_ha) + if mode_ha not in self._attr_hvac_modes: + self._attr_hvac_modes.append(mode_ha) if mode in OPMODES_PRESET: - self._preset_support.append(OPMODES_PRESET[mode]) + self._attr_preset_modes.append(OPMODES_PRESET[mode]) async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" @@ -223,19 +224,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self.controller.register(device.id, self._update_callback) @property - def supported_features(self): - """Return the list of supported features.""" - return self._support_flags - - @property - def fan_modes(self): - """Return the list of available fan modes.""" - if not self._fan_mode_device: - return None - return self._fan_support - - @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the fan setting.""" if not self._fan_mode_device: return None @@ -249,7 +238,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._fan_mode_device.action("setFanMode", HA_FANMODES[fan_mode]) @property - def fibaro_op_mode(self): + def fibaro_op_mode(self) -> int: """Return the operating mode of the device.""" if not self._op_mode_device: return 3 # Default to AUTO @@ -260,17 +249,10 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): return int(self._op_mode_device.fibaro_device.properties.mode) @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode: """Return current operation ie. heat, cool, idle.""" return OPMODES_HVAC[self.fibaro_op_mode] - @property - def hvac_modes(self): - """Return the list of available operation modes.""" - if not self._op_mode_device: - return [HVACMode.AUTO] # Default to this - return self._hvac_support - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target operation mode.""" if not self._op_mode_device: @@ -284,7 +266,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._op_mode_device.action("setMode", HA_OPMODES_HVAC[hvac_mode]) @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp. Requires ClimateEntityFeature.PRESET_MODE. @@ -301,16 +283,6 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): return None return OPMODES_PRESET[mode] - @property - def preset_modes(self): - """Return a list of available preset modes. - - Requires ClimateEntityFeature.PRESET_MODE. - """ - if not self._op_mode_device: - return None - return self._preset_support - def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if self._op_mode_device is None: @@ -323,12 +295,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._op_mode_device.action("setMode", HA_OPMODES_PRESET[preset_mode]) @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._unit_of_temp - - @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" if self._temp_sensor_device: device = self._temp_sensor_device.fibaro_device @@ -338,7 +305,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): return None @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" if self._target_temp_device: device = self._target_temp_device.fibaro_device @@ -351,7 +318,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): """Set new target temperatures.""" temperature = kwargs.get(ATTR_TEMPERATURE) target = self._target_temp_device - if temperature is not None: + if target is not None and temperature is not None: if "setThermostatSetpoint" in target.fibaro_device.actions: target.action("setThermostatSetpoint", self.fibaro_op_mode, temperature) elif "setHeatingThermostatSetpoint" in target.fibaro_device.actions: From 0482d50d135e304f0a9719e010dfc45f3700b09c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:54:22 +0200 Subject: [PATCH 653/903] Improve type hint in frontier silicon media player (#77167) --- .../frontier_silicon/media_player.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 4c1e4390e61..87c7c4036f2 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -125,7 +125,7 @@ class AFSAPIDevice(MediaPlayerEntity): self._supports_sound_mode: bool = True - async def async_update(self): + async def async_update(self) -> None: """Get the latest date and update device state.""" afsapi = self.fs_device try: @@ -291,11 +291,19 @@ class AFSAPIDevice(MediaPlayerEntity): volume = int(volume * self._max_volume) await self.fs_device.set_volume(volume) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" await self.fs_device.set_power(True) - await self.fs_device.set_mode(self.__modes_by_label.get(source)) + if ( + self.__modes_by_label + and (mode := self.__modes_by_label.get(source)) is not None + ): + await self.fs_device.set_mode(mode) - async def async_select_sound_mode(self, sound_mode): + async def async_select_sound_mode(self, sound_mode: str) -> None: """Select EQ Preset.""" - await self.fs_device.set_eq_preset(self.__sound_modes_by_label[sound_mode]) + if ( + self.__sound_modes_by_label + and (mode := self.__sound_modes_by_label.get(sound_mode)) is not None + ): + await self.fs_device.set_eq_preset(mode) From ab6bb7cd9388957957dfa8b6f7b609ffb69455d3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:55:06 +0200 Subject: [PATCH 654/903] Fix issue with flexit fan mode (#77157) --- homeassistant/components/flexit/climate.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 3c5aca2928c..a7c230c316c 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -59,6 +59,7 @@ async def async_setup_platform( class Flexit(ClimateEntity): """Representation of a Flexit AC unit.""" + _attr_fan_modes = ["Off", "Low", "Medium", "High"] _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) @@ -72,8 +73,7 @@ class Flexit(ClimateEntity): self._slave = modbus_slave self._target_temperature = None self._current_temperature = None - self._current_fan_mode = None - self._fan_modes = ["Off", "Low", "Medium", "High"] + self._attr_fan_mode = None self._filter_hours = None self._filter_alarm = None self._heat_recovery = None @@ -92,8 +92,8 @@ class Flexit(ClimateEntity): CALL_TYPE_REGISTER_INPUT, 9 ) res = await self._async_read_int16_from_register(CALL_TYPE_REGISTER_HOLDING, 17) - if res < len(self._fan_modes): - self._current_fan_mode = res + if self.fan_modes and res < len(self.fan_modes): + self._attr_fan_mode = self.fan_modes[res] self._filter_hours = await self._async_read_int16_from_register( CALL_TYPE_REGISTER_INPUT, 8 ) @@ -187,16 +187,6 @@ class Flexit(ClimateEntity): """ return [HVACMode.COOL] - @property - def fan_mode(self): - """Return the fan setting.""" - return self._current_fan_mode - - @property - def fan_modes(self): - """Return the list of available fan modes.""" - return self._fan_modes - async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if kwargs.get(ATTR_TEMPERATURE) is not None: @@ -212,10 +202,10 @@ class Flexit(ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" - if await self._async_write_int16_to_register( + if self.fan_modes and await self._async_write_int16_to_register( 17, self.fan_modes.index(fan_mode) ): - self._current_fan_mode = self.fan_modes.index(fan_mode) + self._attr_fan_mode = fan_mode else: _LOGGER.error("Modbus error setting fan mode to Flexit") From b5f9f08aa86606f47967570c958ddc3e0904664b Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 26 Aug 2022 10:02:10 +0100 Subject: [PATCH 655/903] Use UUID identifier in System Bridge (#76921) --- homeassistant/components/system_bridge/__init__.py | 6 ++---- homeassistant/components/system_bridge/binary_sensor.py | 4 ++-- homeassistant/components/system_bridge/sensor.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 74be1faed40..392273dee45 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -286,6 +286,7 @@ class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]): f"http://{self._hostname}:{api_port}/app/settings.html" ) self._mac_address = coordinator.data.system.mac_address + self._uuid = coordinator.data.system.uuid self._version = coordinator.data.system.version @property @@ -298,16 +299,13 @@ class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]): """Return the name of the entity.""" return self._name - -class SystemBridgeDeviceEntity(SystemBridgeEntity): - """Defines a System Bridge device entity.""" - @property def device_info(self) -> DeviceInfo: """Return device information about this System Bridge instance.""" return DeviceInfo( configuration_url=self._configuration_url, connections={(dr.CONNECTION_NETWORK_MAC, self._mac_address)}, + identifiers={(DOMAIN, self._uuid)}, name=self._hostname, sw_version=self._version, ) diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index 9225aebf492..8feb1114285 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SystemBridgeDeviceEntity +from . import SystemBridgeEntity from .const import DOMAIN from .coordinator import SystemBridgeDataUpdateCoordinator @@ -72,7 +72,7 @@ async def async_setup_entry( async_add_entities(entities) -class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity): +class SystemBridgeBinarySensor(SystemBridgeEntity, BinarySensorEntity): """Define a System Bridge binary sensor.""" entity_description: SystemBridgeBinarySensorEntityDescription diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index bdfe5047e56..f9d6e09f0c6 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -29,7 +29,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utcnow -from . import SystemBridgeDeviceEntity +from . import SystemBridgeEntity from .const import DOMAIN from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator @@ -512,7 +512,7 @@ async def async_setup_entry( async_add_entities(entities) -class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity): +class SystemBridgeSensor(SystemBridgeEntity, SensorEntity): """Define a System Bridge sensor.""" entity_description: SystemBridgeSensorEntityDescription From 516dc3372fc937c6d13619fd06204eba6d4f8a5b Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 26 Aug 2022 11:04:09 +0200 Subject: [PATCH 656/903] Migrate BMW Connected Drive to new entity naming (#77045) Co-authored-by: rikroe --- .../bmw_connected_drive/__init__.py | 3 ++- .../bmw_connected_drive/binary_sensor.py | 6 ++---- .../components/bmw_connected_drive/button.py | 12 +++++------- .../bmw_connected_drive/device_tracker.py | 5 +++-- .../components/bmw_connected_drive/lock.py | 19 +++++++++++-------- .../components/bmw_connected_drive/sensor.py | 12 ++++++++++-- 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 959fbe04240..a47f2bed591 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -163,6 +163,7 @@ class BMWBaseEntity(CoordinatorEntity[BMWDataUpdateCoordinator]): coordinator: BMWDataUpdateCoordinator _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -182,7 +183,7 @@ class BMWBaseEntity(CoordinatorEntity[BMWDataUpdateCoordinator]): identifiers={(DOMAIN, self.vehicle.vin)}, manufacturer=vehicle.brand.name, model=vehicle.name, - name=f"{vehicle.brand.name} {vehicle.name}", + name=vehicle.name, ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 87506cc2230..2c8543bd72a 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -121,7 +121,7 @@ class BMWBinarySensorEntityDescription( SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( BMWBinarySensorEntityDescription( key="lids", - name="Doors", + name="Lids", device_class=BinarySensorDeviceClass.OPENING, icon="mdi:car-door-lock", # device class opening: On means open, Off means closed @@ -165,7 +165,7 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( ), BMWBinarySensorEntityDescription( key="check_control_messages", - name="Control messages", + name="Check control messages", device_class=BinarySensorDeviceClass.PROBLEM, icon="mdi:car-tire-alert", # device class problem: On means problem detected, Off means no problem @@ -224,8 +224,6 @@ class BMWBinarySensor(BMWBaseEntity, BinarySensorEntity): super().__init__(coordinator, vehicle) self.entity_description = description self._unit_system = unit_system - - self._attr_name = f"{vehicle.name} {description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}" @callback diff --git a/homeassistant/components/bmw_connected_drive/button.py b/homeassistant/components/bmw_connected_drive/button.py index baa7870ee8c..810edaf9617 100644 --- a/homeassistant/components/bmw_connected_drive/button.py +++ b/homeassistant/components/bmw_connected_drive/button.py @@ -38,31 +38,31 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = ( BMWButtonEntityDescription( key="light_flash", icon="mdi:car-light-alert", - name="Flash Lights", + name="Flash lights", remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_light_flash(), ), BMWButtonEntityDescription( key="sound_horn", icon="mdi:bullhorn", - name="Sound Horn", + name="Sound horn", remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_horn(), ), BMWButtonEntityDescription( key="activate_air_conditioning", icon="mdi:hvac", - name="Activate Air Conditioning", + name="Activate air conditioning", remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning(), ), BMWButtonEntityDescription( key="deactivate_air_conditioning", icon="mdi:hvac-off", - name="Deactivate Air Conditioning", + name="Deactivate air conditioning", remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning_stop(), ), BMWButtonEntityDescription( key="find_vehicle", icon="mdi:crosshairs-question", - name="Find Vehicle", + name="Find vehicle", remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_vehicle_finder(), ), BMWButtonEntityDescription( @@ -112,8 +112,6 @@ class BMWButton(BMWBaseEntity, ButtonEntity): """Initialize BMW vehicle sensor.""" super().__init__(coordinator, vehicle) self.entity_description = description - - self._attr_name = f"{vehicle.name} {description.name}" self._attr_unique_id = f"{vehicle.vin}-{description.key}" async def async_press(self) -> None: diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index c06ecdaa9bb..d26a63f8c0e 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from bimmer_connected.vehicle import MyBMWVehicle @@ -53,10 +54,10 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): super().__init__(coordinator, vehicle) self._attr_unique_id = vehicle.vin - self._attr_name = vehicle.name + self._attr_name = None @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Return entity specific state attributes.""" return {**self._attrs, ATTR_DIRECTION: self.vehicle.vehicle_location.heading} diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 0c2c5a1e832..c9198437e2f 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -7,7 +7,7 @@ from typing import Any from bimmer_connected.vehicle import MyBMWVehicle from bimmer_connected.vehicle.doors_windows import LockState -from homeassistant.components.lock import LockEntity +from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -32,7 +32,13 @@ async def async_setup_entry( for vehicle in coordinator.account.vehicles: if not coordinator.read_only: - entities.append(BMWLock(coordinator, vehicle, "lock", "BMW lock")) + entities.append( + BMWLock( + coordinator, + vehicle, + LockEntityDescription(key="lock", device_class="lock", name="Lock"), + ) + ) async_add_entities(entities) @@ -43,16 +49,13 @@ class BMWLock(BMWBaseEntity, LockEntity): self, coordinator: BMWDataUpdateCoordinator, vehicle: MyBMWVehicle, - attribute: str, - sensor_name: str, + description: LockEntityDescription, ) -> None: """Initialize the lock.""" super().__init__(coordinator, vehicle) - self._attribute = attribute - self._attr_name = f"{vehicle.name} {attribute}" - self._attr_unique_id = f"{vehicle.vin}-{attribute}" - self._sensor_name = sensor_name + self.entity_description = description + self._attr_unique_id = f"{vehicle.vin}-{description.key}" self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes async def async_lock(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index ae3dc0bb8b9..7de0f40e86b 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -56,23 +56,27 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { # --- Generic --- "charging_start_time": BMWSensorEntityDescription( key="charging_start_time", + name="Charging start time", key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, ), "charging_end_time": BMWSensorEntityDescription( key="charging_end_time", + name="Charging end time", key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, ), "charging_status": BMWSensorEntityDescription( key="charging_status", + name="Charging status", key_class="fuel_and_battery", icon="mdi:ev-station", value=lambda x, y: x.value, ), "remaining_battery_percent": BMWSensorEntityDescription( key="remaining_battery_percent", + name="Remaining battery percent", key_class="fuel_and_battery", unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, @@ -80,12 +84,14 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { # --- Specific --- "mileage": BMWSensorEntityDescription( key="mileage", + name="Mileage", icon="mdi:speedometer", unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_total": BMWSensorEntityDescription( key="remaining_range_total", + name="Remaining range total", key_class="fuel_and_battery", icon="mdi:map-marker-distance", unit_type=LENGTH, @@ -93,6 +99,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "remaining_range_electric": BMWSensorEntityDescription( key="remaining_range_electric", + name="Remaining range electric", key_class="fuel_and_battery", icon="mdi:map-marker-distance", unit_type=LENGTH, @@ -100,6 +107,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "remaining_range_fuel": BMWSensorEntityDescription( key="remaining_range_fuel", + name="Remaining range fuel", key_class="fuel_and_battery", icon="mdi:map-marker-distance", unit_type=LENGTH, @@ -107,6 +115,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "remaining_fuel": BMWSensorEntityDescription( key="remaining_fuel", + name="Remaining fuel", key_class="fuel_and_battery", icon="mdi:gas-station", unit_type=VOLUME, @@ -114,6 +123,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "remaining_fuel_percent": BMWSensorEntityDescription( key="remaining_fuel_percent", + name="Remaining fuel percent", key_class="fuel_and_battery", icon="mdi:gas-station", unit_type=PERCENTAGE, @@ -159,8 +169,6 @@ class BMWSensor(BMWBaseEntity, SensorEntity): """Initialize BMW vehicle sensor.""" super().__init__(coordinator, vehicle) self.entity_description = description - - self._attr_name = f"{vehicle.name} {description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}" # Set the correct unit of measurement based on the unit_type From 94cd8e801b8be60d8dc58e943fc6a59353aa56f2 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 26 Aug 2022 19:04:38 +1000 Subject: [PATCH 657/903] Fix attributes scope in Advantage Air Select platform (#76744) --- homeassistant/components/advantage_air/select.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index 97ec0f9705c..f6c46cb7f87 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -29,15 +29,15 @@ class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): """Representation of Advantage Air MyZone control.""" _attr_icon = "mdi:home-thermometer" - _attr_options = [ADVANTAGE_AIR_INACTIVE] - _number_to_name = {0: ADVANTAGE_AIR_INACTIVE} - _name_to_number = {ADVANTAGE_AIR_INACTIVE: 0} _attr_name = "MyZone" def __init__(self, instance, ac_key): """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) self._attr_unique_id += "-myzone" + self._attr_options = [ADVANTAGE_AIR_INACTIVE] + self._number_to_name = {0: ADVANTAGE_AIR_INACTIVE} + self._name_to_number = {ADVANTAGE_AIR_INACTIVE: 0} for zone in instance["coordinator"].data["aircons"][ac_key]["zones"].values(): if zone["type"] > 0: From 452ee0284ac1e69945a488b552243b30bea7218b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 11:34:38 +0200 Subject: [PATCH 658/903] Improve type hints in demo [2/3] (#77185) --- homeassistant/components/demo/camera.py | 2 +- homeassistant/components/demo/config_flow.py | 20 +++++++++---- .../components/demo/device_tracker.py | 4 +-- homeassistant/components/demo/mailbox.py | 17 ++++++----- homeassistant/components/demo/notify.py | 18 ++++++++--- homeassistant/components/demo/stt.py | 8 ++++- homeassistant/components/demo/tts.py | 30 ++++++++++++++----- homeassistant/components/demo/water_heater.py | 16 ++++++---- 8 files changed, 81 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index b8e0a714253..b55fb4ba0e9 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -41,7 +41,7 @@ class DemoCamera(Camera): _attr_motion_detection_enabled = False _attr_supported_features = CameraEntityFeature.ON_OFF - def __init__(self, name, content_type): + def __init__(self, name: str, content_type: str) -> None: """Initialize demo camera component.""" super().__init__() self._attr_name = name diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index 0163123b578..75439e48c08 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure demo component.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -30,9 +32,9 @@ class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, import_info) -> FlowResult: + async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: """Set the config entry up from yaml.""" - return self.async_create_entry(title="Demo", data={}) + return self.async_create_entry(title="Demo", data=import_info) class OptionsFlowHandler(config_entries.OptionsFlow): @@ -43,11 +45,15 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.config_entry = config_entry self.options = dict(config_entry.options) - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" return await self.async_step_options_1() - async def async_step_options_1(self, user_input=None) -> FlowResult: + async def async_step_options_1( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" if user_input is not None: self.options.update(user_input) @@ -70,7 +76,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def async_step_options_2(self, user_input=None) -> FlowResult: + async def async_step_options_2( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options 2.""" if user_input is not None: self.options.update(user_input) @@ -101,6 +109,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def _update_options(self): + async def _update_options(self) -> FlowResult: """Update config entry options.""" return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/demo/device_tracker.py b/homeassistant/components/demo/device_tracker.py index dacbd95219b..de387545368 100644 --- a/homeassistant/components/demo/device_tracker.py +++ b/homeassistant/components/demo/device_tracker.py @@ -18,11 +18,11 @@ def setup_scanner( ) -> bool: """Set up the demo tracker.""" - def offset(): + def offset() -> float: """Return random offset.""" return (random.randrange(500, 2000)) / 2e5 * random.choice((-1, 1)) - def random_see(dev_id, name): + def random_see(dev_id: str, name: str) -> None: """Randomize a sighting.""" see( dev_id=dev_id, diff --git a/homeassistant/components/demo/mailbox.py b/homeassistant/components/demo/mailbox.py index bc5467faada..8a7df70df80 100644 --- a/homeassistant/components/demo/mailbox.py +++ b/homeassistant/components/demo/mailbox.py @@ -4,6 +4,7 @@ from __future__ import annotations from hashlib import sha1 import logging import os +from typing import Any from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError from homeassistant.core import HomeAssistant @@ -27,10 +28,10 @@ async def async_get_handler( class DemoMailbox(Mailbox): """Demo Mailbox.""" - def __init__(self, hass, name): + def __init__(self, hass: HomeAssistant, name: str) -> None: """Initialize Demo mailbox.""" super().__init__(hass, name) - self._messages = {} + self._messages: dict[str, dict[str, Any]] = {} txt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " for idx in range(0, 10): msgtime = int(dt.as_timestamp(dt.utcnow()) - 3600 * 24 * (10 - idx)) @@ -48,21 +49,21 @@ class DemoMailbox(Mailbox): self._messages[msgsha] = msg @property - def media_type(self): + def media_type(self) -> str: """Return the supported media type.""" return CONTENT_TYPE_MPEG @property - def can_delete(self): + def can_delete(self) -> bool: """Return if messages can be deleted.""" return True @property - def has_media(self): + def has_media(self) -> bool: """Return if messages have attached media files.""" return True - async def async_get_media(self, msgid): + async def async_get_media(self, msgid: str) -> bytes: """Return the media blob for the msgid.""" if msgid not in self._messages: raise StreamError("Message not found") @@ -71,7 +72,7 @@ class DemoMailbox(Mailbox): with open(audio_path, "rb") as file: return file.read() - async def async_get_messages(self): + async def async_get_messages(self) -> list[dict[str, Any]]: """Return a list of the current messages.""" return sorted( self._messages.values(), @@ -79,7 +80,7 @@ class DemoMailbox(Mailbox): reverse=True, ) - async def async_delete(self, msgid): + async def async_delete(self, msgid: str) -> bool: """Delete the specified messages.""" if msgid in self._messages: _LOGGER.info("Deleting: %s", msgid) diff --git a/homeassistant/components/demo/notify.py b/homeassistant/components/demo/notify.py index f390c042ce4..3d614d9abf0 100644 --- a/homeassistant/components/demo/notify.py +++ b/homeassistant/components/demo/notify.py @@ -1,10 +1,20 @@ """Demo notification service.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.notify import BaseNotificationService +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType EVENT_NOTIFY = "notify" -def get_service(hass, config, discovery_info=None): +def get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> BaseNotificationService: """Get the demo notification service.""" return DemoNotificationService(hass) @@ -12,16 +22,16 @@ def get_service(hass, config, discovery_info=None): class DemoNotificationService(BaseNotificationService): """Implement demo notification service.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize the service.""" self.hass = hass @property - def targets(self): + def targets(self) -> dict[str, str]: """Return a dictionary of registered targets.""" return {"test target name": "test target id"} - def send_message(self, message="", **kwargs): + def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a user.""" kwargs["message"] = message self.hass.bus.fire(EVENT_NOTIFY, kwargs) diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py index 0497e2335d3..c035021b5ee 100644 --- a/homeassistant/components/demo/stt.py +++ b/homeassistant/components/demo/stt.py @@ -12,11 +12,17 @@ from homeassistant.components.stt.const import ( AudioSampleRates, SpeechResultState, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType SUPPORT_LANGUAGES = ["en", "de"] -async def async_get_engine(hass, config, discovery_info=None): +async def async_get_engine( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> Provider: """Set up Demo speech component.""" return DemoProvider() diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index d3519cd6465..2c9cd654d84 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -1,9 +1,19 @@ """Support for the demo for text to speech service.""" +from __future__ import annotations + import os +from typing import Any import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA, + Provider, + TtsAudioType, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType SUPPORT_LANGUAGES = ["en", "de"] @@ -14,7 +24,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_engine(hass, config, discovery_info=None): +def get_engine( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> Provider: """Set up Demo speech component.""" return DemoProvider(config.get(CONF_LANG, DEFAULT_LANG)) @@ -22,27 +36,29 @@ def get_engine(hass, config, discovery_info=None): class DemoProvider(Provider): """Demo speech API provider.""" - def __init__(self, lang): + def __init__(self, lang: str) -> None: """Initialize demo provider.""" self._lang = lang self.name = "Demo" @property - def default_language(self): + def default_language(self) -> str: """Return the default language.""" return self._lang @property - def supported_languages(self): + def supported_languages(self) -> list[str]: """Return list of supported languages.""" return SUPPORT_LANGUAGES @property - def supported_options(self): + def supported_options(self) -> list[str]: """Return list of supported options like voice, emotions.""" return ["voice", "age"] - def get_tts_audio(self, message, language, options=None): + def get_tts_audio( + self, message: str, language: str, options: dict[str, Any] | None = None + ) -> TtsAudioType: """Load TTS from demo.""" filename = os.path.join(os.path.dirname(__file__), "tts.mp3") try: diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index c3cb2be4fb3..322abb0038b 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -51,21 +51,27 @@ class DemoWaterHeater(WaterHeaterEntity): _attr_supported_features = SUPPORT_FLAGS_HEATER def __init__( - self, name, target_temperature, unit_of_measurement, away, current_operation - ): + self, + name: str, + target_temperature: int, + unit_of_measurement: str, + away: bool, + current_operation: str, + ) -> None: """Initialize the water_heater device.""" self._attr_name = name if target_temperature is not None: self._attr_supported_features = ( - self.supported_features | WaterHeaterEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features + | WaterHeaterEntityFeature.TARGET_TEMPERATURE ) if away is not None: self._attr_supported_features = ( - self.supported_features | WaterHeaterEntityFeature.AWAY_MODE + self._attr_supported_features | WaterHeaterEntityFeature.AWAY_MODE ) if current_operation is not None: self._attr_supported_features = ( - self.supported_features | WaterHeaterEntityFeature.OPERATION_MODE + self._attr_supported_features | WaterHeaterEntityFeature.OPERATION_MODE ) self._attr_target_temperature = target_temperature self._attr_temperature_unit = unit_of_measurement From b043053aadab57435c768d1e0fe3892367686999 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 11:37:12 +0200 Subject: [PATCH 659/903] Improve entity type hints [g] (#77145) --- .../components/gc100/binary_sensor.py | 2 +- homeassistant/components/gc100/switch.py | 8 +++++--- homeassistant/components/gdacs/sensor.py | 4 ++-- .../components/generic_thermostat/climate.py | 7 ++++--- homeassistant/components/geniushub/switch.py | 5 +++-- .../components/geniushub/water_heater.py | 2 +- .../components/geo_rss_events/sensor.py | 2 +- .../components/geofency/device_tracker.py | 4 ++-- .../components/geonetnz_quakes/sensor.py | 4 ++-- .../components/geonetnz_volcano/sensor.py | 4 ++-- homeassistant/components/gitlab_ci/sensor.py | 2 +- homeassistant/components/gitter/sensor.py | 2 +- homeassistant/components/glances/sensor.py | 8 ++++---- .../components/google_travel_time/sensor.py | 2 +- homeassistant/components/google_wifi/sensor.py | 11 ++++++++--- .../components/gpslogger/device_tracker.py | 4 ++-- homeassistant/components/gree/climate.py | 9 +++++---- homeassistant/components/gree/switch.py | 18 ++++++++++-------- .../components/growatt_server/sensor.py | 2 +- .../components/gstreamer/media_player.py | 17 ++++++++++------- 20 files changed, 66 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/gc100/binary_sensor.py b/homeassistant/components/gc100/binary_sensor.py index f93076196a3..e750f928cf7 100644 --- a/homeassistant/components/gc100/binary_sensor.py +++ b/homeassistant/components/gc100/binary_sensor.py @@ -59,7 +59,7 @@ class GC100BinarySensor(BinarySensorEntity): """Return the state of the entity.""" return self._state - def update(self): + def update(self) -> None: """Update the sensor state.""" self._gc100.read_sensor(self._port_addr, self.set_state) diff --git a/homeassistant/components/gc100/switch.py b/homeassistant/components/gc100/switch.py index 3372203231f..d88b4c9fa79 100644 --- a/homeassistant/components/gc100/switch.py +++ b/homeassistant/components/gc100/switch.py @@ -1,6 +1,8 @@ """Support for switches using GC100.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -54,15 +56,15 @@ class GC100Switch(SwitchEntity): """Return the state of the entity.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._gc100.write_switch(self._port_addr, 1, self.set_state) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._gc100.write_switch(self._port_addr, 0, self.set_state) - def update(self): + def update(self) -> None: """Update the sensor state.""" self._gc100.read_sensor(self._port_addr, self.set_state) diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index c5d8e76b289..531eb05dcf9 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -59,7 +59,7 @@ class GdacsSensor(SensorEntity): self._removed = None self._remove_signal_status = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_status = async_dispatcher_connect( self.hass, @@ -81,7 +81,7 @@ class GdacsSensor(SensorEntity): _LOGGER.debug("Received status update for %s", self._config_entry_id) self.async_schedule_update_ha_state(True) - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._config_entry_id) if self._manager: diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index c2eecf413f7..9dd49dd851d 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import logging import math +from typing import Any import voluptuous as vol @@ -366,7 +367,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): """List of available operation modes.""" return self._hvac_list - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" if hvac_mode == HVACMode.HEAT: self._hvac_mode = HVACMode.HEAT @@ -384,7 +385,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): # Ensure we update the current operation after changing the mode self.async_write_ha_state() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -537,7 +538,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context ) - async def async_set_preset_mode(self, preset_mode: str): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode not in (self._attr_preset_modes or []): raise ValueError( diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index acf747f3e51..cf29d0ea802 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any import voluptuous as vol @@ -71,13 +72,13 @@ class GeniusSwitch(GeniusZone, SwitchEntity): """ return self._zone.data["mode"] == "override" and self._zone.data["setpoint"] - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the zone to Timer mode. The zone is deemed 'off' in this mode, although the plugs may actually be on. """ await self._zone.set_mode("timer") - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Set the zone to override/on ({'setpoint': true}) for x seconds.""" await self._zone.set_override(1, kwargs.get(ATTR_DURATION, 3600)) diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 3a6b446b8db..ea8b1a43961 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -78,6 +78,6 @@ class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterEntity): """Return the current operation mode.""" return GH_STATE_TO_HA[self._zone.data["mode"]] # type: ignore[return-value] - async def async_set_operation_mode(self, operation_mode) -> None: + async def async_set_operation_mode(self, operation_mode: str) -> None: """Set a new operation mode for this boiler.""" await self._zone.set_mode(HA_OPMODE_TO_GH[operation_mode]) diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 5605ffc803b..eba903a4cdf 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -150,7 +150,7 @@ class GeoRssServiceSensor(SensorEntity): """Return the state attributes.""" return self._state_attributes - def update(self): + def update(self) -> None: """Update this sensor from the GeoRSS service.""" status, feed_entries = self._feed.update() diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 61deb9ede7d..85197239ccd 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -100,7 +100,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): """Return the source type, eg gps or router, of the device.""" return SourceType.GPS - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" await super().async_added_to_hass() self._unsub_dispatcher = async_dispatcher_connect( @@ -117,7 +117,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): attr = state.attributes self._gps = (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Clean up after entity before removal.""" await super().async_will_remove_from_hass() self._unsub_dispatcher() diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py index 357c86a3b8f..9183aead169 100644 --- a/homeassistant/components/geonetnz_quakes/sensor.py +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -60,7 +60,7 @@ class GeonetnzQuakesSensor(SensorEntity): self._removed = None self._remove_signal_status = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_status = async_dispatcher_connect( self.hass, @@ -82,7 +82,7 @@ class GeonetnzQuakesSensor(SensorEntity): _LOGGER.debug("Received status update for %s", self._config_entry_id) self.async_schedule_update_ha_state(True) - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._config_entry_id) if self._manager: diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index e11a9394579..add35bfbcd7 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -81,7 +81,7 @@ class GeonetnzVolcanoSensor(SensorEntity): self._feed_last_update_successful = None self._remove_signal_update = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_update = async_dispatcher_connect( self.hass, @@ -99,7 +99,7 @@ class GeonetnzVolcanoSensor(SensorEntity): """Call update method.""" self.async_schedule_update_ha_state(True) - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index 4206a1184ad..21e1221e6b8 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -126,7 +126,7 @@ class GitLabSensor(SensorEntity): return ICON_SAD return ICON_OTHER - def update(self): + def update(self) -> None: """Collect updated data from GitLab API.""" self._gitlab_data.update() diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index 3069ea04347..514cb9e0ad5 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -98,7 +98,7 @@ class GitterSensor(SensorEntity): """Return the icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" try: diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index af6f307ef3a..0b2ce1801e1 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -20,7 +20,7 @@ from homeassistant.const import ( TEMP_CELSIUS, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -330,7 +330,7 @@ class GlancesSensor(SensorEntity): self.glances_data = glances_data self._sensor_name_prefix = sensor_name_prefix self._state = None - self.unsub_update = None + self.unsub_update: CALLBACK_TYPE | None = None self.entity_description = description self._attr_name = f"{sensor_name_prefix} {description.name_suffix}" @@ -342,7 +342,7 @@ class GlancesSensor(SensorEntity): self._attr_unique_id = f"{self.glances_data.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self.glances_data.available @@ -351,7 +351,7 @@ class GlancesSensor(SensorEntity): """Return the state of the resources.""" return self._state - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" self.unsub_update = async_dispatcher_connect( self.hass, DATA_UPDATED, self._schedule_immediate_update diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 3ee2a18455c..df5221dea2f 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -186,7 +186,7 @@ class GoogleTravelTimeSensor(SensorEntity): await self.hass.async_add_executor_job(self.update) self.async_write_ha_state() - def update(self): + def update(self) -> None: """Get the latest data from Google.""" options_copy = self._config_entry.options.copy() dtime = options_copy.get(CONF_DEPARTURE_TIME) diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 8c4cae9bd79..2ebbd44b81b 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -136,18 +136,23 @@ class GoogleWifiSensor(SensorEntity): entity_description: GoogleWifiSensorEntityDescription - def __init__(self, api, name, description: GoogleWifiSensorEntityDescription): + def __init__( + self, + api: GoogleWifiAPI, + name: str, + description: GoogleWifiSensorEntityDescription, + ) -> None: """Initialize a Google Wifi sensor.""" self.entity_description = description self._api = api self._attr_name = f"{name}_{description.key}" @property - def available(self): + def available(self) -> bool: """Return availability of Google Wifi API.""" return self._api.available - def update(self): + def update(self) -> None: """Get the latest data from the Google Wifi API.""" self._api.update() if self.available: diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 22e9529706f..f18b486917a 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -122,7 +122,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): """Return the source type, eg gps or router, of the device.""" return SourceType.GPS - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" await super().async_added_to_hass() self._unsub_dispatcher = async_dispatcher_connect( @@ -158,7 +158,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): } self._battery = attr.get(ATTR_BATTERY_LEVEL) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Clean up after entity before removal.""" await super().async_will_remove_from_hass() self._unsub_dispatcher() diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 2b6833dff2c..6d8f32aa21c 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from greeclimate.device import ( TEMP_MAX, @@ -166,7 +167,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): """Return the target temperature for the device.""" return self.coordinator.device.target_temperature - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if ATTR_TEMPERATURE not in kwargs: raise ValueError(f"Missing parameter {ATTR_TEMPERATURE}") @@ -265,7 +266,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return PRESET_BOOST return PRESET_NONE - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode not in PRESET_MODES: raise ValueError(f"Invalid preset mode: {preset_mode}") @@ -304,7 +305,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): speed = self.coordinator.device.fan_speed return FAN_MODES.get(speed) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if fan_mode not in FAN_MODES_REVERSE: raise ValueError(f"Invalid fan mode: {fan_mode}") @@ -332,7 +333,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return SWING_VERTICAL return SWING_OFF - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" if swing_mode not in SWING_MODES: raise ValueError(f"Invalid swing mode: {swing_mode}") diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index 91e407d246b..62189fdde06 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -1,6 +1,8 @@ """Support for interface with a Gree climate systems.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -60,13 +62,13 @@ class GreePanelLightSwitchEntity(GreeEntity, SwitchEntity): """Return if the light is turned on.""" return self.coordinator.device.light - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self.coordinator.device.light = True await self.coordinator.push_state_update() self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self.coordinator.device.light = False await self.coordinator.push_state_update() @@ -90,13 +92,13 @@ class GreeQuietModeSwitchEntity(GreeEntity, SwitchEntity): """Return if the state is turned on.""" return self.coordinator.device.quiet - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self.coordinator.device.quiet = True await self.coordinator.push_state_update() self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self.coordinator.device.quiet = False await self.coordinator.push_state_update() @@ -120,13 +122,13 @@ class GreeFreshAirSwitchEntity(GreeEntity, SwitchEntity): """Return if the state is turned on.""" return self.coordinator.device.fresh_air - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self.coordinator.device.fresh_air = True await self.coordinator.push_state_update() self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self.coordinator.device.fresh_air = False await self.coordinator.push_state_update() @@ -150,13 +152,13 @@ class GreeXFanSwitchEntity(GreeEntity, SwitchEntity): """Return if the state is turned on.""" return self.coordinator.device.xfan - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self.coordinator.device.xfan = True await self.coordinator.push_state_update() self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self.coordinator.device.xfan = False await self.coordinator.push_state_update() diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index c90bfa6f3fb..eceba2f7bce 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -171,7 +171,7 @@ class GrowattInverter(SensorEntity): return self.probe.get_data("currency") return super().native_unit_of_measurement - def update(self): + def update(self) -> None: """Get the latest data from the Growat API and updates the state.""" self.probe.update() diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index 723be2880ff..87329bdbc66 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from gsp import GstreamerPlayer import voluptuous as vol @@ -78,7 +79,7 @@ class GstreamerDevice(MediaPlayerEntity): self._artist = None self._album = None - def update(self): + def update(self) -> None: """Update properties.""" self._state = self._player.state self._volume = self._player.volume @@ -88,11 +89,13 @@ class GstreamerDevice(MediaPlayerEntity): self._album = self._player.album self._artist = self._player.artist - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set the volume level.""" self._player.volume = volume - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play media.""" # Handle media_source if media_source.is_media_source_id(media_id): @@ -109,15 +112,15 @@ class GstreamerDevice(MediaPlayerEntity): await self.hass.async_add_executor_job(self._player.queue, media_id) - def media_play(self): + def media_play(self) -> None: """Play.""" self._player.play() - def media_pause(self): + def media_pause(self) -> None: """Pause.""" self._player.pause() - def media_next_track(self): + def media_next_track(self) -> None: """Next track.""" self._player.next() @@ -167,7 +170,7 @@ class GstreamerDevice(MediaPlayerEntity): return self._album async def async_browse_media( - self, media_content_type=None, media_content_id=None + self, media_content_type: str | None = None, media_content_id: str | None = None ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( From 7fb9c4a37fedcfa8b515d17d76187fec7f2725f4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 11:43:20 +0200 Subject: [PATCH 660/903] Improve type hint in flexit climate entity (#77159) --- homeassistant/components/flexit/climate.py | 89 +++++++--------------- 1 file changed, 27 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index a7c230c316c..8129f063a86 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -60,35 +61,36 @@ class Flexit(ClimateEntity): """Representation of a Flexit AC unit.""" _attr_fan_modes = ["Off", "Low", "Medium", "High"] + _attr_hvac_mode = HVACMode.COOL + _attr_hvac_modes = [HVACMode.COOL] _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) + _attr_temperature_unit = TEMP_CELSIUS def __init__( self, hub: ModbusHub, modbus_slave: int | None, name: str | None ) -> None: """Initialize the unit.""" self._hub = hub - self._name = name + self._attr_name = name self._slave = modbus_slave - self._target_temperature = None - self._current_temperature = None self._attr_fan_mode = None - self._filter_hours = None - self._filter_alarm = None - self._heat_recovery = None - self._heater_enabled = False - self._heating = None - self._cooling = None + self._filter_hours: int | None = None + self._filter_alarm: int | None = None + self._heat_recovery: int | None = None + self._heater_enabled: int | None = None + self._heating: int | None = None + self._cooling: int | None = None self._alarm = False - self._outdoor_air_temp = None + self._outdoor_air_temp: float | None = None - async def async_update(self): + async def async_update(self) -> None: """Update unit attributes.""" - self._target_temperature = await self._async_read_temp_from_register( + self._attr_target_temperature = await self._async_read_temp_from_register( CALL_TYPE_REGISTER_HOLDING, 8 ) - self._current_temperature = await self._async_read_temp_from_register( + self._attr_current_temperature = await self._async_read_temp_from_register( CALL_TYPE_REGISTER_INPUT, 9 ) res = await self._async_read_int16_from_register(CALL_TYPE_REGISTER_HOLDING, 17) @@ -137,7 +139,7 @@ class Flexit(ClimateEntity): self._attr_hvac_action = HVACAction.OFF @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { "filter_hours": self._filter_hours, @@ -149,54 +151,14 @@ class Flexit(ClimateEntity): "outdoor_air_temp": self._outdoor_air_temp, } - @property - def should_poll(self): - """Return the polling state.""" - return True - - @property - def name(self): - """Return the name of the climate device.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" - return HVACMode.COOL - - @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - return [HVACMode.COOL] - - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if kwargs.get(ATTR_TEMPERATURE) is not None: - target_temperature = kwargs.get(ATTR_TEMPERATURE) - else: + if (target_temperature := kwargs.get(ATTR_TEMPERATURE)) is None: _LOGGER.error("Received invalid temperature") return - if await self._async_write_int16_to_register(8, target_temperature * 10): - self._target_temperature = target_temperature + if await self._async_write_int16_to_register(8, int(target_temperature * 10)): + self._attr_target_temperature = target_temperature else: _LOGGER.error("Modbus error setting target temperature to Flexit") @@ -210,7 +172,9 @@ class Flexit(ClimateEntity): _LOGGER.error("Modbus error setting fan mode to Flexit") # Based on _async_read_register in ModbusThermostat class - async def _async_read_int16_from_register(self, register_type, register) -> int: + async def _async_read_int16_from_register( + self, register_type: str, register: int + ) -> int: """Read register using the Modbus hub slave.""" result = await self._hub.async_pymodbus_call( self._slave, register, 1, register_type @@ -221,7 +185,9 @@ class Flexit(ClimateEntity): return int(result.registers[0]) - async def _async_read_temp_from_register(self, register_type, register) -> float: + async def _async_read_temp_from_register( + self, register_type: str, register: int + ) -> float: result = float( await self._async_read_int16_from_register(register_type, register) ) @@ -229,8 +195,7 @@ class Flexit(ClimateEntity): return -1 return result / 10.0 - async def _async_write_int16_to_register(self, register, value) -> bool: - value = int(value) + async def _async_write_int16_to_register(self, register: int, value: int) -> bool: result = await self._hub.async_pymodbus_call( self._slave, register, value, CALL_TYPE_WRITE_REGISTER ) From dfc3e7d80fd6c87aeb3d26b94c702aecd2d77dad Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 26 Aug 2022 11:51:36 +0200 Subject: [PATCH 661/903] Don't expose attribute option in state selector (#77347) --- homeassistant/helpers/selector.py | 7 +++++-- tests/helpers/test_selector.py | 5 ----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 7f7a4d16595..e5fa493330e 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -781,7 +781,6 @@ class StateSelectorConfig(TypedDict, total=False): """Class to represent an state selector config.""" entity_id: str - attribute: str @SELECTORS.register("state") @@ -793,7 +792,11 @@ class StateSelector(Selector): CONFIG_SCHEMA = vol.Schema( { vol.Required("entity_id"): cv.entity_id, - vol.Optional("attribute"): str, + # The attribute to filter on, is currently deliberately not + # configurable/exposed. We are considering separating state + # selectors into two types: one for state and one for attribute. + # Limiting the public use, prevents breaking changes in the future. + # vol.Optional("attribute"): str, } ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 5472e6609f0..4c20a3b7906 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -322,11 +322,6 @@ def test_time_selector_schema(schema, valid_selections, invalid_selections): ("on", "armed"), (None, True, 1), ), - ( - {"entity_id": "sensor.abc", "attribute": "device_class"}, - ("temperature", "humidity"), - (None,), - ), ), ) def test_state_selector_schema(schema, valid_selections, invalid_selections): From 1fb8fbf5de7ae58d2a159368a995d9424355d568 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Fri, 26 Aug 2022 07:46:11 -0400 Subject: [PATCH 662/903] Refactor and unify device fetching for UniFi Protect (#77341) --- .../components/unifiprotect/binary_sensor.py | 6 ++--- .../components/unifiprotect/button.py | 6 ++--- .../components/unifiprotect/camera.py | 8 +++---- homeassistant/components/unifiprotect/data.py | 22 ++++++++++--------- .../components/unifiprotect/entity.py | 4 +++- .../components/unifiprotect/light.py | 5 +---- homeassistant/components/unifiprotect/lock.py | 9 ++++---- .../components/unifiprotect/media_player.py | 14 +++++++----- .../components/unifiprotect/media_source.py | 11 ++++++++-- .../components/unifiprotect/sensor.py | 9 ++++---- 10 files changed, 50 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 62a4893692b..0d127a55554 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -10,6 +10,7 @@ from pyunifiprotect.data import ( Camera, Event, Light, + ModelType, MountType, ProtectAdoptableDeviceModel, ProtectModelWithId, @@ -409,12 +410,9 @@ def _async_motion_entities( ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] devices = ( - data.api.bootstrap.cameras.values() if ufp_device is None else [ufp_device] + data.get_by_types({ModelType.CAMERA}) if ufp_device is None else [ufp_device] ) for device in devices: - if not device.is_adopted: - continue - for description in MOTION_SENSORS: entities.append(ProtectEventBinarySensor(data, device, description)) _LOGGER.debug( diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 5b8ea4d0c4e..29007571927 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -100,10 +100,8 @@ def _async_remove_adopt_button( ) -> None: entity_registry = er.async_get(hass) - if device.is_adopted_by_us and ( - entity_id := entity_registry.async_get_entity_id( - Platform.BUTTON, DOMAIN, f"{device.mac}_adopt" - ) + if entity_id := entity_registry.async_get_entity_id( + Platform.BUTTON, DOMAIN, f"{device.mac}_adopt" ): entity_registry.async_remove(entity_id) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index bff8af7be98..8f561e5556f 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -3,10 +3,12 @@ from __future__ import annotations from collections.abc import Generator import logging +from typing import cast from pyunifiprotect.data import ( Camera as UFPCamera, CameraChannel, + ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, StateType, @@ -42,12 +44,10 @@ def get_camera_channels( """Get all the camera channels.""" devices = ( - data.api.bootstrap.cameras.values() if ufp_device is None else [ufp_device] + data.get_by_types({ModelType.CAMERA}) if ufp_device is None else [ufp_device] ) for camera in devices: - if not camera.is_adopted_by_us: - continue - + camera = cast(UFPCamera, camera) if not camera.channels: if ufp_device is None: # only warn on startup diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index cb37897c9a8..20b5747a342 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -4,12 +4,13 @@ from __future__ import annotations from collections.abc import Callable, Generator, Iterable from datetime import timedelta import logging -from typing import Any, Union +from typing import Any, Union, cast from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( NVR, Bootstrap, + Camera, Event, EventType, Liveview, @@ -35,11 +36,7 @@ from .const import ( DISPATCH_CHANNELS, DOMAIN, ) -from .utils import ( - async_dispatch_id as _ufpd, - async_get_devices, - async_get_devices_by_type, -) +from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] @@ -92,13 +89,17 @@ class ProtectData: return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) def get_by_types( - self, device_types: Iterable[ModelType] + self, device_types: Iterable[ModelType], ignore_unadopted: bool = True ) -> Generator[ProtectAdoptableDeviceModel, None, None]: """Get all devices matching types.""" for device_type in device_types: - yield from async_get_devices_by_type( + devices = async_get_devices_by_type( self.api.bootstrap, device_type ).values() + for device in devices: + if ignore_unadopted and not device.is_adopted_by_us: + continue + yield device async def async_setup(self) -> None: """Subscribe and do the refresh.""" @@ -202,7 +203,8 @@ class ProtectData: "Doorbell messages updated. Updating devices with LCD screens" ) self.api.bootstrap.nvr.update_all_messages() - for camera in self.api.bootstrap.cameras.values(): + for camera in self.get_by_types({ModelType.CAMERA}): + camera = cast(Camera, camera) if camera.feature_flags.has_lcd_screen: self._async_signal_device_update(camera) @@ -250,7 +252,7 @@ class ProtectData: return self._async_signal_device_update(self.api.bootstrap.nvr) - for device in async_get_devices(self.api.bootstrap, DEVICES_THAT_ADOPT): + for device in self.get_by_types(DEVICES_THAT_ADOPT): self._async_signal_device_update(device) @callback diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 23af21e825e..9777ccbd72a 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -46,7 +46,9 @@ def _async_device_entities( entities: list[ProtectDeviceEntity] = [] devices = ( - [ufp_device] if ufp_device is not None else data.get_by_types({model_type}) + [ufp_device] + if ufp_device is not None + else data.get_by_types({model_type}, ignore_unadopted=False) ) for device in devices: assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime)) diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index 817e0ba1d6b..feb0be66ecd 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -44,10 +44,7 @@ async def async_setup_entry( ) entities = [] - for device in data.api.bootstrap.lights.values(): - if not device.is_adopted_by_us: - continue - + for device in data.get_by_types({ModelType.LIGHT}): if device.can_write(data.api.bootstrap.auth_user): entities.append(ProtectLight(data, device)) diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 6a33289234e..4fa9ebf4001 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -2,11 +2,12 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from pyunifiprotect.data import ( Doorlock, LockStatusType, + ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, ) @@ -42,10 +43,8 @@ async def async_setup_entry( ) entities = [] - for device in data.api.bootstrap.doorlocks.values(): - if not device.is_adopted_by_us: - continue - + for device in data.get_by_types({ModelType.DOORLOCK}): + device = cast(Doorlock, device) entities.append(ProtectLock(data, device)) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index f426d878ae2..34686f52519 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -2,9 +2,14 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast -from pyunifiprotect.data import Camera, ProtectAdoptableDeviceModel, ProtectModelWithId +from pyunifiprotect.data import ( + Camera, + ModelType, + ProtectAdoptableDeviceModel, + ProtectModelWithId, +) from pyunifiprotect.exceptions import StreamError from homeassistant.components import media_source @@ -51,9 +56,8 @@ async def async_setup_entry( ) entities = [] - for device in data.api.bootstrap.cameras.values(): - if not device.is_adopted_by_us: - continue + for device in data.get_by_types({ModelType.CAMERA}): + device = cast(Camera, device) if device.feature_flags.has_speaker: entities.append(ProtectMediaPlayer(data, device)) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 104323eeaa2..58b14ab9b3b 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -7,7 +7,13 @@ from datetime import date, datetime, timedelta from enum import Enum from typing import Any, cast -from pyunifiprotect.data import Camera, Event, EventType, SmartDetectObjectType +from pyunifiprotect.data import ( + Camera, + Event, + EventType, + ModelType, + SmartDetectObjectType, +) from pyunifiprotect.exceptions import NvrError from pyunifiprotect.utils import from_js_time from yarl import URL @@ -810,7 +816,8 @@ class ProtectMediaSource(MediaSource): cameras: list[BrowseMediaSource] = [await self._build_camera(data, "all")] - for camera in data.api.bootstrap.cameras.values(): + for camera in data.get_by_types({ModelType.CAMERA}): + camera = cast(Camera, camera) if not camera.can_read_media(data.api.bootstrap.auth_user): continue cameras.append(await self._build_camera(data, camera.id)) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 3dac8e46aee..c74bd00e055 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -4,13 +4,14 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime import logging -from typing import Any +from typing import Any, cast from pyunifiprotect.data import ( NVR, Camera, Event, Light, + ModelType, ProtectAdoptableDeviceModel, ProtectDeviceModel, ProtectModelWithId, @@ -649,12 +650,10 @@ def _async_motion_entities( ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] devices = ( - data.api.bootstrap.cameras.values() if ufp_device is None else [ufp_device] + data.get_by_types({ModelType.CAMERA}) if ufp_device is None else [ufp_device] ) for device in devices: - if not device.is_adopted_by_us: - continue - + device = cast(Camera, device) for description in MOTION_TRIP_SENSORS: entities.append(ProtectDeviceSensor(data, device, description)) _LOGGER.debug( From 38ca74b547d21f87c16ffe021adce2f489aca5bf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 14:27:13 +0200 Subject: [PATCH 663/903] Adjust pylint plugin for absolute/relative imports (#77219) * Adjust pylint plugin for absolute/relative imports * Adjust components * One more * Adjust mqtt * Adjust mqtt.DOMAIN import * Adjust internal import * Add tests for valid local component imports * Adjust relative path check * Fixes * Fixes --- homeassistant/components/plaato/sensor.py | 3 +- homeassistant/components/pushover/notify.py | 2 +- homeassistant/components/sabnzbd/sensor.py | 16 ++- homeassistant/components/wallbox/__init__.py | 2 +- pylint/plugins/hass_imports.py | 33 +++++- tests/pylint/conftest.py | 18 +++ tests/pylint/test_imports.py | 117 +++++++++++++++++++ 7 files changed, 176 insertions(+), 15 deletions(-) create mode 100644 tests/pylint/test_imports.py diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index dc3039ea355..b43e18e52f6 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -6,7 +6,7 @@ from pyplaato.plaato import PlaatoKeg from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -15,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ATTR_TEMP, SENSOR_UPDATE -from ...core import callback from .const import ( CONF_USE_WEBHOOK, COORDINATOR, diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 16ad452fd9a..dd711c80aaf 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -18,11 +18,11 @@ from homeassistant.components.notify import ( from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from ...exceptions import HomeAssistantError from .const import ( ATTR_ATTACHMENT, ATTR_CALLBACK_URL, diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 043a344ec7b..b2828a30969 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -8,15 +8,19 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DATA_GIGABYTES, + DATA_MEGABYTES, + DATA_RATE_MEGABYTES_PER_SECOND, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, SIGNAL_SABNZBD_UPDATED -from ...config_entries import ConfigEntry -from ...const import DATA_GIGABYTES, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND -from ...core import HomeAssistant -from ...helpers.device_registry import DeviceEntryType -from ...helpers.entity import DeviceInfo -from ...helpers.entity_platform import AddEntitiesCallback from .const import DEFAULT_NAME, KEY_API_DATA, KEY_NAME diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index f0392f808ae..9175f42827d 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -13,12 +13,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from ...helpers.entity import DeviceInfo from .const import ( CHARGER_CURRENT_VERSION_KEY, CHARGER_DATA_KEY, diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index c7abe0ad6ac..62fd262eacc 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -41,15 +41,15 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { "homeassistant.components.automation": [ ObsoleteImportMatch( reason="replaced by TriggerActionType from helpers.trigger", - constant=re.compile(r"^AutomationActionType$") + constant=re.compile(r"^AutomationActionType$"), ), ObsoleteImportMatch( reason="replaced by TriggerData from helpers.trigger", - constant=re.compile(r"^AutomationTriggerData$") + constant=re.compile(r"^AutomationTriggerData$"), ), ObsoleteImportMatch( reason="replaced by TriggerInfo from helpers.trigger", - constant=re.compile(r"^AutomationTriggerInfo$") + constant=re.compile(r"^AutomationTriggerInfo$"), ), ], "homeassistant.components.binary_sensor": [ @@ -111,13 +111,13 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { "homeassistant.components.device_tracker": [ ObsoleteImportMatch( reason="replaced by SourceType enum", - constant=re.compile(r"^SOURCE_TYPE_\w+$") + constant=re.compile(r"^SOURCE_TYPE_\w+$"), ), ], "homeassistant.components.device_tracker.const": [ ObsoleteImportMatch( reason="replaced by SourceType enum", - constant=re.compile(r"^SOURCE_TYPE_\w+$") + constant=re.compile(r"^SOURCE_TYPE_\w+$"), ), ], "homeassistant.components.fan": [ @@ -277,6 +277,11 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] "hass-deprecated-import", "Used when import is deprecated", ), + "W7423": ( + "Absolute import should be used", + "hass-absolute-import", + "Used when relative import should be replaced with absolute import", + ), } options = () @@ -298,9 +303,27 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] if module.startswith(f"{self.current_package}."): self.add_message("hass-relative-import", node=node) + def _visit_importfrom_relative(self, current_package: str, node: nodes.ImportFrom) -> None: + """Called when a ImportFrom node is visited.""" + if node.level <= 1 or not current_package.startswith("homeassistant.components"): + return + split_package = current_package.split(".") + if not node.modname and len(split_package) == node.level + 1: + for name in node.names: + # Allow relative import to component root + if name[0] != split_package[2]: + self.add_message("hass-absolute-import", node=node) + return + return + if len(split_package) < node.level + 2: + self.add_message("hass-absolute-import", node=node) + def visit_importfrom(self, node: nodes.ImportFrom) -> None: """Called when a ImportFrom node is visited.""" + if not self.current_package: + return if node.level is not None: + self._visit_importfrom_relative(self.current_package, node) return if node.modname == self.current_package or node.modname.startswith( f"{self.current_package}." diff --git a/tests/pylint/conftest.py b/tests/pylint/conftest.py index edbcec27375..e8748434350 100644 --- a/tests/pylint/conftest.py +++ b/tests/pylint/conftest.py @@ -32,3 +32,21 @@ def type_hint_checker_fixture(hass_enforce_type_hints, linter) -> BaseChecker: type_hint_checker = hass_enforce_type_hints.HassTypeHintChecker(linter) type_hint_checker.module = "homeassistant.components.pylint_test" return type_hint_checker + + +@pytest.fixture(name="hass_imports", scope="session") +def hass_imports_fixture() -> ModuleType: + """Fixture to provide a requests mocker.""" + loader = SourceFileLoader( + "hass_imports", + str(BASE_PATH.joinpath("pylint/plugins/hass_imports.py")), + ) + return loader.load_module(None) + + +@pytest.fixture(name="imports_checker") +def imports_checker_fixture(hass_imports, linter) -> BaseChecker: + """Fixture to provide a requests mocker.""" + type_hint_checker = hass_imports.HassImportsFormatChecker(linter) + type_hint_checker.module = "homeassistant.components.pylint_test" + return type_hint_checker diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py new file mode 100644 index 00000000000..6367427eea7 --- /dev/null +++ b/tests/pylint/test_imports.py @@ -0,0 +1,117 @@ +"""Tests for pylint hass_imports plugin.""" +# pylint:disable=protected-access +from __future__ import annotations + +import astroid +from pylint.checkers import BaseChecker +import pylint.testutils +from pylint.testutils.unittest_linter import UnittestLinter +import pytest + +from . import assert_adds_messages, assert_no_messages + + +@pytest.mark.parametrize( + ("module_name", "import_from", "import_what"), + [ + ( + "homeassistant.components.pylint_test.sensor", + "homeassistant.const", + "CONSTANT", + ), + ("homeassistant.components.pylint_test.sensor", ".const", "CONSTANT"), + ("homeassistant.components.pylint_test.sensor", ".", "CONSTANT"), + ("homeassistant.components.pylint_test.sensor", "..", "pylint_test"), + ( + "homeassistant.components.pylint_test.api.hub", + "homeassistant.const", + "CONSTANT", + ), + ("homeassistant.components.pylint_test.api.hub", "..const", "CONSTANT"), + ("homeassistant.components.pylint_test.api.hub", "..", "CONSTANT"), + ("homeassistant.components.pylint_test.api.hub", "...", "pylint_test"), + ], +) +def test_good_import( + linter: UnittestLinter, + imports_checker: BaseChecker, + module_name: str, + import_from: str, + import_what: str, +) -> None: + """Ensure good imports pass through ok.""" + + import_node = astroid.extract_node( + f"from {import_from} import {import_what} #@", + module_name, + ) + imports_checker.visit_module(import_node.parent) + + with assert_no_messages(linter): + imports_checker.visit_importfrom(import_node) + + +@pytest.mark.parametrize( + ("module_name", "import_from", "import_what", "error_code"), + [ + ( + "homeassistant.components.pylint_test.sensor", + "homeassistant.components.pylint_test.const", + "CONSTANT", + "hass-relative-import", + ), + ( + "homeassistant.components.pylint_test.sensor", + "..const", + "CONSTANT", + "hass-absolute-import", + ), + ( + "homeassistant.components.pylint_test.sensor", + "...const", + "CONSTANT", + "hass-absolute-import", + ), + ( + "homeassistant.components.pylint_test.api.hub", + "homeassistant.components.pylint_test.api.const", + "CONSTANT", + "hass-relative-import", + ), + ( + "homeassistant.components.pylint_test.api.hub", + "...const", + "CONSTANT", + "hass-absolute-import", + ), + ], +) +def test_bad_import( + linter: UnittestLinter, + imports_checker: BaseChecker, + module_name: str, + import_from: str, + import_what: str, + error_code: str, +) -> None: + """Ensure bad imports are rejected.""" + + import_node = astroid.extract_node( + f"from {import_from} import {import_what} #@", + module_name, + ) + imports_checker.visit_module(import_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id=error_code, + node=import_node, + args=None, + line=1, + col_offset=0, + end_line=1, + end_col_offset=len(import_from) + 21, + ), + ): + imports_checker.visit_importfrom(import_node) From 36d77d1f33cf439c199a8fd33c372e0995b38b42 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Fri, 26 Aug 2022 09:21:45 -0400 Subject: [PATCH 664/903] Add diagnostics to Fully Kiosk Browser integration (#77274) --- .../components/fully_kiosk/diagnostics.py | 64 +++++++++++++++++++ .../fully_kiosk/test_diagnostics.py | 43 +++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 homeassistant/components/fully_kiosk/diagnostics.py create mode 100644 tests/components/fully_kiosk/test_diagnostics.py diff --git a/homeassistant/components/fully_kiosk/diagnostics.py b/homeassistant/components/fully_kiosk/diagnostics.py new file mode 100644 index 00000000000..89a894d5353 --- /dev/null +++ b/homeassistant/components/fully_kiosk/diagnostics.py @@ -0,0 +1,64 @@ +"""Provides diagnostics for Fully Kiosk Browser.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics.util import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import DOMAIN + +DEVICE_INFO_TO_REDACT = { + "serial", + "Mac", + "ip6", + "hostname6", + "ip4", + "hostname4", + "deviceID", + "startUrl", + "currentPage", + "SSID", + "BSSID", +} +SETTINGS_TO_REDACT = { + "startURL", + "mqttBrokerPassword", + "mqttBrokerUsername", + "remoteAdminPassword", + "wifiKey", + "authPassword", + "authUsername", + "mqttBrokerUrl", + "kioskPin", + "wifiSSID", + "screensaverWallpaperURL", + "barcodeScanTargetUrl", + "launcherBgUrl", + "clientCaUrl", + "urlWhitelist", + "alarmSoundFileUrl", + "errorURL", + "actionBarIconUrl", + "kioskWifiPin", + "knoxApnConfig", + "injectJsCode", + "mdmApnConfig", + "mdmProxyConfig", + "wifiEnterpriseIdentity", + "sebExamKey", + "sebConfigKey", + "kioskPinEnc", +} + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: dr.DeviceEntry +) -> dict[str, Any]: + """Return device diagnostics.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + data = coordinator.data + data["settings"] = async_redact_data(data["settings"], SETTINGS_TO_REDACT) + return async_redact_data(data, DEVICE_INFO_TO_REDACT) diff --git a/tests/components/fully_kiosk/test_diagnostics.py b/tests/components/fully_kiosk/test_diagnostics.py new file mode 100644 index 00000000000..8136147fb6a --- /dev/null +++ b/tests/components/fully_kiosk/test_diagnostics.py @@ -0,0 +1,43 @@ +"""Test the Fully Kiosk Browser diagnostics.""" +from unittest.mock import MagicMock + +from aiohttp import ClientSession + +from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.fully_kiosk.const import DOMAIN +from homeassistant.components.fully_kiosk.diagnostics import ( + DEVICE_INFO_TO_REDACT, + SETTINGS_TO_REDACT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_device + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test Fully Kiosk diagnostics.""" + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, "abcdef-123456")}) + + diagnostics = await get_diagnostics_for_device( + hass, hass_client, init_integration, device + ) + + assert diagnostics + for key in DEVICE_INFO_TO_REDACT: + if hasattr(diagnostics, key): + assert diagnostics[key] == REDACTED + for key in SETTINGS_TO_REDACT: + if hasattr(diagnostics["settings"], key): + assert ( + diagnostics["settings"][key] == REDACTED + or diagnostics["settings"][key] == "" + ) From 0881ff2d1fe4034b33821775fa236589d8836912 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 26 Aug 2022 10:00:46 -0400 Subject: [PATCH 665/903] Add guard to enhanced current hue usage in ZHA (#77359) --- homeassistant/components/zha/light.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 88bd5299ff7..5a8011e2386 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -605,7 +605,10 @@ class Light(BaseLight, ZhaEntity): and not self._zha_config_always_prefer_xy_color_mode ): self._attr_supported_color_modes.add(ColorMode.HS) - if self._color_channel.enhanced_hue_supported: + if ( + self._color_channel.enhanced_hue_supported + and self._color_channel.enhanced_current_hue is not None + ): curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 else: curr_hue = self._color_channel.current_hue * 254 / 360 From 9b8912a55801a157e8d3b179e0025243127eb9af Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 19:43:36 +0200 Subject: [PATCH 666/903] Remove unnecessary property from proliphix (#77363) --- homeassistant/components/proliphix/climate.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 0907602ed9d..e47aa0fc8f0 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -61,11 +61,6 @@ class ProliphixThermostat(ClimateEntity): self._pdp = pdp self._name = None - @property - def should_poll(self): - """Set up polling needed for thermostat.""" - return True - def update(self): """Update the data from the thermostat.""" self._pdp.update() From 5af015dd7d25f7f3bf8cc93dda89602ac2d17ac3 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 26 Aug 2022 13:44:34 -0400 Subject: [PATCH 667/903] Fix missing entities in ZHA for IKEA STARKVIND (#77360) --- homeassistant/components/zha/fan.py | 5 ++++- homeassistant/components/zha/switch.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index d947fca10ab..c38a71e3249 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -248,7 +248,10 @@ IKEA_NAME_TO_PRESET_MODE = {v: k for k, v in IKEA_PRESET_MODES_TO_NAME.items()} IKEA_PRESET_MODES = list(IKEA_NAME_TO_PRESET_MODE) -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH( + channel_names="ikea_airpurifier", + models={"STARKVIND Air purifier", "STARKVIND Air purifier table"}, +) class IkeaFan(BaseFan, ZhaEntity): """Representation of a ZHA fan.""" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 881401f31da..752df6d568e 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -292,7 +292,8 @@ class P1MotionTriggerIndicatorSwitch( @CONFIG_DIAGNOSTIC_MATCH( - channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} + channel_names="ikea_airpurifier", + models={"STARKVIND Air purifier", "STARKVIND Air purifier table"}, ) class ChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"): """ZHA BinarySensor.""" @@ -301,7 +302,8 @@ class ChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"): @CONFIG_DIAGNOSTIC_MATCH( - channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} + channel_names="ikea_airpurifier", + models={"STARKVIND Air purifier", "STARKVIND Air purifier table"}, ) class DisableLed(ZHASwitchConfigurationEntity, id_suffix="disable_led"): """ZHA BinarySensor.""" From b36321988f2fb4f7fa43d8f6543c516ec3938ba9 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 26 Aug 2022 21:57:43 +0300 Subject: [PATCH 668/903] Deprecate speedtest service (#77261) deprecate speedtest service --- .../components/speedtestdotnet/__init__.py | 20 +++++++++++++ .../components/speedtestdotnet/manifest.json | 1 + .../components/speedtestdotnet/strings.json | 13 ++++++++ .../speedtestdotnet/translations/en.json | 13 ++++++++ tests/components/speedtestdotnet/test_init.py | 30 +++++++++++++++++++ 5 files changed, 77 insertions(+) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 2684b24c81f..0f236402eb4 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -6,6 +6,8 @@ import logging import speedtest +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, HomeAssistant, ServiceCall @@ -142,6 +144,24 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): async def request_update(call: ServiceCall) -> None: """Request update.""" + async_create_issue( + self.hass, + DOMAIN, + "deprecated_service", + breaks_in_ha_version="2022.11.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_service", + ) + + _LOGGER.warning( + ( + 'The "%s" service is deprecated and will be removed in "2022.11.0"; ' + 'use the "homeassistant.update_entity" service and pass it a target Speedtest entity_id' + ), + SPEED_TEST_SERVICE, + ) await self.async_request_refresh() self.hass.services.async_register(DOMAIN, SPEED_TEST_SERVICE, request_update) diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index 1df9d6c236a..04400c14781 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -3,6 +3,7 @@ "name": "Speedtest.net", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", + "dependencies": ["repairs"], "requirements": ["speedtest-cli==2.1.3"], "codeowners": ["@rohankapoorcom", "@engrbm87"], "iot_class": "cloud_polling" diff --git a/homeassistant/components/speedtestdotnet/strings.json b/homeassistant/components/speedtestdotnet/strings.json index c4dad30cb09..d4117d82cf3 100644 --- a/homeassistant/components/speedtestdotnet/strings.json +++ b/homeassistant/components/speedtestdotnet/strings.json @@ -19,5 +19,18 @@ } } } + }, + "issues": { + "deprecated_service": { + "title": "The speedtest service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The speedtest service is being removed", + "description": "Update any automations or scripts that use this service to instead use the `homeassistant.update_entity` service with a target Speedtest entity_id. Then, click SUBMIT below to mark this issue as resolved." + } + } + } + } } } diff --git a/homeassistant/components/speedtestdotnet/translations/en.json b/homeassistant/components/speedtestdotnet/translations/en.json index eab480073bc..623aee3b2e8 100644 --- a/homeassistant/components/speedtestdotnet/translations/en.json +++ b/homeassistant/components/speedtestdotnet/translations/en.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this service to instead use the `homeassistant.update_entity` service with a target Speedtest entity_id. Then, click SUBMIT below to mark this issue as resolved.", + "title": "The speedtest service is being removed" + } + } + }, + "title": "The speedtest service is being removed" + } + }, "options": { "step": { "init": { diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 61487ca8329..b4186ecacef 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -1,7 +1,11 @@ """Tests for SpeedTest integration.""" + +from collections.abc import Awaitable from datetime import timedelta +from typing import Callable from unittest.mock import MagicMock +from aiohttp import ClientWebSocketResponse import speedtest from homeassistant.components.speedtestdotnet.const import ( @@ -17,6 +21,7 @@ from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.repairs import get_repairs async def test_successful_config_entry(hass: HomeAssistant) -> None: @@ -120,3 +125,28 @@ async def test_get_best_server_error(hass: HomeAssistant, mock_api: MagicMock) - state = hass.states.get("sensor.speedtest_ping") assert state is not None assert state.state == STATE_UNAVAILABLE + + +async def test_deprecated_service_alert( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test that an issue is raised if deprecated services is called.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + DOMAIN, + "speedtest", + {}, + blocking=True, + ) + await hass.async_block_till_done() + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "deprecated_service" From dff9baf880702b8c9c58ee0efc60366063fb722a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 21:19:37 +0200 Subject: [PATCH 669/903] Use _attr_should_poll in components [j-n] (#77357) --- homeassistant/components/kaiterra/air_quality.py | 7 ++----- homeassistant/components/keenetic_ndms2/device_tracker.py | 7 ++----- homeassistant/components/kira/sensor.py | 7 ++----- homeassistant/components/konnected/binary_sensor.py | 7 ++----- homeassistant/components/lcn/__init__.py | 7 ++----- homeassistant/components/litejet/switch.py | 7 ++----- homeassistant/components/lutron/__init__.py | 7 ++----- homeassistant/components/lw12wifi/light.py | 6 +----- homeassistant/components/mediaroom/media_player.py | 6 +----- homeassistant/components/microsoft_face/__init__.py | 7 ++----- homeassistant/components/mold_indicator/sensor.py | 7 ++----- homeassistant/components/mqtt/mixins.py | 6 +----- homeassistant/components/mysensors/device.py | 5 +---- homeassistant/components/mystrom/binary_sensor.py | 7 ++----- homeassistant/components/ness_alarm/binary_sensor.py | 7 ++----- homeassistant/components/nest/climate_sdm.py | 6 +----- homeassistant/components/nest/legacy/__init__.py | 7 ++----- homeassistant/components/nest/legacy/climate.py | 7 ++----- homeassistant/components/nmap_tracker/device_tracker.py | 7 ++----- homeassistant/components/numato/binary_sensor.py | 7 ++----- homeassistant/components/numato/switch.py | 7 ++----- homeassistant/components/nws/weather.py | 7 ++----- homeassistant/components/nx584/binary_sensor.py | 7 ++----- 23 files changed, 41 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index 4d3e8bf70f3..edbddb361c9 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -37,6 +37,8 @@ async def async_setup_platform( class KaiterraAirQuality(AirQualityEntity): """Implementation of a Kaittera air quality sensor.""" + _attr_should_poll = False + def __init__(self, api, name, device_id): """Initialize the sensor.""" self._api = api @@ -50,11 +52,6 @@ class KaiterraAirQuality(AirQualityEntity): def _device(self): return self._api.data.get(self._device_id, {}) - @property - def should_poll(self): - """Return that the sensor should not be polled.""" - return False - @property def available(self): """Return the availability of the sensor.""" diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 397d1888a9e..116f82afe3a 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -86,6 +86,8 @@ def update_items(router: KeeneticRouter, async_add_entities, tracked: set[str]): class KeeneticTracker(ScannerEntity): """Representation of network device.""" + _attr_should_poll = False + def __init__(self, device: Device, router: KeeneticRouter) -> None: """Initialize the tracked device.""" self._device = device @@ -94,11 +96,6 @@ class KeeneticTracker(ScannerEntity): 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.""" diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py index f5824d806f9..d4488781849 100644 --- a/homeassistant/components/kira/sensor.py +++ b/homeassistant/components/kira/sensor.py @@ -34,6 +34,8 @@ def setup_platform( class KiraReceiver(SensorEntity): """Implementation of a Kira Receiver.""" + _attr_should_poll = False + def __init__(self, name, kira): """Initialize the sensor.""" self._name = name @@ -69,11 +71,6 @@ class KiraReceiver(SensorEntity): """Return the state attributes of the device.""" return {CONF_DEVICE: self._device} - @property - def should_poll(self) -> bool: - """Entity should not be polled.""" - return False - @property def force_update(self) -> bool: """Kira should force updates. Repeated states have meaning.""" diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index 307c8bc24a7..e1823c1c7d9 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -37,6 +37,8 @@ async def async_setup_entry( class KonnectedBinarySensor(BinarySensorEntity): """Representation of a Konnected binary sensor.""" + _attr_should_poll = False + def __init__(self, device_id, zone_num, data): """Initialize the Konnected binary sensor.""" self._data = data @@ -62,11 +64,6 @@ class KonnectedBinarySensor(BinarySensorEntity): """Return the state of the sensor.""" return self._state - @property - def should_poll(self): - """No polling needed.""" - return False - @property def device_class(self): """Return the device class.""" diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 8df579fddd7..d4a20c4d849 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -237,6 +237,8 @@ def _async_fire_send_keys_event( class LcnEntity(Entity): """Parent class for all entities associated with the LCN component.""" + _attr_should_poll = False + def __init__( self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType ) -> None: @@ -280,11 +282,6 @@ class LcnEntity(Entity): ), } - @property - def should_poll(self) -> bool: - """Lcn device entity pushes its state to HA.""" - return False - async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" if not self.device_connection.is_group: diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index 0204c05335a..66b68a345f2 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -35,6 +35,8 @@ async def async_setup_entry( class LiteJetSwitch(SwitchEntity): """Representation of a single LiteJet switch.""" + _attr_should_poll = False + def __init__(self, entry_id, lj, i, name): # pylint: disable=invalid-name """Initialize a LiteJet switch.""" self._entry_id = entry_id @@ -78,11 +80,6 @@ class LiteJetSwitch(SwitchEntity): """Return if the switch is pressed.""" return self._state - @property - def should_poll(self): - """Return that polling is not necessary.""" - return False - @property def extra_state_attributes(self): """Return the device-specific state attributes.""" diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 1583d8b74eb..75561dd275b 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -116,6 +116,8 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: class LutronDevice(Entity): """Representation of a Lutron device entity.""" + _attr_should_poll = False + def __init__(self, area_name, lutron_device, controller): """Initialize the device.""" self._lutron_device = lutron_device @@ -135,11 +137,6 @@ class LutronDevice(Entity): """Return the name of the device.""" return f"{self._area_name} {self._lutron_device.name}" - @property - def should_poll(self): - """No polling needed.""" - return False - @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index de23436a560..7814475b41f 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -59,6 +59,7 @@ class LW12WiFi(LightEntity): """LW-12 WiFi LED Controller.""" _attr_color_mode = ColorMode.HS + _attr_should_poll = False _attr_supported_color_modes = {ColorMode.HS} _attr_supported_features = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION @@ -115,11 +116,6 @@ class LW12WiFi(LightEntity): """Return True if unable to access real state of the entity.""" return True - @property - def should_poll(self) -> bool: - """Return False to not poll the state of this entity.""" - return False - def turn_on(self, **kwargs): """Instruct the light to turn on.""" self._light.light_on() diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 31c8d634912..b3a800c489e 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -117,6 +117,7 @@ async def async_setup_platform( class MediaroomDevice(MediaPlayerEntity): """Representation of a Mediaroom set-up-box on the network.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.TURN_ON @@ -163,11 +164,6 @@ class MediaroomDevice(MediaPlayerEntity): else: self._unique_id = None - @property - def should_poll(self): - """No polling needed.""" - return False - @property def available(self): """Return True if entity is available.""" diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 6b8fcde63f1..82a9accac59 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -207,6 +207,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class MicrosoftFaceGroupEntity(Entity): """Person-Group state/data Entity.""" + _attr_should_poll = False + def __init__(self, hass, api, g_id, name): """Initialize person/group entity.""" self.hass = hass @@ -229,11 +231,6 @@ class MicrosoftFaceGroupEntity(Entity): """Return the state of the entity.""" return len(self._api.store[self._id]) - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - @property def extra_state_attributes(self): """Return device specific state attributes.""" diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index bbd609f5fb8..db4f8d069ab 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -80,6 +80,8 @@ async def async_setup_platform( class MoldIndicator(SensorEntity): """Represents a MoldIndication sensor.""" + _attr_should_poll = False + def __init__( self, name, @@ -353,11 +355,6 @@ class MoldIndicator(SensorEntity): _LOGGER.debug("Mold indicator humidity: %s", self._state) - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name.""" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index e4a24c0f5ad..faef154dd84 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -950,6 +950,7 @@ class MqttEntity( ): """Representation of an MQTT entity.""" + _attr_should_poll = False _entity_id_format: str def __init__(self, hass, config, config_entry, discovery_data): @@ -1076,11 +1077,6 @@ class MqttEntity( """Return the name of the device if any.""" return self._config.get(CONF_NAME) - @property - def should_poll(self): - """No polling needed.""" - return False - @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index b1e562e878c..d9f95fa391f 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -217,10 +217,7 @@ def get_mysensors_devices( class MySensorsEntity(MySensorsDevice, Entity): """Representation of a MySensors entity.""" - @property - def should_poll(self) -> bool: - """Return the polling state. The gateway pushes its states.""" - return False + _attr_should_poll = False @property def available(self) -> bool: diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index e86c4745ad5..91a7814df2d 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -72,6 +72,8 @@ class MyStromView(HomeAssistantView): class MyStromBinarySensor(BinarySensorEntity): """Representation of a myStrom button.""" + _attr_should_poll = False + def __init__(self, button_id): """Initialize the myStrom Binary sensor.""" self._button_id = button_id @@ -82,11 +84,6 @@ class MyStromBinarySensor(BinarySensorEntity): """Return the name of the sensor.""" return self._button_id - @property - def should_poll(self): - """No polling needed.""" - return False - @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/ness_alarm/binary_sensor.py b/homeassistant/components/ness_alarm/binary_sensor.py index 5bf2a4ee3c2..4855ce28b72 100644 --- a/homeassistant/components/ness_alarm/binary_sensor.py +++ b/homeassistant/components/ness_alarm/binary_sensor.py @@ -46,6 +46,8 @@ async def async_setup_platform( class NessZoneBinarySensor(BinarySensorEntity): """Representation of an Ness alarm zone as a binary sensor.""" + _attr_should_poll = False + def __init__(self, zone_id, name, zone_type): """Initialize the binary_sensor.""" self._zone_id = zone_id @@ -66,11 +68,6 @@ class NessZoneBinarySensor(BinarySensorEntity): """Return the name of the entity.""" return self._name - @property - def should_poll(self): - """No polling needed.""" - return False - @property def is_on(self): """Return true if sensor is on.""" diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index c13370776ad..87cbf2331f4 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -98,6 +98,7 @@ class ThermostatEntity(ClimateEntity): _attr_min_temp = MIN_TEMP _attr_max_temp = MAX_TEMP _attr_has_entity_name = True + _attr_should_poll = False def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" @@ -105,11 +106,6 @@ class ThermostatEntity(ClimateEntity): self._device_info = NestDeviceInfo(device) self._attr_supported_features = 0 - @property - def should_poll(self) -> bool: - """Disable polling since entities have state pushed via pubsub.""" - return False - @property def unique_id(self) -> str | None: """Return a unique ID.""" diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index 79579a1c3df..b244dea182e 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -348,6 +348,8 @@ class NestLegacyDevice: class NestSensorDevice(Entity): """Representation of a Nest sensor.""" + _attr_should_poll = False + def __init__(self, structure, device, variable): """Initialize the sensor.""" self.structure = structure @@ -370,11 +372,6 @@ class NestSensorDevice(Entity): """Return the name of the nest, if any.""" return self._name - @property - def should_poll(self): - """Do not need poll thanks using Nest streaming API.""" - return False - @property def unique_id(self): """Return unique id based on device serial and variable.""" diff --git a/homeassistant/components/nest/legacy/climate.py b/homeassistant/components/nest/legacy/climate.py index 07238a46ea9..3a735fe44c3 100644 --- a/homeassistant/components/nest/legacy/climate.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -86,6 +86,8 @@ async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: class NestThermostat(ClimateEntity): """Representation of a Nest thermostat.""" + _attr_should_poll = False + def __init__(self, structure, device, temp_unit): """Initialize the thermostat.""" self._unit = temp_unit @@ -136,11 +138,6 @@ class NestThermostat(ClimateEntity): self._min_temperature = None self._max_temperature = None - @property - def should_poll(self): - """Do not need poll thanks using Nest streaming API.""" - return False - async def async_added_to_hass(self): """Register update signal handler.""" diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index afb931b82dd..9203288f03a 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -46,6 +46,8 @@ async def async_setup_entry( class NmapTrackerEntity(ScannerEntity): """An Nmap Tracker entity.""" + _attr_should_poll = False + def __init__( self, nmap_tracker: NmapDeviceScanner, mac_address: str, active: bool ) -> None: @@ -97,11 +99,6 @@ class NmapTrackerEntity(ScannerEntity): """Return tracker source type.""" return SourceType.ROUTER - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @property def icon(self) -> str: """Return device icon.""" diff --git a/homeassistant/components/numato/binary_sensor.py b/homeassistant/components/numato/binary_sensor.py index 095ad811422..b3881cc0493 100644 --- a/homeassistant/components/numato/binary_sensor.py +++ b/homeassistant/components/numato/binary_sensor.py @@ -81,6 +81,8 @@ def setup_platform( class NumatoGpioBinarySensor(BinarySensorEntity): """Represents a binary sensor (input) port of a Numato GPIO expander.""" + _attr_should_poll = False + def __init__(self, name, device_id, port, invert_logic, api): """Initialize the Numato GPIO based binary sensor object.""" self._name = name or DEVICE_DEFAULT_NAME @@ -106,11 +108,6 @@ class NumatoGpioBinarySensor(BinarySensorEntity): self._state = level self.async_write_ha_state() - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/numato/switch.py b/homeassistant/components/numato/switch.py index 312ad4c02a5..fb18866ae93 100644 --- a/homeassistant/components/numato/switch.py +++ b/homeassistant/components/numato/switch.py @@ -67,6 +67,8 @@ def setup_platform( class NumatoGpioSwitch(SwitchEntity): """Representation of a Numato USB GPIO switch port.""" + _attr_should_poll = False + def __init__(self, name, device_id, port, invert_logic, api): """Initialize the port.""" self._name = name or DEVICE_DEFAULT_NAME @@ -81,11 +83,6 @@ class NumatoGpioSwitch(SwitchEntity): """Return the name of the switch.""" return self._name - @property - def should_poll(self): - """No polling needed.""" - return False - @property def is_on(self): """Return true if port is turned on.""" diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 5bb1bb0bf6c..60f93f20177 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -94,6 +94,8 @@ async def async_setup_entry( class NWSWeather(WeatherEntity): """Representation of a weather condition.""" + _attr_should_poll = False + def __init__(self, entry_data, hass_data, mode, units): """Initialise the platform with a data instance and station name.""" self.nws = hass_data[NWS_DATA] @@ -133,11 +135,6 @@ class NWSWeather(WeatherEntity): self.async_write_ha_state() - @property - def should_poll(self) -> bool: - """Entities do not individually poll.""" - return False - @property def attribution(self): """Return the attribution.""" diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 6fdea44f836..853f5686831 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -87,6 +87,8 @@ def setup_platform( class NX584ZoneSensor(BinarySensorEntity): """Representation of a NX584 zone as a sensor.""" + _attr_should_poll = False + def __init__(self, zone, zone_type): """Initialize the nx594 binary sensor.""" self._zone = zone @@ -97,11 +99,6 @@ class NX584ZoneSensor(BinarySensorEntity): """Return the class of this sensor, from DEVICE_CLASSES.""" return self._zone_type - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the binary sensor.""" From d8b2563b3d8222141f7049a79716724850c8a4ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 21:22:27 +0200 Subject: [PATCH 670/903] Use _attr_should_poll in components [u-z] (#77371) --- homeassistant/components/unifi/unifi_entity_base.py | 7 ++----- homeassistant/components/universal/media_player.py | 7 ++----- homeassistant/components/upb/__init__.py | 7 ++----- homeassistant/components/utility_meter/sensor.py | 7 ++----- homeassistant/components/velux/__init__.py | 7 ++----- homeassistant/components/w800rf32/binary_sensor.py | 7 ++----- homeassistant/components/waterfurnace/sensor.py | 7 ++----- homeassistant/components/wiffi/__init__.py | 7 ++----- homeassistant/components/withings/common.py | 7 ++----- homeassistant/components/xbox_live/sensor.py | 7 ++----- homeassistant/components/xiaomi_miio/remote.py | 7 ++----- homeassistant/components/yamaha_musiccast/media_player.py | 7 ++----- homeassistant/components/zhong_hong/climate.py | 6 +----- 13 files changed, 25 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index 466764714cf..11b5eac2d3c 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -19,6 +19,8 @@ _LOGGER = logging.getLogger(__name__) class UniFiBase(Entity): """UniFi entity base class.""" + _attr_should_poll = False + DOMAIN = "" TYPE = "" @@ -93,8 +95,3 @@ class UniFiBase(Entity): er.async_get(self.hass).async_remove(self.entity_id) else: await self.async_remove(force_remove=True) - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 352703f8f9b..099d786b901 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -152,6 +152,8 @@ async def async_setup_platform( class UniversalMediaPlayer(MediaPlayerEntity): """Representation of an universal media player.""" + _attr_should_poll = False + def __init__( self, hass, @@ -274,11 +276,6 @@ class UniversalMediaPlayer(MediaPlayerEntity): DOMAIN, service_name, service_data, blocking=True, context=self._context ) - @property - def should_poll(self): - """No polling needed.""" - return False - @property def device_class(self) -> str | None: """Return the class of this device.""" diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index 5e03df71750..0b3926f813f 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -68,6 +68,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class UpbEntity(Entity): """Base class for all UPB entities.""" + _attr_should_poll = False + def __init__(self, element, unique_id, upb): """Initialize the base of all UPB devices.""" self._upb = upb @@ -80,11 +82,6 @@ class UpbEntity(Entity): """Return unique id of the element.""" return self._unique_id - @property - def should_poll(self) -> bool: - """Don't poll this device.""" - return False - @property def extra_state_attributes(self): """Return the default attributes of the element.""" diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index d2a2d2ba8ca..e1f5ef052e0 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -304,6 +304,8 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): class UtilityMeterSensor(RestoreSensor): """Representation of an utility meter sensor.""" + _attr_should_poll = False + def __init__( self, *, @@ -581,11 +583,6 @@ class UtilityMeterSensor(RestoreSensor): """Return the unit the value is expressed in.""" return self._unit_of_measurement - @property - def should_poll(self): - """No polling needed.""" - return False - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 789eb25273c..90045358136 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -88,6 +88,8 @@ class VeluxModule: class VeluxEntity(Entity): """Abstraction for al Velux entities.""" + _attr_should_poll = False + def __init__(self, node): """Initialize the Velux device.""" self.node = node @@ -117,8 +119,3 @@ class VeluxEntity(Entity): if not self.node.name: return "#" + str(self.node.node_id) return self.node.name - - @property - def should_poll(self): - """No polling needed within Velux.""" - return False diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index eb7a1492930..2473d193197 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -76,6 +76,8 @@ async def async_setup_platform( class W800rf32BinarySensor(BinarySensorEntity): """A representation of a w800rf32 binary sensor.""" + _attr_should_poll = False + def __init__(self, device_id, name, device_class=None, off_delay=None): """Initialize the w800rf32 sensor.""" self._signal = W800RF32_DEVICE.format(device_id) @@ -96,11 +98,6 @@ class W800rf32BinarySensor(BinarySensorEntity): """Return the device name.""" return self._name - @property - def should_poll(self): - """No polling needed.""" - return False - @property def device_class(self): """Return the sensor class.""" diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 8418992ac5d..c6ef9610bca 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -96,6 +96,8 @@ def setup_platform( class WaterFurnaceSensor(SensorEntity): """Implementing the Waterfurnace sensor.""" + _attr_should_poll = False + def __init__(self, client, config): """Initialize the sensor.""" self.client = client @@ -131,11 +133,6 @@ class WaterFurnaceSensor(SensorEntity): """Return the units of measurement.""" return self._unit_of_measurement - @property - def should_poll(self): - """Return the polling state.""" - return False - async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index 0472a0bc3b3..d44c3aaefb7 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -138,6 +138,8 @@ class WiffiIntegrationApi: class WiffiEntity(Entity): """Common functionality for all wiffi entities.""" + _attr_should_poll = False + def __init__(self, device, metric, options): """Initialize the base elements of a wiffi entity.""" self._id = generate_unique_id(device, metric) @@ -170,11 +172,6 @@ class WiffiEntity(Entity): ) ) - @property - def should_poll(self): - """Disable polling because data driven .""" - return False - @property def device_info(self): """Return wiffi device info which is shared between all entities of a device.""" diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 11badca8d8c..68238791f48 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -912,6 +912,8 @@ async def async_get_entity_id( class BaseWithingsSensor(Entity): """Base class for withings sensors.""" + _attr_should_poll = False + def __init__(self, data_manager: DataManager, attribute: WithingsAttribute) -> None: """Initialize the Withings sensor.""" self._data_manager = data_manager @@ -922,11 +924,6 @@ class BaseWithingsSensor(Entity): self._unique_id = get_attribute_unique_id(self._attribute, self._user_id) self._state_data: Any | None = None - @property - def should_poll(self) -> bool: - """Return False to indicate HA should not poll for changes.""" - return False - @property def name(self) -> str: """Return the name of the sensor.""" diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index f9283b459e9..b1500e6cedc 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -83,6 +83,8 @@ def get_user_gamercard(api, xuid): class XboxSensor(SensorEntity): """A class for the Xbox account.""" + _attr_should_poll = False + def __init__(self, api, xuid, gamercard, interval): """Initialize the sensor.""" self._state = None @@ -100,11 +102,6 @@ class XboxSensor(SensorEntity): """Return the name of the sensor.""" return self._gamertag - @property - def should_poll(self): - """Return False as this entity has custom polling.""" - return False - @property def native_value(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 199f5dd6c5d..39a9c984a8c 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -181,6 +181,8 @@ async def async_setup_platform( class XiaomiMiioRemote(RemoteEntity): """Representation of a Xiaomi Miio Remote device.""" + _attr_should_poll = False + def __init__(self, friendly_name, device, unique_id, slot, timeout, commands): """Initialize the remote.""" self._name = friendly_name @@ -225,11 +227,6 @@ class XiaomiMiioRemote(RemoteEntity): except DeviceException: return False - @property - def should_poll(self): - """We should not be polled for device up state.""" - return False - async def async_turn_on(self, **kwargs): """Turn the device on.""" _LOGGER.error( diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index cee6253531b..123e62ab3a2 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -79,6 +79,8 @@ async def async_setup_entry( class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): """The musiccast media player.""" + _attr_should_poll = False + def __init__(self, zone_id, name, entry_id, coordinator): """Initialize the musiccast device.""" self._player_state = STATE_PLAYING @@ -119,11 +121,6 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): self.update_all_mc_entities ) - @property - def should_poll(self): - """Push an update after each command.""" - return False - @property def ip_address(self): """Return the ip address of the musiccast device.""" diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 633dc50bcb7..9ceae1fea72 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -124,6 +124,7 @@ class ZhongHongClimate(ClimateEntity): HVACMode.FAN_ONLY, HVACMode.OFF, ] + _attr_should_poll = False _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) @@ -160,11 +161,6 @@ class ZhongHongClimate(ClimateEntity): self._target_temperature = self._device.target_temperature self.schedule_update_ha_state() - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name of the thermostat, if any.""" From 4142530368b67feb96cfabc7b573683726ae6b79 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 21:40:28 +0200 Subject: [PATCH 671/903] Adjust inheritance in ring (#77366) * Adjust inheritance in ring * Fix extra_state_attributes --- homeassistant/components/ring/camera.py | 4 +--- homeassistant/components/ring/entity.py | 25 +++++++++---------------- homeassistant/components/ring/sensor.py | 1 - 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 72d8a51f01e..5bf440cfcd9 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -16,7 +16,7 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from . import ATTRIBUTION, DOMAIN +from . import DOMAIN from .entity import RingEntityMixin FORCE_REFRESH_INTERVAL = timedelta(minutes=3) @@ -48,8 +48,6 @@ async def async_setup_entry( class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" - _attr_attribution = ATTRIBUTION - def __init__(self, config_entry_id, ffmpeg_manager, device): """Initialize a Ring Door Bell camera.""" super().__init__(config_entry_id, device) diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index 84f4816115f..16aa86511be 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -1,30 +1,33 @@ """Base class for Ring entity.""" -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, Entity from . import ATTRIBUTION, DOMAIN -class RingEntityMixin: +class RingEntityMixin(Entity): """Base implementation for Ring device.""" + _attr_attribution = ATTRIBUTION + _attr_should_poll = False + def __init__(self, config_entry_id, device): """Initialize a sensor for Ring device.""" super().__init__() self._config_entry_id = config_entry_id self._device = device + self._attr_extra_state_attributes = {} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.ring_objects["device_data"].async_add_listener(self._update_callback) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks.""" self.ring_objects["device_data"].async_remove_listener(self._update_callback) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_write_ha_state() @@ -33,16 +36,6 @@ class RingEntityMixin: """Return the Ring API objects.""" return self.hass.data[DOMAIN][self._config_entry_id] - @property - def should_poll(self): - """Return False, updates are controlled via the hub.""" - return False - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - @property def device_info(self) -> DeviceInfo: """Return device info.""" diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 4036c9adb4d..26068c149ce 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -42,7 +42,6 @@ class RingSensor(RingEntityMixin, SensorEntity): """A sensor implementation for Ring device.""" entity_description: RingSensorEntityDescription - _attr_should_poll = False # updates are controlled via the hub def __init__( self, From d32f3e359f1fabe2d79b0e07e375b3723b7cb07c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 26 Aug 2022 21:41:41 +0200 Subject: [PATCH 672/903] Use _attr_should_poll in components [o-r] (#77364) --- .../components/opentherm_gw/binary_sensor.py | 7 ++----- homeassistant/components/opentherm_gw/climate.py | 6 +----- homeassistant/components/opentherm_gw/sensor.py | 7 ++----- homeassistant/components/otp/sensor.py | 7 ++----- homeassistant/components/person/__init__.py | 10 ++-------- homeassistant/components/pilight/base_class.py | 7 ++----- homeassistant/components/pilight/sensor.py | 7 ++----- homeassistant/components/plaato/entity.py | 7 ++----- homeassistant/components/plant/__init__.py | 7 ++----- homeassistant/components/plum_lightpad/light.py | 13 +++---------- homeassistant/components/point/__init__.py | 7 ++----- homeassistant/components/qwikswitch/__init__.py | 7 ++----- homeassistant/components/raspyrfm/switch.py | 7 ++----- .../components/remote_rpi_gpio/binary_sensor.py | 7 ++----- homeassistant/components/remote_rpi_gpio/switch.py | 7 ++----- homeassistant/components/roomba/irobot_base.py | 7 ++----- homeassistant/components/roon/media_player.py | 6 +----- .../components/russound_rio/media_player.py | 6 +----- 18 files changed, 34 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index e4880ed26e9..083fb103481 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -83,6 +83,8 @@ async def async_setup_entry( class OpenThermBinarySensor(BinarySensorEntity): """Represent an OpenTherm Gateway binary sensor.""" + _attr_should_poll = False + def __init__(self, gw_dev, var, source, device_class, friendly_name_format): """Initialize the binary sensor.""" self.entity_id = async_generate_entity_id( @@ -162,11 +164,6 @@ class OpenThermBinarySensor(BinarySensorEntity): """Return the class of this device.""" return self._device_class - @property - def should_poll(self): - """Return False because entity pushes its state.""" - return False - class DeprecatedOpenThermBinarySensor(OpenThermBinarySensor): """Represent a deprecated OpenTherm Gateway Binary Sensor.""" diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 0350c446a3f..fecc99a4cca 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -62,6 +62,7 @@ async def async_setup_entry( class OpenThermClimate(ClimateEntity): """Representation of a climate device.""" + _attr_should_poll = False _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) @@ -201,11 +202,6 @@ class OpenThermClimate(ClimateEntity): return PRECISION_HALVES return PRECISION_WHOLE - @property - def should_poll(self): - """Disable polling for this entity.""" - return False - @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 7fb518b0e6d..5eea4fca099 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -86,6 +86,8 @@ async def async_setup_entry( class OpenThermSensor(SensorEntity): """Representation of an OpenTherm Gateway sensor.""" + _attr_should_poll = False + def __init__(self, gw_dev, var, source, device_class, unit, friendly_name_format): """Initialize the OpenTherm Gateway sensor.""" self.entity_id = async_generate_entity_id( @@ -171,11 +173,6 @@ class OpenThermSensor(SensorEntity): """Return the unit of measurement.""" return self._unit - @property - def should_poll(self): - """Return False because entity pushes its state.""" - return False - class DeprecatedOpenThermSensor(OpenThermSensor): """Represent a deprecated OpenTherm Gateway Sensor.""" diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 726e9ed9e42..ff5a2965795 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -44,6 +44,8 @@ async def async_setup_platform( class TOTPSensor(SensorEntity): """Representation of a TOTP sensor.""" + _attr_should_poll = False + def __init__(self, name, token): """Initialize the sensor.""" self._name = name @@ -75,11 +77,6 @@ class TOTPSensor(SensorEntity): """Return the state of the sensor.""" return self._state - @property - def should_poll(self): - """No polling needed.""" - return False - @property def icon(self): """Return the icon to use in the frontend.""" diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 85a6cf6135e..2eb80ed69cc 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -356,6 +356,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class Person(RestoreEntity): """Represent a tracked person.""" + _attr_should_poll = False + def __init__(self, config): """Set up person.""" self._config = config @@ -384,14 +386,6 @@ class Person(RestoreEntity): """Return entity picture.""" return self._config.get(CONF_PICTURE) - @property - def should_poll(self): - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return False - @property def state(self): """Return the state of the person.""" diff --git a/homeassistant/components/pilight/base_class.py b/homeassistant/components/pilight/base_class.py index 517beab850f..cb96d89e6a2 100644 --- a/homeassistant/components/pilight/base_class.py +++ b/homeassistant/components/pilight/base_class.py @@ -56,6 +56,8 @@ SWITCHES_SCHEMA = vol.Schema( class PilightBaseDevice(RestoreEntity): """Base class for pilight switches and lights.""" + _attr_should_poll = False + def __init__(self, hass, name, config): """Initialize a device.""" self._hass = hass @@ -95,11 +97,6 @@ class PilightBaseDevice(RestoreEntity): """Get the name of the switch.""" return self._name - @property - def should_poll(self): - """No polling needed, state set when correct code is received.""" - return False - @property def assumed_state(self): """Return True if unable to access real state of the entity.""" diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 131fd22a780..0f707489e9a 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -51,6 +51,8 @@ def setup_platform( class PilightSensor(SensorEntity): """Representation of a sensor that can be updated using Pilight.""" + _attr_should_poll = False + def __init__(self, hass, name, variable, payload, unit_of_measurement): """Initialize the sensor.""" self._state = None @@ -62,11 +64,6 @@ class PilightSensor(SensorEntity): hass.bus.listen(pilight.EVENT, self._handle_code) - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py index 33ac5d910aa..8bdb7848bb1 100644 --- a/homeassistant/components/plaato/entity.py +++ b/homeassistant/components/plaato/entity.py @@ -19,6 +19,8 @@ from .const import ( class PlaatoEntity(entity.Entity): """Representation of a Plaato Entity.""" + _attr_should_poll = False + def __init__(self, data, sensor_type, coordinator=None): """Initialize the sensor.""" self._coordinator = coordinator @@ -83,11 +85,6 @@ class PlaatoEntity(entity.Entity): 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: diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 75d412f7f5f..0d95ccbc300 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -130,6 +130,8 @@ class Plant(Entity): configurable min and max values. """ + _attr_should_poll = False + READINGS = { READING_BATTERY: { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, @@ -323,11 +325,6 @@ class Plant(Entity): _LOGGER.debug("Initializing from database completed") - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 4b7f34f942f..f990efc3fcc 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -65,6 +65,8 @@ async def async_setup_entry( class PlumLight(LightEntity): """Representation of a Plum Lightpad dimmer.""" + _attr_should_poll = False + def __init__(self, load): """Initialize the light.""" self._load = load @@ -79,11 +81,6 @@ class PlumLight(LightEntity): self._brightness = event["level"] self.schedule_update_ha_state() - @property - def should_poll(self): - """No polling needed.""" - return False - @property def unique_id(self): """Combine logical load ID with .light to guarantee it is unique.""" @@ -142,6 +139,7 @@ class GlowRing(LightEntity): """Representation of a Plum Lightpad dimmer glow ring.""" _attr_color_mode = ColorMode.HS + _attr_should_poll = False _attr_supported_color_modes = {ColorMode.HS} def __init__(self, lightpad): @@ -178,11 +176,6 @@ class GlowRing(LightEntity): """Return the hue and saturation color value [float, float].""" return color_util.color_RGB_to_hs(self._red, self._green, self._blue) - @property - def should_poll(self): - """No polling needed.""" - return False - @property def unique_id(self): """Combine LightPad ID with .glow to guarantee it is unique.""" diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 99108323187..d4b83772300 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -256,6 +256,8 @@ class MinutPointClient: class MinutPointEntity(Entity): """Base Entity used by the sensors.""" + _attr_should_poll = False + def __init__(self, point_client, device_id, device_class): """Initialize the entity.""" self._async_unsub_dispatcher_connect = None @@ -347,11 +349,6 @@ class MinutPointEntity(Entity): last_update = parse_datetime(self.device.last_update) return last_update - @property - def should_poll(self): - """No polling needed for point.""" - return False - @property def unique_id(self): """Return the unique id of the sensor.""" diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index 2df4a2ab73e..d11b71a6dd4 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -72,6 +72,8 @@ CONFIG_SCHEMA = vol.Schema( class QSEntity(Entity): """Qwikswitch Entity base.""" + _attr_should_poll = False + def __init__(self, qsid, name): """Initialize the QSEntity.""" self._name = name @@ -82,11 +84,6 @@ class QSEntity(Entity): """Return the name of the sensor.""" return self._name - @property - def should_poll(self): - """QS sensors gets packets in update_packet.""" - return False - @property def unique_id(self): """Return a unique identifier for this sensor.""" diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index 695a8415cc7..6d4e09a96c8 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -96,6 +96,8 @@ def setup_platform( class RaspyRFMSwitch(SwitchEntity): """Representation of a RaspyRFM switch.""" + _attr_should_poll = False + def __init__(self, raspyrfm_client, name: str, gateway, controlunit): """Initialize the switch.""" self._raspyrfm_client = raspyrfm_client @@ -111,11 +113,6 @@ class RaspyRFMSwitch(SwitchEntity): """Return the name of the device if any.""" return self._name - @property - def should_poll(self): - """Return True if polling should be used.""" - return False - @property def assumed_state(self): """Return True when the current state can not be queried.""" diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index 78272384748..9af1a83b2e9 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -66,6 +66,8 @@ def setup_platform( class RemoteRPiGPIOBinarySensor(BinarySensorEntity): """Represent a binary sensor that uses a Remote Raspberry Pi GPIO.""" + _attr_should_poll = False + def __init__(self, name, sensor, invert_logic): """Initialize the RPi binary sensor.""" self._name = name @@ -84,11 +86,6 @@ class RemoteRPiGPIOBinarySensor(BinarySensorEntity): self._sensor.when_deactivated = read_gpio self._sensor.when_activated = read_gpio - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 5479b035556..9e7aca37663 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -52,6 +52,8 @@ def setup_platform( class RemoteRPiGPIOSwitch(SwitchEntity): """Representation of a Remote Raspberry Pi GPIO.""" + _attr_should_poll = False + def __init__(self, name, led): """Initialize the pin.""" self._name = name or DEVICE_DEFAULT_NAME @@ -63,11 +65,6 @@ class RemoteRPiGPIOSwitch(SwitchEntity): """Return the name of the switch.""" return self._name - @property - def should_poll(self): - """No polling needed.""" - return False - @property def assumed_state(self): """If unable to access real state of the entity.""" diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index dd076f6fb63..f443f72279f 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -60,6 +60,8 @@ STATE_MAP = { class IRobotEntity(Entity): """Base class for iRobot Entities.""" + _attr_should_poll = False + def __init__(self, roomba, blid): """Initialize the iRobot handler.""" self.vacuum = roomba @@ -69,11 +71,6 @@ class IRobotEntity(Entity): self._version = self.vacuum_state.get("softwareVer") self._sku = self.vacuum_state.get("sku") - @property - def should_poll(self): - """Disable polling.""" - return False - @property def robot_unique_id(self): """Return the uniqueid of the vacuum cleaner.""" diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 3651bd9ec05..673316f64a3 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -76,6 +76,7 @@ async def async_setup_entry( class RoonDevice(MediaPlayerEntity): """Representation of an Roon device.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.GROUPING @@ -295,11 +296,6 @@ class RoonDevice(MediaPlayerEntity): """Return the id of this roon client.""" return self._unique_id - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False - @property def zone_id(self): """Return current session Id.""" diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 054448cd9ad..e905ab0c726 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -70,6 +70,7 @@ async def async_setup_platform( class RussoundZoneDevice(MediaPlayerEntity): """Representation of a Russound Zone.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_SET @@ -119,11 +120,6 @@ class RussoundZoneDevice(MediaPlayerEntity): self._russ.add_zone_callback(self._zone_callback_handler) self._russ.add_source_callback(self._source_callback_handler) - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the zone.""" From 487cd313c1c80c7472b97b3319d35de19703ee95 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Fri, 26 Aug 2022 19:15:29 -0400 Subject: [PATCH 673/903] Bump version of pyunifiprotect to 4.1.8 (#77389) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 41b62e3631b..78278993178 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.1.7", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.1.8", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index ec53475282c..2ba19d5cd1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2019,7 +2019,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.7 +pyunifiprotect==4.1.8 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27c96bf2599..696f4c0aa8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1382,7 +1382,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.7 +pyunifiprotect==4.1.8 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From c916fcb2c6e9f51f4902677271a46eb4423c3eda Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 27 Aug 2022 00:24:30 +0000 Subject: [PATCH 674/903] [ci skip] Translation update --- .../android_ip_webcam/translations/ca.json | 3 +- .../automation/translations/ca.json | 12 ++++++ .../automation/translations/de.json | 13 ++++++ .../automation/translations/el.json | 13 ++++++ .../automation/translations/es.json | 13 ++++++ .../automation/translations/fr.json | 12 ++++++ .../automation/translations/hu.json | 13 ++++++ .../automation/translations/id.json | 13 ++++++ .../automation/translations/no.json | 13 ++++++ .../automation/translations/zh-Hant.json | 13 ++++++ .../components/bluetooth/translations/ca.json | 12 +++++- .../components/bluetooth/translations/hu.json | 6 ++- .../components/bluetooth/translations/ru.json | 6 ++- .../lacrosse_view/translations/ca.json | 3 +- .../components/lametric/translations/ca.json | 40 +++++++++++++++++++ .../components/lametric/translations/de.json | 2 +- .../landisgyr_heat_meter/translations/ca.json | 23 +++++++++++ .../components/mqtt/translations/el.json | 6 +++ .../components/mqtt/translations/hu.json | 6 +++ .../components/mqtt/translations/no.json | 6 +++ .../p1_monitor/translations/ca.json | 3 ++ .../pure_energie/translations/ca.json | 3 ++ .../components/pushover/translations/ca.json | 34 ++++++++++++++++ .../components/risco/translations/ca.json | 18 +++++++++ .../components/risco/translations/hu.json | 18 +++++++++ .../components/risco/translations/ru.json | 18 +++++++++ .../components/skybell/translations/ca.json | 13 ++++++ .../components/skybell/translations/el.json | 7 ++++ .../components/skybell/translations/hu.json | 7 ++++ .../components/skybell/translations/no.json | 7 ++++ .../speedtestdotnet/translations/el.json | 13 ++++++ .../speedtestdotnet/translations/es.json | 13 ++++++ .../components/thermopro/translations/ca.json | 21 ++++++++++ .../components/thermopro/translations/el.json | 21 ++++++++++ .../components/thermopro/translations/hu.json | 21 ++++++++++ .../components/thermopro/translations/no.json | 21 ++++++++++ .../volvooncall/translations/ca.json | 28 +++++++++++++ .../components/zha/translations/ca.json | 3 ++ 38 files changed, 489 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/lametric/translations/ca.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/ca.json create mode 100644 homeassistant/components/pushover/translations/ca.json create mode 100644 homeassistant/components/thermopro/translations/ca.json create mode 100644 homeassistant/components/thermopro/translations/el.json create mode 100644 homeassistant/components/thermopro/translations/hu.json create mode 100644 homeassistant/components/thermopro/translations/no.json create mode 100644 homeassistant/components/volvooncall/translations/ca.json diff --git a/homeassistant/components/android_ip_webcam/translations/ca.json b/homeassistant/components/android_ip_webcam/translations/ca.json index daebd1c3cd6..5b582b1467b 100644 --- a/homeassistant/components/android_ip_webcam/translations/ca.json +++ b/homeassistant/components/android_ip_webcam/translations/ca.json @@ -4,7 +4,8 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { "user": { diff --git a/homeassistant/components/automation/translations/ca.json b/homeassistant/components/automation/translations/ca.json index c1d35331e2b..4a6cc33e04c 100644 --- a/homeassistant/components/automation/translations/ca.json +++ b/homeassistant/components/automation/translations/ca.json @@ -1,4 +1,16 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "title": "{name} utilitza un servei desconegut" + } + } + }, + "title": "{name} utilitza un servei desconegut" + } + }, "state": { "_": { "off": "OFF", diff --git a/homeassistant/components/automation/translations/de.json b/homeassistant/components/automation/translations/de.json index 9920c73d447..9cadf8c51f8 100644 --- a/homeassistant/components/automation/translations/de.json +++ b/homeassistant/components/automation/translations/de.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Die Automatisierung \"{name}\" (`{entity_id}`) hat eine Aktion, die einen unbekannten Dienst aufruft: `{service}`.\n\nDieser Fehler verhindert, dass die Automatisierung ordnungsgem\u00e4\u00df ausgef\u00fchrt wird. Vielleicht ist dieser Dienst nicht mehr verf\u00fcgbar oder vielleicht hat ein Tippfehler ihn verursacht.\n\nUm diesen Fehler zu beheben, [bearbeite die Automatisierung]({edit}) und entferne die Aktion, die diesen Dienst aufruft.\n\nKlicke unten auf SENDEN, um zu best\u00e4tigen, dass du diese Automatisierung korrigiert hast.", + "title": "{name} verwendet einen unbekannten Dienst" + } + } + }, + "title": "{name} verwendet einen unbekannten Dienst" + } + }, "state": { "_": { "off": "Aus", diff --git a/homeassistant/components/automation/translations/el.json b/homeassistant/components/automation/translations/el.json index 064b36438c6..0eaaebb540c 100644 --- a/homeassistant/components/automation/translations/el.json +++ b/homeassistant/components/automation/translations/el.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u039f \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2 \"{name}\" (`{entity_id}`) \u03ad\u03c7\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03ba\u03b1\u03bb\u03b5\u03af \u03bc\u03b9\u03b1 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03b7 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1: `{service}`.\n\n\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03bc\u03c0\u03bf\u03b4\u03af\u03b6\u03b5\u03b9 \u03c4\u03b7\u03bd \u03bf\u03c1\u03b8\u03ae \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd. \u038a\u03c3\u03c9\u03c2 \u03b1\u03c5\u03c4\u03ae \u03b7 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03ae \u03af\u03c3\u03c9\u03c2 \u03ad\u03bd\u03b1 \u03c4\u03c5\u03c0\u03bf\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bb\u03ac\u03b8\u03bf\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03c0\u03c1\u03bf\u03ba\u03ac\u03bb\u03b5\u03c3\u03b5.\n\n\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7]({edit}) \u03ba\u03b1\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03ba\u03b1\u03bb\u03b5\u03af \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1.\n\n\u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf SUBMIT (\u03a5\u03a0\u039f\u0392\u039f\u039b\u0397) \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b1\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03cc.", + "title": "{name} \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03bc\u03b9\u03b1 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03b7 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1" + } + } + }, + "title": "{name} \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03bc\u03b9\u03b1 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03b7 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1" + } + }, "state": { "_": { "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", diff --git a/homeassistant/components/automation/translations/es.json b/homeassistant/components/automation/translations/es.json index 08d1cc7df07..75f137b717e 100644 --- a/homeassistant/components/automation/translations/es.json +++ b/homeassistant/components/automation/translations/es.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "La automatizaci\u00f3n \"{name}\" (`{entity_id}`) tiene una acci\u00f3n que llama a un servicio desconocido: `{service}`.\n\nEste error impide que la automatizaci\u00f3n se ejecute correctamente. Tal vez este servicio ya no est\u00e1 disponible, o quiz\u00e1s est\u00e1 causado por un error tipogr\u00e1fico.\n\nPara corregir este error, [edita la automatizaci\u00f3n]({edit}) y elimina la acci\u00f3n que llama a este servicio.\n\nHaz clic en ENVIAR a continuaci\u00f3n para confirmar que has corregido esta automatizaci\u00f3n.", + "title": "{name} usa un servicio desconocido" + } + } + }, + "title": "{name} usa un servicio desconocido" + } + }, "state": { "_": { "off": "Apagada", diff --git a/homeassistant/components/automation/translations/fr.json b/homeassistant/components/automation/translations/fr.json index 62731da356a..399d193b60d 100644 --- a/homeassistant/components/automation/translations/fr.json +++ b/homeassistant/components/automation/translations/fr.json @@ -1,4 +1,16 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "title": "{name} utilise un service inconnu" + } + } + }, + "title": "{name} utilise un service inconnu" + } + }, "state": { "_": { "off": "D\u00e9sactiv\u00e9", diff --git a/homeassistant/components/automation/translations/hu.json b/homeassistant/components/automation/translations/hu.json index 559523b1b12..5ebb2c43351 100644 --- a/homeassistant/components/automation/translations/hu.json +++ b/homeassistant/components/automation/translations/hu.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "A \"{name}\" (`{entity_id}`) automatizmusnak van egy m\u0171velete, amely egy ismeretlen szolg\u00e1ltat\u00e1st h\u00edv meg: `{service}`.\n\nEz a hiba megakad\u00e1lyozza az automatizmus megfelel\u0151 m\u0171k\u00f6d\u00e9s\u00e9t. Lehet, hogy ez a szolg\u00e1ltat\u00e1s m\u00e1r nem \u00e9rhet\u0151 el, vagy tal\u00e1n egy el\u00edr\u00e1s okozta.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz [szerkessze az automatizmust]({edit}), \u00e9s t\u00e1vol\u00edtsa el a szolg\u00e1ltat\u00e1st h\u00edv\u00f3 m\u0171veletet.\n\nKattintson az al\u00e1bbi MEHET gombra annak meger\u0151s\u00edt\u00e9s\u00e9hez, hogy jav\u00edtotta-e ezt az automatiz\u00e1l\u00e1st.", + "title": "{name} egy ismeretlen szolg\u00e1ltat\u00e1st haszn\u00e1l" + } + } + }, + "title": "{name} egy ismeretlen szolg\u00e1ltat\u00e1st haszn\u00e1l" + } + }, "state": { "_": { "off": "Ki", diff --git a/homeassistant/components/automation/translations/id.json b/homeassistant/components/automation/translations/id.json index 58e8497c8b9..acf1dfab41b 100644 --- a/homeassistant/components/automation/translations/id.json +++ b/homeassistant/components/automation/translations/id.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Otomasi \"{nama}\" (`{entity_id}`) memiliki aksi yang memanggil layanan yang tidak diketahui: `{service}`.\n\nKesalahan ini mencegah otomasi berjalan dengan benar. Mungkin layanan ini tidak lagi tersedia, atau mungkin kesalahan ketik yang menyebabkannya.\n\nUntuk memperbaiki kesalahan ini, [edit otomasi]({edit}) dan hapus aksi yang memanggil layanan ini.\n\nKlik KIRIM di bawah ini untuk mengonfirmasi bahwa Anda telah memperbaiki otomasi ini.", + "title": "{name} menggunakan layanan yang tidak dikenal" + } + } + }, + "title": "{name} menggunakan layanan yang tidak dikenal" + } + }, "state": { "_": { "off": "Mati", diff --git a/homeassistant/components/automation/translations/no.json b/homeassistant/components/automation/translations/no.json index 64e00db42ca..b1508f8270e 100644 --- a/homeassistant/components/automation/translations/no.json +++ b/homeassistant/components/automation/translations/no.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Automatiseringen \" {name} \" (` {entity_id} `) har en handling som kaller en ukjent tjeneste: ` {service} `. \n\n Denne feilen hindrer automatiseringen i \u00e5 kj\u00f8re riktig. Kanskje denne tjenesten ikke lenger er tilgjengelig, eller kanskje en skrivefeil for\u00e5rsaket det. \n\n For \u00e5 fikse denne feilen, [rediger automatiseringen]( {edit} ) og fjern handlingen som kaller denne tjenesten. \n\n Klikk p\u00e5 SEND nedenfor for \u00e5 bekrefte at du har fikset denne automatiseringen.", + "title": "{name} bruker en ukjent tjeneste" + } + } + }, + "title": "{name} bruker en ukjent tjeneste" + } + }, "state": { "_": { "off": "Av", diff --git a/homeassistant/components/automation/translations/zh-Hant.json b/homeassistant/components/automation/translations/zh-Hant.json index 3fd099ef8d8..8665cbb9ed7 100644 --- a/homeassistant/components/automation/translations/zh-Hant.json +++ b/homeassistant/components/automation/translations/zh-Hant.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u81ea\u52d5\u5316 \"{name}\" (`{entity_id}`) \u89f8\u767c\u52d5\u4f5c\u4f7f\u7528\u4e86\u672a\u77e5\u670d\u52d9\uff1a`{service}`\u3002\n\n\u6b64\u932f\u8aa4\u6703\u5c0e\u81f4\u81ea\u52d5\u5316\u7121\u6cd5\u6b63\u5e38\u57f7\u884c\u3002\u53ef\u80fd\u539f\u56e0\u70ba\u8a72\u670d\u52d9\u7121\u6cd5\u4f7f\u7528\u6216\u53ef\u80fd\u53ea\u662f\u8f38\u5165\u932f\u8aa4\u6240\u5c0e\u81f4\u3002\n\n\u6b32\u4fee\u6b63\u6b64\u932f\u8aa4\uff0c[\u7de8\u8f2f\u81ea\u52d5\u5316]({edit}) \u4e26\u79fb\u9664\u4f7f\u7528\u8a72\u670d\u52d9\u4e4b\u89f8\u767c\u52d5\u4f5c\u3002\n\n\u9ede\u9078\u4e0b\u65b9\u50b3\u9001\u4ee5\u4fee\u6b63\u6b64\u81ea\u52d5\u5316\u3002", + "title": "{name} \u4f7f\u7528\u672a\u77e5\u670d\u52d9" + } + } + }, + "title": "{name} \u4f7f\u7528\u672a\u77e5\u670d\u52d9" + } + }, "state": { "_": { "off": "\u95dc\u9589", diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json index 082803a48dc..6c1554dc0d9 100644 --- a/homeassistant/components/bluetooth/translations/ca.json +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Vols configurar Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adaptador" + }, + "description": "Selecciona un adaptador Bluetooth per configurar-lo" + }, + "single_adapter": { + "description": "Vols configurar l'adaptador Bluetooth {name}?" + }, "user": { "data": { "address": "Dispositiu" @@ -24,7 +33,8 @@ "step": { "init": { "data": { - "adapter": "Adaptador Bluetooth a utilitzar per escanejar" + "adapter": "Adaptador Bluetooth a utilitzar per escanejar", + "passive": "Escolta passiva" } } } diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json index 591362de0e3..a79bac619d8 100644 --- a/homeassistant/components/bluetooth/translations/hu.json +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "A szkennel\u00e9shez haszn\u00e1lhat\u00f3 Bluetooth-adapter" - } + "adapter": "A szkennel\u00e9shez haszn\u00e1lhat\u00f3 Bluetooth-adapter", + "passive": "Passz\u00edv hallgat\u00e1s" + }, + "description": "A passz\u00edv hallgat\u00e1shoz BlueZ 5.63 vagy \u00fajabb verzi\u00f3ra van sz\u00fcks\u00e9g, a k\u00eds\u00e9rleti funkci\u00f3k enged\u00e9lyez\u00e9s\u00e9vel." } } } diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json index 17108371409..3270f8b840d 100644 --- a/homeassistant/components/bluetooth/translations/ru.json +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" - } + "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0435 \u0441\u043b\u0443\u0448\u0430\u043d\u0438\u0435" + }, + "description": "\u0414\u043b\u044f \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f BlueZ 5.63 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u044f\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0441 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u043c\u0438 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u043c\u0438." } } } diff --git a/homeassistant/components/lacrosse_view/translations/ca.json b/homeassistant/components/lacrosse_view/translations/ca.json index bdf5e41bf54..3c4dc4f8c4b 100644 --- a/homeassistant/components/lacrosse_view/translations/ca.json +++ b/homeassistant/components/lacrosse_view/translations/ca.json @@ -1,7 +1,8 @@ { "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": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", diff --git a/homeassistant/components/lametric/translations/ca.json b/homeassistant/components/lametric/translations/ca.json new file mode 100644 index 00000000000..bdeed6a5759 --- /dev/null +++ b/homeassistant/components/lametric/translations/ca.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "invalid_discovery_info": "S'ha rebut informaci\u00f3 de descobriment no v\u00e0lida", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "Introdueix manualment", + "pick_implementation": "Importa des de LaMetric.com (recomanat)" + } + }, + "manual_entry": { + "data": { + "api_key": "Clau API", + "host": "Amfitri\u00f3" + }, + "data_description": { + "api_key": "Pots trobar aquesta clau API a la [p\u00e0gina de dispositius del teu compte de desenvolupador de LaMetric](https://developer.lametric.com/user/devices).", + "host": "Adre\u00e7a IP o nom d'amfitri\u00f3 de LaMetric TIME dins la teva xarxa." + } + }, + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "user_cloud_select_device": { + "data": { + "device": "Selecciona el dispositiu LaMetric que vulguis afegir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/de.json b/homeassistant/components/lametric/translations/de.json index d44ad04ec0a..dab436c4b2d 100644 --- a/homeassistant/components/lametric/translations/de.json +++ b/homeassistant/components/lametric/translations/de.json @@ -32,7 +32,7 @@ } }, "pick_implementation": { - "title": "W\u00e4hle eine Authentifizierungsmethode" + "title": "W\u00e4hle die Authentifizierungsmethode" }, "user_cloud_select_device": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ca.json b/homeassistant/components/landisgyr_heat_meter/translations/ca.json new file mode 100644 index 00000000000..1ad4d3b9362 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Ruta del dispositiu USB" + } + }, + "user": { + "data": { + "device": "Selecciona dispositiu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/el.json b/homeassistant/components/mqtt/translations/el.json index 6921604bdee..129b2de45f8 100644 --- a/homeassistant/components/mqtt/translations/el.json +++ b/homeassistant/components/mqtt/translations/el.json @@ -49,6 +49,12 @@ "button_triple_press": "\u03a4\u03c1\u03b9\u03c0\u03bb\u03cc \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \"{subtype}\"" } }, + "issues": { + "deprecated_yaml": { + "description": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 {\u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1}(\u03b5\u03c2) MQTT \u03ba\u03ac\u03c4\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 `{platform}`.\n\n\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03bc\u03b5\u03c4\u03b1\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 `mqtt` \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({more_info_url}), \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", + "title": "\u039f\u03b9 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b5\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b1\u03c2 MQTT {platform}(s) \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03bf\u03c7\u03ae" + } + }, "options": { "error": { "bad_birth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 birth.", diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index 0203014d19d..805b8106fba 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" tripla kattint\u00e1s" } }, + "issues": { + "deprecated_yaml": { + "description": "A manu\u00e1lisan konfigur\u00e1lt MQTT {platform} a `{platform}` platformkulcs alatt tal\u00e1lhat\u00f3. \n\n A probl\u00e9ma megold\u00e1s\u00e1hoz helyezze \u00e1t a konfigur\u00e1ci\u00f3t az \"mqtt\" integr\u00e1ci\u00f3s kulcsra, \u00e9s ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st. Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse meg a [dokument\u00e1ci\u00f3t]({more_info_url}).", + "title": "A manu\u00e1lisan konfigur\u00e1lt MQTT {platform} figyelmet ig\u00e9nyel" + } + }, "options": { "error": { "bad_birth": "\u00c9rv\u00e9nytelen 'birth' topik.", diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index b6f7753d3a9..aa1633f6b27 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" trippel klikket" } }, + "issues": { + "deprecated_yaml": { + "description": "Manuelt konfigurert MQTT {platform} (er) funnet under plattformn\u00f8kkelen ` {platform} `. \n\n Flytt konfigurasjonen til `mqtt`-integrasjonsn\u00f8kkelen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet. Se [dokumentasjonen]( {more_info_url} ), for mer informasjon.", + "title": "Din manuelt konfigurerte MQTT {platform} (er) trenger oppmerksomhet" + } + }, "options": { "error": { "bad_birth": "Ugyldig f\u00f8dselsemne.", diff --git a/homeassistant/components/p1_monitor/translations/ca.json b/homeassistant/components/p1_monitor/translations/ca.json index d82a93389fe..6d65ba16b5c 100644 --- a/homeassistant/components/p1_monitor/translations/ca.json +++ b/homeassistant/components/p1_monitor/translations/ca.json @@ -9,6 +9,9 @@ "host": "Amfitri\u00f3", "name": "Nom" }, + "data_description": { + "host": "Adre\u00e7a IP o nom d'amfitri\u00f3 de la instal\u00b7laci\u00f3 P1 Monitor." + }, "description": "Configura la integraci\u00f3 de P1 Monitor amb Home Assistant." } } diff --git a/homeassistant/components/pure_energie/translations/ca.json b/homeassistant/components/pure_energie/translations/ca.json index cb725e87646..eaeb8c7c2ef 100644 --- a/homeassistant/components/pure_energie/translations/ca.json +++ b/homeassistant/components/pure_energie/translations/ca.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Amfitri\u00f3" + }, + "data_description": { + "host": "Adre\u00e7a IP o nom d'amfitri\u00f3 del teu mesurador Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/ca.json b/homeassistant/components/pushover/translations/ca.json new file mode 100644 index 00000000000..1a14a4ce3d5 --- /dev/null +++ b/homeassistant/components/pushover/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "invalid_user_key": "Clau d'usuari inv\u00e0lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "api_key": "Clau API", + "name": "Nom", + "user_key": "Clau d'usuari" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Pushover mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Pushover del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Pushover est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/ca.json b/homeassistant/components/risco/translations/ca.json index 2872924dbbb..072f9521543 100644 --- a/homeassistant/components/risco/translations/ca.json +++ b/homeassistant/components/risco/translations/ca.json @@ -9,11 +9,29 @@ "unknown": "Error inesperat" }, "step": { + "cloud": { + "data": { + "password": "Contrasenya", + "pin": "Codi PIN", + "username": "Nom d'usuari" + } + }, + "local": { + "data": { + "host": "Amfitri\u00f3", + "pin": "Codi PIN", + "port": "Port" + } + }, "user": { "data": { "password": "Contrasenya", "pin": "Codi PIN", "username": "Nom d'usuari" + }, + "menu_options": { + "cloud": "Risco Cloud (recomanat)", + "local": "Panell Risco local (avan\u00e7at)" } } } diff --git a/homeassistant/components/risco/translations/hu.json b/homeassistant/components/risco/translations/hu.json index 198c30d2b02..0de2158f626 100644 --- a/homeassistant/components/risco/translations/hu.json +++ b/homeassistant/components/risco/translations/hu.json @@ -9,11 +9,29 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "cloud": { + "data": { + "password": "Jelsz\u00f3", + "pin": "PIN-k\u00f3d", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "local": { + "data": { + "host": "C\u00edm", + "pin": "PIN-k\u00f3d", + "port": "Port" + } + }, "user": { "data": { "password": "Jelsz\u00f3", "pin": "PIN-k\u00f3d", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "menu_options": { + "cloud": "Risco Cloud (aj\u00e1nlott)", + "local": "Helyi Risco panel (halad\u00f3)" } } } diff --git a/homeassistant/components/risco/translations/ru.json b/homeassistant/components/risco/translations/ru.json index 200c38fb213..116168a60de 100644 --- a/homeassistant/components/risco/translations/ru.json +++ b/homeassistant/components/risco/translations/ru.json @@ -9,11 +9,29 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "cloud": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "pin": "PIN-\u043a\u043e\u0434", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, + "local": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "pin": "PIN-\u043a\u043e\u0434", + "port": "\u041f\u043e\u0440\u0442" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "pin": "PIN-\u043a\u043e\u0434", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "menu_options": { + "cloud": "\u041e\u0431\u043b\u0430\u043a\u043e Risco (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)", + "local": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u0430\u043d\u0435\u043b\u044c Risco (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f)" } } } diff --git a/homeassistant/components/skybell/translations/ca.json b/homeassistant/components/skybell/translations/ca.json index 6aea0bdfc8f..498ba049582 100644 --- a/homeassistant/components/skybell/translations/ca.json +++ b/homeassistant/components/skybell/translations/ca.json @@ -10,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Si us plau, actualitza la contrasenya de {email}", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "email": "Correu electr\u00f2nic", @@ -17,5 +24,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de Skybell mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Skybell s'ha eliminat" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/el.json b/homeassistant/components/skybell/translations/el.json index 0f049c3cc9f..38ea01e7e9e 100644 --- a/homeassistant/components/skybell/translations/el.json +++ b/homeassistant/components/skybell/translations/el.json @@ -10,6 +10,13 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {email}", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/skybell/translations/hu.json b/homeassistant/components/skybell/translations/hu.json index 08a151e711b..ca267fab1d9 100644 --- a/homeassistant/components/skybell/translations/hu.json +++ b/homeassistant/components/skybell/translations/hu.json @@ -10,6 +10,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rj\u00fck, friss\u00edtse jelszav\u00e1t a k\u00f6vetkez\u0151h\u00f6z: {email}", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "email": "E-mail", diff --git a/homeassistant/components/skybell/translations/no.json b/homeassistant/components/skybell/translations/no.json index 365009e60d3..25735dcf804 100644 --- a/homeassistant/components/skybell/translations/no.json +++ b/homeassistant/components/skybell/translations/no.json @@ -10,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Vennligst oppdater passordet ditt for {email}", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "email": "E-post", diff --git a/homeassistant/components/speedtestdotnet/translations/el.json b/homeassistant/components/speedtestdotnet/translations/el.json index 25b5e23ab69..1b4637fd76a 100644 --- a/homeassistant/components/speedtestdotnet/translations/el.json +++ b/homeassistant/components/speedtestdotnet/translations/el.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `homeassistant.update_entity` \u03bc\u03b5 \u03ad\u03bd\u03b1 target Speedtest entity_id. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03a0\u039f\u0392\u039f\u039b\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c3\u03b7\u03bc\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1 \u03c9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03c5\u03bc\u03ad\u03bd\u03bf.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 speedtest \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } + }, + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 speedtest \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index 9ba5fcbd4bb..e9ce4b15f9b 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `homeassistant.update_entity` con un entity_id de Speedtest como objetivo. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", + "title": "El servicio speedtest se va a eliminar" + } + } + }, + "title": "El servicio speedtest se va a eliminar" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/thermopro/translations/ca.json b/homeassistant/components/thermopro/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/thermopro/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/el.json b/homeassistant/components/thermopro/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/thermopro/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/hu.json b/homeassistant/components/thermopro/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/thermopro/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/no.json b/homeassistant/components/thermopro/translations/no.json new file mode 100644 index 00000000000..28ec4582177 --- /dev/null +++ b/homeassistant/components/thermopro/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/ca.json b/homeassistant/components/volvooncall/translations/ca.json new file mode 100644 index 00000000000..12012ffc522 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "mutable": "Permet l'engegada / bloqueig / etc, remot.", + "password": "Contrasenya", + "region": "Regi\u00f3", + "scandinavian_miles": "Utilitza milles escandinaves", + "username": "Nom d'usuari" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de la plataforma 'Volvo On Call' mitjan\u00e7ant YAML s'eliminar\u00e0 en una versi\u00f3 futura de Home Assistant. \n\nLa configuraci\u00f3 existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Volvo On Call est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index dbb8a20be82..6d387bc1306 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -13,6 +13,9 @@ "confirm": { "description": "Vols configurar {name}?" }, + "confirm_hardware": { + "description": "Vols configurar {name}?" + }, "pick_radio": { "data": { "radio_type": "Tipus de r\u00e0dio" From 2b9116f1f866226dee8ca468be364feb63ea6453 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 27 Aug 2022 04:03:50 +0200 Subject: [PATCH 675/903] Use _attr_should_poll in components [s-t] (#77368) * Use _attr_should_poll in components [s-t] * Adjust touchline Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/saj/sensor.py | 7 ++----- .../components/satel_integra/binary_sensor.py | 7 ++----- homeassistant/components/satel_integra/switch.py | 7 ++----- homeassistant/components/scsgate/cover.py | 7 ++----- homeassistant/components/scsgate/switch.py | 7 ++----- homeassistant/components/sense/binary_sensor.py | 7 ++----- homeassistant/components/serial/sensor.py | 7 ++----- .../components/sighthound/image_processing.py | 6 +----- homeassistant/components/smartthings/__init__.py | 7 ++----- homeassistant/components/snapcast/media_player.py | 12 ++---------- homeassistant/components/songpal/media_player.py | 6 +----- homeassistant/components/srp_energy/sensor.py | 7 ++----- homeassistant/components/starline/entity.py | 7 ++----- homeassistant/components/syncthing/sensor.py | 7 ++----- homeassistant/components/tado/entity.py | 14 ++++---------- homeassistant/components/tellduslive/entry.py | 7 ++----- .../components/threshold/binary_sensor.py | 7 ++----- homeassistant/components/tod/binary_sensor.py | 7 ++----- homeassistant/components/touchline/climate.py | 5 ----- homeassistant/components/transmission/sensor.py | 7 ++----- homeassistant/components/transmission/switch.py | 7 ++----- homeassistant/components/trend/binary_sensor.py | 7 ++----- 22 files changed, 42 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 818ce7ea57a..eec6755e015 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -176,6 +176,8 @@ def async_track_time_interval_backoff(hass, action) -> CALLBACK_TYPE: class SAJsensor(SensorEntity): """Representation of a SAJ sensor.""" + _attr_should_poll = False + def __init__(self, serialnumber, pysaj_sensor, inverter_name=None): """Initialize the SAJ sensor.""" self._sensor = pysaj_sensor @@ -216,11 +218,6 @@ class SAJsensor(SensorEntity): if self.native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT): return SensorDeviceClass.TEMPERATURE - @property - def should_poll(self) -> bool: - """SAJ sensors are updated & don't poll.""" - return False - @property def per_day_basis(self) -> bool: """Return if the sensors value is on daily basis or not.""" diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 2206b88a7cd..f55545239fb 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -60,6 +60,8 @@ async def async_setup_platform( class SatelIntegraBinarySensor(BinarySensorEntity): """Representation of an Satel Integra binary sensor.""" + _attr_should_poll = False + def __init__( self, controller, device_number, device_name, zone_type, react_to_signal ): @@ -100,11 +102,6 @@ class SatelIntegraBinarySensor(BinarySensorEntity): if self._zone_type is BinarySensorDeviceClass.SMOKE: return "mdi:fire" - @property - def should_poll(self): - """No polling needed.""" - return False - @property def is_on(self): """Return true if sensor is on.""" diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index f465ef1c62c..7c7b91c4ac6 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -51,6 +51,8 @@ async def async_setup_platform( class SatelIntegraSwitch(SwitchEntity): """Representation of an Satel switch.""" + _attr_should_poll = False + def __init__(self, controller, device_number, device_name, code): """Initialize the binary_sensor.""" self._device_number = device_number @@ -104,8 +106,3 @@ class SatelIntegraSwitch(SwitchEntity): def name(self): """Return the name of the switch.""" return self._name - - @property - def should_poll(self): - """Don't poll.""" - return False diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index 4aa08cae3bd..f68b089e2d7 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -59,6 +59,8 @@ def setup_platform( class SCSGateCover(CoverEntity): """Representation of SCSGate cover.""" + _attr_should_poll = False + def __init__(self, scs_id, name, logger, scsgate): """Initialize the cover.""" self._scs_id = scs_id @@ -71,11 +73,6 @@ class SCSGateCover(CoverEntity): """Return the SCSGate ID.""" return self._scs_id - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @property def name(self) -> str: """Return the name of the cover.""" diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index 0520f352186..b9c25207745 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -91,6 +91,8 @@ def _setup_scenario_switches(logger, config, scsgate, hass): class SCSGateSwitch(SwitchEntity): """Representation of a SCSGate switch.""" + _attr_should_poll = False + def __init__(self, scs_id, name, logger, scsgate): """Initialize the switch.""" self._name = name @@ -104,11 +106,6 @@ class SCSGateSwitch(SwitchEntity): """Return the SCS ID.""" return self._scs_id - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the device if any.""" diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index 10399fa8e2b..b9c1a3cb9eb 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -73,6 +73,8 @@ def sense_to_mdi(sense_icon): class SenseDevice(BinarySensorEntity): """Implementation of a Sense energy device binary sensor.""" + _attr_should_poll = False + def __init__(self, sense_devices_data, device, sense_monitor_id): """Initialize the Sense binary sensor.""" self._name = device["name"] @@ -124,11 +126,6 @@ class SenseDevice(BinarySensorEntity): """Return the device class of the binary sensor.""" return BinarySensorDeviceClass.POWER - @property - def should_poll(self): - """Return the deviceshould not poll for updates.""" - return False - async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 7e5f0bf0fd5..9f1e04f1373 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -113,6 +113,8 @@ async def async_setup_platform( class SerialSensor(SensorEntity): """Representation of a Serial sensor.""" + _attr_should_poll = False + def __init__( self, name, @@ -242,11 +244,6 @@ class SerialSensor(SensorEntity): """Return the name of the sensor.""" return self._name - @property - def should_poll(self): - """No polling needed.""" - return False - @property def extra_state_attributes(self): """Return the attributes of the entity (if any JSON present).""" diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index 605c6c63344..69776f4e2ac 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -86,6 +86,7 @@ def setup_platform( class SighthoundEntity(ImageProcessingEntity): """Create a sighthound entity.""" + _attr_should_poll = False _attr_unit_of_measurement = ATTR_PEOPLE def __init__( @@ -167,11 +168,6 @@ class SighthoundEntity(ImageProcessingEntity): """Return the name of the sensor.""" return self._name - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def state(self): """Return the state of the entity.""" diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index c45702773b2..f5b462e9642 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -405,6 +405,8 @@ class DeviceBroker: class SmartThingsEntity(Entity): """Defines a SmartThings entity.""" + _attr_should_poll = False + def __init__(self, device: DeviceEntity) -> None: """Initialize the instance.""" self._device = device @@ -443,11 +445,6 @@ class SmartThingsEntity(Entity): """Return the name of the device.""" return self._device.label - @property - def should_poll(self) -> bool: - """No polling needed for this device.""" - return False - @property def unique_id(self) -> str: """Return a unique ID.""" diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index ad71266d65c..703cb41a38f 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -116,6 +116,7 @@ async def handle_set_latency(entity, service_call): class SnapcastGroupDevice(MediaPlayerEntity): """Representation of a Snapcast group device.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_SET @@ -173,11 +174,6 @@ class SnapcastGroupDevice(MediaPlayerEntity): name = f"{self._group.friendly_name} {GROUP_SUFFIX}" return {"friendly_name": name} - @property - def should_poll(self): - """Do not poll for state.""" - return False - async def async_select_source(self, source): """Set input source.""" streams = self._group.streams_by_name() @@ -208,6 +204,7 @@ class SnapcastGroupDevice(MediaPlayerEntity): class SnapcastClientDevice(MediaPlayerEntity): """Representation of a Snapcast client device.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_SET @@ -276,11 +273,6 @@ class SnapcastClientDevice(MediaPlayerEntity): state_attrs["friendly_name"] = name return state_attrs - @property - def should_poll(self): - """Do not poll for state.""" - return False - @property def latency(self): """Latency for Client.""" diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index fed1762bb44..973c57adbbb 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -89,6 +89,7 @@ async def async_setup_entry( class SongpalEntity(MediaPlayerEntity): """Class representing a Songpal device.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP @@ -118,11 +119,6 @@ class SongpalEntity(MediaPlayerEntity): self._active_source = None self._sources = {} - @property - def should_poll(self): - """Return True if the device should be polled.""" - return False - async def async_added_to_hass(self): """Run when entity is added to hass.""" await self.async_activate_websocket() diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 683616a5e56..f1a97af9820 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -83,6 +83,8 @@ async def async_setup_entry( class SrpEntity(SensorEntity): """Implementation of a Srp Energy Usage sensor.""" + _attr_should_poll = False + def __init__(self, coordinator): """Initialize the SrpEntity class.""" self._name = SENSOR_NAME @@ -125,11 +127,6 @@ class SrpEntity(SensorEntity): return f"{self.coordinator.data:.2f}" return None - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - @property def extra_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py index 727960e5f46..20e5eaed07e 100644 --- a/homeassistant/components/starline/entity.py +++ b/homeassistant/components/starline/entity.py @@ -11,6 +11,8 @@ from .account import StarlineAccount, StarlineDevice class StarlineEntity(Entity): """StarLine base entity class.""" + _attr_should_poll = False + def __init__( self, account: StarlineAccount, device: StarlineDevice, key: str, name: str ) -> None: @@ -21,11 +23,6 @@ class StarlineEntity(Entity): self._name = name self._unsubscribe_api: Callable | None = None - @property - def should_poll(self): - """No polling needed.""" - return False - @property def available(self): """Return True if entity is available.""" diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index fea01eb22af..f6e090075a9 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -57,6 +57,8 @@ async def async_setup_entry( class FolderSensor(SensorEntity): """A Syncthing folder sensor.""" + _attr_should_poll = False + STATE_ATTRIBUTES = { "errors": "errors", "globalBytes": "global_bytes", @@ -131,11 +133,6 @@ class FolderSensor(SensorEntity): """Return the state attributes.""" return self._state - @property - def should_poll(self): - """Return the polling requirement for this sensor.""" - return False - @property def device_info(self) -> DeviceInfo: """Return device information.""" diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index e208335033d..40e06313fa8 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -7,6 +7,8 @@ from .const import DEFAULT_NAME, DOMAIN, TADO_HOME, TADO_ZONE class TadoDeviceEntity(Entity): """Base implementation for Tado device.""" + _attr_should_poll = False + def __init__(self, device_info): """Initialize a Tado device.""" super().__init__() @@ -27,11 +29,6 @@ class TadoDeviceEntity(Entity): via_device=(DOMAIN, self._device_info["serialNo"]), ) - @property - def should_poll(self): - """Do not poll.""" - return False - class TadoHomeEntity(Entity): """Base implementation for Tado home.""" @@ -57,6 +54,8 @@ class TadoHomeEntity(Entity): class TadoZoneEntity(Entity): """Base implementation for Tado zone.""" + _attr_should_poll = False + def __init__(self, zone_name, home_id, zone_id): """Initialize a Tado zone.""" super().__init__() @@ -75,8 +74,3 @@ class TadoZoneEntity(Entity): model=TADO_ZONE, suggested_area=self.zone_name, ) - - @property - def should_poll(self): - """Do not poll.""" - return False diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index 9e0bf7e9693..db32d41cede 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -25,6 +25,8 @@ ATTR_LAST_UPDATED = "time_last_updated" class TelldusLiveEntity(Entity): """Base class for all Telldus Live entities.""" + _attr_should_poll = False + def __init__(self, client, device_id): """Initialize the entity.""" self._id = device_id @@ -66,11 +68,6 @@ class TelldusLiveEntity(Entity): """Return the state of the device.""" return self.device.state - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def assumed_state(self): """Return true if unable to access real state of entity.""" diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 802aa22d759..8cec85bf20d 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -112,6 +112,8 @@ async def async_setup_platform( class ThresholdSensor(BinarySensorEntity): """Representation of a Threshold sensor.""" + _attr_should_poll = False + def __init__( self, hass, entity_id, name, lower, upper, hysteresis, device_class, unique_id ): @@ -168,11 +170,6 @@ class ThresholdSensor(BinarySensorEntity): """Return true if sensor is on.""" return self._state - @property - def should_poll(self): - """No polling needed.""" - return False - @property def device_class(self): """Return the sensor class of the sensor.""" diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 1100892b876..d287431d712 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -98,6 +98,8 @@ def _is_sun_event(sun_event): class TodSensor(BinarySensorEntity): """Time of the Day Sensor.""" + _attr_should_poll = False + def __init__(self, name, after, after_offset, before, before_offset, unique_id): """Init the ToD Sensor...""" self._attr_unique_id = unique_id @@ -109,11 +111,6 @@ class TodSensor(BinarySensorEntity): self._after = after self._unsub_update: Callable[[], None] = None - @property - def should_poll(self): - """Sensor does not need to be polled.""" - return False - @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 584f23b1ded..8c5eca09d3d 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -84,11 +84,6 @@ class Touchline(ClimateEntity): (self.unit.get_operation_mode(), self.unit.get_week_program()) ) - @property - def should_poll(self): - """Return the polling state.""" - return True - @property def name(self): """Return the name of the climate device.""" diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 0c6899e6b3e..c3f160cb040 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -49,6 +49,8 @@ async def async_setup_entry( class TransmissionSensor(SensorEntity): """A base class for all Transmission sensors.""" + _attr_should_poll = False + def __init__(self, tm_client, client_name, sensor_name, sub_type=None): """Initialize the sensor.""" self._tm_client: TransmissionClient = tm_client @@ -72,11 +74,6 @@ class TransmissionSensor(SensorEntity): """Return the state of the sensor.""" return self._state - @property - def should_poll(self): - """Return the polling requirement for this sensor.""" - return False - @property def available(self): """Could the device be accessed during the last update call.""" diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index a646286e34f..63477eecf48 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -33,6 +33,8 @@ async def async_setup_entry( class TransmissionSwitch(SwitchEntity): """Representation of a Transmission switch.""" + _attr_should_poll = False + def __init__(self, switch_type, switch_name, tm_client, name): """Initialize the Transmission switch.""" self._name = switch_name @@ -53,11 +55,6 @@ class TransmissionSwitch(SwitchEntity): """Return the unique id of the entity.""" return f"{self._tm_client.api.host}-{self.name}" - @property - def should_poll(self): - """Poll for status regularly.""" - return False - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 87aadc78415..e3bd4816f44 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -112,6 +112,8 @@ def setup_platform( class SensorTrend(BinarySensorEntity): """Representation of a trend Sensor.""" + _attr_should_poll = False + def __init__( self, hass, @@ -167,11 +169,6 @@ class SensorTrend(BinarySensorEntity): ATTR_SAMPLE_DURATION: self._sample_duration, } - @property - def should_poll(self): - """No polling needed.""" - return False - async def async_added_to_hass(self): """Complete device setup after being added to hass.""" From 9c6780f76ab825a2053a4b43f90203bee3385c30 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 26 Aug 2022 22:11:25 -0400 Subject: [PATCH 676/903] Rework Accuweather sensors (#76567) --- .../components/accuweather/__init__.py | 5 +- homeassistant/components/accuweather/const.py | 6 + .../components/accuweather/sensor.py | 262 +++++++++--------- .../components/accuweather/weather.py | 2 +- 4 files changed, 147 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 9123648a38d..0484dd0c8e7 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -81,7 +81,6 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Initialize.""" self.location_key = location_key self.forecast = forecast - self.is_metric = hass.config.units.is_metric self.accuweather = AccuWeather(api_key, session, location_key=location_key) self.device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -116,7 +115,9 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): async with timeout(10): current = await self.accuweather.async_get_current_conditions() forecast = ( - await self.accuweather.async_get_forecast(metric=self.is_metric) + await self.accuweather.async_get_forecast( + metric=self.hass.config.units.is_metric + ) if self.forecast else {} ) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index c1b90de09e7..1336e31f415 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -23,7 +23,13 @@ from homeassistant.components.weather import ( API_IMPERIAL: Final = "Imperial" API_METRIC: Final = "Metric" ATTRIBUTION: Final = "Data provided by AccuWeather" +ATTR_CATEGORY: Final = "Category" +ATTR_DIRECTION: Final = "Direction" +ATTR_ENGLISH: Final = "English" +ATTR_LEVEL: Final = "level" ATTR_FORECAST: Final = "forecast" +ATTR_SPEED: Final = "Speed" +ATTR_VALUE: Final = "Value" CONF_FORECAST: Final = "forecast" DOMAIN: Final = "accuweather" MANUFACTURER: Final = "AccuWeather, Inc." diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index c13dedcdceb..f57af15714d 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -1,6 +1,7 @@ """Support for the AccuWeather service.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from typing import Any, cast @@ -34,7 +35,13 @@ from . import AccuWeatherDataUpdateCoordinator from .const import ( API_IMPERIAL, API_METRIC, + ATTR_CATEGORY, + ATTR_DIRECTION, + ATTR_ENGLISH, ATTR_FORECAST, + ATTR_LEVEL, + ATTR_SPEED, + ATTR_VALUE, ATTRIBUTION, DOMAIN, MAX_FORECAST_DAYS, @@ -44,11 +51,20 @@ PARALLEL_UPDATES = 1 @dataclass -class AccuWeatherSensorDescription(SensorEntityDescription): +class AccuWeatherSensorDescriptionMixin: + """Mixin for AccuWeather sensor.""" + + value_fn: Callable[[dict[str, Any], str], StateType] + + +@dataclass +class AccuWeatherSensorDescription( + SensorEntityDescription, AccuWeatherSensorDescriptionMixin +): """Class describing AccuWeather sensor entities.""" - unit_metric: str | None = None - unit_imperial: str | None = None + attr_fn: Callable[[dict[str, Any]], dict[str, StateType]] = lambda _: {} + unit_fn: Callable[[bool], str | None] = lambda _: None FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( @@ -56,145 +72,162 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( key="CloudCoverDay", icon="mdi:weather-cloudy", name="Cloud cover day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, entity_registry_enabled_default=False, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="CloudCoverNight", icon="mdi:weather-cloudy", name="Cloud cover night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, entity_registry_enabled_default=False, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="Grass", icon="mdi:grass", name="Grass pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, + unit_fn=lambda _: CONCENTRATION_PARTS_PER_CUBIC_METER, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="HoursOfSun", icon="mdi:weather-partly-cloudy", name="Hours of sun", - unit_metric=TIME_HOURS, - unit_imperial=TIME_HOURS, + unit_fn=lambda _: TIME_HOURS, + value_fn=lambda data, _: cast(float, data), ), AccuWeatherSensorDescription( key="Mold", icon="mdi:blur", name="Mold pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, + unit_fn=lambda _: CONCENTRATION_PARTS_PER_CUBIC_METER, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="Ozone", icon="mdi:vector-triangle", name="Ozone", - unit_metric=None, - unit_imperial=None, entity_registry_enabled_default=False, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="Ragweed", icon="mdi:sprout", name="Ragweed pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_fn=lambda _: CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="RealFeelTemperatureMax", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, _: cast(float, data[ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperatureMin", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, _: cast(float, data[ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperatureShadeMax", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature shade max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, _: cast(float, data[ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperatureShadeMin", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature shade min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, _: cast(float, data[ATTR_VALUE]), ), AccuWeatherSensorDescription( key="ThunderstormProbabilityDay", icon="mdi:weather-lightning", name="Thunderstorm probability day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="ThunderstormProbabilityNight", icon="mdi:weather-lightning", name="Thunderstorm probability night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="Tree", icon="mdi:tree-outline", name="Tree pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_fn=lambda _: CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="UVIndex", icon="mdi:weather-sunny", name="UV index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, + unit_fn=lambda _: UV_INDEX, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="WindGustDay", icon="mdi:weather-windy", name="Wind gust day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, entity_registry_enabled_default=False, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, _: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, ), AccuWeatherSensorDescription( key="WindGustNight", icon="mdi:weather-windy", name="Wind gust night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, entity_registry_enabled_default=False, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, _: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, ), AccuWeatherSensorDescription( key="WindDay", icon="mdi:weather-windy", name="Wind day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, _: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, ), AccuWeatherSensorDescription( key="WindNight", icon="mdi:weather-windy", name="Wind night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, _: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, ), ) @@ -203,112 +236,117 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( key="ApparentTemperature", device_class=SensorDeviceClass.TEMPERATURE, name="Apparent temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="Ceiling", icon="mdi:weather-fog", name="Cloud ceiling", - unit_metric=LENGTH_METERS, - unit_imperial=LENGTH_FEET, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: LENGTH_METERS if metric else LENGTH_FEET, + value_fn=lambda data, unit: round(data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="CloudCover", icon="mdi:weather-cloudy", name="Cloud cover", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="DewPoint", device_class=SensorDeviceClass.TEMPERATURE, name="Dew point", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperature", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperatureShade", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature shade", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="Precipitation", icon="mdi:weather-rainy", name="Precipitation", - unit_metric=LENGTH_MILLIMETERS, - unit_imperial=LENGTH_INCHES, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: LENGTH_MILLIMETERS if metric else LENGTH_INCHES, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), + attr_fn=lambda data: {"type": data["PrecipitationType"]}, ), AccuWeatherSensorDescription( key="PressureTendency", device_class="accuweather__pressure_tendency", icon="mdi:gauge", name="Pressure tendency", - unit_metric=None, - unit_imperial=None, + value_fn=lambda data, _: cast(str, data["LocalizedText"]).lower(), ), AccuWeatherSensorDescription( key="UVIndex", icon="mdi:weather-sunny", name="UV index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda _: UV_INDEX, + value_fn=lambda data, _: cast(int, data), + attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]}, ), AccuWeatherSensorDescription( key="WetBulbTemperature", device_class=SensorDeviceClass.TEMPERATURE, name="Wet bulb temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="WindChillTemperature", device_class=SensorDeviceClass.TEMPERATURE, name="Wind chill temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="Wind", icon="mdi:weather-windy", name="Wind", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, unit: cast(float, data[ATTR_SPEED][unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="WindGust", icon="mdi:weather-windy", name="Wind gust", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, unit: cast(float, data[ATTR_SPEED][unit][ATTR_VALUE]), ), ) @@ -328,9 +366,9 @@ async def async_setup_entry( # Some air quality/allergy sensors are only available for certain # locations. sensors.extend( - AccuWeatherSensor(coordinator, description, forecast_day=day) - for description in FORECAST_SENSOR_TYPES + AccuWeatherForecastSensor(coordinator, description, forecast_day=day) for day in range(MAX_FORECAST_DAYS + 1) + for description in FORECAST_SENSOR_TYPES if description.key in coordinator.data[ATTR_FORECAST][0] ) @@ -356,9 +394,8 @@ class AccuWeatherSensor( super().__init__(coordinator) self.entity_description = description self._sensor_data = _get_sensor_data( - coordinator.data, forecast_day, description.key + coordinator.data, description.key, forecast_day ) - self._attrs: dict[str, Any] = {} if forecast_day is not None: self._attr_name = f"{description.name} {forecast_day}d" self._attr_unique_id = ( @@ -368,82 +405,40 @@ class AccuWeatherSensor( self._attr_unique_id = ( f"{coordinator.location_key}-{description.key}".lower() ) - if coordinator.is_metric: + if self.coordinator.hass.config.units.is_metric: self._unit_system = API_METRIC - self._attr_native_unit_of_measurement = description.unit_metric else: self._unit_system = API_IMPERIAL - self._attr_native_unit_of_measurement = description.unit_imperial + self._attr_native_unit_of_measurement = self.entity_description.unit_fn( + self.coordinator.hass.config.units.is_metric + ) self._attr_device_info = coordinator.device_info - self.forecast_day = forecast_day + if forecast_day is not None: + self.forecast_day = forecast_day @property def native_value(self) -> StateType: """Return the state.""" - if self.forecast_day is not None: - if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: - return cast(float, self._sensor_data["Value"]) - if self.entity_description.key == "UVIndex": - return cast(int, self._sensor_data["Value"]) - if self.entity_description.key in ("Grass", "Mold", "Ragweed", "Tree", "Ozone"): - return cast(int, self._sensor_data["Value"]) - if self.entity_description.key == "Ceiling": - return round(self._sensor_data[self._unit_system]["Value"]) - if self.entity_description.key == "PressureTendency": - return cast(str, self._sensor_data["LocalizedText"].lower()) - if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: - return cast(float, self._sensor_data[self._unit_system]["Value"]) - if self.entity_description.key == "Precipitation": - return cast(float, self._sensor_data[self._unit_system]["Value"]) - if self.entity_description.key in ("Wind", "WindGust"): - return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"]) - if self.entity_description.key in ( - "WindDay", - "WindNight", - "WindGustDay", - "WindGustNight", - ): - return cast(StateType, self._sensor_data["Speed"]["Value"]) - return cast(StateType, self._sensor_data) + return self.entity_description.value_fn(self._sensor_data, self._unit_system) @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - if self.forecast_day is not None: - if self.entity_description.key in ( - "WindDay", - "WindNight", - "WindGustDay", - "WindGustNight", - ): - self._attrs["direction"] = self._sensor_data["Direction"]["English"] - elif self.entity_description.key in ( - "Grass", - "Mold", - "Ozone", - "Ragweed", - "Tree", - "UVIndex", - ): - self._attrs["level"] = self._sensor_data["Category"] - return self._attrs - if self.entity_description.key == "UVIndex": - self._attrs["level"] = self.coordinator.data["UVIndexText"] - elif self.entity_description.key == "Precipitation": - self._attrs["type"] = self.coordinator.data["PrecipitationType"] - return self._attrs + return self.entity_description.attr_fn(self.coordinator.data) @callback def _handle_coordinator_update(self) -> None: """Handle data update.""" self._sensor_data = _get_sensor_data( - self.coordinator.data, self.forecast_day, self.entity_description.key + self.coordinator.data, self.entity_description.key ) self.async_write_ha_state() def _get_sensor_data( - sensors: dict[str, Any], forecast_day: int | None, kind: str + sensors: dict[str, Any], + kind: str, + forecast_day: int | None = None, ) -> Any: """Get sensor data.""" if forecast_day is not None: @@ -453,3 +448,20 @@ def _get_sensor_data( return sensors["PrecipitationSummary"][kind] return sensors[kind] + + +class AccuWeatherForecastSensor(AccuWeatherSensor): + """Define an AccuWeather forecast entity.""" + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + return self.entity_description.attr_fn(self._sensor_data) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle data update.""" + self._sensor_data = _get_sensor_data( + self.coordinator.data, self.entity_description.key, self.forecast_day + ) + self.async_write_ha_state() diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index e8a5b0ab396..2bbbe7b9160 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -70,7 +70,7 @@ class AccuWeatherEntity( # Coordinator data is used also for sensors which don't have units automatically # converted, hence the weather entity's native units follow the configured unit # system - if coordinator.is_metric: + if coordinator.hass.config.units.is_metric: self._attr_native_precipitation_unit = LENGTH_MILLIMETERS self._attr_native_pressure_unit = PRESSURE_HPA self._attr_native_temperature_unit = TEMP_CELSIUS From f6bc5ad8b14c6bad9bc843f350d2e0b9e3d7d501 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Aug 2022 21:44:10 -0500 Subject: [PATCH 677/903] Add Thermobeacon (BLE) integration (#77313) --- CODEOWNERS | 2 + .../components/thermobeacon/__init__.py | 49 +++++ .../components/thermobeacon/config_flow.py | 94 ++++++++ .../components/thermobeacon/const.py | 3 + .../components/thermobeacon/device.py | 31 +++ .../components/thermobeacon/manifest.json | 31 +++ .../components/thermobeacon/sensor.py | 140 ++++++++++++ .../components/thermobeacon/strings.json | 22 ++ .../thermobeacon/translations/en.json | 22 ++ homeassistant/generated/bluetooth.py | 32 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/thermobeacon/__init__.py | 26 +++ tests/components/thermobeacon/conftest.py | 8 + .../thermobeacon/test_config_flow.py | 200 ++++++++++++++++++ tests/components/thermobeacon/test_sensor.py | 53 +++++ 17 files changed, 720 insertions(+) create mode 100644 homeassistant/components/thermobeacon/__init__.py create mode 100644 homeassistant/components/thermobeacon/config_flow.py create mode 100644 homeassistant/components/thermobeacon/const.py create mode 100644 homeassistant/components/thermobeacon/device.py create mode 100644 homeassistant/components/thermobeacon/manifest.json create mode 100644 homeassistant/components/thermobeacon/sensor.py create mode 100644 homeassistant/components/thermobeacon/strings.json create mode 100644 homeassistant/components/thermobeacon/translations/en.json create mode 100644 tests/components/thermobeacon/__init__.py create mode 100644 tests/components/thermobeacon/conftest.py create mode 100644 tests/components/thermobeacon/test_config_flow.py create mode 100644 tests/components/thermobeacon/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index aff9c981129..db255191c38 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1105,6 +1105,8 @@ build.json @home-assistant/supervisor /homeassistant/components/tesla_wall_connector/ @einarhauks /tests/components/tesla_wall_connector/ @einarhauks /homeassistant/components/tfiac/ @fredrike @mellado +/homeassistant/components/thermobeacon/ @bdraco +/tests/components/thermobeacon/ @bdraco /homeassistant/components/thermopro/ @bdraco /tests/components/thermopro/ @bdraco /homeassistant/components/thethingsnetwork/ @fabaff diff --git a/homeassistant/components/thermobeacon/__init__.py b/homeassistant/components/thermobeacon/__init__.py new file mode 100644 index 00000000000..92b5ef4b4f6 --- /dev/null +++ b/homeassistant/components/thermobeacon/__init__.py @@ -0,0 +1,49 @@ +"""The ThermoBeacon integration.""" +from __future__ import annotations + +import logging + +from thermobeacon_ble import ThermoBeaconBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up ThermoBeacon BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = ThermoBeaconBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/thermobeacon/config_flow.py b/homeassistant/components/thermobeacon/config_flow.py new file mode 100644 index 00000000000..864e9532c0e --- /dev/null +++ b/homeassistant/components/thermobeacon/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for thermobeacon ble integration.""" +from __future__ import annotations + +from typing import Any + +from thermobeacon_ble import ThermoBeaconBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class ThermoBeaconConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for thermobeacon.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/thermobeacon/const.py b/homeassistant/components/thermobeacon/const.py new file mode 100644 index 00000000000..5782fcd0a20 --- /dev/null +++ b/homeassistant/components/thermobeacon/const.py @@ -0,0 +1,3 @@ +"""Constants for the ThermoBeacon integration.""" + +DOMAIN = "thermobeacon" diff --git a/homeassistant/components/thermobeacon/device.py b/homeassistant/components/thermobeacon/device.py new file mode 100644 index 00000000000..327a206042a --- /dev/null +++ b/homeassistant/components/thermobeacon/device.py @@ -0,0 +1,31 @@ +"""Support for ThermoBeacon devices.""" +from __future__ import annotations + +from thermobeacon_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a thermobeacon device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json new file mode 100644 index 00000000000..878199e299e --- /dev/null +++ b/homeassistant/components/thermobeacon/manifest.json @@ -0,0 +1,31 @@ +{ + "domain": "thermobeacon", + "name": "ThermoBeacon", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/thermobeacon", + "bluetooth": [ + { + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 16, + "manufacturer_data_start": [0], + "connectable": false + }, + { + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 17, + "manufacturer_data_start": [0], + "connectable": false + }, + { + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 21, + "manufacturer_data_start": [0], + "connectable": false + }, + { "local_name": "ThermoBeacon", "connectable": false } + ], + "requirements": ["thermobeacon-ble==0.3.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/thermobeacon/sensor.py b/homeassistant/components/thermobeacon/sensor.py new file mode 100644 index 00000000000..83b616f8d84 --- /dev/null +++ b/homeassistant/components/thermobeacon/sensor.py @@ -0,0 +1,140 @@ +"""Support for ThermoBeacon sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from thermobeacon_ble import ( + SensorDeviceClass as ThermoBeaconSensorDeviceClass, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ELECTRIC_POTENTIAL_VOLT, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +SENSOR_DESCRIPTIONS = { + (ThermoBeaconSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{ThermoBeaconSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (ThermoBeaconSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{ThermoBeaconSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + ThermoBeaconSensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{ThermoBeaconSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + ( + ThermoBeaconSensorDeviceClass.TEMPERATURE, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{ThermoBeaconSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + ThermoBeaconSensorDeviceClass.VOLTAGE, + Units.ELECTRIC_POTENTIAL_VOLT, + ): SensorEntityDescription( + key=f"{ThermoBeaconSensorDeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_VOLT}", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the ThermoBeacon BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + ThermoBeaconBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class ThermoBeaconBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a ThermoBeacon sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/thermobeacon/strings.json b/homeassistant/components/thermobeacon/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/thermobeacon/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/thermobeacon/translations/en.json b/homeassistant/components/thermobeacon/translations/en.json new file mode 100644 index 00000000000..ebd9760c161 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Device not supported" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 7ceb13bce9f..246ee56acb6 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -143,6 +143,38 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", "connectable": False }, + { + "domain": "thermobeacon", + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 16, + "manufacturer_data_start": [ + 0 + ], + "connectable": False + }, + { + "domain": "thermobeacon", + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 17, + "manufacturer_data_start": [ + 0 + ], + "connectable": False + }, + { + "domain": "thermobeacon", + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 21, + "manufacturer_data_start": [ + 0 + ], + "connectable": False + }, + { + "domain": "thermobeacon", + "local_name": "ThermoBeacon", + "connectable": False + }, { "domain": "thermopro", "local_name": "TP35*", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 237090420c1..1ff8832f102 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -377,6 +377,7 @@ FLOWS = { "tautulli", "tellduslive", "tesla_wall_connector", + "thermobeacon", "thermopro", "tibber", "tile", diff --git a/requirements_all.txt b/requirements_all.txt index 2ba19d5cd1d..30c1a8b298a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2338,6 +2338,9 @@ tesla-wall-connector==1.0.1 # homeassistant.components.tensorflow # tf-models-official==2.5.0 +# homeassistant.components.thermobeacon +thermobeacon-ble==0.3.1 + # homeassistant.components.thermopro thermopro-ble==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 696f4c0aa8e..af2d1930de7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1587,6 +1587,9 @@ tesla-powerwall==0.3.18 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 +# homeassistant.components.thermobeacon +thermobeacon-ble==0.3.1 + # homeassistant.components.thermopro thermopro-ble==0.4.0 diff --git a/tests/components/thermobeacon/__init__.py b/tests/components/thermobeacon/__init__.py new file mode 100644 index 00000000000..1ff1ad20df1 --- /dev/null +++ b/tests/components/thermobeacon/__init__.py @@ -0,0 +1,26 @@ +"""Tests for the ThermoBeacon integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_THERMOBEACON_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +THERMOBEACON_SERVICE_INFO = BluetoothServiceInfo( + name="ThermoBeacon", + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={}, + manufacturer_data={ + 16: b"\x00\x00\xb0\x02\x00\x00G\xa4\xe2\x0c\x80\x01\xb6\x02J\x00\x00\x00" + }, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + source="local", +) diff --git a/tests/components/thermobeacon/conftest.py b/tests/components/thermobeacon/conftest.py new file mode 100644 index 00000000000..ca17cdbfe4c --- /dev/null +++ b/tests/components/thermobeacon/conftest.py @@ -0,0 +1,8 @@ +"""ThermoBeacon session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/thermobeacon/test_config_flow.py b/tests/components/thermobeacon/test_config_flow.py new file mode 100644 index 00000000000..8f34db3d65f --- /dev/null +++ b/tests/components/thermobeacon/test_config_flow.py @@ -0,0 +1,200 @@ +"""Test the ThermoBeacon config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.thermobeacon.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import NOT_THERMOBEACON_SERVICE_INFO, THERMOBEACON_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=THERMOBEACON_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.thermobeacon.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Lanyard/mini hygrometer EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_thermobeacon(hass): + """Test discovery via bluetooth not thermobeacon.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_THERMOBEACON_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.thermobeacon.config_flow.async_discovered_service_info", + return_value=[THERMOBEACON_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.thermobeacon.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Lanyard/mini hygrometer EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.thermobeacon.config_flow.async_discovered_service_info", + return_value=[THERMOBEACON_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.thermobeacon.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.thermobeacon.config_flow.async_discovered_service_info", + return_value=[THERMOBEACON_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=THERMOBEACON_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=THERMOBEACON_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=THERMOBEACON_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=THERMOBEACON_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.thermobeacon.config_flow.async_discovered_service_info", + return_value=[THERMOBEACON_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.thermobeacon.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Lanyard/mini hygrometer EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/thermobeacon/test_sensor.py b/tests/components/thermobeacon/test_sensor.py new file mode 100644 index 00000000000..147f37787b8 --- /dev/null +++ b/tests/components/thermobeacon/test_sensor.py @@ -0,0 +1,53 @@ +"""Test the ThermoBeacon sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.thermobeacon.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import THERMOBEACON_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + saved_callback(THERMOBEACON_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 4 + + humid_sensor = hass.states.get("sensor.lanyard_mini_hygrometer_eeff_humidity") + humid_sensor_attrs = humid_sensor.attributes + assert humid_sensor.state == "43.38" + assert ( + humid_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Lanyard/mini hygrometer EEFF Humidity" + ) + assert humid_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert humid_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 61d5ed1dcfb2554ed7e4c259421da2b069c7bce2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Aug 2022 22:07:51 -0500 Subject: [PATCH 678/903] Index bluetooth matchers to resolve performance concerns with many adapters/remotes (#77372) --- .../components/bluetooth/__init__.py | 1 + homeassistant/components/bluetooth/manager.py | 63 +- homeassistant/components/bluetooth/match.py | 271 +++++++- tests/components/bluetooth/test_init.py | 618 +++++++++++++++++- 4 files changed, 865 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 208bbe6952b..632635f7dbc 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -209,6 +209,7 @@ async def async_get_adapter_from_address( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) + integration_matcher.async_setup() manager = BluetoothManager(hass, integration_matcher) manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 2fff99c830c..be5038a6d31 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -27,8 +27,11 @@ from .const import ( ) from .match import ( ADDRESS, + CALLBACK, CONNECTABLE, BluetoothCallbackMatcher, + BluetoothCallbackMatcherIndex, + BluetoothCallbackMatcherWithCallback, IntegrationMatcher, ble_device_matches, ) @@ -132,12 +135,7 @@ class BluetoothManager: self._connectable_unavailable_callbacks: dict[ str, list[Callable[[str], None]] ] = {} - self._callbacks: list[ - tuple[BluetoothCallback, BluetoothCallbackMatcher | None] - ] = [] - self._connectable_callbacks: list[ - tuple[BluetoothCallback, BluetoothCallbackMatcher | None] - ] = [] + self._callback_index = BluetoothCallbackMatcherIndex() self._bleak_callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] @@ -255,7 +253,7 @@ class BluetoothManager: device = service_info.device connectable = service_info.connectable address = device.address - all_history = self._get_history_by_type(connectable) + all_history = self._connectable_history if connectable else self._history old_service_info = all_history.get(address) if old_service_info and _prefer_previous_adv(old_service_info, service_info): return @@ -281,24 +279,13 @@ class BluetoothManager: matched_domains, ) - if ( - not matched_domains - and not self._callbacks - and not self._connectable_callbacks - ): - return + for match in self._callback_index.match_callbacks(service_info): + callback = match[CALLBACK] + try: + callback(service_info, BluetoothChange.ADVERTISEMENT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") - for connectable_callback in (True, False): - for callback, matcher in self._get_callbacks_by_type(connectable_callback): - if matcher and not ble_device_matches(matcher, service_info): - continue - try: - callback(service_info, BluetoothChange.ADVERTISEMENT) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in bluetooth callback") - - if not matched_domains: - return for domain in matched_domains: discovery_flow.async_create_flow( self.hass, @@ -330,28 +317,30 @@ class BluetoothManager: matcher: BluetoothCallbackMatcher | None, ) -> Callable[[], None]: """Register a callback.""" + callback_matcher = BluetoothCallbackMatcherWithCallback(callback=callback) if not matcher: - matcher = BluetoothCallbackMatcher(connectable=True) - if CONNECTABLE not in matcher: - matcher[CONNECTABLE] = True - connectable = matcher[CONNECTABLE] + callback_matcher[CONNECTABLE] = True + else: + # We could write out every item in the typed dict here + # but that would be a bit inefficient and verbose. + callback_matcher.update(matcher) # type: ignore[typeddict-item] + callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True) - callback_entry = (callback, matcher) - callbacks = self._get_callbacks_by_type(connectable) - callbacks.append(callback_entry) + connectable = callback_matcher[CONNECTABLE] + self._callback_index.add_with_address(callback_matcher) @hass_callback def _async_remove_callback() -> None: - callbacks.remove(callback_entry) + self._callback_index.remove_with_address(callback_matcher) # If we have history for the subscriber, we can trigger the callback # immediately with the last packet so the subscriber can see the # device. all_history = self._get_history_by_type(connectable) if ( - (address := matcher.get(ADDRESS)) + (address := callback_matcher.get(ADDRESS)) and (service_info := all_history.get(address)) - and ble_device_matches(matcher, service_info) + and ble_device_matches(callback_matcher, service_info) ): try: callback(service_info, BluetoothChange.ADVERTISEMENT) @@ -407,12 +396,6 @@ class BluetoothManager: """Return the history by type.""" return self._connectable_history if connectable else self._history - def _get_callbacks_by_type( - self, connectable: bool - ) -> list[tuple[BluetoothCallback, BluetoothCallbackMatcher | None]]: - """Return the callbacks by type.""" - return self._connectable_callbacks if connectable else self._callbacks - def async_register_scanner( self, scanner: BaseHaScanner, connectable: bool ) -> CALLBACK_TYPE: diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 4a0aa8ee995..e9f535200b5 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -5,13 +5,14 @@ from dataclasses import dataclass from fnmatch import translate from functools import lru_cache import re -from typing import TYPE_CHECKING, Final, TypedDict +from typing import TYPE_CHECKING, Final, Generic, TypedDict, TypeVar from lru import LRU # pylint: disable=no-name-in-module +from homeassistant.core import callback from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional -from .models import BluetoothServiceInfoBleak +from .models import BluetoothCallback, BluetoothServiceInfoBleak if TYPE_CHECKING: from collections.abc import MutableMapping @@ -21,7 +22,8 @@ if TYPE_CHECKING: MAX_REMEMBER_ADDRESSES: Final = 2048 - +CALLBACK: Final = "callback" +DOMAIN: Final = "domain" ADDRESS: Final = "address" CONNECTABLE: Final = "connectable" LOCAL_NAME: Final = "local_name" @@ -30,6 +32,8 @@ SERVICE_DATA_UUID: Final = "service_data_uuid" MANUFACTURER_ID: Final = "manufacturer_id" MANUFACTURER_DATA_START: Final = "manufacturer_data_start" +LOCAL_NAME_MIN_MATCH_LENGTH = 3 + class BluetoothCallbackMatcherOptional(TypedDict, total=False): """Matcher for the bluetooth integration for callback optional fields.""" @@ -44,6 +48,19 @@ class BluetoothCallbackMatcher( """Callback matcher for the bluetooth integration.""" +class _BluetoothCallbackMatcherWithCallback(TypedDict): + """Callback for the bluetooth integration.""" + + callback: BluetoothCallback + + +class BluetoothCallbackMatcherWithCallback( + _BluetoothCallbackMatcherWithCallback, + BluetoothCallbackMatcher, +): + """Callback matcher for the bluetooth integration that stores the callback.""" + + @dataclass(frozen=False) class IntegrationMatchHistory: """Track which fields have been seen.""" @@ -86,23 +103,26 @@ class IntegrationMatcher: self._matched_connectable: MutableMapping[str, IntegrationMatchHistory] = LRU( MAX_REMEMBER_ADDRESSES ) + self._index = BluetoothMatcherIndex() + + @callback + def async_setup(self) -> None: + """Set up the matcher.""" + for matcher in self._integration_matchers: + self._index.add(matcher) + self._index.build() def async_clear_address(self, address: str) -> None: """Clear the history matches for a set of domains.""" self._matched.pop(address, None) self._matched_connectable.pop(address, None) - def _get_matched_by_type( - self, connectable: bool - ) -> MutableMapping[str, IntegrationMatchHistory]: - """Return the matches by type.""" - return self._matched_connectable if connectable else self._matched - def match_domains(self, service_info: BluetoothServiceInfoBleak) -> set[str]: """Return the domains that are matched.""" device = service_info.device advertisement_data = service_info.advertisement - matched = self._get_matched_by_type(service_info.connectable) + connectable = service_info.connectable + matched = self._matched_connectable if connectable else self._matched matched_domains: set[str] = set() if (previous_match := matched.get(device.address)) and seen_all_fields( previous_match, advertisement_data @@ -110,9 +130,7 @@ class IntegrationMatcher: # We have seen all fields so we can skip the rest of the matchers return matched_domains matched_domains = { - matcher["domain"] - for matcher in self._integration_matchers - if ble_device_matches(matcher, service_info) + matcher[DOMAIN] for matcher in self._index.match(service_info) } if not matched_domains: return matched_domains @@ -131,14 +149,209 @@ class IntegrationMatcher: return matched_domains +_T = TypeVar("_T", BluetoothMatcher, BluetoothCallbackMatcherWithCallback) + + +class BluetoothMatcherIndexBase(Generic[_T]): + """Bluetooth matcher base for the bluetooth integration. + + The indexer puts each matcher in the bucket that it is most + likely to match. This allows us to only check the service infos + against each bucket to see if we should match against the data. + + This is optimized for cases were no service infos will be matched in + any bucket and we can quickly reject the service info as not matching. + """ + + def __init__(self) -> None: + """Initialize the matcher index.""" + self.local_name: dict[str, list[_T]] = {} + self.service_uuid: dict[str, list[_T]] = {} + self.service_data_uuid: dict[str, list[_T]] = {} + self.manufacturer_id: dict[int, list[_T]] = {} + self.service_uuid_set: set[str] = set() + self.service_data_uuid_set: set[str] = set() + self.manufacturer_id_set: set[int] = set() + + def add(self, matcher: _T) -> None: + """Add a matcher to the index. + + Matchers must end up only in one bucket. + + We put them in the bucket that they are most likely to match. + """ + if LOCAL_NAME in matcher: + self.local_name.setdefault( + _local_name_to_index_key(matcher[LOCAL_NAME]), [] + ).append(matcher) + return + + if SERVICE_UUID in matcher: + self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher) + return + + if SERVICE_DATA_UUID in matcher: + self.service_data_uuid.setdefault(matcher[SERVICE_DATA_UUID], []).append( + matcher + ) + return + + if MANUFACTURER_ID in matcher: + self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( + matcher + ) + return + + def remove(self, matcher: _T) -> None: + """Remove a matcher from the index. + + Matchers only end up in one bucket, so once we have + removed one, we are done. + """ + if LOCAL_NAME in matcher: + self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].remove( + matcher + ) + return + + if SERVICE_UUID in matcher: + self.service_uuid[matcher[SERVICE_UUID]].remove(matcher) + return + + if SERVICE_DATA_UUID in matcher: + self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher) + return + + if MANUFACTURER_ID in matcher: + self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) + return + + def build(self) -> None: + """Rebuild the index sets.""" + self.service_uuid_set = set(self.service_uuid) + self.service_data_uuid_set = set(self.service_data_uuid) + self.manufacturer_id_set = set(self.manufacturer_id) + + def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]: + """Check for a match.""" + matches = [] + if len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH: + for matcher in self.local_name.get( + service_info.name[:LOCAL_NAME_MIN_MATCH_LENGTH], [] + ): + if ble_device_matches(matcher, service_info): + matches.append(matcher) + + for service_data_uuid in self.service_data_uuid_set.intersection( + service_info.service_data + ): + for matcher in self.service_data_uuid[service_data_uuid]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) + + for manufacturer_id in self.manufacturer_id_set.intersection( + service_info.manufacturer_data + ): + for matcher in self.manufacturer_id[manufacturer_id]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) + + for service_uuid in self.service_uuid_set.intersection( + service_info.service_uuids + ): + for matcher in self.service_uuid[service_uuid]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) + + return matches + + +class BluetoothMatcherIndex(BluetoothMatcherIndexBase[BluetoothMatcher]): + """Bluetooth matcher for the bluetooth integration.""" + + +class BluetoothCallbackMatcherIndex( + BluetoothMatcherIndexBase[BluetoothCallbackMatcherWithCallback] +): + """Bluetooth matcher for the bluetooth integration that supports matching on addresses.""" + + def __init__(self) -> None: + """Initialize the matcher index.""" + super().__init__() + self.address: dict[str, list[BluetoothCallbackMatcherWithCallback]] = {} + + def add_with_address(self, matcher: BluetoothCallbackMatcherWithCallback) -> None: + """Add a matcher to the index. + + Matchers must end up only in one bucket. + + We put them in the bucket that they are most likely to match. + """ + if ADDRESS in matcher: + self.address.setdefault(matcher[ADDRESS], []).append(matcher) + return + + super().add(matcher) + self.build() + + def remove_with_address( + self, matcher: BluetoothCallbackMatcherWithCallback + ) -> None: + """Remove a matcher from the index. + + Matchers only end up in one bucket, so once we have + removed one, we are done. + """ + if ADDRESS in matcher: + self.address[matcher[ADDRESS]].remove(matcher) + return + + super().remove(matcher) + self.build() + + def match_callbacks( + self, service_info: BluetoothServiceInfoBleak + ) -> list[BluetoothCallbackMatcherWithCallback]: + """Check for a match.""" + matches = self.match(service_info) + for matcher in self.address.get(service_info.address, []): + if ble_device_matches(matcher, service_info): + matches.append(matcher) + return matches + + +def _local_name_to_index_key(local_name: str) -> str: + """Convert a local name to an index. + + We check the local name matchers here and raise a ValueError + if they try to setup a matcher that will is overly broad + as would match too many devices and cause a performance hit. + """ + if len(local_name) < LOCAL_NAME_MIN_MATCH_LENGTH: + raise ValueError( + "Local name matchers must be at least " + f"{LOCAL_NAME_MIN_MATCH_LENGTH} characters long ({local_name})" + ) + match_part = local_name[:LOCAL_NAME_MIN_MATCH_LENGTH] + if "*" in match_part or "[" in match_part: + raise ValueError( + "Local name matchers may not have patterns in the first " + f"{LOCAL_NAME_MIN_MATCH_LENGTH} characters because they " + f"would match too broadly ({local_name})" + ) + return match_part + + def ble_device_matches( - matcher: BluetoothCallbackMatcher | BluetoothMatcher, + matcher: BluetoothMatcherOptional, service_info: BluetoothServiceInfoBleak, ) -> bool: """Check if a ble device and advertisement_data matches the matcher.""" device = service_info.device - if (address := matcher.get(ADDRESS)) is not None and device.address != address: - return False + + # Do don't check address here since all callers already + # check the address and we don't want to double check + # since it would result in an unreachable reject case. if matcher.get(CONNECTABLE, True) and not service_info.connectable: return False @@ -146,28 +359,26 @@ def ble_device_matches( advertisement_data = service_info.advertisement if ( service_uuid := matcher.get(SERVICE_UUID) - ) is not None and service_uuid not in advertisement_data.service_uuids: + ) and service_uuid not in advertisement_data.service_uuids: return False if ( service_data_uuid := matcher.get(SERVICE_DATA_UUID) - ) is not None and service_data_uuid not in advertisement_data.service_data: + ) and service_data_uuid not in advertisement_data.service_data: return False - if ( - manfacturer_id := matcher.get(MANUFACTURER_ID) - ) is not None and manfacturer_id not in advertisement_data.manufacturer_data: - return False - - if (manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START)) is not None: - manufacturer_data_start_bytes = bytearray(manufacturer_data_start) - if not any( - manufacturer_data.startswith(manufacturer_data_start_bytes) - for manufacturer_data in advertisement_data.manufacturer_data.values() - ): + if manfacturer_id := matcher.get(MANUFACTURER_ID): + if manfacturer_id not in advertisement_data.manufacturer_data: return False + if manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START): + manufacturer_data_start_bytes = bytearray(manufacturer_data_start) + if not any( + manufacturer_data.startswith(manufacturer_data_start_bytes) + for manufacturer_data in advertisement_data.manufacturer_data.values() + ): + return False - if (local_name := matcher.get(LOCAL_NAME)) is not None and ( + if (local_name := matcher.get(LOCAL_NAME)) and ( (device_name := advertisement_data.local_name or device.name) is None or not _memorized_fnmatch( device_name, diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 9b958e2fade..a005a71f048 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -26,6 +26,14 @@ from homeassistant.components.bluetooth.const import ( SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) +from homeassistant.components.bluetooth.match import ( + ADDRESS, + CONNECTABLE, + LOCAL_NAME, + MANUFACTURER_ID, + SERVICE_DATA_UUID, + SERVICE_UUID, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback @@ -987,8 +995,6 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo ) -> None: """Fake subscriber for the BleakScanner.""" callbacks.append((service_info, change)) - if len(callbacks) >= 3: - raise ValueError with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -1001,7 +1007,7 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {"service_uuids": {"cba20d00-224d-11e6-9fb8-0002a5d5c51b"}}, + {SERVICE_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}, BluetoothScanningMode.ACTIVE, ) @@ -1026,17 +1032,15 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - # 3rd callback raises ValueError but is still tracked inject_advertisement(hass, empty_device, empty_adv) await hass.async_block_till_done() cancel() - # 4th callback should not be tracked since we canceled inject_advertisement(hass, empty_device, empty_adv) await hass.async_block_till_done() - assert len(callbacks) == 3 + assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] assert service_info.name == "wohand" @@ -1044,17 +1048,63 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo assert service_info.manufacturer == "Nordic Semiconductor ASA" assert service_info.manufacturer_id == 89 - service_info: BluetoothServiceInfo = callbacks[1][0] - assert service_info.name == "empty" - assert service_info.source == SOURCE_LOCAL - assert service_info.manufacturer is None - assert service_info.manufacturer_id is None - service_info: BluetoothServiceInfo = callbacks[2][0] - assert service_info.name == "empty" +async def test_register_callbacks_raises_exception( + hass, mock_bleak_scanner_start, enable_bluetooth, caplog +): + """Test registering a callback that raises ValueError.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + raise ValueError + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init"): + await async_setup_with_default_adapter(hass) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {SERVICE_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + inject_advertisement(hass, switchbot_device, switchbot_adv) + + cancel() + + inject_advertisement(hass, switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(callbacks) == 1 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "wohand" assert service_info.source == SOURCE_LOCAL - assert service_info.manufacturer is None - assert service_info.manufacturer_id is None + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + assert "ValueError" in caplog.text async def test_register_callback_by_address( @@ -1124,7 +1174,7 @@ async def test_register_callback_by_address( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {"address": "44:44:33:11:23:45"}, + {ADDRESS: "44:44:33:11:23:45"}, BluetoothScanningMode.ACTIVE, ) cancel() @@ -1134,7 +1184,7 @@ async def test_register_callback_by_address( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {"address": "44:44:33:11:23:45"}, + {ADDRESS: "44:44:33:11:23:45"}, BluetoothScanningMode.ACTIVE, ) cancel() @@ -1148,6 +1198,537 @@ async def test_register_callback_by_address( assert service_info.manufacturer_id == 89 +async def test_register_callback_by_address_connectable_only( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by address connectable only.""" + mock_bt = [] + connectable_callbacks = [] + non_connectable_callbacks = [] + + def _fake_connectable_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + connectable_callbacks.append((service_info, change)) + + def _fake_non_connectable_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + non_connectable_callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_connectable_subscriber, + {ADDRESS: "44:44:33:11:23:45", CONNECTABLE: True}, + BluetoothScanningMode.ACTIVE, + ) + cancel2 = bluetooth.async_register_callback( + hass, + _fake_non_connectable_subscriber, + {ADDRESS: "44:44:33:11:23:45", CONNECTABLE: False}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + inject_advertisement_with_time_and_source_connectable( + hass, switchbot_device, switchbot_adv, time.monotonic(), "test", False + ) + inject_advertisement_with_time_and_source_connectable( + hass, switchbot_device, switchbot_adv, time.monotonic(), "test", True + ) + + cancel() + cancel2() + + assert len(connectable_callbacks) == 1 + # Non connectable will take either a connectable + # or non-connectable device + assert len(non_connectable_callbacks) == 2 + + +async def test_register_callback_by_manufacturer_id( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by manufacturer_id.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {MANUFACTURER_ID: 76}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_adv = AdvertisementData( + local_name="apple", + manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + inject_advertisement(hass, empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 1 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "apple" + assert service_info.manufacturer == "Apple, Inc." + assert service_info.manufacturer_id == 76 + + +async def test_register_callback_by_address_connectable_manufacturer_id( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by address, manufacturer_id, and connectable.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {MANUFACTURER_ID: 76, CONNECTABLE: False, ADDRESS: "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_adv = AdvertisementData( + local_name="apple", + manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + apple_device_wrong_address = BLEDevice("44:44:33:11:23:46", "apple") + + inject_advertisement(hass, apple_device_wrong_address, apple_adv) + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 1 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "apple" + assert service_info.manufacturer == "Apple, Inc." + assert service_info.manufacturer_id == 76 + + +async def test_register_callback_by_manufacturer_id_and_address( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by manufacturer_id and address.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {MANUFACTURER_ID: 76, ADDRESS: "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_adv = AdvertisementData( + local_name="apple", + manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + yale_device = BLEDevice("44:44:33:11:23:45", "apple") + yale_adv = AdvertisementData( + local_name="yale", + manufacturer_data={465: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, yale_device, yale_adv) + await hass.async_block_till_done() + + other_apple_device = BLEDevice("44:44:33:11:23:22", "apple") + other_apple_adv = AdvertisementData( + local_name="apple", + manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + ) + inject_advertisement(hass, other_apple_device, other_apple_adv) + + cancel() + + assert len(callbacks) == 1 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "apple" + assert service_info.manufacturer == "Apple, Inc." + assert service_info.manufacturer_id == 76 + + +async def test_register_callback_by_service_uuid_and_address( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by service_uuid and address.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + { + SERVICE_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + ADDRESS: "44:44:33:11:23:45", + }, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_dev = BLEDevice("44:44:33:11:23:45", "switchbot") + switchbot_adv = AdvertisementData( + local_name="switchbot", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ) + + inject_advertisement(hass, switchbot_dev, switchbot_adv) + + switchbot_missing_service_uuid_dev = BLEDevice("44:44:33:11:23:45", "switchbot") + switchbot_missing_service_uuid_adv = AdvertisementData( + local_name="switchbot", + ) + + inject_advertisement( + hass, switchbot_missing_service_uuid_dev, switchbot_missing_service_uuid_adv + ) + await hass.async_block_till_done() + + service_uuid_wrong_address_dev = BLEDevice("44:44:33:11:23:22", "switchbot2") + service_uuid_wrong_address_adv = AdvertisementData( + local_name="switchbot2", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ) + inject_advertisement( + hass, service_uuid_wrong_address_dev, service_uuid_wrong_address_adv + ) + + cancel() + + assert len(callbacks) == 1 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "switchbot" + + +async def test_register_callback_by_service_data_uuid_and_address( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by service_data_uuid and address.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + { + SERVICE_DATA_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + ADDRESS: "44:44:33:11:23:45", + }, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_dev = BLEDevice("44:44:33:11:23:45", "switchbot") + switchbot_adv = AdvertisementData( + local_name="switchbot", + service_data={"cba20d00-224d-11e6-9fb8-0002a5d5c51b": b"x"}, + ) + + inject_advertisement(hass, switchbot_dev, switchbot_adv) + + switchbot_missing_service_uuid_dev = BLEDevice("44:44:33:11:23:45", "switchbot") + switchbot_missing_service_uuid_adv = AdvertisementData( + local_name="switchbot", + ) + + inject_advertisement( + hass, switchbot_missing_service_uuid_dev, switchbot_missing_service_uuid_adv + ) + await hass.async_block_till_done() + + service_uuid_wrong_address_dev = BLEDevice("44:44:33:11:23:22", "switchbot2") + service_uuid_wrong_address_adv = AdvertisementData( + local_name="switchbot2", + service_data={"cba20d00-224d-11e6-9fb8-0002a5d5c51b": b"x"}, + ) + inject_advertisement( + hass, service_uuid_wrong_address_dev, service_uuid_wrong_address_adv + ) + + cancel() + + assert len(callbacks) == 1 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "switchbot" + + +async def test_register_callback_by_local_name( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by local_name.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {LOCAL_NAME: "apple"}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_adv = AdvertisementData( + local_name="apple", + manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + inject_advertisement(hass, empty_device, empty_adv) + + apple_device_2 = BLEDevice("44:44:33:11:23:45", "apple") + apple_adv_2 = AdvertisementData( + local_name="apple2", + manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + ) + inject_advertisement(hass, apple_device_2, apple_adv_2) + + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 1 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "apple" + assert service_info.manufacturer == "Apple, Inc." + assert service_info.manufacturer_id == 76 + + +async def test_register_callback_by_local_name_overly_broad( + hass, mock_bleak_scanner_start, enable_bluetooth, caplog +): + """Test registering a callback by local_name that is too broad.""" + mock_bt = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with pytest.raises(ValueError): + bluetooth.async_register_callback( + hass, + _fake_subscriber, + {LOCAL_NAME: "a"}, + BluetoothScanningMode.ACTIVE, + ) + + with pytest.raises(ValueError): + bluetooth.async_register_callback( + hass, + _fake_subscriber, + {LOCAL_NAME: "ab*"}, + BluetoothScanningMode.ACTIVE, + ) + + +async def test_register_callback_by_service_data_uuid( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by service_data_uuid.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {SERVICE_DATA_UUID: "0000fe95-0000-1000-8000-00805f9b34fb"}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "xiaomi") + apple_adv = AdvertisementData( + local_name="xiaomi", + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"\xd8.\xad\xcd\r\x85" + }, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + inject_advertisement(hass, empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 1 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "xiaomi" + + async def test_register_callback_survives_reload( hass, mock_bleak_scanner_start, enable_bluetooth ): @@ -1169,7 +1750,7 @@ async def test_register_callback_survives_reload( hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - bluetooth.async_register_callback( + cancel = bluetooth.async_register_callback( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, @@ -1203,6 +1784,7 @@ async def test_register_callback_survives_reload( assert service_info.name == "wohand" assert service_info.manufacturer == "Nordic Semiconductor ASA" assert service_info.manufacturer_id == 89 + cancel() async def test_process_advertisements_bail_on_good_advertisement( From fcba6def496a9854bbb17c3901dcf187865b2fe4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 27 Aug 2022 13:50:41 +0200 Subject: [PATCH 679/903] Replace STATE_HOME with STATE_IDLE (#77385) --- homeassistant/components/roku/media_player.py | 5 +---- tests/components/roku/test_media_player.py | 5 ++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index d7b9a3489c9..d9866a3d77a 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -30,7 +30,6 @@ from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVI from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_NAME, - STATE_HOME, STATE_IDLE, STATE_ON, STATE_PAUSED, @@ -161,13 +160,11 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if ( self.coordinator.data.app.name == "Power Saver" + or self.coordinator.data.app.name == "Roku" or self.coordinator.data.app.screensaver ): return STATE_IDLE - if self.coordinator.data.app.name == "Roku": - return STATE_HOME - if self.coordinator.data.media: if self.coordinator.data.media.paused: return STATE_PAUSED diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index c95eda2288a..8950bafe094 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -68,7 +68,6 @@ from homeassistant.const import ( SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP, - STATE_HOME, STATE_IDLE, STATE_ON, STATE_PAUSED, @@ -195,7 +194,7 @@ async def test_availability( mock_roku.update.side_effect = None async_fire_time_changed(hass, future) await hass.async_block_till_done() - assert hass.states.get(MAIN_ENTITY_ID).state == STATE_HOME + assert hass.states.get(MAIN_ENTITY_ID).state == STATE_IDLE async def test_supported_features( @@ -253,7 +252,7 @@ async def test_attributes( """Test attributes.""" state = hass.states.get(MAIN_ENTITY_ID) assert state - assert state.state == STATE_HOME + assert state.state == STATE_IDLE assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) is None assert state.attributes.get(ATTR_APP_ID) is None From a6770f8b03c67a129080df0dee4467dca6bf81f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Aug 2022 08:23:47 -0500 Subject: [PATCH 680/903] Adjust bluetooth matcher comments (#77409) --- homeassistant/components/bluetooth/match.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index e9f535200b5..813acfc8cda 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -159,7 +159,7 @@ class BluetoothMatcherIndexBase(Generic[_T]): likely to match. This allows us to only check the service infos against each bucket to see if we should match against the data. - This is optimized for cases were no service infos will be matched in + This is optimized for cases when no service infos will be matched in any bucket and we can quickly reject the service info as not matching. """ @@ -349,7 +349,7 @@ def ble_device_matches( """Check if a ble device and advertisement_data matches the matcher.""" device = service_info.device - # Do don't check address here since all callers already + # Don't check address here since all callers already # check the address and we don't want to double check # since it would result in an unreachable reject case. From b2e958292caf7838a8d15a810b81f257786805d1 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sat, 27 Aug 2022 15:25:29 +0200 Subject: [PATCH 681/903] Add support for BThome (#77224) * Add BThome BLE * Update BThome to latest version * 0.3.4 * Rename to bthome 2 * Fix uuids not being found * Make energy a total increasing state class * Change unit of measurement of VOC * Use short identifier * Fix the reauth flow * Bump bthome_ble * Parameterize sensor tests * Remove Move function to parameter Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 + homeassistant/components/bthome/__init__.py | 76 +++ .../components/bthome/config_flow.py | 197 ++++++ homeassistant/components/bthome/const.py | 3 + homeassistant/components/bthome/device.py | 31 + homeassistant/components/bthome/manifest.json | 20 + homeassistant/components/bthome/sensor.py | 201 +++++++ homeassistant/components/bthome/strings.json | 32 + .../components/bthome/translations/en.json | 34 ++ homeassistant/generated/bluetooth.py | 10 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/bthome/__init__.py | 124 ++++ tests/components/bthome/conftest.py | 8 + tests/components/bthome/test_config_flow.py | 560 ++++++++++++++++++ tests/components/bthome/test_sensor.py | 291 +++++++++ 17 files changed, 1596 insertions(+) create mode 100644 homeassistant/components/bthome/__init__.py create mode 100644 homeassistant/components/bthome/config_flow.py create mode 100644 homeassistant/components/bthome/const.py create mode 100644 homeassistant/components/bthome/device.py create mode 100644 homeassistant/components/bthome/manifest.json create mode 100644 homeassistant/components/bthome/sensor.py create mode 100644 homeassistant/components/bthome/strings.json create mode 100644 homeassistant/components/bthome/translations/en.json create mode 100644 tests/components/bthome/__init__.py create mode 100644 tests/components/bthome/conftest.py create mode 100644 tests/components/bthome/test_config_flow.py create mode 100644 tests/components/bthome/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index db255191c38..e22d2468f25 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -159,6 +159,8 @@ build.json @home-assistant/supervisor /homeassistant/components/bsblan/ @liudger /tests/components/bsblan/ @liudger /homeassistant/components/bt_smarthub/ @jxwolstenholme +/homeassistant/components/bthome/ @Ernst79 +/tests/components/bthome/ @Ernst79 /homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221 /tests/components/buienradar/ @mjj4791 @ties @Robbie1221 /homeassistant/components/button/ @home-assistant/core diff --git a/homeassistant/components/bthome/__init__.py b/homeassistant/components/bthome/__init__.py new file mode 100644 index 00000000000..4cc3b5cf4da --- /dev/null +++ b/homeassistant/components/bthome/__init__.py @@ -0,0 +1,76 @@ +"""The BThome Bluetooth integration.""" +from __future__ import annotations + +import logging + +from bthome_ble import BThomeBluetoothDeviceData, SensorUpdate +from bthome_ble.parser import EncryptionScheme + +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +def process_service_info( + hass: HomeAssistant, + entry: ConfigEntry, + data: BThomeBluetoothDeviceData, + service_info: BluetoothServiceInfoBleak, +) -> SensorUpdate: + """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + update = data.update(service_info) + # If that payload was encrypted and the bindkey was not verified then we need to reauth + if data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified: + entry.async_start_reauth(hass, data={"device": data}) + + return update + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up BThome Bluetooth from a config entry.""" + address = entry.unique_id + assert address is not None + + kwargs = {} + if bindkey := entry.data.get("bindkey"): + kwargs["bindkey"] = bytes.fromhex(bindkey) + data = BThomeBluetoothDeviceData(**kwargs) + + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=lambda service_info: process_service_info( + hass, entry, data, service_info + ), + connectable=False, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/bthome/config_flow.py b/homeassistant/components/bthome/config_flow.py new file mode 100644 index 00000000000..e8e49cab566 --- /dev/null +++ b/homeassistant/components/bthome/config_flow.py @@ -0,0 +1,197 @@ +"""Config flow for BThome Bluetooth integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import dataclasses +from typing import Any + +from bthome_ble import BThomeBluetoothDeviceData as DeviceData +from bthome_ble.parser import EncryptionScheme +import voluptuous as vol + +from homeassistant.components import onboarding +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +@dataclasses.dataclass +class Discovery: + """A discovered bluetooth device.""" + + title: str + discovery_info: BluetoothServiceInfo + device: DeviceData + + +def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: + return device.title or device.get_device_name() or discovery_info.name + + +class BThomeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for BThome Bluetooth.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, Discovery] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + + title = _title(discovery_info, device) + self.context["title_placeholders"] = {"name": title} + self._discovery_info = discovery_info + self._discovered_device = device + + if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY: + return await self.async_step_get_encryption_key() + return await self.async_step_bluetooth_confirm() + + async def async_step_get_encryption_key( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Enter a bindkey for an encrypted BThome device.""" + assert self._discovery_info + assert self._discovered_device + + errors = {} + + if user_input is not None: + bindkey = user_input["bindkey"] + + if len(bindkey) != 32: + errors["bindkey"] = "expected_32_characters" + else: + self._discovered_device.bindkey = bytes.fromhex(bindkey) + + # If we got this far we already know supported will + # return true so we don't bother checking that again + # We just want to retry the decryption + self._discovered_device.supported(self._discovery_info) + + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) + + errors["bindkey"] = "decryption_failed" + + return self.async_show_form( + step_id="get_encryption_key", + description_placeholders=self.context["title_placeholders"], + data_schema=vol.Schema({vol.Required("bindkey"): vol.All(str, vol.Strip)}), + errors=errors, + ) + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + return self._async_get_or_create_entry() + + self._set_confirm_only() + return self.async_show_form( + step_id="bluetooth_confirm", + description_placeholders=self.context["title_placeholders"], + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + discovery = self._discovered_devices[address] + + self.context["title_placeholders"] = {"name": discovery.title} + + self._discovery_info = discovery.discovery_info + self._discovered_device = discovery.device + + if discovery.device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY: + return await self.async_step_get_encryption_key() + + return self._async_get_or_create_entry() + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = Discovery( + title=_title(discovery_info, device), + discovery_info=discovery_info, + device=device, + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + titles = { + address: discovery.title + for (address, discovery) in self._discovered_devices.items() + } + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a flow initialized by a reauth event.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry is not None + + device: DeviceData = entry_data["device"] + self._discovered_device = device + + self._discovery_info = device.last_service_info + + if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY: + return await self.async_step_get_encryption_key() + + # Otherwise there wasn't actually encryption so abort + return self.async_abort(reason="reauth_successful") + + def _async_get_or_create_entry(self, bindkey=None): + data = {} + if bindkey: + data["bindkey"] = bindkey + + if entry_id := self.context.get("entry_id"): + entry = self.hass.config_entries.async_get_entry(entry_id) + assert entry is not None + + self.hass.config_entries.async_update_entry(entry, data=data) + + # Reload the config entry to notify of updated config + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data=data, + ) diff --git a/homeassistant/components/bthome/const.py b/homeassistant/components/bthome/const.py new file mode 100644 index 00000000000..e397e288071 --- /dev/null +++ b/homeassistant/components/bthome/const.py @@ -0,0 +1,3 @@ +"""Constants for the BThome Bluetooth integration.""" + +DOMAIN = "bthome" diff --git a/homeassistant/components/bthome/device.py b/homeassistant/components/bthome/device.py new file mode 100644 index 00000000000..f16b2f49998 --- /dev/null +++ b/homeassistant/components/bthome/device.py @@ -0,0 +1,31 @@ +"""Support for BThome Bluetooth devices.""" +from __future__ import annotations + +from bthome_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json new file mode 100644 index 00000000000..d823d70dd39 --- /dev/null +++ b/homeassistant/components/bthome/manifest.json @@ -0,0 +1,20 @@ +{ + "domain": "bthome", + "name": "BThome", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/bthome", + "bluetooth": [ + { + "connectable": false, + "service_data_uuid": "0000181c-0000-1000-8000-00805f9b34fb" + }, + { + "connectable": false, + "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" + } + ], + "requirements": ["bthome-ble==0.4.0"], + "dependencies": ["bluetooth"], + "codeowners": ["@Ernst79"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py new file mode 100644 index 00000000000..5cc3317ea82 --- /dev/null +++ b/homeassistant/components/bthome/sensor.py @@ -0,0 +1,201 @@ +"""Support for BThome sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from bthome_ble import DeviceClass, SensorUpdate, Units + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + LIGHT_LUX, + MASS_KILOGRAMS, + PERCENTAGE, + POWER_WATT, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( + key=f"{DeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( + key=str(Units.ELECTRIC_POTENTIAL_VOLT), + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.ENERGY, Units.ENERGY_KILO_WATT_HOUR): SensorEntityDescription( + key=str(Units.ENERGY_KILO_WATT_HOUR), + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + (DeviceClass.POWER, Units.POWER_WATT): SensorEntityDescription( + key=str(Units.POWER_WATT), + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.PM10, + Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ): SensorEntityDescription( + key=f"{DeviceClass.PM10}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.PM25, + Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ): SensorEntityDescription( + key=f"{DeviceClass.PM25}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.CO2, Units.CONCENTRATION_PARTS_PER_MILLION,): SensorEntityDescription( + key=f"{DeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ): SensorEntityDescription( + key=f"{DeviceClass.VOLATILE_ORGANIC_COMPOUNDS}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + # Used for e.g. weight sensor + (None, Units.MASS_KILOGRAMS): SensorEntityDescription( + key=str(Units.MASS_KILOGRAMS), + device_class=None, + native_unit_of_measurement=MASS_KILOGRAMS, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.native_unit_of_measurement + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the BThome BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + BThomeBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class BThomeBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a BThome BLE sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/bthome/strings.json b/homeassistant/components/bthome/strings.json new file mode 100644 index 00000000000..f2fdcc64826 --- /dev/null +++ b/homeassistant/components/bthome/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + }, + "get_encryption_key": { + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 32 character hexadecimal bindkey.", + "data": { + "bindkey": "Bindkey" + } + } + }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, + "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/bthome/translations/en.json b/homeassistant/components/bthome/translations/en.json new file mode 100644 index 00000000000..bb2f09bafab --- /dev/null +++ b/homeassistant/components/bthome/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", + "no_devices_found": "No devices found on the network", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Bindkey" + }, + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 32 character hexadecimal bindkey." + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 246ee56acb6..bda76859688 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,16 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ + { + "domain": "bthome", + "connectable": False, + "service_data_uuid": "0000181c-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "bthome", + "connectable": False, + "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" + }, { "domain": "fjaraskupan", "manufacturer_id": 20296, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1ff8832f102..07a5cdce04f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -57,6 +57,7 @@ FLOWS = { "brother", "brunt", "bsblan", + "bthome", "buienradar", "canary", "cast", diff --git a/requirements_all.txt b/requirements_all.txt index 30c1a8b298a..9376815076d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -457,6 +457,9 @@ bsblan==0.5.0 # homeassistant.components.bluetooth_tracker bt_proximity==0.2.1 +# homeassistant.components.bthome +bthome-ble==0.4.0 + # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af2d1930de7..71760258e21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -358,6 +358,9 @@ brunt==1.2.0 # homeassistant.components.bsblan bsblan==0.5.0 +# homeassistant.components.bthome +bthome-ble==0.4.0 + # homeassistant.components.buienradar buienradar==1.0.5 diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py new file mode 100644 index 00000000000..7cb6496b5c5 --- /dev/null +++ b/tests/components/bthome/__init__.py @@ -0,0 +1,124 @@ +"""Tests for the BThome integration.""" + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + +TEMP_HUMI_SERVICE_INFO = BluetoothServiceInfoBleak( + name="ATC 8D18B2", + address="A4:C1:38:8D:18:B2", + device=BLEDevice("A4:C1:38:8D:18:B2", None), + rssi=-63, + manufacturer_data={}, + service_data={ + "0000181c-0000-1000-8000-00805f9b34fb": b"#\x02\xca\t\x03\x03\xbf\x13" + }, + service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, +) + +TEMP_HUMI_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( + name="TEST DEVICE 8F80A5", + address="54:48:E6:8F:80:A5", + device=BLEDevice("54:48:E6:8F:80:A5", None), + rssi=-63, + manufacturer_data={}, + service_data={ + "0000181e-0000-1000-8000-00805f9b34fb": b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99' + }, + service_uuids=["0000181e-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, +) + +PM_SERVICE_INFO = BluetoothServiceInfoBleak( + name="TEST DEVICE 8F80A5", + address="54:48:E6:8F:80:A5", + device=BLEDevice("54:48:E6:8F:80:A5", None), + rssi=-63, + manufacturer_data={}, + service_data={ + "0000181c-0000-1000-8000-00805f9b34fb": b"\x03\r\x12\x0c\x03\x0e\x02\x1c" + }, + service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, +) + +INVALID_PAYLOAD = BluetoothServiceInfoBleak( + name="ATC 565384", + address="A4:C1:38:56:53:84", + device=BLEDevice("A4:C1:38:56:53:84", None), + rssi=-56, + manufacturer_data={}, + service_data={ + "0000181c-0000-1000-8000-00805f9b34fb": b"0X[\x05\x02\x84\x53\x568\xc1\xa4\x08", + }, + service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, +) + +NOT_BTHOME_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Not it", + address="00:00:00:00:00:00", + device=BLEDevice("00:00:00:00:00:00", None), + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", + advertisement=AdvertisementData(local_name="Not it"), + time=0, + connectable=False, +) + + +def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBleak: + """Make a dummy advertisement.""" + return BluetoothServiceInfoBleak( + name="Test Device", + address=address, + device=BLEDevice(address, None), + rssi=-56, + manufacturer_data={}, + service_data={ + "0000181c-0000-1000-8000-00805f9b34fb": payload, + }, + service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=AdvertisementData(local_name="Test Device"), + time=0, + connectable=False, + ) + + +def make_encrypted_advertisement( + address: str, payload: bytes +) -> BluetoothServiceInfoBleak: + """Make a dummy encrypted advertisement.""" + return BluetoothServiceInfoBleak( + name="ATC 8F80A5", + address=address, + device=BLEDevice(address, None), + rssi=-56, + manufacturer_data={}, + service_data={ + "0000181e-0000-1000-8000-00805f9b34fb": payload, + }, + service_uuids=["0000181e-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=AdvertisementData(local_name="ATC 8F80A5"), + time=0, + connectable=False, + ) diff --git a/tests/components/bthome/conftest.py b/tests/components/bthome/conftest.py new file mode 100644 index 00000000000..9fce8e85ea8 --- /dev/null +++ b/tests/components/bthome/conftest.py @@ -0,0 +1,8 @@ +"""Session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/bthome/test_config_flow.py b/tests/components/bthome/test_config_flow.py new file mode 100644 index 00000000000..b1154ca9223 --- /dev/null +++ b/tests/components/bthome/test_config_flow.py @@ -0,0 +1,560 @@ +"""Test the BThome config flow.""" + +from unittest.mock import patch + +from bthome_ble import BThomeBluetoothDeviceData as DeviceData + +from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.bthome.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + NOT_BTHOME_SERVICE_INFO, + PM_SERVICE_INFO, + TEMP_HUMI_ENCRYPTED_SERVICE_INFO, + TEMP_HUMI_SERVICE_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TEMP_HUMI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "ATC 18B2" + assert result2["data"] == {} + assert result2["result"].unique_id == "A4:C1:38:8D:18:B2" + + +async def test_async_step_bluetooth_during_onboarding(hass): + """Test discovery via bluetooth during onboarding.""" + with patch( + "homeassistant.components.bthome.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TEMP_HUMI_SERVICE_INFO, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ATC 18B2" + assert result["data"] == {} + assert result["result"].unique_id == "A4:C1:38:8D:18:B2" + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + +async def test_async_step_bluetooth_valid_device_with_encryption(hass): + """Test discovery via bluetooth with a valid device, with encryption.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TEMP_HUMI_ENCRYPTED_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key" + + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TEST DEVICE 80A5" + assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} + assert result2["result"].unique_id == "54:48:E6:8F:80:A5" + + +async def test_async_step_bluetooth_valid_device_encryption_wrong_key(hass): + """Test discovery via bluetooth with a valid device, with encryption and invalid key.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TEMP_HUMI_ENCRYPTED_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Test can finish flow + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TEST DEVICE 80A5" + assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} + assert result2["result"].unique_id == "54:48:E6:8F:80:A5" + + +async def test_async_step_bluetooth_valid_device_encryption_wrong_key_length(hass): + """Test discovery via bluetooth with a valid device, with encryption and wrong key length.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TEMP_HUMI_ENCRYPTED_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aa"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key" + assert result2["errors"]["bindkey"] == "expected_32_characters" + + # Test can finish flow + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TEST DEVICE 80A5" + assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} + assert result2["result"].unique_id == "54:48:E6:8F:80:A5" + + +async def test_async_step_bluetooth_not_supported(hass): + """Test discovery via bluetooth not supported.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_BTHOME_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_no_devices_found_2(hass): + """ + Test setup from service info cache with no devices found. + + This variant tests with a non-BThome device known to us. + """ + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[NOT_BTHOME_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.bthome.config_flow.async_discovered_service_info", + return_value=[PM_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:48:E6:8F:80:A5"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TEST DEVICE 80A5" + assert result2["data"] == {} + assert result2["result"].unique_id == "54:48:E6:8F:80:A5" + + +async def test_async_step_user_with_found_devices_encryption(hass): + """Test setup from service info cache with devices found, with encryption.""" + with patch( + "homeassistant.components.bthome.config_flow.async_discovered_service_info", + return_value=[TEMP_HUMI_ENCRYPTED_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:48:E6:8F:80:A5"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key" + + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TEST DEVICE 80A5" + assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} + assert result2["result"].unique_id == "54:48:E6:8F:80:A5" + + +async def test_async_step_user_with_found_devices_encryption_wrong_key(hass): + """Test setup from service info cache with devices found, with encryption and wrong key.""" + # Get a list of devices + with patch( + "homeassistant.components.bthome.config_flow.async_discovered_service_info", + return_value=[TEMP_HUMI_ENCRYPTED_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Pick a device + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:48:E6:8F:80:A5"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key" + + # Try an incorrect key + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Check can still finish flow + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TEST DEVICE 80A5" + assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} + assert result2["result"].unique_id == "54:48:E6:8F:80:A5" + + +async def test_async_step_user_with_found_devices_encryption_wrong_key_length(hass): + """Test setup from service info cache with devices found, with encryption and wrong key length.""" + # Get a list of devices + with patch( + "homeassistant.components.bthome.config_flow.async_discovered_service_info", + return_value=[TEMP_HUMI_ENCRYPTED_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Select a single device + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:48:E6:8F:80:A5"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key" + + # Try an incorrect key + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aa"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key" + assert result2["errors"]["bindkey"] == "expected_32_characters" + + # Check can still finish flow + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TEST DEVICE 80A5" + assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} + assert result2["result"].unique_id == "54:48:E6:8F:80:A5" + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.bthome.config_flow.async_discovered_service_info", + return_value=[TEMP_HUMI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:8D:18:B2", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:8D:18:B2"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:8D:18:B2", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bthome.config_flow.async_discovered_service_info", + return_value=[TEMP_HUMI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:48:E6:8F:80:A5", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=PM_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=PM_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=PM_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=PM_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.bthome.config_flow.async_discovered_service_info", + return_value=[PM_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:48:E6:8F:80:A5"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "TEST DEVICE 80A5" + assert result2["data"] == {} + assert result2["result"].unique_id == "54:48:E6:8F:80:A5" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) + + +async def test_async_step_reauth(hass): + """Test reauth with a key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:48:E6:8F:80:A5", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + saved_callback(TEMP_HUMI_ENCRYPTED_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_wrong_key(hass): + """Test reauth with a bad key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:48:E6:8F:80:A5", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + saved_callback(TEMP_HUMI_ENCRYPTED_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_abort_early(hass): + """ + Test we can abort the reauth if there is no encryption. + + (This can't currently happen in practice). + """ + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:48:E6:8F:80:A5", + ) + entry.add_to_hass(hass) + + device = DeviceData() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + }, + data=entry.data | {"device": device}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py new file mode 100644 index 00000000000..07ccfe2288c --- /dev/null +++ b/tests/components/bthome/test_sensor.py @@ -0,0 +1,291 @@ +"""Test the BThome sensors.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.bthome.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import make_advertisement, make_encrypted_advertisement + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "mac_address, advertisement, bind_key, result", + [ + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"#\x02\xca\t\x03\x03\xbf\x13", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature", + "friendly_name": "Test Device 18B2 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity", + "friendly_name": "Test Device 18B2 Humidity", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "50.55", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x02\x00\xa8#\x02]\t\x03\x03\xb7\x18\x02\x01]", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature", + "friendly_name": "Test Device 18B2 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "23.97", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity", + "friendly_name": "Test Device 18B2 Humidity", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "63.27", + }, + { + "sensor_entity": "sensor.test_device_18b2_battery", + "friendly_name": "Test Device 18B2 Battery", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "93", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x02\x00\x0c\x04\x04\x13\x8a\x01", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_pressure", + "friendly_name": "Test Device 18B2 Pressure", + "unit_of_measurement": "mbar", + "state_class": "measurement", + "expected_state": "1008.83", + }, + ], + ), + ( + "AA:BB:CC:DD:EE:FF", + make_advertisement( + "AA:BB:CC:DD:EE:FF", + b"\x04\x05\x13\x8a\x14", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_eeff_illuminance", + "friendly_name": "Test Device EEFF Illuminance", + "unit_of_measurement": "lx", + "state_class": "measurement", + "expected_state": "13460.67", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x04\n\x13\x8a\x14", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_energy", + "friendly_name": "Test Device 18B2 Energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + "expected_state": "1346.067", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x04\x0b\x02\x1b\x00", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_power", + "friendly_name": "Test Device 18B2 Power", + "unit_of_measurement": "W", + "state_class": "measurement", + "expected_state": "69.14", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x03\x0c\x02\x0c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_voltage", + "friendly_name": "Test Device 18B2 Voltage", + "unit_of_measurement": "V", + "state_class": "measurement", + "expected_state": "3.074", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x03\r\x12\x0c\x03\x0e\x02\x1c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_pm10", + "friendly_name": "Test Device 18B2 Pm10", + "unit_of_measurement": "µg/m³", + "state_class": "measurement", + "expected_state": "7170", + }, + { + "sensor_entity": "sensor.test_device_18b2_pm25", + "friendly_name": "Test Device 18B2 Pm25", + "unit_of_measurement": "µg/m³", + "state_class": "measurement", + "expected_state": "3090", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x03\x12\xe2\x04", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_carbon_dioxide", + "friendly_name": "Test Device 18B2 Carbon Dioxide", + "unit_of_measurement": "ppm", + "state_class": "measurement", + "expected_state": "1250", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x03\x133\x01", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_volatile_organic_compounds", + "friendly_name": "Test Device 18B2 Volatile Organic Compounds", + "unit_of_measurement": "µg/m³", + "state_class": "measurement", + "expected_state": "307", + }, + ], + ), + ( + "54:48:E6:8F:80:A5", + make_encrypted_advertisement( + "54:48:E6:8F:80:A5", + b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99', + ), + "231d39c1d7cc1ab1aee224cd096db932", + [ + { + "sensor_entity": "sensor.atc_80a5_temperature", + "friendly_name": "ATC 80A5 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.atc_80a5_humidity", + "friendly_name": "ATC 80A5 Humidity", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "50.55", + }, + ], + ), + ], +) +async def test_sensors( + hass, + mac_address, + advertisement, + bind_key, + result, +): + """Test the different measurement sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=mac_address, + data={"bindkey": bind_key}, + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + saved_callback( + advertisement, + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == len(result) + + for meas in result: + sensor = hass.states.get(meas["sensor_entity"]) + sensor_attr = sensor.attributes + assert sensor.state == meas["expected_state"] + + assert sensor_attr[ATTR_FRIENDLY_NAME] == meas["friendly_name"] + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == meas["unit_of_measurement"] + assert sensor_attr[ATTR_STATE_CLASS] == meas["state_class"] + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 448b720eb7bf05911efd5caf1b5ccbd8700cdc60 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 27 Aug 2022 17:17:02 +0200 Subject: [PATCH 682/903] Fix trait processing Fan state without percentage_step (#77351) * Fix trait processing Fan without percentage_step * Update homeassistant/components/google_assistant/trait.py Co-authored-by: Joakim Plate * Fix test * Fix formatting Co-authored-by: Joakim Plate --- .../components/google_assistant/trait.py | 4 ++- .../components/google_assistant/test_trait.py | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index defc5b0cc89..4f2971c01fa 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1397,7 +1397,9 @@ class FanSpeedTrait(_Trait): if state.domain == fan.DOMAIN: speed_count = min( FAN_SPEED_MAX_SPEED_COUNT, - round(100 / self.state.attributes.get(fan.ATTR_PERCENTAGE_STEP) or 1.0), + round( + 100 / (self.state.attributes.get(fan.ATTR_PERCENTAGE_STEP) or 1.0) + ), ) self._ordered_speed = [ f"{speed}/{speed_count}" for speed in range(1, speed_count + 1) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a3024c184d6..bd12fdab61a 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1618,6 +1618,32 @@ async def test_fan_speed(hass): assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10} +async def test_fan_speed_without_percentage_step(hass): + """Test FanSpeed trait speed control percentage step for fan domain.""" + assert helpers.get_google_type(fan.DOMAIN, None) is not None + assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None, None) + + trt = trait.FanSpeedTrait( + hass, + State( + "fan.living_room_fan", + STATE_ON, + ), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == { + "reversible": False, + "supportsFanSpeedPercent": True, + "availableFanSpeeds": ANY, + } + # If a fan state has (temporary) no percentage_step attribute return 1 available + assert trt.query_attributes() == { + "currentFanSpeedPercent": 0, + "currentFanSpeedSetting": "1/5", + } + + @pytest.mark.parametrize( "percentage,percentage_step, speed, speeds, percentage_result", [ From 1addf5a51bb68f8430c0ca6dcea861b91357df79 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 27 Aug 2022 18:59:57 +0300 Subject: [PATCH 683/903] Upgarde PyRisco to 0.5.3 (#77407) Upgarde PyRisco --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 0136e8f54de..38035e22c62 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.2"], + "requirements": ["pyrisco==0.5.3"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 9376815076d..c383e782d52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1799,7 +1799,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.2 +pyrisco==0.5.3 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71760258e21..f34a5a48818 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1255,7 +1255,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.2 +pyrisco==0.5.3 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From 15ad10643a92502ed55622e6a43001cc2ca6b2a0 Mon Sep 17 00:00:00 2001 From: Kris Molendyke Date: Sat, 27 Aug 2022 12:07:56 -0400 Subject: [PATCH 684/903] Bump Tank Utility Version (#77103) * Bump Tank Utility Version Bumps to a new release which includes the license in the artifact. * Regen all reqs --- homeassistant/components/tank_utility/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tank_utility/manifest.json b/homeassistant/components/tank_utility/manifest.json index a9ebcb546b5..c7afeb835e8 100644 --- a/homeassistant/components/tank_utility/manifest.json +++ b/homeassistant/components/tank_utility/manifest.json @@ -2,7 +2,7 @@ "domain": "tank_utility", "name": "Tank Utility", "documentation": "https://www.home-assistant.io/integrations/tank_utility", - "requirements": ["tank_utility==1.4.0"], + "requirements": ["tank_utility==1.4.1"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["tank_utility"] diff --git a/requirements_all.txt b/requirements_all.txt index c383e782d52..55bb47eb0e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2309,7 +2309,7 @@ systembridgeconnector==3.4.4 tailscale==0.2.0 # homeassistant.components.tank_utility -tank_utility==1.4.0 +tank_utility==1.4.1 # homeassistant.components.tapsaff tapsaff==0.2.1 From 8e88e039f7091f77270fba420a674e4a6f00abca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Aug 2022 16:41:49 -0500 Subject: [PATCH 685/903] Add diagnostics to bluetooth (#77393) --- .../components/bluetooth/diagnostics.py | 28 ++++ homeassistant/components/bluetooth/manager.py | 26 +++- .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/models.py | 14 ++ homeassistant/components/bluetooth/scanner.py | 11 ++ homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/conftest.py | 5 + .../components/bluetooth/test_diagnostics.py | 126 ++++++++++++++++++ 10 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/bluetooth/diagnostics.py create mode 100644 tests/components/bluetooth/test_diagnostics.py diff --git a/homeassistant/components/bluetooth/diagnostics.py b/homeassistant/components/bluetooth/diagnostics.py new file mode 100644 index 00000000000..612c51806dd --- /dev/null +++ b/homeassistant/components/bluetooth/diagnostics.py @@ -0,0 +1,28 @@ +"""Diagnostics support for bluetooth.""" +from __future__ import annotations + +import platform +from typing import Any + +from bluetooth_adapters import get_dbus_managed_objects + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import _get_manager + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + manager = _get_manager(hass) + manager_diagnostics = await manager.async_diagnostics() + adapters = await manager.async_get_bluetooth_adapters() + diagnostics = { + "manager": manager_diagnostics, + "adapters": adapters, + } + if platform.system() == "Linux": + diagnostics["dbus"] = await get_dbus_managed_objects() + return diagnostics diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index be5038a6d31..984d37d806d 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -1,11 +1,13 @@ """The bluetooth integration.""" from __future__ import annotations +import asyncio from collections.abc import Callable, Iterable +from dataclasses import asdict from datetime import datetime, timedelta import itertools import logging -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Any, Final from bleak.backends.scanner import AdvertisementDataCallback @@ -145,6 +147,28 @@ class BluetoothManager: self._connectable_scanners: list[BaseHaScanner] = [] self._adapters: dict[str, AdapterDetails] = {} + async def async_diagnostics(self) -> dict[str, Any]: + """Diagnostics for the manager.""" + scanner_diagnostics = await asyncio.gather( + *[ + scanner.async_diagnostics() + for scanner in itertools.chain( + self._scanners, self._connectable_scanners + ) + ] + ) + return { + "adapters": self._adapters, + "scanners": scanner_diagnostics, + "connectable_history": [ + asdict(service_info) + for service_info in self._connectable_history.values() + ], + "history": [ + asdict(service_info) for service_info in self._history.values() + ], + } + def _find_adapter_by_address(self, address: str) -> str | None: for adapter, details in self._adapters.items(): if details[ADAPTER_ADDRESS] == address: diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index cfe9590b2db..8e4f0eb75de 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.15.1", - "bluetooth-adapters==0.2.0", + "bluetooth-adapters==0.3.2", "bluetooth-auto-recovery==0.2.2" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 285e991ff81..6c70633f597 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -70,6 +70,20 @@ class BaseHaScanner: def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" + async def async_diagnostics(self) -> dict[str, Any]: + """Return diagnostic information about the scanner.""" + return { + "type": self.__class__.__name__, + "discovered_devices": [ + { + "name": device.name, + "address": device.address, + "rssi": device.rssi, + } + for device in self.discovered_devices + ], + } + class HaBleakScannerWrapper(BaseBleakScanner): """A wrapper that uses the single instance.""" diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index d186f613c94..78979198e5c 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -146,6 +146,17 @@ class HaScanner(BaseHaScanner): """Return a list of discovered devices.""" return self.scanner.discovered_devices + async def async_diagnostics(self) -> dict[str, Any]: + """Return diagnostic information about the scanner.""" + base_diag = await super().async_diagnostics() + return base_diag | { + "adapter": self.adapter, + "source": self.source, + "name": self.name, + "last_detection": self._last_detection, + "start_time": self._start_time, + } + @hass_callback def async_register_callback( self, callback: Callable[[BluetoothServiceInfoBleak], None] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 412a1841394..5de59a22eb7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.1 -bluetooth-adapters==0.2.0 +bluetooth-adapters==0.3.2 bluetooth-auto-recovery==0.2.2 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 55bb47eb0e4..cc1e87871d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.2.0 +bluetooth-adapters==0.3.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f34a5a48818..0d76676ec8f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.2.0 +bluetooth-adapters==0.3.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.2.2 diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 1ea9b8706d4..44b9a60d1b5 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -53,6 +53,11 @@ def one_adapter_fixture(): def two_adapters_fixture(): """Fixture that mocks two adapters on Linux.""" with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ), patch( + "homeassistant.components.bluetooth.scanner.platform.system", + return_value="Linux", + ), patch( "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" ), patch( "bluetooth_adapters.get_bluetooth_adapter_details", diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py new file mode 100644 index 00000000000..6f5eeefa5b6 --- /dev/null +++ b/tests/components/bluetooth/test_diagnostics.py @@ -0,0 +1,126 @@ +"""Test bluetooth diagnostics.""" + + +from unittest.mock import ANY, patch + +from bleak.backends.scanner import BLEDevice + +from homeassistant.components import bluetooth + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass, hass_client, mock_bleak_scanner_start, enable_bluetooth, two_adapters +): + """Test we can setup and unsetup bluetooth with multiple adapters.""" + # Normally we do not want to patch our classes, but since bleak will import + # a different scanner based on the operating system, we need to patch here + # because we cannot import the scanner class directly without it throwing an + # error if the test is not running on linux since we won't have the correct + # deps installed when testing on MacOS. + with patch( + "homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices", + [BLEDevice(name="x", rssi=-60, address="44:44:33:11:23:45")], + ), patch( + "homeassistant.components.bluetooth.diagnostics.platform.system", + return_value="Linux", + ), patch( + "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", + return_value={ + "org.bluez": { + "/org/bluez/hci0": { + "Interfaces": {"org.bluez.Adapter1": {"Discovering": False}} + } + } + }, + ): + entry1 = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry1.add_to_hass(hass) + + entry2 = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:02" + ) + entry2.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry1.entry_id) + await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry2.entry_id) + await hass.async_block_till_done() + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1) + assert diag == { + "adapters": { + "hci0": { + "address": "00:00:00:00:00:01", + "hw_version": "usbid:1234", + "sw_version": "BlueZ 4.63", + }, + "hci1": { + "address": "00:00:00:00:00:02", + "hw_version": "usbid:1234", + "sw_version": "BlueZ 4.63", + }, + }, + "dbus": { + "org.bluez": { + "/org/bluez/hci0": { + "Interfaces": {"org.bluez.Adapter1": {"Discovering": False}} + } + } + }, + "manager": { + "adapters": { + "hci0": { + "address": "00:00:00:00:00:01", + "hw_version": "usbid:1234", + "sw_version": "BlueZ 4.63", + }, + "hci1": { + "address": "00:00:00:00:00:02", + "hw_version": "usbid:1234", + "sw_version": "BlueZ 4.63", + }, + }, + "connectable_history": [], + "history": [], + "scanners": [ + { + "adapter": "hci0", + "discovered_devices": [ + {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60} + ], + "last_detection": ANY, + "name": "hci0 (00:00:00:00:00:01)", + "source": "hci0", + "start_time": ANY, + "type": "HaScanner", + }, + { + "adapter": "hci0", + "discovered_devices": [ + {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60} + ], + "last_detection": ANY, + "name": "hci0 (00:00:00:00:00:01)", + "source": "hci0", + "start_time": ANY, + "type": "HaScanner", + }, + { + "adapter": "hci1", + "discovered_devices": [ + {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60} + ], + "last_detection": ANY, + "name": "hci1 (00:00:00:00:00:02)", + "source": "hci1", + "start_time": ANY, + "type": "HaScanner", + }, + ], + }, + } From d25a76d3d6ab19bbcc08412cb85826fc401f9d29 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 27 Aug 2022 18:47:46 -0400 Subject: [PATCH 686/903] Use Platform and ValueType enum in zwave_js.discovery (#77402) --- .../components/zwave_js/discovery.py | 173 ++++++++++-------- 1 file changed, 93 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index e94f9645444..565d8af4ab0 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -43,6 +43,8 @@ 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 +from homeassistant.backports.enum import StrEnum +from homeassistant.const import Platform from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntry @@ -59,6 +61,15 @@ from .discovery_data_template import ( from .helpers import ZwaveValueID +class ValueType(StrEnum): + """Enum with all value types.""" + + ANY = "any" + BOOLEAN = "boolean" + NUMBER = "number" + STRING = "string" + + class DataclassMustHaveAtLeastOne: """A dataclass that must have at least one input parameter that is not None.""" @@ -97,7 +108,7 @@ class ZwaveDiscoveryInfo: # bool to specify whether state is assumed and events should be fired on value update assumed_state: bool # the home assistant platform for which an entity should be created - platform: str + platform: Platform # helper data to use in platform setup platform_data: Any # additional values that need to be watched by entity @@ -145,7 +156,7 @@ class ZWaveDiscoverySchema: """ # specify the hass platform for which this scheme applies (e.g. light, sensor) - platform: str + platform: Platform # primary value belonging to this discovery scheme primary_value: ZWaveValueDiscoverySchema # [optional] hint for platform @@ -194,7 +205,7 @@ def get_config_parameter_discovery_schema( and primary_value. """ return ZWaveDiscoverySchema( - platform="sensor", + platform=Platform.SENSOR, hint="config_parameter", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.CONFIGURATION}, @@ -202,7 +213,7 @@ def get_config_parameter_discovery_schema( property_name=property_name, property_key=property_key, property_key_name=property_key_name, - type={"number"}, + type={ValueType.NUMBER}, ), entity_registry_enabled_default=False, **kwargs, @@ -212,13 +223,13 @@ def get_config_parameter_discovery_schema( DOOR_LOCK_CURRENT_MODE_SCHEMA = ZWaveValueDiscoverySchema( command_class={CommandClass.DOOR_LOCK}, property={CURRENT_MODE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ) SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( command_class={CommandClass.SWITCH_MULTILEVEL}, property={CURRENT_VALUE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ) SWITCH_BINARY_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( @@ -228,7 +239,7 @@ SWITCH_BINARY_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( SIREN_TONE_SCHEMA = ZWaveValueDiscoverySchema( command_class={CommandClass.SOUND_SWITCH}, property={TONE_ID_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ) # For device class mapping see: @@ -237,7 +248,7 @@ DISCOVERY_SCHEMAS = [ # ====== START OF DEVICE SPECIFIC MAPPING SCHEMAS ======= # Honeywell 39358 In-Wall Fan Control using switch multilevel CC ZWaveDiscoverySchema( - platform="fan", + platform=Platform.FAN, manufacturer_id={0x0039}, product_id={0x3131}, product_type={0x4944}, @@ -245,7 +256,7 @@ DISCOVERY_SCHEMAS = [ ), # GE/Jasco - In-Wall Smart Fan Control - 12730 / ZW4002 ZWaveDiscoverySchema( - platform="fan", + platform=Platform.FAN, hint="has_fan_value_mapping", manufacturer_id={0x0063}, product_id={0x3034}, @@ -257,7 +268,7 @@ DISCOVERY_SCHEMAS = [ ), # GE/Jasco - In-Wall Smart Fan Control - 14287 / ZW4002 ZWaveDiscoverySchema( - platform="fan", + platform=Platform.FAN, hint="has_fan_value_mapping", manufacturer_id={0x0063}, product_id={0x3131}, @@ -269,7 +280,7 @@ DISCOVERY_SCHEMAS = [ ), # GE/Jasco - In-Wall Smart Fan Control - 14314 / ZW4002 ZWaveDiscoverySchema( - platform="fan", + platform=Platform.FAN, manufacturer_id={0x0063}, product_id={0x3138}, product_type={0x4944}, @@ -277,7 +288,7 @@ DISCOVERY_SCHEMAS = [ ), # Leviton ZW4SF fan controllers using switch multilevel CC ZWaveDiscoverySchema( - platform="fan", + platform=Platform.FAN, manufacturer_id={0x001D}, product_id={0x0002}, product_type={0x0038}, @@ -286,7 +297,7 @@ DISCOVERY_SCHEMAS = [ # Inovelli LZW36 light / fan controller combo using switch multilevel CC # The fan is endpoint 2, the light is endpoint 1. ZWaveDiscoverySchema( - platform="fan", + platform=Platform.FAN, hint="has_fan_value_mapping", manufacturer_id={0x031E}, product_id={0x0001}, @@ -295,7 +306,7 @@ DISCOVERY_SCHEMAS = [ command_class={CommandClass.SWITCH_MULTILEVEL}, endpoint={2}, property={CURRENT_VALUE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), data_template=FixedFanValueMappingDataTemplate( FanValueMapping( @@ -305,7 +316,7 @@ DISCOVERY_SCHEMAS = [ ), # HomeSeer HS-FC200+ ZWaveDiscoverySchema( - platform="fan", + platform=Platform.FAN, hint="has_fan_value_mapping", manufacturer_id={0x000C}, product_id={0x0001}, @@ -323,7 +334,7 @@ DISCOVERY_SCHEMAS = [ ), # Fibaro Shutter Fibaro FGR222 ZWaveDiscoverySchema( - platform="cover", + platform=Platform.COVER, hint="window_shutter_tilt", manufacturer_id={0x010F}, product_id={0x1000, 0x1001}, @@ -347,7 +358,7 @@ DISCOVERY_SCHEMAS = [ ), # Qubino flush shutter ZWaveDiscoverySchema( - platform="cover", + platform=Platform.COVER, hint="window_shutter", manufacturer_id={0x0159}, product_id={0x0052, 0x0053}, @@ -356,7 +367,7 @@ DISCOVERY_SCHEMAS = [ ), # Graber/Bali/Spring Fashion Covers ZWaveDiscoverySchema( - platform="cover", + platform=Platform.COVER, hint="window_blind", manufacturer_id={0x026E}, product_id={0x5A31}, @@ -365,7 +376,7 @@ DISCOVERY_SCHEMAS = [ ), # iBlinds v2 window blind motor ZWaveDiscoverySchema( - platform="cover", + platform=Platform.COVER, hint="window_blind", manufacturer_id={0x0287}, product_id={0x000D}, @@ -374,7 +385,7 @@ DISCOVERY_SCHEMAS = [ ), # Vision Security ZL7432 In Wall Dual Relay Switch ZWaveDiscoverySchema( - platform="switch", + platform=Platform.SWITCH, manufacturer_id={0x0109}, product_id={0x1711, 0x1717}, product_type={0x2017}, @@ -383,7 +394,7 @@ DISCOVERY_SCHEMAS = [ ), # Heatit Z-TRM3 ZWaveDiscoverySchema( - platform="climate", + platform=Platform.CLIMATE, hint="dynamic_current_temp", manufacturer_id={0x019B}, product_id={0x0203}, @@ -391,7 +402,7 @@ DISCOVERY_SCHEMAS = [ primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.THERMOSTAT_MODE}, property={THERMOSTAT_MODE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), data_template=DynamicCurrentTempClimateDataTemplate( lookup_table={ @@ -431,7 +442,7 @@ DISCOVERY_SCHEMAS = [ ), # Heatit Z-TRM2fx ZWaveDiscoverySchema( - platform="climate", + platform=Platform.CLIMATE, hint="dynamic_current_temp", manufacturer_id={0x019B}, product_id={0x0202}, @@ -440,7 +451,7 @@ DISCOVERY_SCHEMAS = [ primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.THERMOSTAT_MODE}, property={THERMOSTAT_MODE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), data_template=DynamicCurrentTempClimateDataTemplate( lookup_table={ @@ -469,7 +480,7 @@ DISCOVERY_SCHEMAS = [ ), # FortrezZ SSA1/SSA2/SSA3 ZWaveDiscoverySchema( - platform="select", + platform=Platform.SELECT, hint="multilevel_switch", manufacturer_id={0x0084}, product_id={0x0107, 0x0108, 0x010B, 0x0205}, @@ -486,7 +497,7 @@ DISCOVERY_SCHEMAS = [ ), # HomeSeer HSM-200 v1 ZWaveDiscoverySchema( - platform="light", + platform=Platform.LIGHT, hint="black_is_off", manufacturer_id={0x001E}, product_id={0x0001}, @@ -508,20 +519,22 @@ DISCOVERY_SCHEMAS = [ # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks # Door Lock CC - ZWaveDiscoverySchema(platform="lock", primary_value=DOOR_LOCK_CURRENT_MODE_SCHEMA), + ZWaveDiscoverySchema( + platform=Platform.LOCK, primary_value=DOOR_LOCK_CURRENT_MODE_SCHEMA + ), # Only discover the Lock CC if the Door Lock CC isn't also present on the node ZWaveDiscoverySchema( - platform="lock", + platform=Platform.LOCK, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.LOCK}, property={LOCKED_PROPERTY}, - type={"boolean"}, + type={ValueType.BOOLEAN}, ), absent_values=[DOOR_LOCK_CURRENT_MODE_SCHEMA], ), # door lock door status ZWaveDiscoverySchema( - platform="binary_sensor", + platform=Platform.BINARY_SENSOR, hint="property", primary_value=ZWaveValueDiscoverySchema( command_class={ @@ -529,53 +542,53 @@ DISCOVERY_SCHEMAS = [ CommandClass.DOOR_LOCK, }, property={DOOR_STATUS_PROPERTY}, - type={"any"}, + type={ValueType.ANY}, ), ), # thermostat fan ZWaveDiscoverySchema( - platform="fan", + platform=Platform.FAN, hint="thermostat_fan", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.THERMOSTAT_FAN_MODE}, property={THERMOSTAT_FAN_MODE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), entity_registry_enabled_default=False, ), # humidifier # hygrostats supporting mode (and optional setpoint) ZWaveDiscoverySchema( - platform="humidifier", + platform=Platform.HUMIDIFIER, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.HUMIDITY_CONTROL_MODE}, property={HUMIDITY_CONTROL_MODE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), ), # climate # thermostats supporting mode (and optional setpoint) ZWaveDiscoverySchema( - platform="climate", + platform=Platform.CLIMATE, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.THERMOSTAT_MODE}, property={THERMOSTAT_MODE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), ), # thermostats supporting setpoint only (and thus not mode) ZWaveDiscoverySchema( - platform="climate", + platform=Platform.CLIMATE, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.THERMOSTAT_SETPOINT}, property={THERMOSTAT_SETPOINT_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), absent_values=[ # mode must not be present to prevent dupes ZWaveValueDiscoverySchema( command_class={CommandClass.THERMOSTAT_MODE}, property={THERMOSTAT_MODE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), ], ), @@ -583,68 +596,68 @@ DISCOVERY_SCHEMAS = [ # When CC is Sensor Binary and device class generic is Binary Sensor, entity should # be enabled by default ZWaveDiscoverySchema( - platform="binary_sensor", + platform=Platform.BINARY_SENSOR, hint="boolean", device_class_generic={"Binary Sensor"}, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.SENSOR_BINARY}, - type={"boolean"}, + type={ValueType.BOOLEAN}, ), ), # Legacy binary sensors are phased out (replaced by notification sensors) # Disable by default to not confuse users ZWaveDiscoverySchema( - platform="binary_sensor", + platform=Platform.BINARY_SENSOR, hint="boolean", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.SENSOR_BINARY}, - type={"boolean"}, + type={ValueType.BOOLEAN}, ), entity_registry_enabled_default=False, ), ZWaveDiscoverySchema( - platform="binary_sensor", + platform=Platform.BINARY_SENSOR, hint="boolean", primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.BATTERY, CommandClass.SENSOR_ALARM, }, - type={"boolean"}, + type={ValueType.BOOLEAN}, ), ), ZWaveDiscoverySchema( - platform="binary_sensor", + platform=Platform.BINARY_SENSOR, hint="notification", primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.NOTIFICATION, }, - type={"number"}, + type={ValueType.NUMBER}, ), allow_multi=True, ), # generic text sensors ZWaveDiscoverySchema( - platform="sensor", + platform=Platform.SENSOR, hint="string_sensor", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.SENSOR_ALARM}, - type={"string"}, + type={ValueType.STRING}, ), ), ZWaveDiscoverySchema( - platform="sensor", + platform=Platform.SENSOR, hint="string_sensor", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.INDICATOR}, - type={"string"}, + type={ValueType.STRING}, ), entity_registry_enabled_default=False, ), # generic numeric sensors ZWaveDiscoverySchema( - platform="sensor", + platform=Platform.SENSOR, hint="numeric_sensor", primary_value=ZWaveValueDiscoverySchema( command_class={ @@ -652,55 +665,55 @@ DISCOVERY_SCHEMAS = [ CommandClass.SENSOR_ALARM, CommandClass.BATTERY, }, - type={"number"}, + type={ValueType.NUMBER}, ), data_template=NumericSensorDataTemplate(), ), ZWaveDiscoverySchema( - platform="sensor", + platform=Platform.SENSOR, hint="numeric_sensor", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.INDICATOR}, - type={"number"}, + type={ValueType.NUMBER}, ), data_template=NumericSensorDataTemplate(), entity_registry_enabled_default=False, ), # Meter sensors for Meter CC ZWaveDiscoverySchema( - platform="sensor", + platform=Platform.SENSOR, hint="meter", primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.METER, }, - type={"number"}, + type={ValueType.NUMBER}, property={VALUE_PROPERTY}, ), data_template=NumericSensorDataTemplate(), ), # special list sensors (Notification CC) ZWaveDiscoverySchema( - platform="sensor", + platform=Platform.SENSOR, hint="list_sensor", primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.NOTIFICATION, }, - type={"number"}, + type={ValueType.NUMBER}, ), allow_multi=True, entity_registry_enabled_default=False, ), # number for Basic CC ZWaveDiscoverySchema( - platform="number", + platform=Platform.NUMBER, hint="Basic", primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.BASIC, }, - type={"number"}, + type={ValueType.NUMBER}, property={CURRENT_VALUE_PROPERTY}, ), required_values=[ @@ -708,7 +721,7 @@ DISCOVERY_SCHEMAS = [ command_class={ CommandClass.BASIC, }, - type={"number"}, + type={ValueType.NUMBER}, property={TARGET_VALUE_PROPERTY}, ) ], @@ -717,24 +730,24 @@ DISCOVERY_SCHEMAS = [ ), # binary switches ZWaveDiscoverySchema( - platform="switch", + platform=Platform.SWITCH, primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA, ), # binary switch # barrier operator signaling states ZWaveDiscoverySchema( - platform="switch", + platform=Platform.SWITCH, hint="barrier_event_signaling_state", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.BARRIER_OPERATOR}, property={SIGNALING_STATE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), ), # cover # window coverings ZWaveDiscoverySchema( - platform="cover", + platform=Platform.COVER, hint="window_cover", device_class_generic={"Multilevel Switch"}, device_class_specific={ @@ -748,24 +761,24 @@ DISCOVERY_SCHEMAS = [ # cover # motorized barriers ZWaveDiscoverySchema( - platform="cover", + platform=Platform.COVER, hint="motorized_barrier", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.BARRIER_OPERATOR}, property={CURRENT_STATE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), required_values=[ ZWaveValueDiscoverySchema( command_class={CommandClass.BARRIER_OPERATOR}, property={TARGET_STATE_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), ], ), # fan ZWaveDiscoverySchema( - platform="fan", + platform=Platform.FAN, hint="fan", device_class_generic={"Multilevel Switch"}, device_class_specific={"Fan Switch"}, @@ -774,7 +787,7 @@ DISCOVERY_SCHEMAS = [ # number platform # valve control for thermostats ZWaveDiscoverySchema( - platform="number", + platform=Platform.NUMBER, hint="Valve control", device_class_generic={"Thermostat"}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, @@ -785,46 +798,46 @@ DISCOVERY_SCHEMAS = [ # NOTE: keep this at the bottom of the discovery scheme, # to handle all others that need the multilevel CC first ZWaveDiscoverySchema( - platform="light", + platform=Platform.LIGHT, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # sirens ZWaveDiscoverySchema( - platform="siren", + platform=Platform.SIREN, primary_value=SIREN_TONE_SCHEMA, ), # select # siren default tone ZWaveDiscoverySchema( - platform="select", + platform=Platform.SELECT, hint="Default tone", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.SOUND_SWITCH}, property={DEFAULT_TONE_ID_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), required_values=[SIREN_TONE_SCHEMA], ), # number # siren default volume ZWaveDiscoverySchema( - platform="number", + platform=Platform.NUMBER, hint="volume", primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.SOUND_SWITCH}, property={DEFAULT_VOLUME_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), required_values=[SIREN_TONE_SCHEMA], ), # select # protection CC ZWaveDiscoverySchema( - platform="select", + platform=Platform.SELECT, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.PROTECTION}, property={LOCAL_PROPERTY, RF_PROPERTY}, - type={"number"}, + type={ValueType.NUMBER}, ), ), ] From a28aeeeca7805bcd22b12ba236be308b0ef5f9d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Aug 2022 18:18:54 -0500 Subject: [PATCH 687/903] Hide bluetooth passive option if its not available on the host system (#77421) * Hide bluetooth passive option if its not available - We now have a way to determine in advance if passive scanning is supported by BlueZ * drop string --- .../components/bluetooth/config_flow.py | 4 +- homeassistant/components/bluetooth/const.py | 2 + homeassistant/components/bluetooth/manager.py | 6 +++ .../components/bluetooth/strings.json | 3 +- .../components/bluetooth/translations/en.json | 9 +--- homeassistant/components/bluetooth/util.py | 3 ++ tests/components/bluetooth/conftest.py | 12 ++++- .../components/bluetooth/test_config_flow.py | 50 ++++++++++++++----- .../components/bluetooth/test_diagnostics.py | 26 +++++++++- 9 files changed, 87 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 0fa2304468f..324520a8b5b 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,7 +1,6 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations -import platform from typing import TYPE_CHECKING, Any, cast import voluptuous as vol @@ -11,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.core import callback from homeassistant.helpers.typing import DiscoveryInfoType +from . import models from .const import ( ADAPTER_ADDRESS, CONF_ADAPTER, @@ -134,7 +134,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): @callback def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" - return platform.system() == "Linux" + return bool(models.MANAGER and models.MANAGER.supports_passive_scan) class OptionsFlowHandler(OptionsFlow): diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 540310e9747..3174603f08e 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -59,8 +59,10 @@ class AdapterDetails(TypedDict, total=False): address: str sw_version: str hw_version: str + passive_scan: bool ADAPTER_ADDRESS: Final = "address" ADAPTER_SW_VERSION: Final = "sw_version" ADAPTER_HW_VERSION: Final = "hw_version" +ADAPTER_PASSIVE_SCAN: Final = "passive_scan" diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 984d37d806d..b1193c47245 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -23,6 +23,7 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( ADAPTER_ADDRESS, + ADAPTER_PASSIVE_SCAN, STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, AdapterDetails, @@ -147,6 +148,11 @@ class BluetoothManager: self._connectable_scanners: list[BaseHaScanner] = [] self._adapters: dict[str, AdapterDetails] = {} + @property + def supports_passive_scan(self) -> bool: + """Return if passive scan is supported.""" + return any(adapter[ADAPTER_PASSIVE_SCAN] for adapter in self._adapters.values()) + async def async_diagnostics(self) -> dict[str, Any]: """Diagnostics for the manager.""" scanner_diagnostics = await asyncio.gather( diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index 1912242ea6a..f838cd97798 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -29,9 +29,8 @@ "options": { "step": { "init": { - "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled.", "data": { - "passive": "Passive listening" + "passive": "Passive scanning" } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index a3a7b97260e..9fcd0e5e1ee 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -33,10 +30,8 @@ "step": { "init": { "data": { - "adapter": "The Bluetooth Adapter to use for scanning", - "passive": "Passive listening" - }, - "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled." + "passive": "Passive scanning" + } } } } diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 450c1812483..3f6c862e53d 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -24,6 +24,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: WINDOWS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( address=DEFAULT_ADDRESS, sw_version=platform.release(), + passive_scan=False, ) } if platform.system() == "Darwin": @@ -31,6 +32,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: MACOS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( address=DEFAULT_ADDRESS, sw_version=platform.release(), + passive_scan=False, ) } from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel @@ -45,6 +47,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: address=adapter1["Address"], sw_version=adapter1["Name"], # This is actually the BlueZ version hw_version=adapter1["Modalias"], + passive_scan="org.bluez.AdvertisementMonitorManager1" in details, ) return adapters diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 44b9a60d1b5..3447012ace5 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -42,7 +42,11 @@ def one_adapter_fixture(): "Address": "00:00:00:00:00:01", "Name": "BlueZ 4.63", "Modalias": "usbid:1234", - } + }, + "org.bluez.AdvertisementMonitorManager1": { + "SupportedMonitorTypes": ["or_patterns"], + "SupportedFeatures": [], + }, }, }, ): @@ -74,7 +78,11 @@ def two_adapters_fixture(): "Address": "00:00:00:00:00:02", "Name": "BlueZ 4.63", "Modalias": "usbid:1234", - } + }, + "org.bluez.AdvertisementMonitorManager1": { + "SupportedMonitorTypes": ["or_patterns"], + "SupportedFeatures": [], + }, }, }, ): diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 61763eef257..aa40666c80a 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -17,6 +17,29 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +async def test_options_flow_disabled_not_setup( + hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter +): + """Test options are disabled if the integration has not been setup.""" + await async_setup_component(hass, "config", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={}, options={}, unique_id=DEFAULT_ADDRESS + ) + entry.add_to_hass(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/get", + "domain": "bluetooth", + "type_filter": "integration", + } + ) + response = await ws_client.receive_json() + assert response["result"][0]["supports_options"] is False + + async def test_async_step_user_macos(hass, macos_adapter): """Test setting up manually with one adapter on MacOS.""" result = await hass.config_entries.flow.async_init( @@ -287,19 +310,18 @@ async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter): assert result["data"][CONF_PASSIVE] is False -@patch( - "homeassistant.components.bluetooth.config_flow.platform.system", - return_value="Darwin", -) -async def test_options_flow_disabled_macos(mock_system, hass, hass_ws_client): +async def test_options_flow_disabled_macos( + hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter +): """Test options are disabled on MacOS.""" await async_setup_component(hass, "config", {}) entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, + domain=DOMAIN, data={}, options={}, unique_id=DEFAULT_ADDRESS ) entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + ws_client = await hass_ws_client(hass) await ws_client.send_json( @@ -314,19 +336,21 @@ async def test_options_flow_disabled_macos(mock_system, hass, hass_ws_client): assert response["result"][0]["supports_options"] is False -@patch( - "homeassistant.components.bluetooth.config_flow.platform.system", - return_value="Linux", -) -async def test_options_flow_enabled_linux(mock_system, hass, hass_ws_client): +async def test_options_flow_enabled_linux( + hass, hass_ws_client, mock_bleak_scanner_start, one_adapter +): """Test options are enabled on Linux.""" await async_setup_component(hass, "config", {}) entry = MockConfigEntry( domain=DOMAIN, data={}, options={}, + unique_id="00:00:00:00:00:01", ) entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + ws_client = await hass_ws_client(hass) await ws_client.send_json( diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 6f5eeefa5b6..9059fcb9ab1 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -31,7 +31,16 @@ async def test_diagnostics( return_value={ "org.bluez": { "/org/bluez/hci0": { - "Interfaces": {"org.bluez.Adapter1": {"Discovering": False}} + "org.bluez.Adapter1": { + "Name": "BlueZ 5.63", + "Alias": "BlueZ 5.63", + "Modalias": "usb:v1D6Bp0246d0540", + "Discovering": False, + }, + "org.bluez.AdvertisementMonitorManager1": { + "SupportedMonitorTypes": ["or_patterns"], + "SupportedFeatures": [], + }, } } }, @@ -57,18 +66,29 @@ async def test_diagnostics( "hci0": { "address": "00:00:00:00:00:01", "hw_version": "usbid:1234", + "passive_scan": False, "sw_version": "BlueZ 4.63", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usbid:1234", + "passive_scan": True, "sw_version": "BlueZ 4.63", }, }, "dbus": { "org.bluez": { "/org/bluez/hci0": { - "Interfaces": {"org.bluez.Adapter1": {"Discovering": False}} + "org.bluez.Adapter1": { + "Alias": "BlueZ " "5.63", + "Discovering": False, + "Modalias": "usb:v1D6Bp0246d0540", + "Name": "BlueZ " "5.63", + }, + "org.bluez.AdvertisementMonitorManager1": { + "SupportedFeatures": [], + "SupportedMonitorTypes": ["or_patterns"], + }, } } }, @@ -77,11 +97,13 @@ async def test_diagnostics( "hci0": { "address": "00:00:00:00:00:01", "hw_version": "usbid:1234", + "passive_scan": False, "sw_version": "BlueZ 4.63", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usbid:1234", + "passive_scan": True, "sw_version": "BlueZ 4.63", }, }, From 7f1a203721ed19150183b2cee04c918271b61ac6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 28 Aug 2022 00:29:27 +0000 Subject: [PATCH 688/903] [ci skip] Translation update --- .../automation/translations/et.json | 13 ++++++++ .../automation/translations/it.json | 13 ++++++++ .../automation/translations/pt-BR.json | 13 ++++++++ .../components/bluetooth/translations/en.json | 7 +++- .../components/bluetooth/translations/fr.json | 2 +- .../components/bthome/translations/el.json | 32 +++++++++++++++++++ .../components/bthome/translations/en.json | 2 -- .../components/bthome/translations/es.json | 32 +++++++++++++++++++ .../components/bthome/translations/et.json | 32 +++++++++++++++++++ .../components/bthome/translations/fr.json | 22 +++++++++++++ .../components/bthome/translations/it.json | 32 +++++++++++++++++++ .../components/mqtt/translations/et.json | 6 ++++ .../components/skybell/translations/et.json | 7 ++++ .../components/skybell/translations/it.json | 7 ++++ .../speedtestdotnet/translations/ca.json | 12 +++++++ .../speedtestdotnet/translations/es.json | 4 +-- .../speedtestdotnet/translations/et.json | 13 ++++++++ .../speedtestdotnet/translations/pt-BR.json | 13 ++++++++ .../speedtestdotnet/translations/zh-Hant.json | 13 ++++++++ .../thermobeacon/translations/ca.json | 22 +++++++++++++ .../thermobeacon/translations/el.json | 22 +++++++++++++ .../thermobeacon/translations/es.json | 22 +++++++++++++ .../thermobeacon/translations/et.json | 22 +++++++++++++ .../thermobeacon/translations/fr.json | 22 +++++++++++++ .../thermobeacon/translations/it.json | 22 +++++++++++++ .../thermobeacon/translations/pt-BR.json | 22 +++++++++++++ .../thermobeacon/translations/zh-Hant.json | 22 +++++++++++++ .../components/thermopro/translations/et.json | 21 ++++++++++++ .../components/thermopro/translations/it.json | 21 ++++++++++++ .../volvooncall/translations/es.json | 2 +- 30 files changed, 488 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/bthome/translations/el.json create mode 100644 homeassistant/components/bthome/translations/es.json create mode 100644 homeassistant/components/bthome/translations/et.json create mode 100644 homeassistant/components/bthome/translations/fr.json create mode 100644 homeassistant/components/bthome/translations/it.json create mode 100644 homeassistant/components/thermobeacon/translations/ca.json create mode 100644 homeassistant/components/thermobeacon/translations/el.json create mode 100644 homeassistant/components/thermobeacon/translations/es.json create mode 100644 homeassistant/components/thermobeacon/translations/et.json create mode 100644 homeassistant/components/thermobeacon/translations/fr.json create mode 100644 homeassistant/components/thermobeacon/translations/it.json create mode 100644 homeassistant/components/thermobeacon/translations/pt-BR.json create mode 100644 homeassistant/components/thermobeacon/translations/zh-Hant.json create mode 100644 homeassistant/components/thermopro/translations/et.json create mode 100644 homeassistant/components/thermopro/translations/it.json diff --git a/homeassistant/components/automation/translations/et.json b/homeassistant/components/automation/translations/et.json index 71df51e9147..7e5205c2c6a 100644 --- a/homeassistant/components/automation/translations/et.json +++ b/homeassistant/components/automation/translations/et.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Automaatiseeringul \"{name}\" (`{entity_id}`) on tegevus mis kutsub tundmatut teenust: `{service}`.\n\nSee viga takistab automaatika korrektset k\u00e4ivitamist. V\u00f5ib-olla ei ole see teenus enam saadaval v\u00f5i on selle p\u00f5hjuseks tr\u00fckiviga.\n\nSelle vea parandamiseks [redigeeri automatiseerimist]({edit}) ja eemaldage tegevus, mis kutsub seda teenust.\n\nKl\u00f5psa allpool ESITA, et kinnitada, et oled selle automatiseerimise parandanud.", + "title": "{name} kasutab tundmatut teenust" + } + } + }, + "title": "{name} kasutab tundmatut teenust" + } + }, "state": { "_": { "off": "V\u00e4ljas", diff --git a/homeassistant/components/automation/translations/it.json b/homeassistant/components/automation/translations/it.json index c913ae7de4d..72987f29477 100644 --- a/homeassistant/components/automation/translations/it.json +++ b/homeassistant/components/automation/translations/it.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "L'automazione \"{name}\" (`{entity_id}`) ha un'azione che chiama un servizio sconosciuto: `{service}`. \n\nQuesto errore impedisce il corretto funzionamento dell'automazione. Forse questo servizio non \u00e8 pi\u00f9 disponibile, o forse un errore di battitura lo ha causato. \n\nPer correggere questo errore, [modifica l'automazione]({edit}) e rimuovi l'azione che chiama questo servizio. \n\nFai clic su INVIA di seguito per confermare di aver corretto questa automazione.", + "title": "{name} utilizza un servizio sconosciuto" + } + } + }, + "title": "{name} utilizza un servizio sconosciuto" + } + }, "state": { "_": { "off": "Spento", diff --git a/homeassistant/components/automation/translations/pt-BR.json b/homeassistant/components/automation/translations/pt-BR.json index 447658433e5..77780195324 100644 --- a/homeassistant/components/automation/translations/pt-BR.json +++ b/homeassistant/components/automation/translations/pt-BR.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "A automa\u00e7\u00e3o \" {name} \" (`{entity_id}`) tem uma a\u00e7\u00e3o que chama um servi\u00e7o desconhecido: `{service}`. \n\n Este erro impede que a automa\u00e7\u00e3o seja executada corretamente. Talvez este servi\u00e7o n\u00e3o esteja mais dispon\u00edvel, ou talvez um erro de digita\u00e7\u00e3o o tenha causado. \n\n Para corrigir esse erro, [edite a automa\u00e7\u00e3o]({edit}) e remova a a\u00e7\u00e3o que chama este servi\u00e7o. \n\n Clique em ENVIAR abaixo para confirmar que voc\u00ea corrigiu essa automa\u00e7\u00e3o.", + "title": "{name} usa um servi\u00e7o desconhecido" + } + } + }, + "title": "{name} usa um servi\u00e7o desconhecido" + } + }, "state": { "_": { "off": "Desligado", diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 9fcd0e5e1ee..7d76740602d 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,6 +9,9 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -30,8 +33,10 @@ "step": { "init": { "data": { + "adapter": "The Bluetooth Adapter to use for scanning", "passive": "Passive scanning" - } + }, + "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled." } } } diff --git a/homeassistant/components/bluetooth/translations/fr.json b/homeassistant/components/bluetooth/translations/fr.json index 2eb71ec1fe8..c7a1155b216 100644 --- a/homeassistant/components/bluetooth/translations/fr.json +++ b/homeassistant/components/bluetooth/translations/fr.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "passive": "\u00c9coute passive" + "passive": "Recherche passive" }, "description": "L'\u00e9coute passive n\u00e9cessite BlueZ version 5.63 ou ult\u00e9rieure avec les fonctions exp\u00e9rimentales activ\u00e9es." } diff --git a/homeassistant/components/bthome/translations/el.json b/homeassistant/components/bthome/translations/el.json new file mode 100644 index 00000000000..21a02401495 --- /dev/null +++ b/homeassistant/components/bthome/translations/el.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "decryption_failed": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5, \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b1\u03bd \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "expected_32_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "get_encryption_key": { + "data": { + "bindkey": "Bindkey" + }, + "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03bf \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd." + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/en.json b/homeassistant/components/bthome/translations/en.json index bb2f09bafab..29115e12781 100644 --- a/homeassistant/components/bthome/translations/en.json +++ b/homeassistant/components/bthome/translations/en.json @@ -3,8 +3,6 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful" }, diff --git a/homeassistant/components/bthome/translations/es.json b/homeassistant/components/bthome/translations/es.json new file mode 100644 index 00000000000..4cf15c9c1d3 --- /dev/null +++ b/homeassistant/components/bthome/translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "decryption_failed": "La clave de enlace proporcionada no funcion\u00f3, los datos del sensor no se pudieron descifrar. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", + "expected_32_characters": "Se esperaba una clave de enlace hexadecimal de 32 caracteres." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Clave de enlace" + }, + "description": "Los datos del sensor transmitidos por el sensor est\u00e1n cifrados. Para descifrarlos necesitamos una clave de enlace hexadecimal de 32 caracteres." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/et.json b/homeassistant/components/bthome/translations/et.json new file mode 100644 index 00000000000..0def5a2f7e4 --- /dev/null +++ b/homeassistant/components/bthome/translations/et.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "decryption_failed": "Esitatud sidumisv\u00f5ti ei t\u00f6\u00f6tanud, anduri andmeid ei saanud dekr\u00fcpteerida. Palun kontrolli seda ja proovi uuesti.", + "expected_32_characters": "Eeldati 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas h\u00e4\u00e4lestada {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Sidumisv\u00f5ti" + }, + "description": "Anduri poolt edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks on vaja 32-kohalist hex sidumisv\u00f5tit." + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/fr.json b/homeassistant/components/bthome/translations/fr.json new file mode 100644 index 00000000000..4c9b9b980ed --- /dev/null +++ b/homeassistant/components/bthome/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/it.json b/homeassistant/components/bthome/translations/it.json new file mode 100644 index 00000000000..9ec4af86278 --- /dev/null +++ b/homeassistant/components/bthome/translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "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", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controlla e riprova.", + "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Chiave di collegamento" + }, + "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 32 caratteri." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index 0fee989f25d..9b85b6e51bb 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" on kolmekordselt kl\u00f5psatud" } }, + "issues": { + "deprecated_yaml": { + "description": "K\u00e4sitsi seadistatud MQTT {platform} leiti platvormi v\u00f5tme ` {platform} ` alt. \n\n Selle probleemi lahendamiseks teisalda konfiguratsioon sidumisv\u00f5tmesse \"mqtt\" ja taask\u00e4ivitage Home Assistant. Lisateabe saamiseks vaata [dokumentatsiooni]( {more_info_url} ).", + "title": "K\u00e4sitsi seadistatud MQTT {platform} vajab t\u00e4helepanu" + } + }, "options": { "error": { "bad_birth": "Kehtetu loomise teavitus.", diff --git a/homeassistant/components/skybell/translations/et.json b/homeassistant/components/skybell/translations/et.json index 565aabd84d6..e98550c52d8 100644 --- a/homeassistant/components/skybell/translations/et.json +++ b/homeassistant/components/skybell/translations/et.json @@ -10,6 +10,13 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "V\u00e4rskenda oma salas\u00f5na {email} jaoks", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "email": "E-posti aadress", diff --git a/homeassistant/components/skybell/translations/it.json b/homeassistant/components/skybell/translations/it.json index d9e798e534c..d17a3db1234 100644 --- a/homeassistant/components/skybell/translations/it.json +++ b/homeassistant/components/skybell/translations/it.json @@ -10,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Aggiorna la tua password per {email}", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/speedtestdotnet/translations/ca.json b/homeassistant/components/speedtestdotnet/translations/ca.json index c1c5dda71cf..1f4d059555c 100644 --- a/homeassistant/components/speedtestdotnet/translations/ca.json +++ b/homeassistant/components/speedtestdotnet/translations/ca.json @@ -9,6 +9,18 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "El servei speedtest est\u00e0 sent eliminat" + } + } + }, + "title": "El servei speedtest est\u00e0 sent eliminat" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index e9ce4b15f9b..f439b2e1079 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -15,11 +15,11 @@ "step": { "confirm": { "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `homeassistant.update_entity` con un entity_id de Speedtest como objetivo. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", - "title": "El servicio speedtest se va a eliminar" + "title": "Se va a eliminar el servicio speedtest" } } }, - "title": "El servicio speedtest se va a eliminar" + "title": "Se va a eliminar el servicio speedtest" } }, "options": { diff --git a/homeassistant/components/speedtestdotnet/translations/et.json b/homeassistant/components/speedtestdotnet/translations/et.json index ac1915e760a..abfe42c3cfa 100644 --- a/homeassistant/components/speedtestdotnet/translations/et.json +++ b/homeassistant/components/speedtestdotnet/translations/et.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte, et kasutada selle asemel teenust \"homeassistant.update_entity\" sihtv\u00e4\u00e4rtusega Speedtest entity_id. Seej\u00e4rel kl\u00f5psa selle probleemi lahendatuks m\u00e4rkimiseks allpool nuppu ESITA.", + "title": "Speedtest teenus eemaldatakse" + } + } + }, + "title": "Speedtest teenus eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/pt-BR.json b/homeassistant/components/speedtestdotnet/translations/pt-BR.json index 739b3b41875..1259d18bc18 100644 --- a/homeassistant/components/speedtestdotnet/translations/pt-BR.json +++ b/homeassistant/components/speedtestdotnet/translations/pt-BR.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `homeassistant.update_entity` com um ID de entidade do Speedtest de destino. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", + "title": "O servi\u00e7o Speedtest est\u00e1 sendo removido" + } + } + }, + "title": "O servi\u00e7o Speedtest est\u00e1 sendo removido" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json index 43d30d4aeb8..3d12b9454d0 100644 --- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json +++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19 entity_id \u70ba Speedtest \u4e4b `homeassistant.update_entity` \u670d\u52d9\uff0c\u7136\u5f8c\u9ede\u9078\u50b3\u9001\u4ee5\u6a19\u793a\u554f\u984c\u5df2\u89e3\u6c7a\u3002", + "title": "Seedtest \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + } + }, + "title": "Speedtest \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/thermobeacon/translations/ca.json b/homeassistant/components/thermobeacon/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Dispositiu no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/el.json b/homeassistant/components/thermobeacon/translations/el.json new file mode 100644 index 00000000000..cdb57c8ac1b --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/es.json b/homeassistant/components/thermobeacon/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_supported": "Dispositivo no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/et.json b/homeassistant/components/thermobeacon/translations/et.json new file mode 100644 index 00000000000..5e8ad5a3d92 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "not_supported": "Seda seadet ei toetata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/fr.json b/homeassistant/components/thermobeacon/translations/fr.json new file mode 100644 index 00000000000..8ddb4af4dbc --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "not_supported": "Appareil non pris en charge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/it.json b/homeassistant/components/thermobeacon/translations/it.json new file mode 100644 index 00000000000..7784ed3a240 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Dispositivo non supportato" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/pt-BR.json b/homeassistant/components/thermobeacon/translations/pt-BR.json new file mode 100644 index 00000000000..0da7639fa2a --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/zh-Hant.json b/homeassistant/components/thermobeacon/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\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": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/et.json b/homeassistant/components/thermopro/translations/et.json new file mode 100644 index 00000000000..ccd82a30be5 --- /dev/null +++ b/homeassistant/components/thermopro/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/it.json b/homeassistant/components/thermopro/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/thermopro/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/es.json b/homeassistant/components/volvooncall/translations/es.json index 2eff271f511..39174aa1253 100644 --- a/homeassistant/components/volvooncall/translations/es.json +++ b/homeassistant/components/volvooncall/translations/es.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "La configuraci\u00f3n de la plataforma Volvo On Call mediante YAML se eliminar\u00e1 en una versi\u00f3n futura de Home Assistant. \n\nTu configuraci\u00f3n existente se ha importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de la plataforma Volvo On Call mediante YAML en una versi\u00f3n futura de Home Assistant. \n\nTu configuraci\u00f3n existente se ha importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Volvo On Call" } } From eab0ff5185c0a2c256d634dbd28e1129bcc1a86b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 27 Aug 2022 21:27:41 -0400 Subject: [PATCH 689/903] Bump zwave-js-server-python to 0.41.0 (#76903) --- homeassistant/components/zwave_js/api.py | 13 +++++++------ homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 7 ++++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 1a9435509f2..2e565a3be7c 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -11,6 +11,7 @@ import voluptuous as vol from zwave_js_server.client import Client from zwave_js_server.const import ( CommandClass, + ExclusionStrategy, InclusionStrategy, LogLevel, Protocols, @@ -153,7 +154,7 @@ STATUS = "status" REQUESTED_SECURITY_CLASSES = "requested_security_classes" FEATURE = "feature" -UNPROVISION = "unprovision" +STRATEGY = "strategy" # https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/security/QR.ts#L41 MINIMUM_QR_STRING_LENGTH = 52 @@ -480,12 +481,12 @@ async def websocket_network_status( "sdk_version": controller.sdk_version, "type": controller.controller_type, "own_node_id": controller.own_node_id, - "is_secondary": controller.is_secondary, + "is_primary": controller.is_primary, "is_using_home_id_from_other_network": controller.is_using_home_id_from_other_network, "is_sis_present": controller.is_SIS_present, "was_real_primary": controller.was_real_primary, - "is_static_update_controller": controller.is_static_update_controller, - "is_slave": controller.is_slave, + "is_suc": controller.is_suc, + "node_type": controller.node_type, "firmware_version": controller.firmware_version, "manufacturer_id": controller.manufacturer_id, "product_id": controller.product_id, @@ -1056,7 +1057,7 @@ async def websocket_stop_exclusion( { vol.Required(TYPE): "zwave_js/remove_node", vol.Required(ENTRY_ID): str, - vol.Optional(UNPROVISION): bool, + vol.Optional(STRATEGY): vol.Coerce(ExclusionStrategy), } ) @websocket_api.async_response @@ -1106,7 +1107,7 @@ async def websocket_remove_node( controller.on("node removed", node_removed), ] - result = await controller.async_begin_exclusion(msg.get(UNPROVISION)) + result = await controller.async_begin_exclusion(msg.get(STRATEGY)) connection.send_result( msg[ID], result, diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 5f9c8df7afc..b906efec96c 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.40.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.41.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index cc1e87871d0..a637284cbd8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2575,7 +2575,7 @@ zigpy==0.50.2 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.40.0 +zwave-js-server-python==0.41.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d76676ec8f..fd60fae2e00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1761,7 +1761,7 @@ zigpy-znp==0.8.2 zigpy==0.50.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.40.0 +zwave-js-server-python==0.41.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 68618edfbeb..0d633720639 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest from zwave_js_server.const import ( + ExclusionStrategy, InclusionState, InclusionStrategy, LogLevel, @@ -68,8 +69,8 @@ from homeassistant.components.zwave_js.api import ( SECURITY_CLASSES, SPECIFIC_DEVICE_CLASS, STATUS, + STRATEGY, TYPE, - UNPROVISION, VALUE, VERSION, ) @@ -1528,7 +1529,7 @@ async def test_remove_node( ID: 2, TYPE: "zwave_js/remove_node", ENTRY_ID: entry.entry_id, - UNPROVISION: True, + STRATEGY: ExclusionStrategy.EXCLUDE_ONLY, } ) @@ -1538,7 +1539,7 @@ async def test_remove_node( assert len(client.async_send_command.call_args_list) == 1 assert client.async_send_command.call_args[0][0] == { "command": "controller.begin_exclusion", - "unprovision": True, + "strategy": 0, } # Test FailedZWaveCommand is caught From 441d7c04615a6ce01c01cc3097ba09438de42a8d Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 28 Aug 2022 12:36:31 +0200 Subject: [PATCH 690/903] Wait for config entry platforms in KNX (#77437) --- homeassistant/components/knx/__init__.py | 2 +- tests/components/knx/conftest.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1910227a5a4..fa014335df9 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -247,7 +247,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: create_knx_exposure(hass, knx_module.xknx, expose_config) ) - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, [ platform diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index e5cf18b0c3c..23e04b672ba 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -55,16 +55,21 @@ class KNXTestKit: async def setup_integration(self, config): """Create the KNX integration.""" - def disable_rate_limiter(): - """Disable rate limiter for tests.""" + async def patch_xknx_start(): + """Patch `xknx.start` for unittests.""" # after XKNX.__init__() to not overwrite it by the config entry again # before StateUpdater starts to avoid slow down of tests self.xknx.rate_limit = 0 + # set XknxConnectionState.CONNECTED to avoid `unavailable` entities at startup + # and start StateUpdater. This would be awaited on normal startup too. + await self.xknx.connection_manager.connection_state_changed( + XknxConnectionState.CONNECTED + ) def knx_ip_interface_mock(): """Create a xknx knx ip interface mock.""" mock = Mock() - mock.start = AsyncMock(side_effect=disable_rate_limiter) + mock.start = AsyncMock(side_effect=patch_xknx_start) mock.stop = AsyncMock() mock.send_telegram = AsyncMock(side_effect=self._outgoing_telegrams.put) return mock @@ -81,9 +86,6 @@ class KNXTestKit: ): self.mock_config_entry.add_to_hass(self.hass) await async_setup_component(self.hass, KNX_DOMAIN, {KNX_DOMAIN: config}) - await self.xknx.connection_manager.connection_state_changed( - XknxConnectionState.CONNECTED - ) await self.hass.async_block_till_done() ######################## From d29be2390b1ef0aedbbe08ebcfa2d3d3a596ee96 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 28 Aug 2022 13:31:07 -0400 Subject: [PATCH 691/903] Add new features from UniFi Protect 2.2.1-beta5 (#77391) --- .../components/unifiprotect/binary_sensor.py | 3 +- .../components/unifiprotect/media_player.py | 13 +++- .../components/unifiprotect/number.py | 3 +- .../components/unifiprotect/sensor.py | 3 +- .../components/unifiprotect/switch.py | 67 ++++++++++++++++++- tests/components/unifiprotect/conftest.py | 1 + .../unifiprotect/fixtures/sample_nvr.json | 1 + tests/components/unifiprotect/test_switch.py | 55 ++++++++++----- 8 files changed, 124 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 0d127a55554..05f7b37d6c8 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -114,8 +114,9 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="System Sounds", icon="mdi:speaker", entity_category=EntityCategory.DIAGNOSTIC, - ufp_required_field="feature_flags.has_speaker", + ufp_required_field="has_speaker", ufp_value="speaker_settings.are_system_sounds_enabled", + ufp_enabled="feature_flags.has_speaker", ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 34686f52519..d8edc7fe4e9 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -9,6 +9,7 @@ from pyunifiprotect.data import ( ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, + StateType, ) from pyunifiprotect.exceptions import StreamError @@ -48,7 +49,9 @@ async def async_setup_entry( data: ProtectData = hass.data[DOMAIN][entry.entry_id] async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - if isinstance(device, Camera) and device.feature_flags.has_speaker: + if isinstance(device, Camera) and ( + device.has_speaker or device.has_removable_speaker + ): async_add_entities([ProtectMediaPlayer(data, device)]) entry.async_on_unload( @@ -58,7 +61,7 @@ async def async_setup_entry( entities = [] for device in data.get_by_types({ModelType.CAMERA}): device = cast(Camera, device) - if device.feature_flags.has_speaker: + if device.has_speaker or device.has_removable_speaker: entities.append(ProtectMediaPlayer(data, device)) async_add_entities(entities) @@ -107,6 +110,12 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): else: self._attr_state = STATE_IDLE + is_connected = self.data.last_update_success and ( + self.device.state == StateType.CONNECTED + or (not self.device.is_adopted_by_us and self.device.can_adopt) + ) + self._attr_available = is_connected and self.device.feature_flags.has_speaker + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index ed9faf4da40..3b80532a32c 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -82,8 +82,9 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_min=0, ufp_max=100, ufp_step=1, - ufp_required_field="feature_flags.has_mic", + ufp_required_field="has_mic", ufp_value="mic_volume", + ufp_enabled="feature_flags.has_mic", ufp_set_method="set_mic_volume", ufp_perm=PermRequired.WRITE, ), diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index c74bd00e055..57bd4fc7230 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -215,8 +215,9 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( icon="mdi:microphone", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, - ufp_required_field="feature_flags.has_mic", + ufp_required_field="has_mic", ufp_value="mic_volume", + ufp_enabled="feature_flags.has_mic", ufp_perm=PermRequired.NO_WRITE, ), ProtectSensorEntityDescription( diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index fa53cc3b87b..65de9f52913 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from typing import Any from pyunifiprotect.data import ( + NVR, Camera, ProtectAdoptableDeviceModel, ProtectModelWithId, @@ -22,7 +23,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData -from .entity import ProtectDeviceEntity, async_all_device_entities +from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd @@ -90,8 +91,9 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( name="System Sounds", icon="mdi:speaker", entity_category=EntityCategory.CONFIG, - ufp_required_field="feature_flags.has_speaker", + ufp_required_field="has_speaker", ufp_value="speaker_settings.are_system_sounds_enabled", + ufp_enabled="feature_flags.has_speaker", ufp_set_method="set_system_sounds", ufp_perm=PermRequired.WRITE, ), @@ -296,6 +298,25 @@ VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ) +NVR_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( + ProtectSwitchEntityDescription( + key="analytics_enabled", + name="Analytics Enabled", + icon="mdi:google-analytics", + entity_category=EntityCategory.CONFIG, + ufp_value="is_analytics_enabled", + ufp_set_method="set_anonymous_analytics", + ), + ProtectSwitchEntityDescription( + key="insights_enabled", + name="Insights Enabled", + icon="mdi:magnify", + entity_category=EntityCategory.CONFIG, + ufp_value="is_insights_enabled", + ufp_set_method="set_insights", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -342,6 +363,17 @@ async def async_setup_entry( ProtectPrivacyModeSwitch, camera_descs=[PRIVACY_MODE_SWITCH], ) + + if ( + data.api.bootstrap.nvr.can_write(data.api.bootstrap.auth_user) + and data.api.bootstrap.nvr.is_insights_enabled is not None + ): + for switch in NVR_SWITCHES: + entities.append( + ProtectNVRSwitch( + data, device=data.api.bootstrap.nvr, description=switch + ) + ) async_add_entities(entities) @@ -377,6 +409,37 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): await self.entity_description.ufp_set(self.device, False) +class ProtectNVRSwitch(ProtectNVREntity, SwitchEntity): + """A UniFi Protect NVR Switch.""" + + entity_description: ProtectSwitchEntityDescription + + def __init__( + self, + data: ProtectData, + device: NVR, + description: ProtectSwitchEntityDescription, + ) -> None: + """Initialize an UniFi Protect Switch.""" + super().__init__(data, device, description) + self._attr_name = f"{self.device.display_name} {self.entity_description.name}" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self.entity_description.get_ufp_value(self.device) is True + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + + await self.entity_description.ufp_set(self.device, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + + await self.entity_description.ufp_set(self.device, False) + + class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): """A UniFi Protect Switch.""" diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 2a9edb605e7..b006dfbd004 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -209,6 +209,7 @@ def doorbell_fixture(camera: Camera, fixed_now: datetime): SmartDetectObjectType.PERSON, SmartDetectObjectType.VEHICLE, ] + doorbell.has_speaker = True doorbell.feature_flags.has_hdr = True doorbell.feature_flags.has_lcd_screen = True doorbell.feature_flags.has_speaker = True diff --git a/tests/components/unifiprotect/fixtures/sample_nvr.json b/tests/components/unifiprotect/fixtures/sample_nvr.json index 507e75fec09..8777e3ce945 100644 --- a/tests/components/unifiprotect/fixtures/sample_nvr.json +++ b/tests/components/unifiprotect/fixtures/sample_nvr.json @@ -56,6 +56,7 @@ "enableCrashReporting": true, "disableAudio": false, "analyticsData": "anonymous", + "isInsightsEnabled": true, "anonymousDeviceId": "65257f7d-874c-498a-8f1b-00b2dd0a7ae1", "cameraUtilization": 30, "isRecycling": false, diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 7bf9e3f8f83..50f82736ee5 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -49,11 +49,11 @@ async def test_switch_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 0, 0) + assert_entity_counts(hass, Platform.SWITCH, 2, 2) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) async def test_switch_light_remove( @@ -63,11 +63,36 @@ async def test_switch_light_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 2, 1) + assert_entity_counts(hass, Platform.SWITCH, 4, 3) await remove_entities(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 0, 0) + assert_entity_counts(hass, Platform.SWITCH, 2, 2) await adopt_devices(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 2, 1) + assert_entity_counts(hass, Platform.SWITCH, 4, 3) + + +async def test_switch_nvr(hass: HomeAssistant, ufp: MockUFPFixture): + """Test switch entity setup for light devices.""" + + await init_entry(hass, ufp, []) + + assert_entity_counts(hass, Platform.SWITCH, 2, 2) + + nvr = ufp.api.bootstrap.nvr + nvr.__fields__["set_insights"] = Mock() + nvr.set_insights = AsyncMock() + entity_id = "switch.unifiprotect_insights_enabled" + + await hass.services.async_call( + "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + nvr.set_insights.assert_called_once_with(True) + + await hass.services.async_call( + "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + nvr.set_insights.assert_called_with(False) async def test_switch_setup_no_perm( @@ -95,7 +120,7 @@ async def test_switch_setup_light( """Test switch entity setup for light devices.""" await init_entry(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 2, 1) + assert_entity_counts(hass, Platform.SWITCH, 4, 3) entity_registry = er.async_get(hass) @@ -140,7 +165,7 @@ async def test_switch_setup_camera_all( """Test switch entity setup for camera devices (all enabled feature flags).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) entity_registry = er.async_get(hass) @@ -187,7 +212,7 @@ async def test_switch_setup_camera_none( """Test switch entity setup for camera devices (no enabled feature flags).""" await init_entry(hass, ufp, [camera]) - assert_entity_counts(hass, Platform.SWITCH, 6, 5) + assert_entity_counts(hass, Platform.SWITCH, 8, 7) entity_registry = er.async_get(hass) @@ -235,7 +260,7 @@ async def test_switch_light_status( """Tests status light switch for lights.""" await init_entry(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 2, 1) + assert_entity_counts(hass, Platform.SWITCH, 4, 3) description = LIGHT_SWITCHES[1] @@ -263,7 +288,7 @@ async def test_switch_camera_ssh( """Tests SSH switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) description = CAMERA_SWITCHES[0] @@ -296,7 +321,7 @@ async def test_switch_camera_simple( """Tests all simple switches for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) assert description.ufp_set_method is not None @@ -325,7 +350,7 @@ async def test_switch_camera_highfps( """Tests High FPS switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) description = CAMERA_SWITCHES[3] @@ -356,7 +381,7 @@ async def test_switch_camera_privacy( previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) description = PRIVACY_MODE_SWITCH @@ -408,7 +433,7 @@ async def test_switch_camera_privacy_already_on( doorbell.add_privacy_zone() await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) description = PRIVACY_MODE_SWITCH From 533d23ce05deb60c3359eccbd72d227089811627 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Aug 2022 13:46:04 -0500 Subject: [PATCH 692/903] Add thermobeacon supported brands (#77423) Add thermobeacon alternate brands --- homeassistant/components/thermobeacon/manifest.json | 6 +++++- homeassistant/generated/supported_brands.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index 878199e299e..eb13b68a7e2 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -27,5 +27,9 @@ "requirements": ["thermobeacon-ble==0.3.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], - "iot_class": "local_push" + "iot_class": "local_push", + "supported_brands": { + "thermoplus": "ThermoPlus", + "sensorblue": "SensorBlue" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index b4eaa9d8a06..162c953b2b4 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -11,6 +11,7 @@ HAS_SUPPORTED_BRANDS = ( "motion_blinds", "overkiz", "renault", + "thermobeacon", "wemo", "yalexs_ble", "zwave_js" From 0caf998547e84e229792a6fe29840dce31b30c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 28 Aug 2022 20:52:23 +0200 Subject: [PATCH 693/903] Bump awesomeversion from 22.6.0 to 22.8.0 (#77436) --- .../components/homeassistant_alerts/__init__.py | 3 --- .../components/mysensors/config_flow.py | 5 ++++- homeassistant/components/recorder/util.py | 16 ++++++++++++---- homeassistant/helpers/issue_registry.py | 1 - homeassistant/loader.py | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- script/hassfest/manifest.py | 2 +- 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 006f0db54a5..ed7957407a8 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -141,7 +141,6 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) self.ha_version = AwesomeVersion( __version__, ensure_strategy=AwesomeVersionStrategy.CALVER, - find_first_match=False, ) async def _async_update_data(self) -> dict[str, IntegrationAlert]: @@ -161,14 +160,12 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) if "affected_from_version" in alert["homeassistant"]: affected_from_version = AwesomeVersion( alert["homeassistant"]["affected_from_version"], - find_first_match=False, ) if self.ha_version < affected_from_version: continue if "resolved_in_version" in alert["homeassistant"]: resolved_in_version = AwesomeVersion( alert["homeassistant"]["resolved_in_version"], - find_first_match=False, ) if self.ha_version >= resolved_in_version: continue diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 04e95f1dad3..b3c3d11f279 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -82,7 +82,10 @@ def _validate_version(version: str) -> dict[str, str]: try: AwesomeVersion( version, - [AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.SEMVER], + ensure_strategy=[ + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.SEMVER, + ], ) except AwesomeVersionStrategyException: version_okay = False diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index ddc8747f79b..139e73199ed 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -50,10 +50,18 @@ QUERY_RETRY_WAIT = 0.1 SQLITE3_POSTFIXES = ["", "-wal", "-shm"] DEFAULT_YIELD_STATES_ROWS = 32768 -MIN_VERSION_MARIA_DB = AwesomeVersion("10.3.0", AwesomeVersionStrategy.SIMPLEVER) -MIN_VERSION_MYSQL = AwesomeVersion("8.0.0", AwesomeVersionStrategy.SIMPLEVER) -MIN_VERSION_PGSQL = AwesomeVersion("12.0", AwesomeVersionStrategy.SIMPLEVER) -MIN_VERSION_SQLITE = AwesomeVersion("3.31.0", AwesomeVersionStrategy.SIMPLEVER) +MIN_VERSION_MARIA_DB = AwesomeVersion( + "10.3.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MIN_VERSION_MYSQL = AwesomeVersion( + "8.0.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MIN_VERSION_PGSQL = AwesomeVersion( + "12.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MIN_VERSION_SQLITE = AwesomeVersion( + "3.31.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) # This is the maximum time after the recorder ends the session # before we no longer consider startup to be a "restart" and we diff --git a/homeassistant/helpers/issue_registry.py b/homeassistant/helpers/issue_registry.py index b01d56942ac..345ec099d3f 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -303,7 +303,6 @@ def async_create_issue( AwesomeVersion( breaks_in_ha_version, ensure_strategy=AwesomeVersionStrategy.CALVER, - find_first_match=False, ) issue_registry = async_get(hass) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 1d100a42d83..f6da5048d45 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -453,7 +453,7 @@ class Integration: try: AwesomeVersion( integration.version, - [ + ensure_strategy=[ AwesomeVersionStrategy.CALVER, AwesomeVersionStrategy.SEMVER, AwesomeVersionStrategy.SIMPLEVER, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5de59a22eb7..91660db1367 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ async-upnp-client==0.31.2 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==21.2.0 -awesomeversion==22.6.0 +awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.15.1 bluetooth-adapters==0.3.2 diff --git a/pyproject.toml b/pyproject.toml index 3297cb39db2..d68cac82923 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "async_timeout==4.0.2", "attrs==21.2.0", "atomicwrites-homeassistant==1.4.1", - "awesomeversion==22.6.0", + "awesomeversion==22.8.0", "bcrypt==3.1.7", "certifi>=2021.5.30", "ciso8601==2.2.0", diff --git a/requirements.txt b/requirements.txt index f190aa50233..f2ad6f875b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==2.2 async_timeout==4.0.2 attrs==21.2.0 atomicwrites-homeassistant==1.4.1 -awesomeversion==22.6.0 +awesomeversion==22.8.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 53970a4a895..515a617757c 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -138,7 +138,7 @@ def verify_version(value: str): try: AwesomeVersion( value, - [ + ensure_strategy=[ AwesomeVersionStrategy.CALVER, AwesomeVersionStrategy.SEMVER, AwesomeVersionStrategy.SIMPLEVER, From 1210897f83f044e00763637a69e848a189713e24 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 28 Aug 2022 21:14:09 +0200 Subject: [PATCH 694/903] Update pylint to 2.15.0 (#77408) * Update pylint to 2.15.0 * Remove useless suppressions * Fix TypeVar name --- homeassistant/components/nextdns/__init__.py | 8 ++++---- homeassistant/components/nextdns/binary_sensor.py | 8 ++++---- homeassistant/components/nextdns/sensor.py | 12 ++++++------ homeassistant/components/nextdns/switch.py | 8 ++++---- homeassistant/components/switchmate/switch.py | 1 - homeassistant/helpers/template.py | 2 -- requirements_test.txt | 2 +- 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index a92186f6f14..d3cf46828cd 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -46,10 +46,10 @@ from .const import ( UPDATE_INTERVAL_SETTINGS, ) -TCoordinatorData = TypeVar("TCoordinatorData", bound=NextDnsData) +CoordinatorDataT = TypeVar("CoordinatorDataT", bound=NextDnsData) -class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): +class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): """Class to manage fetching NextDNS data API.""" def __init__( @@ -73,7 +73,7 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - async def _async_update_data(self) -> TCoordinatorData: + async def _async_update_data(self) -> CoordinatorDataT: """Update data via internal method.""" try: async with timeout(10): @@ -81,7 +81,7 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: raise UpdateFailed(err) from err - async def _async_update_data_internal(self) -> TCoordinatorData: + async def _async_update_data_internal(self) -> CoordinatorDataT: """Update data via library.""" raise NotImplementedError("Update method not implemented") diff --git a/homeassistant/components/nextdns/binary_sensor.py b/homeassistant/components/nextdns/binary_sensor.py index af80d14a89b..673ea1a53e4 100644 --- a/homeassistant/components/nextdns/binary_sensor.py +++ b/homeassistant/components/nextdns/binary_sensor.py @@ -18,23 +18,23 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NextDnsConnectionUpdateCoordinator, TCoordinatorData +from . import CoordinatorDataT, NextDnsConnectionUpdateCoordinator from .const import ATTR_CONNECTION, DOMAIN PARALLEL_UPDATES = 1 @dataclass -class NextDnsBinarySensorRequiredKeysMixin(Generic[TCoordinatorData]): +class NextDnsBinarySensorRequiredKeysMixin(Generic[CoordinatorDataT]): """Mixin for required keys.""" - state: Callable[[TCoordinatorData, str], bool] + state: Callable[[CoordinatorDataT, str], bool] @dataclass class NextDnsBinarySensorEntityDescription( BinarySensorEntityDescription, - NextDnsBinarySensorRequiredKeysMixin[TCoordinatorData], + NextDnsBinarySensorRequiredKeysMixin[CoordinatorDataT], ): """NextDNS binary sensor entity description.""" diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 62cd671835b..c59440d6220 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NextDnsUpdateCoordinator, TCoordinatorData +from . import CoordinatorDataT, NextDnsUpdateCoordinator from .const import ( ATTR_DNSSEC, ATTR_ENCRYPTION, @@ -40,17 +40,17 @@ PARALLEL_UPDATES = 1 @dataclass -class NextDnsSensorRequiredKeysMixin(Generic[TCoordinatorData]): +class NextDnsSensorRequiredKeysMixin(Generic[CoordinatorDataT]): """Class for NextDNS entity required keys.""" coordinator_type: str - value: Callable[[TCoordinatorData], StateType] + value: Callable[[CoordinatorDataT], StateType] @dataclass class NextDnsSensorEntityDescription( SensorEntityDescription, - NextDnsSensorRequiredKeysMixin[TCoordinatorData], + NextDnsSensorRequiredKeysMixin[CoordinatorDataT], ): """NextDNS sensor entity description.""" @@ -348,7 +348,7 @@ async def async_setup_entry( class NextDnsSensor( - CoordinatorEntity[NextDnsUpdateCoordinator[TCoordinatorData]], SensorEntity + CoordinatorEntity[NextDnsUpdateCoordinator[CoordinatorDataT]], SensorEntity ): """Define an NextDNS sensor.""" @@ -356,7 +356,7 @@ class NextDnsSensor( def __init__( self, - coordinator: NextDnsUpdateCoordinator[TCoordinatorData], + coordinator: NextDnsUpdateCoordinator[CoordinatorDataT], description: NextDnsSensorEntityDescription, ) -> None: """Initialize.""" diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index c0a0973a24c..a0e8f15e44d 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -18,22 +18,22 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NextDnsSettingsUpdateCoordinator, TCoordinatorData +from . import CoordinatorDataT, NextDnsSettingsUpdateCoordinator from .const import ATTR_SETTINGS, DOMAIN PARALLEL_UPDATES = 1 @dataclass -class NextDnsSwitchRequiredKeysMixin(Generic[TCoordinatorData]): +class NextDnsSwitchRequiredKeysMixin(Generic[CoordinatorDataT]): """Class for NextDNS entity required keys.""" - state: Callable[[TCoordinatorData], bool] + state: Callable[[CoordinatorDataT], bool] @dataclass class NextDnsSwitchEntityDescription( - SwitchEntityDescription, NextDnsSwitchRequiredKeysMixin[TCoordinatorData] + SwitchEntityDescription, NextDnsSwitchRequiredKeysMixin[CoordinatorDataT] ): """NextDNS switch entity description.""" diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index 7beb89f8de1..ecc5b9003b5 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta -# pylint: disable=import-error from switchmate import Switchmate import voluptuous as vol diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 36f5dc9b22c..ea6b764a75a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -199,8 +199,6 @@ class TupleWrapper(tuple, ResultWrapper): """Create a new tuple class.""" return super().__new__(cls, tuple(value)) - # pylint: disable=super-init-not-called - def __init__(self, value: tuple, *, render_result: str | None = None) -> None: """Initialize a new tuple class.""" self.render_result = render_result diff --git a/requirements_test.txt b/requirements_test.txt index 94a1e0c3120..d15431a1d85 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.971 pre-commit==2.20.0 -pylint==2.14.5 +pylint==2.15.0 pipdeptree==2.2.1 pytest-aiohttp==0.3.0 pytest-cov==3.0.0 From bf510fcb4c3703de14d22b61a38b406649148a79 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 28 Aug 2022 21:54:00 +0200 Subject: [PATCH 695/903] Add CAQI sensors to Nettigo Air Monitor integration (#76709) * Add CAQI sensors * Add state translation * Add icon * Update tests * Remove unit * Update test * Do not use device_class * Update tests * Remove unit and device_class --- homeassistant/components/nam/const.py | 5 +++ homeassistant/components/nam/sensor.py | 26 +++++++++++++ .../components/nam/strings.sensor.json | 11 ++++++ tests/components/nam/test_sensor.py | 38 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 homeassistant/components/nam/strings.sensor.json diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index e7c6f2532ef..2b6a74383b5 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta from typing import Final +SUFFIX_CAQI: Final = "_caqi" SUFFIX_P0: Final = "_p0" SUFFIX_P1: Final = "_p1" SUFFIX_P2: Final = "_p2" @@ -22,12 +23,16 @@ ATTR_HECA_HUMIDITY: Final = "heca_humidity" ATTR_HECA_TEMPERATURE: Final = "heca_temperature" ATTR_MHZ14A_CARBON_DIOXIDE: Final = "mhz14a_carbon_dioxide" ATTR_SDS011: Final = "sds011" +ATTR_SDS011_CAQI: Final = f"{ATTR_SDS011}{SUFFIX_CAQI}" +ATTR_SDS011_CAQI_LEVEL: Final = f"{ATTR_SDS011}{SUFFIX_CAQI}_level" ATTR_SDS011_P1: Final = f"{ATTR_SDS011}{SUFFIX_P1}" ATTR_SDS011_P2: Final = f"{ATTR_SDS011}{SUFFIX_P2}" ATTR_SHT3X_HUMIDITY: Final = "sht3x_humidity" ATTR_SHT3X_TEMPERATURE: Final = "sht3x_temperature" ATTR_SIGNAL_STRENGTH: Final = "signal" ATTR_SPS30: Final = "sps30" +ATTR_SPS30_CAQI: Final = f"{ATTR_SPS30}{SUFFIX_CAQI}" +ATTR_SPS30_CAQI_LEVEL: Final = f"{ATTR_SPS30}{SUFFIX_CAQI}_level" ATTR_SPS30_P0: Final = f"{ATTR_SPS30}{SUFFIX_P0}" ATTR_SPS30_P1: Final = f"{ATTR_SPS30}{SUFFIX_P1}" ATTR_SPS30_P2: Final = f"{ATTR_SPS30}{SUFFIX_P2}" diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 6229102035e..01107baf31b 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -43,11 +43,15 @@ from .const import ( ATTR_HECA_HUMIDITY, ATTR_HECA_TEMPERATURE, ATTR_MHZ14A_CARBON_DIOXIDE, + ATTR_SDS011_CAQI, + ATTR_SDS011_CAQI_LEVEL, ATTR_SDS011_P1, ATTR_SDS011_P2, ATTR_SHT3X_HUMIDITY, ATTR_SHT3X_TEMPERATURE, ATTR_SIGNAL_STRENGTH, + ATTR_SPS30_CAQI, + ATTR_SPS30_CAQI_LEVEL, ATTR_SPS30_P0, ATTR_SPS30_P1, ATTR_SPS30_P2, @@ -132,6 +136,17 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key=ATTR_SDS011_CAQI, + name="SDS011 CAQI", + icon="mdi:air-filter", + ), + SensorEntityDescription( + key=ATTR_SDS011_CAQI_LEVEL, + name="SDS011 CAQI level", + icon="mdi:air-filter", + device_class="nam__caqi_level", + ), SensorEntityDescription( key=ATTR_SDS011_P1, name="SDS011 particulate matter 10", @@ -160,6 +175,17 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key=ATTR_SPS30_CAQI, + name="SPS30 CAQI", + icon="mdi:air-filter", + ), + SensorEntityDescription( + key=ATTR_SPS30_CAQI_LEVEL, + name="SPS30 CAQI level", + icon="mdi:air-filter", + device_class="nam__caqi_level", + ), SensorEntityDescription( key=ATTR_SPS30_P0, name="SPS30 particulate matter 1.0", diff --git a/homeassistant/components/nam/strings.sensor.json b/homeassistant/components/nam/strings.sensor.json new file mode 100644 index 00000000000..ee53079d7d0 --- /dev/null +++ b/homeassistant/components/nam/strings.sensor.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "very low": "Very low", + "low": "Low", + "medium": "Medium", + "high": "High", + "very high": "Very high" + } + } +} diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 9fbb55a6474..28eaea54186 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -239,6 +239,25 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) + entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi" + + state = hass.states.get("sensor.nettigo_air_monitor_sds011_caqi") + assert state + assert state.state == "19" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + + entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi_level") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi_level" + + state = hass.states.get("sensor.nettigo_air_monitor_sds011_caqi_level") + assert state + assert state.state == "very low" + assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + entry = registry.async_get( "sensor.nettigo_air_monitor_sds011_particulate_matter_10" ) @@ -271,6 +290,25 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) + entry = registry.async_get("sensor.nettigo_air_monitor_sps30_caqi") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi") + assert state + assert state.state == "54" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + + entry = registry.async_get("sensor.nettigo_air_monitor_sps30_caqi_level") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi_level" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi_level") + assert state + assert state.state == "medium" + assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + entry = registry.async_get( "sensor.nettigo_air_monitor_sps30_particulate_matter_1_0" ) From 7be6d6eba2ca0efd2e4c0a18068332e5c6f7dac4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 28 Aug 2022 17:40:53 -0400 Subject: [PATCH 696/903] Use generators for async_add_entities in Anthemav (#76587) --- homeassistant/components/anthemav/media_player.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 7582aa24083..a854ea0653e 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -91,17 +91,14 @@ async def async_setup_entry( avr: Connection = hass.data[DOMAIN][config_entry.entry_id] - entities = [] - for zone_number in avr.protocol.zones: - _LOGGER.debug("Initializing Zone %s", zone_number) - entity = AnthemAVR( - avr.protocol, name, mac_address, model, zone_number, config_entry.entry_id - ) - entities.append(entity) - _LOGGER.debug("Connection data dump: %s", avr.dump_conndata) - async_add_entities(entities) + async_add_entities( + AnthemAVR( + avr.protocol, name, mac_address, model, zone_number, config_entry.entry_id + ) + for zone_number in avr.protocol.zones + ) class AnthemAVR(MediaPlayerEntity): From 50a1de9f73fd724e19a81e5b62fbb3c8febc79f2 Mon Sep 17 00:00:00 2001 From: Tomasz Wieczorek Date: Mon, 29 Aug 2022 00:02:27 +0200 Subject: [PATCH 697/903] Add set default for domain for scaffold script (#76628) * Add set default for domain for scaffold script * Add default domain for config_flow_discovery integration * Extend comment explaining usage --- .../scaffold/templates/config_flow/integration/__init__.py | 6 +++++- .../templates/config_flow_discovery/integration/__init__.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index dc92ecc1d15..704292a2e9b 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -14,7 +14,11 @@ PLATFORMS: list[Platform] = [Platform.LIGHT] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NEW_NAME from a config entry.""" - # TODO Store an API object for your platforms to access + + hass.data.setdefault(DOMAIN, {}) + # TODO 1. Create API instance + # TODO 2. Validate the API connection (and authentication) + # TODO 3. Store an API object for your platforms to access # hass.data[DOMAIN][entry.entry_id] = MyApi(...) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index d7fb1e56eef..73b4bebf9f5 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -14,7 +14,11 @@ PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NEW_NAME from a config entry.""" - # TODO Store an API object for your platforms to access + + hass.data.setdefault(DOMAIN, {}) + # TODO 1. Create API instance + # TODO 2. Validate the API connection (and authentication) + # TODO 3. Store an API object for your platforms to access # hass.data[DOMAIN][entry.entry_id] = MyApi(...) hass.config_entries.async_setup_platforms(entry, PLATFORMS) From 575ac5ae0a240252b7e568713815ff292162c8b9 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Sun, 28 Aug 2022 18:50:44 -0400 Subject: [PATCH 698/903] Squeezebox play now support (#72626) Co-authored-by: J. Nick Koston --- homeassistant/components/squeezebox/manifest.json | 2 +- homeassistant/components/squeezebox/media_player.py | 2 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index 3f58ee1f275..018333d420b 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -3,7 +3,7 @@ "name": "Squeezebox (Logitech Media Server)", "documentation": "https://www.home-assistant.io/integrations/squeezebox", "codeowners": ["@rajlaud"], - "requirements": ["pysqueezebox==0.5.5"], + "requirements": ["pysqueezebox==0.6.0"], "config_flow": true, "dhcp": [ { diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 260228e4fdf..aae0ce638e7 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -482,6 +482,8 @@ class SqueezeBoxEntity(MediaPlayerEntity): cmd = "add" elif enqueue == MediaPlayerEnqueue.NEXT: cmd = "insert" + elif enqueue == MediaPlayerEnqueue.PLAY: + cmd = "play_now" else: cmd = "play" diff --git a/requirements_all.txt b/requirements_all.txt index a637284cbd8..2a1d22a8ee6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1873,7 +1873,7 @@ pysoma==0.0.10 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.5.5 +pysqueezebox==0.6.0 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd60fae2e00..918c0384442 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1308,7 +1308,7 @@ pysoma==0.0.10 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.5.5 +pysqueezebox==0.6.0 # homeassistant.components.syncthru pysyncthru==0.7.10 From 7eb8e1f25dac1f6af606d448028b2d65c6eaed33 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Aug 2022 00:51:10 +0200 Subject: [PATCH 699/903] Improve type hints in demo [1/3] (#77180) --- homeassistant/components/demo/air_quality.py | 28 ++--- .../components/demo/binary_sensor.py | 14 +-- homeassistant/components/demo/calendar.py | 24 ++-- homeassistant/components/demo/climate.py | 14 +-- homeassistant/components/demo/cover.py | 67 +++++----- homeassistant/components/demo/fan.py | 16 +-- homeassistant/components/demo/geo_location.py | 14 +-- .../components/demo/image_processing.py | 14 +-- homeassistant/components/demo/light.py | 40 +++--- homeassistant/components/demo/media_player.py | 118 +++++++++--------- homeassistant/components/demo/vacuum.py | 28 +---- homeassistant/components/demo/weather.py | 20 +-- tests/components/demo/test_media_player.py | 6 +- 13 files changed, 156 insertions(+), 247 deletions(-) diff --git a/homeassistant/components/demo/air_quality.py b/homeassistant/components/demo/air_quality.py index bbeff004ac8..c63729f2cd6 100644 --- a/homeassistant/components/demo/air_quality.py +++ b/homeassistant/components/demo/air_quality.py @@ -32,39 +32,27 @@ async def async_setup_entry( class DemoAirQuality(AirQualityEntity): """Representation of Air Quality data.""" - def __init__(self, name, pm_2_5, pm_10, n2o): + _attr_attribution = "Powered by Home Assistant" + _attr_should_poll = False + + def __init__(self, name: str, pm_2_5: int, pm_10: int, n2o: int | None) -> None: """Initialize the Demo Air Quality.""" - self._name = name + self._attr_name = f"Demo Air Quality {name}" self._pm_2_5 = pm_2_5 self._pm_10 = pm_10 self._n2o = n2o @property - def name(self): - """Return the name of the sensor.""" - return f"Demo Air Quality {self._name}" - - @property - def should_poll(self): - """No polling needed for Demo Air Quality.""" - return False - - @property - def particulate_matter_2_5(self): + def particulate_matter_2_5(self) -> int: """Return the particulate matter 2.5 level.""" return self._pm_2_5 @property - def particulate_matter_10(self): + def particulate_matter_10(self) -> int: """Return the particulate matter 10 level.""" return self._pm_10 @property - def nitrogen_oxide(self): + def nitrogen_oxide(self) -> int | None: """Return the nitrogen oxide (N2O) level.""" return self._n2o - - @property - def attribution(self): - """Return the attribution.""" - return "Powered by Home Assistant" diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index f2b759a9cf3..584c0cf88f1 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -48,6 +48,8 @@ async def async_setup_entry( class DemoBinarySensor(BinarySensorEntity): """representation of a Demo binary sensor.""" + _attr_should_poll = False + def __init__( self, unique_id: str, @@ -57,7 +59,7 @@ class DemoBinarySensor(BinarySensorEntity): ) -> None: """Initialize the demo sensor.""" self._unique_id = unique_id - self._name = name + self._attr_name = name self._state = state self._sensor_type = device_class @@ -82,16 +84,6 @@ class DemoBinarySensor(BinarySensorEntity): """Return the class of this sensor.""" return self._sensor_type - @property - def should_poll(self) -> bool: - """No polling needed for a demo binary sensor.""" - return False - - @property - def name(self) -> str: - """Return the name of the binary sensor.""" - return self._name - @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index f8a803ba860..415ed0dbb8d 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy import datetime +from typing import Any from homeassistant.components.calendar import ( CalendarEntity, @@ -60,18 +61,13 @@ class DemoCalendar(CalendarEntity): def __init__(self, event: CalendarEvent, name: str) -> None: """Initialize demo calendar.""" self._event = event - self._name = name + self._attr_name = name @property def event(self) -> CalendarEvent: """Return the next upcoming event.""" return self._event - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - async def async_get_events( self, hass: HomeAssistant, @@ -87,7 +83,7 @@ class LegacyDemoCalendar(CalendarEventDevice): def __init__(self, name: str) -> None: """Initialize demo calendar.""" - self._name = name + self._attr_name = name one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) self._event = { "start": {"dateTime": one_hour_from_now.isoformat()}, @@ -102,16 +98,16 @@ class LegacyDemoCalendar(CalendarEventDevice): } @property - def event(self): + def event(self) -> dict[str, Any]: """Return the next upcoming event.""" return self._event - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime.datetime, + end_date: datetime.datetime, + ) -> list[dict[str, Any]]: """Get all events in a specific time frame.""" event = copy.copy(self.event) event["title"] = event["summary"] diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index ae633c5937a..546a580f576 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -103,6 +103,8 @@ async def async_setup_entry( class DemoClimate(ClimateEntity): """Representation of a demo climate device.""" + _attr_should_poll = False + def __init__( self, unique_id: str, @@ -125,7 +127,7 @@ class DemoClimate(ClimateEntity): ) -> None: """Initialize the climate device.""" self._unique_id = unique_id - self._name = name + self._attr_name = name self._support_flags = SUPPORT_FLAGS if target_temperature is not None: self._support_flags = ( @@ -186,16 +188,6 @@ class DemoClimate(ClimateEntity): """Return the list of supported features.""" return self._support_flags - @property - def should_poll(self) -> bool: - """Return the polling state.""" - return False - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self._name - @property def temperature_unit(self) -> str: """Return the unit of measurement.""" diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index f867ed3faa4..fbb8a171516 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -1,6 +1,7 @@ """Demo platform for the cover component.""" from __future__ import annotations +from datetime import datetime from typing import Any from homeassistant.components.cover import ( @@ -11,7 +12,7 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_utc_time_change @@ -67,36 +68,38 @@ async def async_setup_entry( class DemoCover(CoverEntity): """Representation of a demo cover.""" + _attr_should_poll = False + def __init__( self, - hass, - unique_id, - name, - position=None, - tilt_position=None, - device_class=None, - supported_features=None, - ): + hass: HomeAssistant, + unique_id: str, + name: str, + position: int | None = None, + tilt_position: int | None = None, + device_class: CoverDeviceClass | None = None, + supported_features: int | None = None, + ) -> None: """Initialize the cover.""" self.hass = hass self._unique_id = unique_id - self._name = name + self._attr_name = name self._position = position self._device_class = device_class self._supported_features = supported_features - self._set_position = None - self._set_tilt_position = None + self._set_position: int | None = None + self._set_tilt_position: int | None = None self._tilt_position = tilt_position self._requested_closing = True self._requested_closing_tilt = True - self._unsub_listener_cover = None - self._unsub_listener_cover_tilt = None + self._unsub_listener_cover: CALLBACK_TYPE | None = None + self._unsub_listener_cover_tilt: CALLBACK_TYPE | None = None self._is_opening = False self._is_closing = False if position is None: self._closed = True else: - self._closed = self.current_cover_position <= 0 + self._closed = position <= 0 @property def device_info(self) -> DeviceInfo: @@ -114,16 +117,6 @@ class DemoCover(CoverEntity): """Return unique ID for cover.""" return self._unique_id - @property - def name(self) -> str: - """Return the name of the cover.""" - return self._name - - @property - def should_poll(self) -> bool: - """No polling needed for a demo cover.""" - return False - @property def current_cover_position(self) -> int | None: """Return the current position of the cover.""" @@ -213,7 +206,9 @@ class DemoCover(CoverEntity): return self._listen_cover() - self._requested_closing = position < self._position + self._requested_closing = ( + self._position is not None and position < self._position + ) async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover til to a specific position.""" @@ -223,7 +218,9 @@ class DemoCover(CoverEntity): return self._listen_cover_tilt() - self._requested_closing_tilt = tilt_position < self._tilt_position + self._requested_closing_tilt = ( + self._tilt_position is not None and tilt_position < self._tilt_position + ) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" @@ -247,15 +244,17 @@ class DemoCover(CoverEntity): self._set_tilt_position = None @callback - def _listen_cover(self): + def _listen_cover(self) -> None: """Listen for changes in cover.""" if self._unsub_listener_cover is None: self._unsub_listener_cover = async_track_utc_time_change( self.hass, self._time_changed_cover ) - async def _time_changed_cover(self, now): + async def _time_changed_cover(self, now: datetime) -> None: """Track time changes.""" + if self._position is None: + return if self._requested_closing: self._position -= 10 else: @@ -264,19 +263,23 @@ class DemoCover(CoverEntity): if self._position in (100, 0, self._set_position): await self.async_stop_cover() - self._closed = self.current_cover_position <= 0 + self._closed = ( + self.current_cover_position is not None and self.current_cover_position <= 0 + ) self.async_write_ha_state() @callback - def _listen_cover_tilt(self): + def _listen_cover_tilt(self) -> None: """Listen for changes in cover tilt.""" if self._unsub_listener_cover_tilt is None: self._unsub_listener_cover_tilt = async_track_utc_time_change( self.hass, self._time_changed_cover_tilt ) - async def _time_changed_cover_tilt(self, now): + async def _time_changed_cover_tilt(self, now: datetime) -> None: """Track time changes.""" + if self._tilt_position is None: + return if self._requested_closing_tilt: self._tilt_position -= 10 else: diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 02623b7b644..8dcffa6e141 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -100,9 +100,11 @@ async def async_setup_entry( class BaseDemoFan(FanEntity): """A demonstration fan component that uses legacy fan speeds.""" + _attr_should_poll = False + def __init__( self, - hass, + hass: HomeAssistant, unique_id: str, name: str, supported_features: int, @@ -117,7 +119,7 @@ class BaseDemoFan(FanEntity): self._preset_mode: str | None = None self._oscillating: bool | None = None self._direction: str | None = None - self._name = name + self._attr_name = name if supported_features & FanEntityFeature.OSCILLATE: self._oscillating = False if supported_features & FanEntityFeature.DIRECTION: @@ -128,16 +130,6 @@ class BaseDemoFan(FanEntity): """Return the unique id.""" return self._unique_id - @property - def name(self) -> str: - """Get entity name.""" - return self._name - - @property - def should_poll(self) -> bool: - """No polling needed for a demo fan.""" - return False - @property def current_direction(self) -> str | None: """Fan direction.""" diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index 27935300959..c47f4e49d4a 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -112,6 +112,8 @@ class DemoManager: class DemoGeolocationEvent(GeolocationEvent): """This represents a demo geolocation event.""" + _attr_should_poll = False + def __init__( self, name: str, @@ -121,7 +123,7 @@ class DemoGeolocationEvent(GeolocationEvent): unit_of_measurement: str, ) -> None: """Initialize entity with data provided.""" - self._name = name + self._attr_name = name self._distance = distance self._latitude = latitude self._longitude = longitude @@ -132,16 +134,6 @@ class DemoGeolocationEvent(GeolocationEvent): """Return source value of this external event.""" return SOURCE - @property - def name(self) -> str | None: - """Return the name of the event.""" - return self._name - - @property - def should_poll(self) -> bool: - """No polling needed for a demo geolocation event.""" - return False - @property def distance(self) -> float | None: """Return distance value of this external event.""" diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 58c884cc439..6ba498114a4 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -39,7 +39,7 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): """Initialize demo ALPR image processing entity.""" super().__init__() - self._name = name + self._attr_name = name self._camera = camera_entity @property @@ -52,11 +52,6 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): """Return minimum confidence for send events.""" return 80 - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - def process_image(self, image: Image) -> None: """Process image.""" demo_data = { @@ -76,7 +71,7 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity): """Initialize demo face image processing entity.""" super().__init__() - self._name = name + self._attr_name = name self._camera = camera_entity @property @@ -89,11 +84,6 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity): """Return minimum confidence for send events.""" return 80 - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - def process_image(self, image: Image) -> None: """Process image.""" demo_data = [ diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 00e5fd4def1..af8afe2c15d 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -106,21 +106,23 @@ async def async_setup_entry( class DemoLight(LightEntity): """Representation of a demo light.""" + _attr_should_poll = False + def __init__( self, unique_id: str, name: str, - state, - available=False, - brightness=180, - ct=None, # pylint: disable=invalid-name + state: bool, + available: bool = False, + brightness: int = 180, + ct: int | None = None, # pylint: disable=invalid-name effect_list: list[str] | None = None, - effect=None, - hs_color=None, - rgbw_color=None, - rgbww_color=None, + effect: str | None = None, + hs_color: tuple[int, int] | None = None, + rgbw_color: tuple[int, int, int, int] | None = None, + rgbww_color: tuple[int, int, int, int, int] | None = None, supported_color_modes: set[ColorMode] | None = None, - ): + ) -> None: """Initialize the light.""" self._available = True self._brightness = brightness @@ -129,7 +131,7 @@ class DemoLight(LightEntity): self._effect_list = effect_list self._features = 0 self._hs_color = hs_color - self._name = name + self._attr_name = name self._rgbw_color = rgbw_color self._rgbww_color = rgbww_color self._state = state @@ -159,16 +161,6 @@ class DemoLight(LightEntity): name=self.name, ) - @property - def should_poll(self) -> bool: - """No polling needed for a demo light.""" - return False - - @property - def name(self) -> str: - """Return the name of the light if any.""" - return self._name - @property def unique_id(self) -> str: """Return unique ID for light.""" @@ -192,17 +184,17 @@ class DemoLight(LightEntity): return self._color_mode @property - def hs_color(self) -> tuple[float, float]: + def hs_color(self) -> tuple[int, int] | None: """Return the hs color value.""" return self._hs_color @property - def rgbw_color(self) -> tuple[int, int, int, int]: + def rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the rgbw color value.""" return self._rgbw_color @property - def rgbww_color(self) -> tuple[int, int, int, int, int]: + def rgbww_color(self) -> tuple[int, int, int, int, int] | None: """Return the rgbww color value.""" return self._rgbww_color @@ -217,7 +209,7 @@ class DemoLight(LightEntity): return self._effect_list @property - def effect(self) -> str: + def effect(self) -> str | None: """Return the current effect.""" return self._effect diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index e52bf8720e1..d5098dc4586 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -1,6 +1,7 @@ """Demo implementation of the media player.""" from __future__ import annotations +from datetime import datetime from typing import Any from homeassistant.components.media_player import ( @@ -108,11 +109,15 @@ NETFLIX_PLAYER_SUPPORT = ( class AbstractDemoPlayer(MediaPlayerEntity): """A demo media players.""" + _attr_should_poll = False + # We only implement the methods that we support - def __init__(self, name, device_class=None): + def __init__( + self, name: str, device_class: MediaPlayerDeviceClass | None = None + ) -> None: """Initialize the demo device.""" - self._name = name + self._attr_name = name self._player_state = STATE_PLAYING self._volume_level = 1.0 self._volume_muted = False @@ -122,47 +127,37 @@ class AbstractDemoPlayer(MediaPlayerEntity): self._device_class = device_class @property - def should_poll(self): - """Push an update after each command.""" - return False - - @property - def name(self): - """Return the name of the media player.""" - return self._name - - @property - def state(self): + def state(self) -> str: """Return the state of the player.""" return self._player_state @property - def volume_level(self): + def volume_level(self) -> float: """Return the volume level of the media player (0..1).""" return self._volume_level @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool: """Return boolean if volume is currently muted.""" return self._volume_muted @property - def shuffle(self): + def shuffle(self) -> bool: """Boolean if shuffling is enabled.""" return self._shuffle @property - def sound_mode(self): + def sound_mode(self) -> str: """Return the current sound mode.""" return self._sound_mode @property - def sound_mode_list(self): + def sound_mode_list(self) -> list[str]: """Return a list of available sound modes.""" return self._sound_mode_list @property - def device_class(self): + def device_class(self) -> MediaPlayerDeviceClass | None: """Return the device class of the media player.""" return self._device_class @@ -227,52 +222,54 @@ class DemoYoutubePlayer(AbstractDemoPlayer): # We only implement the methods that we support - def __init__(self, name, youtube_id=None, media_title=None, duration=360): + def __init__( + self, name: str, youtube_id: str, media_title: str, duration: int + ) -> None: """Initialize the demo device.""" super().__init__(name) self.youtube_id = youtube_id self._media_title = media_title self._duration = duration - self._progress = int(duration * 0.15) + self._progress: int | None = int(duration * 0.15) self._progress_updated_at = dt_util.utcnow() @property - def media_content_id(self): + def media_content_id(self) -> str: """Return the content ID of current playing media.""" return self.youtube_id @property - def media_content_type(self): + def media_content_type(self) -> str: """Return the content type of current playing media.""" return MEDIA_TYPE_MOVIE @property - def media_duration(self): + def media_duration(self) -> int: """Return the duration of current playing media in seconds.""" return self._duration @property - def media_image_url(self): + def media_image_url(self) -> str: """Return the image url of current playing media.""" return f"https://img.youtube.com/vi/{self.youtube_id}/hqdefault.jpg" @property - def media_title(self): + def media_title(self) -> str: """Return the title of current playing media.""" return self._media_title @property - def app_name(self): + def app_name(self) -> str: """Return the current running application.""" return "YouTube" @property - def supported_features(self): + def supported_features(self) -> int: """Flag media player features that are supported.""" return YOUTUBE_PLAYER_SUPPORT @property - def media_position(self): + def media_position(self) -> int | None: """Position of current playing media in seconds.""" if self._progress is None: return None @@ -280,18 +277,21 @@ class DemoYoutubePlayer(AbstractDemoPlayer): position = self._progress if self._player_state == STATE_PLAYING: - position += (dt_util.utcnow() - self._progress_updated_at).total_seconds() + position += int( + (dt_util.utcnow() - self._progress_updated_at).total_seconds() + ) return position @property - def media_position_updated_at(self): + def media_position_updated_at(self) -> datetime | None: """When was the position of the current playing media valid. Returns value from homeassistant.util.dt.utcnow(). """ if self._player_state == STATE_PLAYING: return self._progress_updated_at + return None def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play a piece of media.""" @@ -333,65 +333,65 @@ class DemoMusicPlayer(AbstractDemoPlayer): ), ] - def __init__(self, name="Walkman"): + def __init__(self, name: str = "Walkman") -> None: """Initialize the demo device.""" super().__init__(name) self._cur_track = 0 - self._group_members = [] + self._group_members: list[str] = [] self._repeat = REPEAT_MODE_OFF @property - def group_members(self): + def group_members(self) -> list[str]: """List of players which are currently grouped together.""" return self._group_members @property - def media_content_id(self): + def media_content_id(self) -> str: """Return the content ID of current playing media.""" return "bounzz-1" @property - def media_content_type(self): + def media_content_type(self) -> str: """Return the content type of current playing media.""" return MEDIA_TYPE_MUSIC @property - def media_duration(self): + def media_duration(self) -> int: """Return the duration of current playing media in seconds.""" return 213 @property - def media_image_url(self): + def media_image_url(self) -> str: """Return the image url of current playing media.""" return "https://graph.facebook.com/v2.5/107771475912710/picture?type=large" @property - def media_title(self): + def media_title(self) -> str: """Return the title of current playing media.""" return self.tracks[self._cur_track][1] if self.tracks else "" @property - def media_artist(self): + def media_artist(self) -> str: """Return the artist of current playing media (Music track only).""" return self.tracks[self._cur_track][0] if self.tracks else "" @property - def media_album_name(self): + def media_album_name(self) -> str: """Return the album of current playing media (Music track only).""" return "Bounzz" @property - def media_track(self): + def media_track(self) -> int: """Return the track number of current media (Music track only).""" return self._cur_track + 1 @property - def repeat(self): + def repeat(self) -> str: """Return current repeat mode.""" return self._repeat @property - def supported_features(self): + def supported_features(self) -> int: """Flag media player features that are supported.""" return MUSIC_PLAYER_SUPPORT @@ -439,7 +439,7 @@ class DemoTVShowPlayer(AbstractDemoPlayer): _attr_device_class = MediaPlayerDeviceClass.TV - def __init__(self): + def __init__(self) -> None: """Initialize the demo device.""" super().__init__("Lounge room") self._cur_episode = 1 @@ -448,62 +448,62 @@ class DemoTVShowPlayer(AbstractDemoPlayer): self._source_list = ["dvd", "youtube"] @property - def media_content_id(self): + def media_content_id(self) -> str: """Return the content ID of current playing media.""" return "house-of-cards-1" @property - def media_content_type(self): + def media_content_type(self) -> str: """Return the content type of current playing media.""" return MEDIA_TYPE_TVSHOW @property - def media_duration(self): + def media_duration(self) -> int: """Return the duration of current playing media in seconds.""" return 3600 @property - def media_image_url(self): + def media_image_url(self) -> str: """Return the image url of current playing media.""" return "https://graph.facebook.com/v2.5/HouseofCards/picture?width=400" @property - def media_title(self): + def media_title(self) -> str: """Return the title of current playing media.""" return f"Chapter {self._cur_episode}" @property - def media_series_title(self): + def media_series_title(self) -> str: """Return the series title of current playing media (TV Show only).""" return "House of Cards" @property - def media_season(self): + def media_season(self) -> str: """Return the season of current playing media (TV Show only).""" - return 1 + return "1" @property - def media_episode(self): + def media_episode(self) -> str: """Return the episode of current playing media (TV Show only).""" - return self._cur_episode + return str(self._cur_episode) @property - def app_name(self): + def app_name(self) -> str: """Return the current running application.""" return "Netflix" @property - def source(self): + def source(self) -> str: """Return the current input source.""" return self._source @property - def source_list(self): + def source_list(self) -> list[str]: """List of available sources.""" return self._source_list @property - def supported_features(self): + def supported_features(self) -> int: """Flag media player features that are supported.""" return NETFLIX_PLAYER_SUPPORT diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 10952f86785..015e6b8ca6f 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -104,9 +104,11 @@ async def async_setup_platform( class DemoVacuum(VacuumEntity): """Representation of a demo vacuum.""" + _attr_should_poll = False + def __init__(self, name: str, supported_features: int) -> None: """Initialize the vacuum.""" - self._name = name + self._attr_name = name self._supported_features = supported_features self._state = False self._status = "Charging" @@ -114,16 +116,6 @@ class DemoVacuum(VacuumEntity): self._cleaned_area: float = 0 self._battery_level = 100 - @property - def name(self) -> str: - """Return the name of the vacuum.""" - return self._name - - @property - def should_poll(self) -> bool: - """No polling needed for a demo vacuum.""" - return False - @property def is_on(self) -> bool: """Return true if vacuum is on.""" @@ -258,25 +250,17 @@ class DemoVacuum(VacuumEntity): class StateDemoVacuum(StateVacuumEntity): """Representation of a demo vacuum supporting states.""" + _attr_should_poll = False + def __init__(self, name: str) -> None: """Initialize the vacuum.""" - self._name = name + self._attr_name = name self._supported_features = SUPPORT_STATE_SERVICES self._state = STATE_DOCKED self._fan_speed = FAN_SPEEDS[1] self._cleaned_area: float = 0 self._battery_level = 100 - @property - def name(self) -> str: - """Return the name of the vacuum.""" - return self._name - - @property - def should_poll(self) -> bool: - """No polling needed for a demo vacuum.""" - return False - @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index dabaf8d066c..cd1a3a6258c 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -118,6 +118,9 @@ def setup_platform( class DemoWeather(WeatherEntity): """Representation of a weather condition.""" + _attr_attribution = "Powered by Home Assistant" + _attr_should_poll = False + def __init__( self, name: str, @@ -132,7 +135,7 @@ class DemoWeather(WeatherEntity): forecast: list[list], ) -> None: """Initialize the Demo weather.""" - self._name = name + self._attr_name = f"Demo Weather {name}" self._condition = condition self._native_temperature = temperature self._native_temperature_unit = temperature_unit @@ -143,16 +146,6 @@ class DemoWeather(WeatherEntity): self._native_wind_speed_unit = wind_speed_unit self._forecast = forecast - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"Demo Weather {self._name}" - - @property - def should_poll(self) -> bool: - """No polling needed for a demo weather condition.""" - return False - @property def native_temperature(self) -> float: """Return the temperature.""" @@ -195,11 +188,6 @@ class DemoWeather(WeatherEntity): k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v ][0] - @property - def attribution(self) -> str: - """Return the attribution.""" - return "Powered by Home Assistant" - @property def forecast(self) -> list[Forecast]: """Return the forecast.""" diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index dc54b27a2c2..98e631131fd 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -306,7 +306,7 @@ async def test_prev_next_track(hass): ent_id = "media_player.lounge_room" state = hass.states.get(ent_id) - assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == 1 + assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == "1" await hass.services.async_call( mp.DOMAIN, @@ -315,7 +315,7 @@ async def test_prev_next_track(hass): blocking=True, ) state = hass.states.get(ent_id) - assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == 2 + assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == "2" await hass.services.async_call( mp.DOMAIN, @@ -324,7 +324,7 @@ async def test_prev_next_track(hass): blocking=True, ) state = hass.states.get(ent_id) - assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == 1 + assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == "1" async def test_play_media(hass): From 5c8bd1ec25cd53885aaa5d677fb91cd916c674bb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 28 Aug 2022 17:51:20 -0500 Subject: [PATCH 700/903] Fix Plex to Cast media resuming (#76681) --- homeassistant/components/plex/cast.py | 3 ++- homeassistant/components/plex/models.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plex/cast.py b/homeassistant/components/plex/cast.py index dc8d791f117..c85b6ee3a78 100644 --- a/homeassistant/components/plex/cast.py +++ b/homeassistant/components/plex/cast.py @@ -53,7 +53,8 @@ def _play_media( result = process_plex_payload(hass, media_type, media_id) controller = PlexController() chromecast.register_handler(controller) - controller.play_media(result.media, offset=result.offset) + offset_in_s = result.offset / 1000 + controller.play_media(result.media, offset=offset_in_s) async def async_play_media( diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index ffb6f791419..48eee9d988d 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -157,7 +157,7 @@ class PlexMediaSearchResult: @property def offset(self) -> int: - """Provide the appropriate offset based on payload contents.""" + """Provide the appropriate offset in ms based on payload contents.""" if offset := self._params.get("offset", 0): return offset * 1000 resume = self._params.get("resume", False) From 37395ecd36fb75792a093289c25a44366e3d66af Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 29 Aug 2022 00:51:36 +0200 Subject: [PATCH 701/903] Update tesla-wall-connector to 1.0.2 (#77458) --- homeassistant/components/tesla_wall_connector/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/manifest.json b/homeassistant/components/tesla_wall_connector/manifest.json index a4bc1969954..2faa5c393c3 100644 --- a/homeassistant/components/tesla_wall_connector/manifest.json +++ b/homeassistant/components/tesla_wall_connector/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla Wall Connector", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla_wall_connector", - "requirements": ["tesla-wall-connector==1.0.1"], + "requirements": ["tesla-wall-connector==1.0.2"], "dhcp": [ { "hostname": "teslawallconnector_*", diff --git a/requirements_all.txt b/requirements_all.txt index 2a1d22a8ee6..b56d75d7b66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2336,7 +2336,7 @@ temperusb==1.5.3 tesla-powerwall==0.3.18 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==1.0.1 +tesla-wall-connector==1.0.2 # homeassistant.components.tensorflow # tf-models-official==2.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 918c0384442..82674d34ab5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1588,7 +1588,7 @@ temescal==0.5 tesla-powerwall==0.3.18 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==1.0.1 +tesla-wall-connector==1.0.2 # homeassistant.components.thermobeacon thermobeacon-ble==0.3.1 From a09305742064d897b3edef356fe7f145485b8b0f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 29 Aug 2022 00:27:51 +0000 Subject: [PATCH 702/903] [ci skip] Translation update --- .../accuweather/translations/es-419.json | 4 +++ .../components/airly/translations/es-419.json | 1 + .../airzone/translations/es-419.json | 12 +++++++ .../android_ip_webcam/translations/ja.json | 2 +- .../android_ip_webcam/translations/nl.json | 7 ++++ .../components/anthemav/translations/ja.json | 2 +- .../automation/translations/ja.json | 12 +++++++ .../components/awair/translations/nl.json | 20 ++++++++++-- .../components/bluetooth/translations/es.json | 2 +- .../components/bluetooth/translations/et.json | 2 +- .../components/bluetooth/translations/id.json | 2 +- .../components/bluetooth/translations/ja.json | 6 ++-- .../bluetooth/translations/zh-Hant.json | 2 +- .../components/bthome/translations/ca.json | 32 +++++++++++++++++++ .../components/bthome/translations/id.json | 32 +++++++++++++++++++ .../components/bthome/translations/ja.json | 32 +++++++++++++++++++ .../components/bthome/translations/nl.json | 19 +++++++++++ .../components/bthome/translations/ru.json | 32 +++++++++++++++++++ .../bthome/translations/zh-Hant.json | 32 +++++++++++++++++++ .../components/escea/translations/nl.json | 8 +++++ .../components/google/translations/ja.json | 2 +- .../justnimbus/translations/nl.json | 10 ++++++ .../lacrosse_view/translations/nl.json | 3 +- .../components/lametric/translations/nl.json | 24 ++++++++++++++ .../landisgyr_heat_meter/translations/nl.json | 23 +++++++++++++ .../nam/translations/sensor.en.json | 11 +++++++ .../openexchangerates/translations/ja.json | 2 +- .../openexchangerates/translations/nl.json | 23 +++++++++++++ .../opentherm_gw/translations/nl.json | 3 +- .../components/pushover/translations/ja.json | 2 +- .../components/pushover/translations/nl.json | 26 +++++++++++++++ .../radiotherm/translations/ja.json | 2 +- .../components/risco/translations/ja.json | 18 +++++++++++ .../components/risco/translations/nl.json | 7 ++++ .../components/schedule/translations/nl.json | 8 +++++ .../simplepush/translations/ja.json | 2 +- .../components/skybell/translations/ja.json | 7 ++++ .../components/skybell/translations/ru.json | 7 ++++ .../soundtouch/translations/ja.json | 2 +- .../speedtestdotnet/translations/id.json | 13 ++++++++ .../speedtestdotnet/translations/ja.json | 12 +++++++ .../thermobeacon/translations/id.json | 22 +++++++++++++ .../thermobeacon/translations/ja.json | 22 +++++++++++++ .../thermobeacon/translations/nl.json | 22 +++++++++++++ .../thermobeacon/translations/ru.json | 22 +++++++++++++ .../components/thermopro/translations/ja.json | 21 ++++++++++++ .../components/thermopro/translations/nl.json | 16 ++++++++++ .../components/thermopro/translations/ru.json | 21 ++++++++++++ .../volvooncall/translations/ja.json | 28 ++++++++++++++++ .../volvooncall/translations/nl.json | 16 ++++++++++ .../components/xbox/translations/ja.json | 2 +- .../xiaomi_ble/translations/ca.json | 4 +-- .../xiaomi_ble/translations/nl.json | 3 +- .../yalexs_ble/translations/nl.json | 9 ++++++ 54 files changed, 654 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/airzone/translations/es-419.json create mode 100644 homeassistant/components/android_ip_webcam/translations/nl.json create mode 100644 homeassistant/components/bthome/translations/ca.json create mode 100644 homeassistant/components/bthome/translations/id.json create mode 100644 homeassistant/components/bthome/translations/ja.json create mode 100644 homeassistant/components/bthome/translations/nl.json create mode 100644 homeassistant/components/bthome/translations/ru.json create mode 100644 homeassistant/components/bthome/translations/zh-Hant.json create mode 100644 homeassistant/components/escea/translations/nl.json create mode 100644 homeassistant/components/justnimbus/translations/nl.json create mode 100644 homeassistant/components/lametric/translations/nl.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/nl.json create mode 100644 homeassistant/components/nam/translations/sensor.en.json create mode 100644 homeassistant/components/openexchangerates/translations/nl.json create mode 100644 homeassistant/components/pushover/translations/nl.json create mode 100644 homeassistant/components/schedule/translations/nl.json create mode 100644 homeassistant/components/thermobeacon/translations/id.json create mode 100644 homeassistant/components/thermobeacon/translations/ja.json create mode 100644 homeassistant/components/thermobeacon/translations/nl.json create mode 100644 homeassistant/components/thermobeacon/translations/ru.json create mode 100644 homeassistant/components/thermopro/translations/ja.json create mode 100644 homeassistant/components/thermopro/translations/nl.json create mode 100644 homeassistant/components/thermopro/translations/ru.json create mode 100644 homeassistant/components/volvooncall/translations/ja.json create mode 100644 homeassistant/components/volvooncall/translations/nl.json create mode 100644 homeassistant/components/yalexs_ble/translations/nl.json diff --git a/homeassistant/components/accuweather/translations/es-419.json b/homeassistant/components/accuweather/translations/es-419.json index 2b2b8dc98ce..35413ba7fe7 100644 --- a/homeassistant/components/accuweather/translations/es-419.json +++ b/homeassistant/components/accuweather/translations/es-419.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, + "create_entry": { + "default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n. \nEl pron\u00f3stico del tiempo no est\u00e1 habilitado de forma predeterminada. Puedes habilitarlo en las opciones de integraci\u00f3n." + }, "error": { "cannot_connect": "No se pudo conectar", "invalid_api_key": "Clave de API no v\u00e1lida", @@ -21,6 +24,7 @@ }, "system_health": { "info": { + "can_reach_server": "Llegar al servidor de AccuWeather", "remaining_requests": "Solicitudes permitidas restantes" } } diff --git a/homeassistant/components/airly/translations/es-419.json b/homeassistant/components/airly/translations/es-419.json index 31149641d11..872f71e989e 100644 --- a/homeassistant/components/airly/translations/es-419.json +++ b/homeassistant/components/airly/translations/es-419.json @@ -20,6 +20,7 @@ }, "system_health": { "info": { + "can_reach_server": "Llegar al servidor de Airly", "requests_per_day": "Solicitudes permitidas por d\u00eda", "requests_remaining": "Solicitudes permitidas restantes" } diff --git a/homeassistant/components/airzone/translations/es-419.json b/homeassistant/components/airzone/translations/es-419.json new file mode 100644 index 00000000000..194005f53ce --- /dev/null +++ b/homeassistant/components/airzone/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "invalid_system_id": "ID del sistema Airzone no v\u00e1lido" + }, + "step": { + "user": { + "description": "Configurar la integraci\u00f3n de Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/ja.json b/homeassistant/components/android_ip_webcam/translations/ja.json index beb3f387d64..6696b8e702a 100644 --- a/homeassistant/components/android_ip_webcam/translations/ja.json +++ b/homeassistant/components/android_ip_webcam/translations/ja.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Android IP Webcam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Android IP Webcam\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Android IP Webcam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Android IP Webcam\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Android IP Webcam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/android_ip_webcam/translations/nl.json b/homeassistant/components/android_ip_webcam/translations/nl.json new file mode 100644 index 00000000000..37162761d86 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Ongeldige authenticatie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ja.json b/homeassistant/components/anthemav/translations/ja.json index 2081a17914d..9a349743cf5 100644 --- a/homeassistant/components/anthemav/translations/ja.json +++ b/homeassistant/components/anthemav/translations/ja.json @@ -18,7 +18,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Anthem A/V Receivers\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Anthem A/V Receivers\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Anthem A/V Receivers\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Anthem A/V Receivers\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Anthem A/V Receivers YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/automation/translations/ja.json b/homeassistant/components/automation/translations/ja.json index ffd515979a2..4392cebdbd0 100644 --- a/homeassistant/components/automation/translations/ja.json +++ b/homeassistant/components/automation/translations/ja.json @@ -1,4 +1,16 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "title": "{name} \u306f\u3001\u4e0d\u660e\u306a\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059" + } + } + }, + "title": "{name} \u306f\u3001\u4e0d\u660e\u306a\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059" + } + }, "state": { "_": { "off": "\u30aa\u30d5", diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index ff270b6084f..1f002a5984b 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -2,14 +2,30 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", + "already_configured_account": "Account is al geconfigureerd", + "already_configured_device": "Apparaat is al geconfigureerd", "no_devices_found": "Geen apparaten gevonden op het netwerk", - "reauth_successful": "Herauthenticatie geslaagd" + "reauth_successful": "Herauthenticatie geslaagd", + "unreachable": "Kan geen verbinding maken" }, "error": { "invalid_access_token": "Ongeldig toegangstoken", - "unknown": "Onverwachte fout" + "unknown": "Onverwachte fout", + "unreachable": "Kan geen verbinding maken" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Toegangstoken", + "email": "E-mail" + } + }, + "local": { + "data": { + "host": "IP-adres" + } + }, "reauth": { "data": { "access_token": "Toegangstoken", diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json index e533454d929..b28fc6cc695 100644 --- a/homeassistant/components/bluetooth/translations/es.json +++ b/homeassistant/components/bluetooth/translations/es.json @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "El adaptador Bluetooth que se usar\u00e1 para escanear", - "passive": "Escucha pasiva" + "passive": "Escaneo pasivo" }, "description": "La escucha pasiva requiere BlueZ 5.63 o posterior con funciones experimentales habilitadas." } diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json index d8c6eb7bdf2..5ada590decf 100644 --- a/homeassistant/components/bluetooth/translations/et.json +++ b/homeassistant/components/bluetooth/translations/et.json @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "Sk\u00e4nnimiseks kasutatav Bluetoothi adapter", - "passive": "Passiivne kuulamine" + "passive": "Passiivne sk\u00e4nnimine" }, "description": "Passiivseks kuulamiseks on vaja BlueZ 5.63 v\u00f5i uuemat versiooni koos lubatud eksperimentaalsete funktsioonidega." } diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index 4caaa9bdd8a..c74420cd281 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "Adaptor Bluetooth yang digunakan untuk pemindaian", - "passive": "Mendengarkan secara pasif" + "passive": "Memindai secara pasif" }, "description": "Mendengarkan secara pasif memerlukan BlueZ 5.63 atau lebih baru dengan fitur eksperimental yang diaktifkan." } diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index e7588f378af..ea90f827e41 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "\u30b9\u30ad\u30e3\u30f3\u306b\u4f7f\u7528\u3059\u308bBluetooth\u30a2\u30c0\u30d7\u30bf\u30fc" - } + "adapter": "\u30b9\u30ad\u30e3\u30f3\u306b\u4f7f\u7528\u3059\u308bBluetooth\u30a2\u30c0\u30d7\u30bf\u30fc", + "passive": "\u30d1\u30c3\u30b7\u30d6\u30b9\u30ad\u30e3\u30f3" + }, + "description": "\u30d1\u30c3\u30b7\u30d6 \u30ea\u30b9\u30cb\u30f3\u30b0\u306b\u306f\u3001\u5b9f\u9a13\u7684\u306a\u6a5f\u80fd\u3092\u6709\u52b9\u306b\u3057\u305f\u3001BlueZ 5.63\u4ee5\u964d\u304c\u5fc5\u8981\u3067\u3059\u3002" } } } diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index ca363ff0e30..08b19a67d34 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u7259\u50b3\u8f38\u5668", - "passive": "\u88ab\u52d5\u76e3\u807d" + "passive": "\u88ab\u52d5\u6383\u63cf" }, "description": "\u88ab\u52d5\u76e3\u807d\u9700\u8981 BlueZ 5.63 \u6216\u66f4\u65b0\u7248\u672c\u3001\u4e26\u958b\u555f\u5be6\u9a57\u529f\u80fd\u3002" } diff --git a/homeassistant/components/bthome/translations/ca.json b/homeassistant/components/bthome/translations/ca.json new file mode 100644 index 00000000000..e9d3a0437be --- /dev/null +++ b/homeassistant/components/bthome/translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "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", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "decryption_failed": "La clau d'enlla\u00e7 proporcionada no ha funcionat, les dades del sensor no s'han pogut desxifrar. Comprova-la i torna-ho a provar.", + "expected_32_characters": "S'espera una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Clau d'enlla\u00e7 (bindkey)" + }, + "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals." + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/id.json b/homeassistant/components/bthome/translations/id.json new file mode 100644 index 00000000000..6c405364cd8 --- /dev/null +++ b/homeassistant/components/bthome/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "decryption_failed": "Bindkey yang disediakan tidak berfungsi, data sensor tidak dapat didekripsi. Silakan periksa dan coba lagi.", + "expected_32_characters": "Diharapkan bindkey berupa 32 karakter heksadesimal." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Data sensor yang disiarkan oleh sensor telah dienkripsi. Untuk mendekripsinya, diperlukan 32 karakter bindkey heksadesimal." + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/ja.json b/homeassistant/components/bthome/translations/ja.json new file mode 100644 index 00000000000..74c7e0403ea --- /dev/null +++ b/homeassistant/components/bthome/translations/ja.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "decryption_failed": "\u63d0\u4f9b\u3055\u308c\u305f\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u6a5f\u80fd\u305b\u305a\u3001\u30bb\u30f3\u30b5\u30fc \u30c7\u30fc\u30bf\u3092\u5fa9\u53f7\u5316\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "expected_32_characters": "32\u6587\u5b57\u304b\u3089\u306a\u308b16\u9032\u6570\u306e\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "get_encryption_key": { + "data": { + "bindkey": "\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u304b\u3089\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3055\u308c\u308b\u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\u306f\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5fa9\u53f7\u5316\u3059\u308b\u306b\u306f\u300116\u9032\u6570\u306732\u6587\u5b57\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/nl.json b/homeassistant/components/bthome/translations/nl.json new file mode 100644 index 00000000000..9a4e727ef2e --- /dev/null +++ b/homeassistant/components/bthome/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/ru.json b/homeassistant/components/bthome/translations/ru.json new file mode 100644 index 00000000000..47cb3417bfa --- /dev/null +++ b/homeassistant/components/bthome/translations/ru.json @@ -0,0 +1,32 @@ +{ + "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.", + "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.", + "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": { + "decryption_failed": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b, \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "expected_32_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438" + }, + "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/zh-Hant.json b/homeassistant/components/bthome/translations/zh-Hant.json new file mode 100644 index 00000000000..9e723abf1af --- /dev/null +++ b/homeassistant/components/bthome/translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\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", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "decryption_failed": "\u6240\u63d0\u4f9b\u7684\u7d81\u5b9a\u78bc\u7121\u6cd5\u4f7f\u7528\u3001\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6cd5\u89e3\u5bc6\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "expected_32_characters": "\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "get_encryption_key": { + "data": { + "bindkey": "\u7d81\u5b9a\u78bc" + }, + "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/nl.json b/homeassistant/components/escea/translations/nl.json new file mode 100644 index 00000000000..4d0523cf740 --- /dev/null +++ b/homeassistant/components/escea/translations/nl.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index eaa61a0fd91..057734a2bca 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -35,7 +35,7 @@ }, "issues": { "deprecated_yaml": { - "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u306a\u304a\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fcyaml\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_track_new_yaml": { diff --git a/homeassistant/components/justnimbus/translations/nl.json b/homeassistant/components/justnimbus/translations/nl.json new file mode 100644 index 00000000000..70d636c953e --- /dev/null +++ b/homeassistant/components/justnimbus/translations/nl.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/nl.json b/homeassistant/components/lacrosse_view/translations/nl.json index 44c1bc93f79..35081a52c6f 100644 --- a/homeassistant/components/lacrosse_view/translations/nl.json +++ b/homeassistant/components/lacrosse_view/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", diff --git a/homeassistant/components/lametric/translations/nl.json b/homeassistant/components/lametric/translations/nl.json new file mode 100644 index 00000000000..6cbebff75e3 --- /dev/null +++ b/homeassistant/components/lametric/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "manual_entry": { + "data": { + "api_key": "API-sleutel", + "host": "Host" + } + }, + "pick_implementation": { + "title": "Kies een authenticatie methode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/nl.json b/homeassistant/components/landisgyr_heat_meter/translations/nl.json new file mode 100644 index 00000000000..67eea59125f --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB-apparaatpad" + } + }, + "user": { + "data": { + "device": "Selecteer apparaat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.en.json b/homeassistant/components/nam/translations/sensor.en.json new file mode 100644 index 00000000000..196e086a836 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.en.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "High", + "low": "Low", + "medium": "Medium", + "very high": "Very high", + "very low": "Very low" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ja.json b/homeassistant/components/openexchangerates/translations/ja.json index 0946ca7a4d9..9c7212a44b5 100644 --- a/homeassistant/components/openexchangerates/translations/ja.json +++ b/homeassistant/components/openexchangerates/translations/ja.json @@ -26,7 +26,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Open Exchange Rates\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Open Exchange Rates\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Open Exchange Rates\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Open Exchange Rates\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Open Exchange Rates YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/openexchangerates/translations/nl.json b/homeassistant/components/openexchangerates/translations/nl.json new file mode 100644 index 00000000000..885a94f4c43 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "reauth_successful": "Herauthenticatie geslaagd", + "timeout_connect": "Time-out bij het maken van verbinding" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "timeout_connect": "Time-out bij het maken van verbinding", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json index 97f392075d8..e29c52c9c1f 100644 --- a/homeassistant/components/opentherm_gw/translations/nl.json +++ b/homeassistant/components/opentherm_gw/translations/nl.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", - "id_exists": "Gateway id bestaat al" + "id_exists": "Gateway id bestaat al", + "timeout_connect": "Time-out bij het maken van verbinding" }, "step": { "init": { diff --git a/homeassistant/components/pushover/translations/ja.json b/homeassistant/components/pushover/translations/ja.json index ab1170be1b0..9a229d5d08a 100644 --- a/homeassistant/components/pushover/translations/ja.json +++ b/homeassistant/components/pushover/translations/ja.json @@ -27,7 +27,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Pushover\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Pushover\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Pushover\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Pushover\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Pushover YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/pushover/translations/nl.json b/homeassistant/components/pushover/translations/nl.json new file mode 100644 index 00000000000..fb6ebf9862d --- /dev/null +++ b/homeassistant/components/pushover/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + }, + "title": "Integratie herauthenticeren" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "name": "Naam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ja.json b/homeassistant/components/radiotherm/translations/ja.json index 64d3cca9112..4d24a10e10b 100644 --- a/homeassistant/components/radiotherm/translations/ja.json +++ b/homeassistant/components/radiotherm/translations/ja.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "YAML\u3092\u4f7f\u7528\u3057\u305f\u3001Radio Thermostat climate(\u6c17\u5019)\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "YAML\u3092\u4f7f\u7528\u3057\u305f\u3001Radio Thermostat climate(\u6c17\u5019)\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u306a\u304a\u3001\u65e2\u5b58\u306e\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Radio Thermostat YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/risco/translations/ja.json b/homeassistant/components/risco/translations/ja.json index 1e574a07a32..a2919b167e5 100644 --- a/homeassistant/components/risco/translations/ja.json +++ b/homeassistant/components/risco/translations/ja.json @@ -9,11 +9,29 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "cloud": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "pin": "PIN\u30b3\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, + "local": { + "data": { + "host": "\u30db\u30b9\u30c8", + "pin": "PIN\u30b3\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + } + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "pin": "PIN\u30b3\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "menu_options": { + "cloud": "Risco Cloud(\u63a8\u5968)", + "local": "Local Risco Panel(\u30a2\u30c9\u30d0\u30f3\u30b9\u30c9)" } } } diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index acab8c172c3..9b7bb7a43c8 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -9,6 +9,13 @@ "unknown": "Onverwachte fout" }, "step": { + "cloud": { + "data": { + "password": "Wachtwoord", + "pin": "Pincode", + "username": "Gebruikersnaam" + } + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/schedule/translations/nl.json b/homeassistant/components/schedule/translations/nl.json new file mode 100644 index 00000000000..ea585424fdb --- /dev/null +++ b/homeassistant/components/schedule/translations/nl.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "off": "Uit", + "on": "Aan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index 11e28073318..5c4da266036 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_yaml": { diff --git a/homeassistant/components/skybell/translations/ja.json b/homeassistant/components/skybell/translations/ja.json index 0cac70de54a..91ae512fa30 100644 --- a/homeassistant/components/skybell/translations/ja.json +++ b/homeassistant/components/skybell/translations/ja.json @@ -10,6 +10,13 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{email} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "email": "E\u30e1\u30fc\u30eb", diff --git a/homeassistant/components/skybell/translations/ru.json b/homeassistant/components/skybell/translations/ru.json index 50fe51447c8..30fdd97cc5b 100644 --- a/homeassistant/components/skybell/translations/ru.json +++ b/homeassistant/components/skybell/translations/ru.json @@ -10,6 +10,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {email}", + "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", diff --git a/homeassistant/components/soundtouch/translations/ja.json b/homeassistant/components/soundtouch/translations/ja.json index a6417d9988a..1d2945acb36 100644 --- a/homeassistant/components/soundtouch/translations/ja.json +++ b/homeassistant/components/soundtouch/translations/ja.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Bose SoundTouch\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Bose SoundTouch\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Bose SoundTouch\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Bose SoundTouch\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Bose SoundTouch YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/speedtestdotnet/translations/id.json b/homeassistant/components/speedtestdotnet/translations/id.json index f609c3d384a..8a5e3711d93 100644 --- a/homeassistant/components/speedtestdotnet/translations/id.json +++ b/homeassistant/components/speedtestdotnet/translations/id.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `homeassistant.update_entity` dengan target ID entitas Speedtest. Kemudian, klik KIRIM di bawah ini untuk menandai masalah ini sebagai terselesaikan.", + "title": "Layanan speedtest dalam proses penghapusan" + } + } + }, + "title": "Layanan speedtest dalam proses penghapusan" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index 40f592b2c46..a2f196a5359 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -9,6 +9,18 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "speedtest\u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } + }, + "title": "speedtest\u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/thermobeacon/translations/id.json b/homeassistant/components/thermobeacon/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_supported": "Perangkat tidak didukung" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/ja.json b/homeassistant/components/thermobeacon/translations/ja.json new file mode 100644 index 00000000000..fe1c5746cda --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/ja.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/nl.json b/homeassistant/components/thermobeacon/translations/nl.json new file mode 100644 index 00000000000..d1ed6396d2e --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Het apparaat is niet ondersteund" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/ru.json b/homeassistant/components/thermobeacon/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/thermobeacon/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.", + "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": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/ja.json b/homeassistant/components/thermopro/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/thermopro/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/nl.json b/homeassistant/components/thermopro/translations/nl.json new file mode 100644 index 00000000000..b4f5f4f85e5 --- /dev/null +++ b/homeassistant/components/thermopro/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/ru.json b/homeassistant/components/thermopro/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/thermopro/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.", + "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." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/ja.json b/homeassistant/components/volvooncall/translations/ja.json new file mode 100644 index 00000000000..3a529158503 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/ja.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "mutable": "\u30ea\u30e2\u30fc\u30c8\u30b9\u30bf\u30fc\u30c8/\u30ed\u30c3\u30af\u306a\u3069\u3092\u8a31\u53ef\u3057\u307e\u3059\u3002", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3", + "scandinavian_miles": "\u30b9\u30ab\u30f3\u30b8\u30ca\u30d3\u30a2\u30de\u30a4\u30eb(Scandinavian Miles)\u3092\u4f7f\u7528\u3059\u308b", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML\u3092\u4f7f\u7528\u3057\u305f\u3001Volvo On Call YAML\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant\u306e\u5c06\u6765\u306e\u30ea\u30ea\u30fc\u30b9\u3067\u524a\u9664\u3055\u308c\u307e\u3059\u3002 \n\n\u306a\u304a\u3001\u65e2\u5b58\u306e\u8a2d\u5b9a\u306fUI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "title": "Volvo On Call YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/nl.json b/homeassistant/components/volvooncall/translations/nl.json new file mode 100644 index 00000000000..f0b4ddf59a9 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "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/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index 2534412e55b..3d18fa89970 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -16,7 +16,7 @@ }, "issues": { "deprecated_yaml": { - "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Xbox\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Xbox\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u306a\u304a\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Xbox YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index 1411fc6b35e..d36daedf3a7 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -24,13 +24,13 @@ }, "get_encryption_key_4_5": { "data": { - "bindkey": "Bindkey" + "bindkey": "Clau d'enlla\u00e7 (bindkey)" }, "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals." }, "get_encryption_key_legacy": { "data": { - "bindkey": "Bindkey" + "bindkey": "Clau d'enlla\u00e7 (bindkey)" }, "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals." }, diff --git a/homeassistant/components/xiaomi_ble/translations/nl.json b/homeassistant/components/xiaomi_ble/translations/nl.json index a46f954fe5f..6b79e0311de 100644 --- a/homeassistant/components/xiaomi_ble/translations/nl.json +++ b/homeassistant/components/xiaomi_ble/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", - "no_devices_found": "Geen apparaten gevonden op het netwerk" + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "reauth_successful": "Herauthenticatie geslaagd" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/yalexs_ble/translations/nl.json b/homeassistant/components/yalexs_ble/translations/nl.json new file mode 100644 index 00000000000..04ee25942f6 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/nl.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + } + } +} \ No newline at end of file From 2857739958dde9615e7759c7952ebeaadb1f520e Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 29 Aug 2022 10:44:08 +1000 Subject: [PATCH 703/903] Add light platform to Advantage Air (#75425) --- .../components/advantage_air/__init__.py | 21 ++-- .../components/advantage_air/climate.py | 2 +- .../components/advantage_air/light.py | 90 +++++++++++++++ .../components/advantage_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/advantage_air/__init__.py | 3 + .../advantage_air/fixtures/getSystemData.json | 19 +++- tests/components/advantage_air/test_light.py | 105 ++++++++++++++++++ 9 files changed, 234 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/advantage_air/light.py create mode 100644 tests/components/advantage_air/test_light.py diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index d50224698b8..b5e6e0be024 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -20,6 +20,7 @@ PLATFORMS = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.LIGHT, ] _LOGGER = logging.getLogger(__name__) @@ -50,19 +51,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL), ) - async def async_change(change): - try: - if await api.async_change(change): - await coordinator.async_refresh() - except ApiError as err: - _LOGGER.warning(err) + def error_handle_factory(func): + """Return the provided API function wrapped in an error handler and coordinator refresh.""" + + async def error_handle(param): + try: + if await func(param): + await coordinator.async_refresh() + except ApiError as err: + _LOGGER.warning(err) + + return error_handle await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, - "async_change": async_change, + "async_change": error_handle_factory(api.aircon.async_set), + "async_set_light": error_handle_factory(api.lights.async_set), } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index d889bf35642..c11b01f3ace 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -45,7 +45,7 @@ AC_HVAC_MODES = [ ] ADVANTAGE_AIR_FAN_MODES = { - "auto": FAN_AUTO, + "autoAA": FAN_AUTO, "low": FAN_LOW, "medium": FAN_MEDIUM, "high": FAN_HIGH, diff --git a/homeassistant/components/advantage_air/light.py b/homeassistant/components/advantage_air/light.py new file mode 100644 index 00000000000..b1c8495edf8 --- /dev/null +++ b/homeassistant/components/advantage_air/light.py @@ -0,0 +1,90 @@ +"""Light platform for Advantage Air integration.""" +from typing import Any + +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + ADVANTAGE_AIR_STATE_OFF, + ADVANTAGE_AIR_STATE_ON, + DOMAIN as ADVANTAGE_AIR_DOMAIN, +) +from .entity import AdvantageAirEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AdvantageAir light platform.""" + + instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] + + entities = [] + if "myLights" in instance["coordinator"].data: + for light in instance["coordinator"].data["myLights"]["lights"].values(): + if light.get("relay"): + entities.append(AdvantageAirLight(instance, light)) + else: + entities.append(AdvantageAirLightDimmable(instance, light)) + async_add_entities(entities) + + +class AdvantageAirLight(AdvantageAirEntity, LightEntity): + """Representation of Advantage Air Light.""" + + _attr_supported_color_modes = {ColorMode.ONOFF} + + def __init__(self, instance, light): + """Initialize an Advantage Air Light.""" + super().__init__(instance) + self.async_set_light = instance["async_set_light"] + self._id = light["id"] + self._attr_unique_id += f"-{self._id}" + self._attr_device_info = DeviceInfo( + identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)}, + via_device=(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]), + manufacturer="Advantage Air", + model=light.get("moduleType"), + name=light["name"], + ) + + @property + def _light(self): + """Return the light object.""" + return self.coordinator.data["myLights"]["lights"][self._id] + + @property + def is_on(self) -> bool: + """Return if the light is on.""" + return self._light["state"] == ADVANTAGE_AIR_STATE_ON + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + await self.async_set_light({"id": self._id, "state": ADVANTAGE_AIR_STATE_ON}) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self.async_set_light({"id": self._id, "state": ADVANTAGE_AIR_STATE_OFF}) + + +class AdvantageAirLightDimmable(AdvantageAirLight): + """Representation of Advantage Air Dimmable Light.""" + + _attr_supported_color_modes = {ColorMode.ONOFF, ColorMode.BRIGHTNESS} + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + return round(self._light["value"] * 255 / 100) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on and optionally set the brightness.""" + data = {"id": self._id, "state": ADVANTAGE_AIR_STATE_ON} + if ATTR_BRIGHTNESS in kwargs: + data["value"] = round(kwargs[ATTR_BRIGHTNESS] * 100 / 255) + await self.async_set_light(data) diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json index f95a32e186b..51b6158954e 100644 --- a/homeassistant/components/advantage_air/manifest.json +++ b/homeassistant/components/advantage_air/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/advantage_air", "codeowners": ["@Bre77"], - "requirements": ["advantage_air==0.3.1"], + "requirements": ["advantage_air==0.4.1"], "quality_scale": "platinum", "iot_class": "local_polling", "loggers": ["advantage_air"] diff --git a/requirements_all.txt b/requirements_all.txt index b56d75d7b66..d3246a5aa83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -86,7 +86,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.3.1 +advantage_air==0.4.1 # homeassistant.components.frontier_silicon afsapi==0.2.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82674d34ab5..3fa0fe132f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -76,7 +76,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.3.1 +advantage_air==0.4.1 # homeassistant.components.agent_dvr agent-py==0.0.23 diff --git a/tests/components/advantage_air/__init__.py b/tests/components/advantage_air/__init__.py index 92faaff6359..e415485821f 100644 --- a/tests/components/advantage_air/__init__.py +++ b/tests/components/advantage_air/__init__.py @@ -17,6 +17,9 @@ TEST_SYSTEM_URL = ( f"http://{USER_INPUT[CONF_IP_ADDRESS]}:{USER_INPUT[CONF_PORT]}/getSystemData" ) TEST_SET_URL = f"http://{USER_INPUT[CONF_IP_ADDRESS]}:{USER_INPUT[CONF_PORT]}/setAircon" +TEST_SET_LIGHT_URL = ( + f"http://{USER_INPUT[CONF_IP_ADDRESS]}:{USER_INPUT[CONF_PORT]}/setLight" +) async def add_mock_config(hass): diff --git a/tests/components/advantage_air/fixtures/getSystemData.json b/tests/components/advantage_air/fixtures/getSystemData.json index 35a06c2d468..00ce2b1f095 100644 --- a/tests/components/advantage_air/fixtures/getSystemData.json +++ b/tests/components/advantage_air/fixtures/getSystemData.json @@ -143,9 +143,26 @@ } } }, + "myLights": { + "lights": { + "100": { + "id": "100", + "moduleType": "RM2", + "name": "Light A", + "relay": true, + "state": "off" + }, + "101": { + "id": "101", + "name": "Light B", + "value": 50, + "state": "on" + } + } + }, "system": { "hasAircons": true, - "hasLights": false, + "hasLights": true, "hasSensors": false, "hasThings": false, "hasThingsBOG": false, diff --git a/tests/components/advantage_air/test_light.py b/tests/components/advantage_air/test_light.py new file mode 100644 index 00000000000..85223700dbf --- /dev/null +++ b/tests/components/advantage_air/test_light.py @@ -0,0 +1,105 @@ +"""Test the Advantage Air Switch Platform.""" +from json import loads + +from homeassistant.components.advantage_air.const import ( + ADVANTAGE_AIR_STATE_OFF, + ADVANTAGE_AIR_STATE_ON, +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF +from homeassistant.helpers import entity_registry as er + +from tests.components.advantage_air import ( + TEST_SET_LIGHT_URL, + TEST_SET_RESPONSE, + TEST_SYSTEM_DATA, + TEST_SYSTEM_URL, + add_mock_config, +) + + +async def test_light_async_setup_entry(hass, aioclient_mock): + """Test light setup.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + text=TEST_SYSTEM_DATA, + ) + aioclient_mock.get( + TEST_SET_LIGHT_URL, + text=TEST_SET_RESPONSE, + ) + + await add_mock_config(hass) + + registry = er.async_get(hass) + + assert len(aioclient_mock.mock_calls) == 1 + + # Test Light Entity + entity_id = "light.light_a" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-100" + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 3 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setLight" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["id"] == "100" + assert data["state"] == ADVANTAGE_AIR_STATE_ON + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 5 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setLight" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["id"] == "100" + assert data["state"] == ADVANTAGE_AIR_STATE_OFF + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + # Test Dimmable Light Entity + entity_id = "light.light_b" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-101" + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id], ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 7 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setLight" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["id"] == "101" + assert data["value"] == 50 + assert data["state"] == ADVANTAGE_AIR_STATE_ON + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" From 0867392f967983f47284f1f35501a857cfcf0a52 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 28 Aug 2022 21:35:45 -0400 Subject: [PATCH 704/903] Add ability to ignore devices for UniFi Protect (#77414) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/__init__.py | 30 +++++-- .../components/unifiprotect/config_flow.py | 78 ++++++++++++------- .../components/unifiprotect/const.py | 1 + homeassistant/components/unifiprotect/data.py | 65 +++++++++++++++- .../components/unifiprotect/services.py | 6 +- .../components/unifiprotect/strings.json | 6 +- .../unifiprotect/translations/en.json | 4 + .../components/unifiprotect/utils.py | 46 +++++++---- tests/components/unifiprotect/conftest.py | 1 + .../unifiprotect/test_config_flow.py | 45 ++++++++++- tests/components/unifiprotect/test_init.py | 41 ++++++---- tests/components/unifiprotect/utils.py | 12 ++- 12 files changed, 259 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 30b1d1ad56d..60829223e2f 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -26,6 +26,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( CONF_ALL_UPDATES, + CONF_IGNORED, CONF_OVERRIDE_CHOST, DEFAULT_SCAN_INTERVAL, DEVICES_FOR_SUBSCRIBE, @@ -35,11 +36,11 @@ from .const import ( OUTDATED_LOG_MESSAGE, PLATFORMS, ) -from .data import ProtectData, async_ufp_instance_for_config_entry_ids +from .data import ProtectData from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services -from .utils import _async_unifi_mac_from_hass, async_get_devices +from .utils import async_unifi_mac, convert_mac_list from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) @@ -106,6 +107,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" + + data: ProtectData = hass.data[DOMAIN][entry.entry_id] + changed = data.async_get_changed_options(entry) + + if len(changed) == 1 and CONF_IGNORED in changed: + new_macs = convert_mac_list(entry.options.get(CONF_IGNORED, "")) + added_macs = new_macs - data.ignored_macs + removed_macs = data.ignored_macs - new_macs + # if only ignored macs are added, we can handle without reloading + if not removed_macs and added_macs: + data.async_add_new_ignored_macs(added_macs) + return + await hass.config_entries.async_reload(entry.entry_id) @@ -125,15 +139,15 @@ async def async_remove_config_entry_device( ) -> bool: """Remove ufp config entry from a device.""" unifi_macs = { - _async_unifi_mac_from_hass(connection[1]) + async_unifi_mac(connection[1]) for connection in device_entry.connections if connection[0] == dr.CONNECTION_NETWORK_MAC } - api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id}) - assert api is not None - if api.bootstrap.nvr.mac in unifi_macs: + data: ProtectData = hass.data[DOMAIN][config_entry.entry_id] + if data.api.bootstrap.nvr.mac in unifi_macs: return False - for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT): + for device in data.get_by_types(DEVICES_THAT_ADOPT): if device.is_adopted_by_us and device.mac in unifi_macs: - return False + data.async_ignore_mac(device.mac) + break return True diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index f07ca923a53..1907a201c8d 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -35,6 +35,7 @@ from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, + CONF_IGNORED, CONF_MAX_MEDIA, CONF_OVERRIDE_CHOST, DEFAULT_MAX_MEDIA, @@ -46,7 +47,7 @@ from .const import ( ) from .data import async_last_update_was_successful from .discovery import async_start_discovery -from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass +from .utils import _async_resolve, async_short_mac, async_unifi_mac, convert_mac_list _LOGGER = logging.getLogger(__name__) @@ -120,7 +121,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle integration discovery.""" self._discovered_device = discovery_info - mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"]) + mac = async_unifi_mac(discovery_info["hw_addr"]) await self.async_set_unique_id(mac) source_ip = discovery_info["source_ip"] direct_connect_domain = discovery_info["direct_connect_domain"] @@ -182,7 +183,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders = { "name": discovery_info["hostname"] or discovery_info["platform"] - or f"NVR {_async_short_mac(discovery_info['hw_addr'])}", + or f"NVR {async_short_mac(discovery_info['hw_addr'])}", "ip_address": discovery_info["source_ip"], } self.context["title_placeholders"] = placeholders @@ -224,6 +225,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_ALL_UPDATES: False, CONF_OVERRIDE_CHOST: False, CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA, + CONF_IGNORED: "", }, ) @@ -365,33 +367,53 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" + + values = user_input or self.config_entry.options + schema = vol.Schema( + { + vol.Optional( + CONF_DISABLE_RTSP, + description={ + "suggested_value": values.get(CONF_DISABLE_RTSP, False) + }, + ): bool, + vol.Optional( + CONF_ALL_UPDATES, + description={ + "suggested_value": values.get(CONF_ALL_UPDATES, False) + }, + ): bool, + vol.Optional( + CONF_OVERRIDE_CHOST, + description={ + "suggested_value": values.get(CONF_OVERRIDE_CHOST, False) + }, + ): bool, + vol.Optional( + CONF_MAX_MEDIA, + description={ + "suggested_value": values.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) + }, + ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), + vol.Optional( + CONF_IGNORED, + description={"suggested_value": values.get(CONF_IGNORED, "")}, + ): str, + } + ) + errors: dict[str, str] = {} + if user_input is not None: - return self.async_create_entry(title="", data=user_input) + try: + convert_mac_list(user_input.get(CONF_IGNORED, ""), raise_exception=True) + except vol.Invalid: + errors[CONF_IGNORED] = "invalid_mac_list" + + if not errors: + return self.async_create_entry(title="", data=user_input) return self.async_show_form( step_id="init", - data_schema=vol.Schema( - { - vol.Optional( - CONF_DISABLE_RTSP, - default=self.config_entry.options.get(CONF_DISABLE_RTSP, False), - ): bool, - vol.Optional( - CONF_ALL_UPDATES, - default=self.config_entry.options.get(CONF_ALL_UPDATES, False), - ): bool, - vol.Optional( - CONF_OVERRIDE_CHOST, - default=self.config_entry.options.get( - CONF_OVERRIDE_CHOST, False - ), - ): bool, - vol.Optional( - CONF_MAX_MEDIA, - default=self.config_entry.options.get( - CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA - ), - ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), - } - ), + data_schema=schema, + errors=errors, ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 93a0fa5ff74..080dc41f358 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -20,6 +20,7 @@ CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" +CONF_IGNORED = "ignored_devices" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 20b5747a342..c17b99d639f 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -28,6 +28,7 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_DISABLE_RTSP, + CONF_IGNORED, CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA, DEVICES_THAT_ADOPT, @@ -36,7 +37,11 @@ from .const import ( DISPATCH_CHANNELS, DOMAIN, ) -from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type +from .utils import ( + async_dispatch_id as _ufpd, + async_get_devices_by_type, + convert_mac_list, +) _LOGGER = logging.getLogger(__name__) ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] @@ -67,6 +72,7 @@ class ProtectData: self._hass = hass self._entry = entry + self._existing_options = dict(entry.options) self._hass = hass self._update_interval = update_interval self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {} @@ -74,6 +80,8 @@ class ProtectData: self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None self._auth_failures = 0 + self._ignored_macs: set[str] | None = None + self._ignore_update_cancel: Callable[[], None] | None = None self.last_update_success = False self.api = protect @@ -88,6 +96,47 @@ class ProtectData: """Max number of events to load at once.""" return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) + @property + def ignored_macs(self) -> set[str]: + """Set of ignored MAC addresses.""" + + if self._ignored_macs is None: + self._ignored_macs = convert_mac_list( + self._entry.options.get(CONF_IGNORED, "") + ) + + return self._ignored_macs + + @callback + def async_get_changed_options(self, entry: ConfigEntry) -> dict[str, Any]: + """Get changed options for when entry is updated.""" + + return dict( + set(self._entry.options.items()) - set(self._existing_options.items()) + ) + + @callback + def async_ignore_mac(self, mac: str) -> None: + """Ignores a MAC address for a UniFi Protect device.""" + + new_macs = (self._ignored_macs or set()).copy() + new_macs.add(mac) + _LOGGER.debug("Updating ignored_devices option: %s", self.ignored_macs) + options = dict(self._entry.options) + options[CONF_IGNORED] = ",".join(new_macs) + self._hass.config_entries.async_update_entry(self._entry, options=options) + + @callback + def async_add_new_ignored_macs(self, new_macs: set[str]) -> None: + """Add new ignored MAC addresses and ensures the devices are removed.""" + + for mac in new_macs: + device = self.api.bootstrap.get_device_from_mac(mac) + if device is not None: + self._async_remove_device(device) + self._ignored_macs = None + self._existing_options = dict(self._entry.options) + def get_by_types( self, device_types: Iterable[ModelType], ignore_unadopted: bool = True ) -> Generator[ProtectAdoptableDeviceModel, None, None]: @@ -99,6 +148,8 @@ class ProtectData: for device in devices: if ignore_unadopted and not device.is_adopted_by_us: continue + if device.mac in self.ignored_macs: + continue yield device async def async_setup(self) -> None: @@ -108,6 +159,11 @@ class ProtectData: ) await self.async_refresh() + for mac in self.ignored_macs: + device = self.api.bootstrap.get_device_from_mac(mac) + if device is not None: + self._async_remove_device(device) + async def async_stop(self, *args: Any) -> None: """Stop processing data.""" if self._unsub_websocket: @@ -172,6 +228,7 @@ class ProtectData: @callback def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None: + registry = dr.async_get(self._hass) device_entry = registry.async_get_device( identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} @@ -296,13 +353,13 @@ class ProtectData: @callback -def async_ufp_instance_for_config_entry_ids( +def async_ufp_data_for_config_entry_ids( hass: HomeAssistant, config_entry_ids: set[str] -) -> ProtectApiClient | None: +) -> ProtectData | None: """Find the UFP instance for the config entry ids.""" domain_data = hass.data[DOMAIN] for config_entry_id in config_entry_ids: if config_entry_id in domain_data: protect_data: ProtectData = domain_data[config_entry_id] - return protect_data.api + return protect_data return None diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 915c51b6c0a..914803e9c45 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -25,7 +25,7 @@ from homeassistant.helpers.service import async_extract_referenced_entity_ids from homeassistant.util.read_only_dict import ReadOnlyDict from .const import ATTR_MESSAGE, DOMAIN -from .data import async_ufp_instance_for_config_entry_ids +from .data import async_ufp_data_for_config_entry_ids SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" @@ -70,8 +70,8 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl return _async_get_ufp_instance(hass, device_entry.via_device_id) config_entry_ids = device_entry.config_entries - if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids): - return ufp_instance + if ufp_data := async_ufp_data_for_config_entry_ids(hass, config_entry_ids): + return ufp_data.api raise HomeAssistantError(f"No device found for device id: {device_id}") diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d3cfe24abd2..d9750d31ae1 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -50,9 +50,13 @@ "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "override_connection_host": "Override Connection Host", - "max_media": "Max number of event to load for Media Browser (increases RAM usage)" + "max_media": "Max number of event to load for Media Browser (increases RAM usage)", + "ignored_devices": "Comma separated list of MAC addresses of devices to ignore" } } + }, + "error": { + "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index 5d690e3fd3e..c6050d05284 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" + }, "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "disable_rtsp": "Disable the RTSP stream", + "ignored_devices": "Comma separated list of MAC addresses of devices to ignore", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 808117aac9e..8c368da1c40 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -1,9 +1,9 @@ """UniFi Protect Integration utils.""" from __future__ import annotations -from collections.abc import Generator, Iterable import contextlib from enum import Enum +import re import socket from typing import Any @@ -14,12 +14,16 @@ from pyunifiprotect.data import ( LightModeType, ProtectAdoptableDeviceModel, ) +import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv from .const import DOMAIN, ModelType +MAC_RE = re.compile(r"[0-9A-F]{12}") + def get_nested_attr(obj: Any, attr: str) -> Any: """Fetch a nested attribute.""" @@ -38,15 +42,16 @@ def get_nested_attr(obj: Any, attr: str) -> Any: @callback -def _async_unifi_mac_from_hass(mac: str) -> str: +def async_unifi_mac(mac: str) -> str: + """Convert MAC address to format from UniFi Protect.""" # MAC addresses in UFP are always caps - return mac.replace(":", "").upper() + return mac.replace(":", "").replace("-", "").replace("_", "").upper() @callback -def _async_short_mac(mac: str) -> str: +def async_short_mac(mac: str) -> str: """Get the short mac address from the full mac.""" - return _async_unifi_mac_from_hass(mac)[-6:] + return async_unifi_mac(mac)[-6:] async def _async_resolve(hass: HomeAssistant, host: str) -> str | None: @@ -77,18 +82,6 @@ def async_get_devices_by_type( return devices -@callback -def async_get_devices( - bootstrap: Bootstrap, model_type: Iterable[ModelType] -) -> Generator[ProtectAdoptableDeviceModel, None, None]: - """Return all device by type.""" - return ( - device - for device_type in model_type - for device in async_get_devices_by_type(bootstrap, device_type).values() - ) - - @callback def async_get_light_motion_current(obj: Light) -> str: """Get light motion mode for Flood Light.""" @@ -106,3 +99,22 @@ def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str: """Generate entry specific dispatch ID.""" return f"{DOMAIN}.{entry.entry_id}.{dispatch}" + + +@callback +def convert_mac_list(option: str, raise_exception: bool = False) -> set[str]: + """Convert csv list of MAC addresses.""" + + macs = set() + values = cv.ensure_list_csv(option) + for value in values: + if value == "": + continue + value = async_unifi_mac(value) + if not MAC_RE.match(value): + if raise_exception: + raise vol.Invalid("invalid_mac_list") + continue + macs.add(value) + + return macs diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index b006dfbd004..fa245e8b1cc 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -68,6 +68,7 @@ def mock_ufp_config_entry(): "port": 443, "verify_ssl": False, }, + options={"ignored_devices": "FFFFFFFFFFFF,test"}, version=2, ) diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index d0fb0dba9f2..26a9dd73ee8 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components import dhcp, ssdp from homeassistant.components.unifiprotect.const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, + CONF_IGNORED, CONF_OVERRIDE_CHOST, DOMAIN, ) @@ -269,10 +270,52 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "all_updates": True, "disable_rtsp": True, "override_connection_host": True, - "max_media": 1000, } +async def test_form_options_invalid_mac( + hass: HomeAssistant, ufp_client: ProtectApiClient +) -> None: + """Test we handle options flows.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": False, + "max_media": 1000, + }, + version=2, + unique_id=dr.format_mac(MAC_ADDR), + ) + mock_config.add_to_hass(hass) + + with _patch_discovery(), patch( + "homeassistant.components.unifiprotect.ProtectApiClient" + ) as mock_api: + mock_api.return_value = ufp_client + + await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + assert mock_config.state == config_entries.ConfigEntryState.LOADED + + result = await hass.config_entries.options.async_init(mock_config.entry_id) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + {CONF_IGNORED: "test,test2"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_IGNORED: "invalid_mac_list"} + + @pytest.mark.parametrize( "source, data", [ diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 9392caa30ac..7a1e590b87d 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -7,20 +7,21 @@ from unittest.mock import AsyncMock, patch import aiohttp from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient -from pyunifiprotect.data import NVR, Bootstrap, Light +from pyunifiprotect.data import NVR, Bootstrap, Doorlock, Light, Sensor from homeassistant.components.unifiprotect.const import ( CONF_DISABLE_RTSP, + CONF_IGNORED, DEFAULT_SCAN_INTERVAL, DOMAIN, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import _patch_discovery -from .utils import MockUFPFixture, init_entry, time_changed +from .utils import MockUFPFixture, get_device_from_ufp_device, init_entry, time_changed from tests.common import MockConfigEntry @@ -211,28 +212,38 @@ async def test_device_remove_devices( hass: HomeAssistant, ufp: MockUFPFixture, light: Light, + doorlock: Doorlock, + sensor: Sensor, hass_ws_client: Callable[ [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] ], ) -> None: """Test we can only remove a device that no longer exists.""" - await init_entry(hass, ufp, [light]) - assert await async_setup_component(hass, "config", {}) - entity_id = "light.test_light" - entry_id = ufp.entry.entry_id + sensor.mac = "FFFFFFFFFFFF" - registry: er.EntityRegistry = er.async_get(hass) - entity = registry.async_get(entity_id) - assert entity is not None + await init_entry(hass, ufp, [light, doorlock, sensor], regenerate_ids=False) + assert await async_setup_component(hass, "config", {}) + + entry_id = ufp.entry.entry_id device_registry = dr.async_get(hass) - live_device_entry = device_registry.async_get(entity.device_id) + light_device = get_device_from_ufp_device(hass, light) + assert light_device is not None assert ( - await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id) - is False + await remove_device(await hass_ws_client(hass), light_device.id, entry_id) + is True ) + doorlock_device = get_device_from_ufp_device(hass, doorlock) + assert ( + await remove_device(await hass_ws_client(hass), doorlock_device.id, entry_id) + is True + ) + + sensor_device = get_device_from_ufp_device(hass, sensor) + assert sensor_device is None + dead_device_entry = device_registry.async_get_or_create( config_entry_id=entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")}, @@ -242,6 +253,10 @@ async def test_device_remove_devices( is True ) + await time_changed(hass, 60) + entry = hass.config_entries.async_get_entry(entry_id) + entry.options[CONF_IGNORED] == f"{light.mac},{doorlock.mac}" + async def test_device_remove_devices_nvr( hass: HomeAssistant, diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index bee479b8e2b..3376db4ec51 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -23,7 +23,7 @@ from pyunifiprotect.test_util.anonymize import random_hex from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityDescription import homeassistant.util.dt as dt_util @@ -229,3 +229,13 @@ async def adopt_devices( ufp.ws_msg(mock_msg) await hass.async_block_till_done() + + +def get_device_from_ufp_device( + hass: HomeAssistant, device: ProtectAdoptableDeviceModel +) -> dr.DeviceEntry | None: + """Return all device by type.""" + registry = dr.async_get(hass) + return registry.async_get_device( + identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} + ) From f41ba39a5edc64d0a9ce5274cd4032c25cf73bdb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 28 Aug 2022 22:11:57 -0400 Subject: [PATCH 705/903] Add Litter Robot 4 DHCP discovery (#77463) --- homeassistant/components/litterrobot/manifest.json | 1 + homeassistant/generated/dhcp.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 9c5eb1486bf..32619979270 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", "requirements": ["pylitterbot==2022.8.0"], + "dhcp": [{ "hostname": "litter-robot4" }], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling", "loggers": ["pylitterbot"] diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 841db03c3a6..77255577cc2 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -62,6 +62,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'isy994', 'hostname': 'polisy*', 'macaddress': '000DB9*'}, {'domain': 'lifx', 'macaddress': 'D073D5*'}, {'domain': 'lifx', 'registered_devices': True}, + {'domain': 'litterrobot', 'hostname': 'litter-robot4'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': 'B82CA0*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '00D02D*'}, From 4333d9a7d1064f0fe19b49660175220888cf171e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Aug 2022 22:18:35 -0500 Subject: [PATCH 706/903] Fix recorder being imported before deps are installed (#77460) --- homeassistant/components/recorder/models.py | 10 --------- homeassistant/helpers/recorder.py | 23 ++++++++++++++------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 98c9fc7c9b2..ff53d9be3d1 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,8 +1,6 @@ """Models for Recorder.""" from __future__ import annotations -import asyncio -from dataclasses import dataclass, field from datetime import datetime import logging from typing import Any, TypedDict, overload @@ -32,14 +30,6 @@ class UnsupportedDialect(Exception): """The dialect or its version is not supported.""" -@dataclass -class RecorderData: - """Recorder data stored in hass.data.""" - - recorder_platforms: dict[str, Any] = field(default_factory=dict) - db_connected: asyncio.Future = field(default_factory=asyncio.Future) - - class StatisticResult(TypedDict): """Statistic result data class. diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index 2049300e460..32e08874a63 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -1,9 +1,21 @@ """Helpers to check recorder.""" import asyncio +from dataclasses import dataclass, field +from typing import Any from homeassistant.core import HomeAssistant, callback +DOMAIN = "recorder" + + +@dataclass +class RecorderData: + """Recorder data stored in hass.data.""" + + recorder_platforms: dict[str, Any] = field(default_factory=dict) + db_connected: asyncio.Future = field(default_factory=asyncio.Future) + def async_migration_in_progress(hass: HomeAssistant) -> bool: """Check to see if a recorder migration is in progress.""" @@ -18,10 +30,7 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: @callback def async_initialize_recorder(hass: HomeAssistant) -> None: """Initialize recorder data.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.recorder import const, models - - hass.data[const.DOMAIN] = models.RecorderData() + hass.data[DOMAIN] = RecorderData() async def async_wait_recorder(hass: HomeAssistant) -> bool: @@ -30,9 +39,7 @@ async def async_wait_recorder(hass: HomeAssistant) -> bool: Returns False immediately if the recorder is not enabled. """ # pylint: disable-next=import-outside-toplevel - from homeassistant.components.recorder import const - - if const.DOMAIN not in hass.data: + if DOMAIN not in hass.data: return False - db_connected: asyncio.Future[bool] = hass.data[const.DOMAIN].db_connected + db_connected: asyncio.Future[bool] = hass.data[DOMAIN].db_connected return await db_connected From 7c27be230c4a75030ba25f327e06003aacbebedb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 29 Aug 2022 00:40:28 -0400 Subject: [PATCH 707/903] Add reauth flow to Litterrobot (#77459) Co-authored-by: J. Nick Koston --- CODEOWNERS | 4 +- .../components/litterrobot/__init__.py | 10 +- .../components/litterrobot/config_flow.py | 67 +++++++++--- homeassistant/components/litterrobot/hub.py | 7 +- .../components/litterrobot/manifest.json | 2 +- .../components/litterrobot/strings.json | 10 +- .../litterrobot/translations/en.json | 10 +- .../litterrobot/test_config_flow.py | 101 +++++++++++++++++- 8 files changed, 175 insertions(+), 36 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e22d2468f25..004bc365d89 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -611,8 +611,8 @@ build.json @home-assistant/supervisor /homeassistant/components/linux_battery/ @fabaff /homeassistant/components/litejet/ @joncar /tests/components/litejet/ @joncar -/homeassistant/components/litterrobot/ @natekspencer -/tests/components/litterrobot/ @natekspencer +/homeassistant/components/litterrobot/ @natekspencer @tkdrob +/tests/components/litterrobot/ @natekspencer @tkdrob /homeassistant/components/local_ip/ @issacg /tests/components/local_ip/ @issacg /homeassistant/components/lock/ @home-assistant/core diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 334773c6f86..5aa186d0171 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,12 +1,9 @@ """The Litter-Robot integration.""" from __future__ import annotations -from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException - from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN from .hub import LitterRobotHub @@ -24,12 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) 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 + await hub.login(load_robots=True) if any(hub.litter_robots()): await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index fbe32fa9749..558945ca1db 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -5,15 +5,16 @@ from collections.abc import Mapping import logging from typing import Any +from pylitterbot import Account from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -from .hub import LitterRobotHub _LOGGER = logging.getLogger(__name__) @@ -27,6 +28,38 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + username: str + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a reauthorization flow request.""" + self.username = entry_data[CONF_USERNAME] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle user's reauth credentials.""" + errors = {} + if user_input: + entry_id = self.context["entry_id"] + if entry := self.hass.config_entries.async_get_entry(entry_id): + user_input = user_input | {CONF_USERNAME: self.username} + if not (error := await self._async_validate_input(user_input)): + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | user_input, + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + + errors["base"] = error + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + description_placeholders={CONF_USERNAME: self.username}, + errors=errors, + ) + async def async_step_user( self, user_input: Mapping[str, Any] | None = None ) -> FlowResult: @@ -36,22 +69,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) - hub = LitterRobotHub(self.hass, user_input) - try: - await hub.login() - 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" - - if not errors: + if not (error := await self._async_validate_input(user_input)): return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) + errors["base"] = error return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + + async def _async_validate_input(self, user_input: Mapping[str, Any]) -> str: + """Validate login credentials.""" + account = Account(websession=async_get_clientsession(self.hass)) + try: + await account.connect( + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + await account.disconnect() + except LitterRobotLoginException: + return "invalid_auth" + except LitterRobotException: + return "cannot_connect" + except Exception as ex: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception: %s", ex) + return "unknown" + return "" diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 40ba9e74a7a..627075208cc 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -11,6 +11,7 @@ from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginExcepti from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -52,11 +53,9 @@ class LitterRobotHub: ) return except LitterRobotLoginException as ex: - _LOGGER.error("Invalid credentials") - raise ex + raise ConfigEntryAuthFailed("Invalid credentials") from ex except LitterRobotException as ex: - _LOGGER.error("Unable to connect to Litter-Robot API") - raise ex + raise ConfigEntryNotReady("Unable to connect to Litter-Robot API") from ex def litter_robots(self) -> Generator[LitterRobot, Any, Any]: """Get Litter-Robots from the account.""" diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 32619979270..5b2f0f106b9 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -4,8 +4,8 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", "requirements": ["pylitterbot==2022.8.0"], + "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], - "codeowners": ["@natekspencer"], "iot_class": "cloud_polling", "loggers": ["pylitterbot"] } diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index f7a539fe0e6..140a0308188 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -6,6 +6,13 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "description": "Please update your password for {username}", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -14,7 +21,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "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/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json index a6c0889765f..2ca3d2f0dc2 100644 --- a/homeassistant/components/litterrobot/translations/en.json +++ b/homeassistant/components/litterrobot/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -14,6 +15,13 @@ "password": "Password", "username": "Username" } + }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please update your password for {username}", + "title": "Reauthenticate Integration" } } } diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index 7bfb1321d9e..ee5b718fa60 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -1,10 +1,14 @@ """Test the Litter-Robot config flow.""" from unittest.mock import patch +from pylitterbot import Account from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from homeassistant import config_entries from homeassistant.components import litterrobot +from homeassistant.const import CONF_PASSWORD, CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import CONF_USERNAME, CONFIG, DOMAIN @@ -21,7 +25,7 @@ async def test_form(hass, mock_account): assert result["errors"] == {} with patch( - "homeassistant.components.litterrobot.hub.Account", + "homeassistant.components.litterrobot.config_flow.Account.connect", return_value=mock_account, ), patch( "homeassistant.components.litterrobot.async_setup_entry", @@ -62,7 +66,7 @@ async def test_form_invalid_auth(hass): ) with patch( - "pylitterbot.Account.connect", + "homeassistant.components.litterrobot.config_flow.Account.connect", side_effect=LitterRobotLoginException, ): result2 = await hass.config_entries.flow.async_configure( @@ -80,7 +84,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "pylitterbot.Account.connect", + "homeassistant.components.litterrobot.config_flow.Account.connect", side_effect=LitterRobotException, ): result2 = await hass.config_entries.flow.async_configure( @@ -96,9 +100,8 @@ async def test_form_unknown_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "pylitterbot.Account.connect", + "homeassistant.components.litterrobot.config_flow.Account.connect", side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( @@ -107,3 +110,91 @@ async def test_form_unknown_error(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} + + +async def test_step_reauth(hass: HomeAssistant, mock_account: Account) -> None: + """Test the reauth flow.""" + entry = MockConfigEntry( + domain=litterrobot.DOMAIN, + data=CONFIG[litterrobot.DOMAIN], + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.litterrobot.config_flow.Account.connect", + return_value=mock_account, + ), patch( + "homeassistant.components.litterrobot.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> None: + """Test the reauth flow fails and recovers.""" + entry = MockConfigEntry( + domain=litterrobot.DOMAIN, + data=CONFIG[litterrobot.DOMAIN], + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.litterrobot.config_flow.Account.connect", + side_effect=LitterRobotLoginException, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + with patch( + "homeassistant.components.litterrobot.config_flow.Account.connect", + return_value=mock_account, + ), patch( + "homeassistant.components.litterrobot.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 From 779e020dc439e2cfbc9c3b02bc11043f23ba3500 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 29 Aug 2022 14:41:37 +1000 Subject: [PATCH 708/903] Add update platform to Advantage Air (#75391) Co-authored-by: J. Nick Koston --- .../components/advantage_air/__init__.py | 1 + .../components/advantage_air/update.py | 52 +++++++++++++++++++ .../advantage_air/fixtures/getSystemData.json | 3 +- .../advantage_air/fixtures/needsUpdate.json | 49 +++++++++++++++++ tests/components/advantage_air/test_update.py | 28 ++++++++++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/advantage_air/update.py create mode 100644 tests/components/advantage_air/fixtures/needsUpdate.json create mode 100644 tests/components/advantage_air/test_update.py diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index b5e6e0be024..3e07a8fbcef 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -20,6 +20,7 @@ PLATFORMS = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, Platform.LIGHT, ] diff --git a/homeassistant/components/advantage_air/update.py b/homeassistant/components/advantage_air/update.py new file mode 100644 index 00000000000..9294ecab238 --- /dev/null +++ b/homeassistant/components/advantage_air/update.py @@ -0,0 +1,52 @@ +"""Advantage Air Update platform.""" +from homeassistant.components.update import UpdateEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN +from .entity import AdvantageAirEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AdvantageAir update platform.""" + + instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] + + async_add_entities([AdvantageAirApp(instance)]) + + +class AdvantageAirApp(AdvantageAirEntity, UpdateEntity): + """Representation of Advantage Air App.""" + + _attr_name = "App" + + def __init__(self, instance): + """Initialize the Advantage Air App.""" + super().__init__(instance) + self._attr_device_info = DeviceInfo( + identifiers={ + (ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]) + }, + manufacturer="Advantage Air", + model=self.coordinator.data["system"]["sysType"], + name=self.coordinator.data["system"]["name"], + sw_version=self.coordinator.data["system"]["myAppRev"], + ) + + @property + def installed_version(self): + """Return the current app version.""" + return self.coordinator.data["system"]["myAppRev"] + + @property + def latest_version(self): + """Return if there is an update.""" + if self.coordinator.data["system"]["needsUpdate"]: + return "Needs Update" + return self.installed_version diff --git a/tests/components/advantage_air/fixtures/getSystemData.json b/tests/components/advantage_air/fixtures/getSystemData.json index 00ce2b1f095..327ee2d53d5 100644 --- a/tests/components/advantage_air/fixtures/getSystemData.json +++ b/tests/components/advantage_air/fixtures/getSystemData.json @@ -167,9 +167,10 @@ "hasThings": false, "hasThingsBOG": false, "hasThingsLight": false, + "needsUpdate": false, "name": "testname", "rid": "uniqueid", "sysType": "e-zone", - "myAppRev": "testversion" + "myAppRev": "1.234" } } diff --git a/tests/components/advantage_air/fixtures/needsUpdate.json b/tests/components/advantage_air/fixtures/needsUpdate.json new file mode 100644 index 00000000000..584aa50cb77 --- /dev/null +++ b/tests/components/advantage_air/fixtures/needsUpdate.json @@ -0,0 +1,49 @@ +{ + "aircons": { + "ac1": { + "info": { + "climateControlModeIsRunning": false, + "countDownToOff": 10, + "countDownToOn": 0, + "fan": "high", + "filterCleanStatus": 0, + "freshAirStatus": "off", + "mode": "vent", + "myZone": 1, + "name": "AC One", + "setTemp": 24, + "state": "on" + }, + "zones": { + "z01": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 25, + "minDamper": 0, + "motion": 20, + "motionConfig": 2, + "name": "Zone open with Sensor", + "number": 1, + "rssi": 40, + "setTemp": 24, + "state": "open", + "type": 1, + "value": 100 + } + } + } + }, + "system": { + "hasAircons": false, + "hasLights": false, + "hasSensors": false, + "hasThings": false, + "hasThingsBOG": false, + "hasThingsLight": false, + "needsUpdate": true, + "name": "testname", + "rid": "uniqueid", + "sysType": "e-zone", + "myAppRev": "1.234" + } +} diff --git a/tests/components/advantage_air/test_update.py b/tests/components/advantage_air/test_update.py new file mode 100644 index 00000000000..2fef887997f --- /dev/null +++ b/tests/components/advantage_air/test_update.py @@ -0,0 +1,28 @@ +"""Test the Advantage Air Update Platform.""" + +from homeassistant.const import STATE_ON +from homeassistant.helpers import entity_registry as er + +from tests.common import load_fixture +from tests.components.advantage_air import TEST_SYSTEM_URL, add_mock_config + + +async def test_update_platform(hass, aioclient_mock): + """Test update platform.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + text=load_fixture("advantage_air/needsUpdate.json"), + ) + await add_mock_config(hass) + + registry = er.async_get(hass) + + entity_id = "update.testname_app" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid" From 0154a1cecbcd75d5cb7890af02687e33df979519 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 29 Aug 2022 08:15:10 +0200 Subject: [PATCH 709/903] Improve deCONZ binary sensor classes (#77419) --- .../components/deconz/alarm_control_panel.py | 7 +- .../components/deconz/binary_sensor.py | 377 ++++++++++-------- .../components/deconz/deconz_device.py | 21 + homeassistant/components/deconz/number.py | 11 +- homeassistant/components/deconz/sensor.py | 18 +- 5 files changed, 232 insertions(+), 202 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 59b4b9e4f8e..179fa2320df 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -83,6 +83,7 @@ async def async_setup_entry( class DeconzAlarmControlPanel(DeconzDevice[AncillaryControl], AlarmControlPanelEntity): """Representation of a deCONZ alarm control panel.""" + _update_key = "panel" TYPE = DOMAIN _attr_code_format = CodeFormat.NUMBER @@ -105,11 +106,7 @@ class DeconzAlarmControlPanel(DeconzDevice[AncillaryControl], AlarmControlPanelE @callback def async_update_callback(self) -> None: """Update the control panels state.""" - keys = {"panel", "reachable"} - if ( - self._device.changed_keys.intersection(keys) - and self._device.panel in DECONZ_TO_ALARM_STATE - ): + if self._device.panel in DECONZ_TO_ALARM_STATE: super().async_update_callback() @property diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 08cb8753bb6..f495fef45c3 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -1,11 +1,11 @@ """Support for deCONZ binary sensors.""" from __future__ import annotations -from collections.abc import Callable -from dataclasses import dataclass +from typing import TYPE_CHECKING, TypeVar from pydeconz.interfaces.sensors import SensorResources from pydeconz.models.event import EventType +from pydeconz.models.sensor import SensorBase as PydeconzSensorBase from pydeconz.models.sensor.alarm import Alarm from pydeconz.models.sensor.carbon_monoxide import CarbonMonoxide from pydeconz.models.sensor.fire import Fire @@ -19,7 +19,6 @@ from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorDeviceClass, BinarySensorEntity, - BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE @@ -32,6 +31,8 @@ from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry +_SensorDeviceT = TypeVar("_SensorDeviceT", bound=PydeconzSensorBase) + ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" ATTR_VIBRATIONSTRENGTH = "vibrationstrength" @@ -48,140 +49,9 @@ PROVIDES_EXTRA_ATTRIBUTES = ( ) -@dataclass -class DeconzBinarySensorDescriptionMixin: - """Required values when describing secondary sensor attributes.""" - - suffix: str - update_key: str - value_fn: Callable[[SensorResources], bool | None] - - -@dataclass -class DeconzBinarySensorDescription( - BinarySensorEntityDescription, - DeconzBinarySensorDescriptionMixin, -): - """Class describing deCONZ binary sensor entities.""" - - -ENTITY_DESCRIPTIONS = { - Alarm: [ - DeconzBinarySensorDescription( - key="alarm", - value_fn=lambda device: device.alarm if isinstance(device, Alarm) else None, - suffix="", - update_key="alarm", - device_class=BinarySensorDeviceClass.SAFETY, - ) - ], - CarbonMonoxide: [ - DeconzBinarySensorDescription( - key="carbon_monoxide", - value_fn=lambda device: device.carbon_monoxide - if isinstance(device, CarbonMonoxide) - else None, - suffix="", - update_key="carbonmonoxide", - device_class=BinarySensorDeviceClass.CO, - ) - ], - Fire: [ - DeconzBinarySensorDescription( - key="fire", - value_fn=lambda device: device.fire if isinstance(device, Fire) else None, - suffix="", - update_key="fire", - device_class=BinarySensorDeviceClass.SMOKE, - ), - DeconzBinarySensorDescription( - key="in_test_mode", - value_fn=lambda device: device.in_test_mode - if isinstance(device, Fire) - else None, - suffix="Test Mode", - update_key="test", - device_class=BinarySensorDeviceClass.SMOKE, - entity_category=EntityCategory.DIAGNOSTIC, - ), - ], - GenericFlag: [ - DeconzBinarySensorDescription( - key="flag", - value_fn=lambda device: device.flag - if isinstance(device, GenericFlag) - else None, - suffix="", - update_key="flag", - ) - ], - OpenClose: [ - DeconzBinarySensorDescription( - key="open", - value_fn=lambda device: device.open - if isinstance(device, OpenClose) - else None, - suffix="", - update_key="open", - device_class=BinarySensorDeviceClass.OPENING, - ) - ], - Presence: [ - DeconzBinarySensorDescription( - key="presence", - value_fn=lambda device: device.presence - if isinstance(device, Presence) - else None, - suffix="", - update_key="presence", - device_class=BinarySensorDeviceClass.MOTION, - ) - ], - Vibration: [ - DeconzBinarySensorDescription( - key="vibration", - value_fn=lambda device: device.vibration - if isinstance(device, Vibration) - else None, - suffix="", - update_key="vibration", - device_class=BinarySensorDeviceClass.VIBRATION, - ) - ], - Water: [ - DeconzBinarySensorDescription( - key="water", - value_fn=lambda device: device.water if isinstance(device, Water) else None, - suffix="", - update_key="water", - device_class=BinarySensorDeviceClass.MOISTURE, - ) - ], -} - -COMMON_BINARY_SENSOR_DESCRIPTIONS = [ - DeconzBinarySensorDescription( - key="tampered", - value_fn=lambda device: device.tampered, - suffix="Tampered", - update_key="tampered", - device_class=BinarySensorDeviceClass.TAMPER, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DeconzBinarySensorDescription( - key="low_battery", - value_fn=lambda device: device.low_battery, - suffix="Low Battery", - update_key="lowbattery", - device_class=BinarySensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - ), -] - - @callback def async_update_unique_id( - hass: HomeAssistant, unique_id: str, description: DeconzBinarySensorDescription + hass: HomeAssistant, unique_id: str, entity_class: DeconzBinarySensor ) -> None: """Update unique ID to always have a suffix. @@ -189,12 +59,12 @@ def async_update_unique_id( """ ent_reg = er.async_get(hass) - new_unique_id = f"{unique_id}-{description.key}" + new_unique_id = f"{unique_id}-{entity_class.unique_id_suffix}" if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): return - if description.suffix: - unique_id = f'{unique_id.split("-", 1)[0]}-{description.suffix.lower()}' + if entity_class.old_unique_id_suffix: + unique_id = f'{unique_id.split("-", 1)[0]}-{entity_class.old_unique_id_suffix}' if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) @@ -214,19 +84,19 @@ async def async_setup_entry( """Add sensor from deCONZ.""" sensor = gateway.api.sensors[sensor_id] - for description in ( - ENTITY_DESCRIPTIONS.get(type(sensor), []) - + COMMON_BINARY_SENSOR_DESCRIPTIONS - ): + for sensor_type, entity_class in ENTITY_CLASSES: + if TYPE_CHECKING: + assert isinstance(entity_class, DeconzBinarySensor) if ( - not hasattr(sensor, description.key) - or description.value_fn(sensor) is None + not isinstance(sensor, sensor_type) + or entity_class.unique_id_suffix is not None + and getattr(sensor, entity_class.unique_id_suffix) is None ): continue - async_update_unique_id(hass, sensor.unique_id, description) + async_update_unique_id(hass, sensor.unique_id, entity_class) - async_add_entities([DeconzBinarySensor(sensor, gateway, description)]) + async_add_entities([entity_class(sensor, gateway)]) gateway.register_platform_add_device_callback( async_add_sensor, @@ -234,51 +104,28 @@ async def async_setup_entry( ) -class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): +class DeconzBinarySensor(DeconzDevice[_SensorDeviceT], BinarySensorEntity): """Representation of a deCONZ binary sensor.""" + old_unique_id_suffix = "" TYPE = DOMAIN - entity_description: DeconzBinarySensorDescription - def __init__( - self, - device: SensorResources, - gateway: DeconzGateway, - description: DeconzBinarySensorDescription, - ) -> None: + def __init__(self, device: _SensorDeviceT, gateway: DeconzGateway) -> None: """Initialize deCONZ binary sensor.""" - self.entity_description: DeconzBinarySensorDescription = description super().__init__(device, gateway) - if description.suffix: - self._attr_name = f"{self._device.name} {description.suffix}" - - self._update_keys = {description.update_key, "reachable"} - if self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES: + if ( + self.unique_id_suffix in PROVIDES_EXTRA_ATTRIBUTES + and self._update_keys is not None + ): self._update_keys.update({"on", "state"}) - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return f"{super().unique_id}-{self.entity_description.key}" - - @callback - def async_update_callback(self) -> None: - """Update the sensor's state.""" - if self._device.changed_keys.intersection(self._update_keys): - super().async_update_callback() - - @property - def is_on(self) -> bool | None: - """Return the state of the sensor.""" - return self.entity_description.value_fn(self._device) - @property def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: """Return the state attributes of the sensor.""" attr: dict[str, bool | float | int | list | None] = {} - if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: + if self.unique_id_suffix not in PROVIDES_EXTRA_ATTRIBUTES: return attr if self._device.on is not None: @@ -298,3 +145,179 @@ class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength return attr + + +class DeconzAlarmBinarySensor(DeconzBinarySensor[Alarm]): + """Representation of a deCONZ alarm binary sensor.""" + + unique_id_suffix = "alarm" + _update_key = "alarm" + + _attr_device_class = BinarySensorDeviceClass.SAFETY + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.alarm + + +class DeconzCarbonMonoxideBinarySensor(DeconzBinarySensor[CarbonMonoxide]): + """Representation of a deCONZ carbon monoxide binary sensor.""" + + unique_id_suffix = "carbon_monoxide" + _update_key = "carbonmonoxide" + + _attr_device_class = BinarySensorDeviceClass.CO + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.carbon_monoxide + + +class DeconzFireBinarySensor(DeconzBinarySensor[Fire]): + """Representation of a deCONZ fire binary sensor.""" + + unique_id_suffix = "fire" + _update_key = "fire" + + _attr_device_class = BinarySensorDeviceClass.SMOKE + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.fire + + +class DeconzFireInTestModeBinarySensor(DeconzBinarySensor[Fire]): + """Representation of a deCONZ fire in-test-mode binary sensor.""" + + _name_suffix = "Test Mode" + unique_id_suffix = "in_test_mode" + old_unique_id_suffix = "test mode" + _update_key = "test" + + _attr_device_class = BinarySensorDeviceClass.SMOKE + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.in_test_mode + + +class DeconzFlagBinarySensor(DeconzBinarySensor[GenericFlag]): + """Representation of a deCONZ generic flag binary sensor.""" + + unique_id_suffix = "flag" + _update_key = "flag" + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.flag + + +class DeconzOpenCloseBinarySensor(DeconzBinarySensor[OpenClose]): + """Representation of a deCONZ open/close binary sensor.""" + + unique_id_suffix = "open" + _update_key = "open" + + _attr_device_class = BinarySensorDeviceClass.OPENING + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.open + + +class DeconzPresenceBinarySensor(DeconzBinarySensor[Presence]): + """Representation of a deCONZ presence binary sensor.""" + + unique_id_suffix = "presence" + _update_key = "presence" + + _attr_device_class = BinarySensorDeviceClass.MOTION + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.presence + + +class DeconzVibrationBinarySensor(DeconzBinarySensor[Vibration]): + """Representation of a deCONZ vibration binary sensor.""" + + unique_id_suffix = "vibration" + _update_key = "vibration" + + _attr_device_class = BinarySensorDeviceClass.VIBRATION + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.vibration + + +class DeconzWaterBinarySensor(DeconzBinarySensor[Water]): + """Representation of a deCONZ water binary sensor.""" + + unique_id_suffix = "water" + _update_key = "water" + + _attr_device_class = BinarySensorDeviceClass.MOISTURE + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.water + + +class DeconzTamperedCommonBinarySensor(DeconzBinarySensor[SensorResources]): + """Representation of a deCONZ tampered binary sensor.""" + + _name_suffix = "Tampered" + unique_id_suffix = "tampered" + old_unique_id_suffix = "tampered" + _update_key = "tampered" + + _attr_device_class = BinarySensorDeviceClass.TAMPER + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def is_on(self) -> bool | None: + """Return the state of the sensor.""" + return self._device.tampered + + +class DeconzLowBatteryCommonBinarySensor(DeconzBinarySensor[SensorResources]): + """Representation of a deCONZ low battery binary sensor.""" + + _name_suffix = "Low Battery" + unique_id_suffix = "low_battery" + old_unique_id_suffix = "low battery" + _update_key = "lowbattery" + + _attr_device_class = BinarySensorDeviceClass.BATTERY + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def is_on(self) -> bool | None: + """Return the state of the sensor.""" + return self._device.low_battery + + +ENTITY_CLASSES = ( + (Alarm, DeconzAlarmBinarySensor), + (CarbonMonoxide, DeconzCarbonMonoxideBinarySensor), + (Fire, DeconzFireBinarySensor), + (Fire, DeconzFireInTestModeBinarySensor), + (GenericFlag, DeconzFlagBinarySensor), + (OpenClose, DeconzOpenCloseBinarySensor), + (Presence, DeconzPresenceBinarySensor), + (Vibration, DeconzVibrationBinarySensor), + (Water, DeconzWaterBinarySensor), + (PydeconzSensorBase, DeconzTamperedCommonBinarySensor), + (PydeconzSensorBase, DeconzLowBatteryCommonBinarySensor), +) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 0ac7acf5b49..c2161baf100 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -32,6 +32,8 @@ _DeviceT = TypeVar( class DeconzBase(Generic[_DeviceT]): """Common base for deconz entities and events.""" + unique_id_suffix: str | None = None + def __init__( self, device: _DeviceT, @@ -45,6 +47,8 @@ class DeconzBase(Generic[_DeviceT]): def unique_id(self) -> str: """Return a unique identifier for this device.""" assert isinstance(self._device, PydeconzDevice) + if self.unique_id_suffix is not None: + return f"{self._device.unique_id}-{self.unique_id_suffix}" return self._device.unique_id @property @@ -78,6 +82,10 @@ class DeconzDevice(DeconzBase[_DeviceT], Entity): _attr_should_poll = False + _name_suffix: str | None = None + _update_key: str | None = None + _update_keys: set[str] | None = None + TYPE = "" def __init__( @@ -90,6 +98,13 @@ class DeconzDevice(DeconzBase[_DeviceT], Entity): self.gateway.entities[self.TYPE].add(self.unique_id) self._attr_name = self._device.name + if self._name_suffix is not None: + self._attr_name += f" {self._name_suffix}" + + if self._update_key is not None: + self._update_keys = {self._update_key} + if self._update_keys is not None: + self._update_keys |= {"reachable"} async def async_added_to_hass(self) -> None: """Subscribe to device events.""" @@ -120,6 +135,12 @@ class DeconzDevice(DeconzBase[_DeviceT], Entity): if self.gateway.ignore_state_updates: return + if ( + self._update_keys is not None + and not self._device.changed_keys.intersection(self._update_keys) + ): + return + self.async_write_ha_state() @property diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index b4a4ba415c0..9baa54efb56 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -94,17 +94,10 @@ class DeconzNumber(DeconzDevice[Presence], NumberEntity): ) -> None: """Initialize deCONZ number entity.""" self.entity_description: DeconzNumberDescription = description + self._update_key = self.entity_description.update_key + self._name_suffix = description.suffix super().__init__(device, gateway) - self._attr_name = f"{device.name} {description.suffix}" - self._update_keys = {self.entity_description.update_key, "reachable"} - - @callback - def async_update_callback(self) -> None: - """Update the number value.""" - if self._device.changed_keys.intersection(self._update_keys): - super().async_update_callback() - @property def native_value(self) -> float | None: """Return the value of the sensor property.""" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 5c1fe61c7a7..055067cc36f 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -287,13 +287,15 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): ) -> None: """Initialize deCONZ sensor.""" self.entity_description = description + self._update_key = description.update_key + if description.suffix: + self._name_suffix = description.suffix super().__init__(device, gateway) - if description.suffix: - self._attr_name = f"{device.name} {description.suffix}" - - self._update_keys = {description.update_key, "reachable"} - if self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES: + if ( + self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES + and self._update_keys is not None + ): self._update_keys.update({"on", "state"}) @property @@ -315,12 +317,6 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): return f"{self.serial}-{self.entity_description.suffix.lower()}" return super().unique_id - @callback - def async_update_callback(self) -> None: - """Update the sensor's state.""" - if self._device.changed_keys.intersection(self._update_keys): - super().async_update_callback() - @property def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" From 067d21a307d9d6220958e0212a4c4b920b55e927 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Aug 2022 08:45:39 +0200 Subject: [PATCH 710/903] Refactor hardware.async_info to return list[HardwareInfo] (#77183) --- .../components/hardkernel/hardware.py | 26 +++++++------ homeassistant/components/hardware/models.py | 4 +- .../components/hardware/websocket_api.py | 2 +- .../homeassistant_sky_connect/hardware.py | 28 +++++++------- .../homeassistant_yellow/hardware.py | 26 +++++++------ .../components/raspberry_pi/hardware.py | 26 +++++++------ tests/components/hardkernel/test_hardware.py | 2 +- .../test_hardware.py | 37 ++++++++++--------- .../homeassistant_yellow/test_hardware.py | 2 +- .../components/raspberry_pi/test_hardware.py | 2 +- 10 files changed, 81 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/hardkernel/hardware.py b/homeassistant/components/hardkernel/hardware.py index ad45e3ac946..47ff5830a84 100644 --- a/homeassistant/components/hardkernel/hardware.py +++ b/homeassistant/components/hardkernel/hardware.py @@ -17,7 +17,7 @@ BOARD_NAMES = { @callback -def async_info(hass: HomeAssistant) -> HardwareInfo: +def async_info(hass: HomeAssistant) -> list[HardwareInfo]: """Return board info.""" if (os_info := get_os_info(hass)) is None: raise HomeAssistantError @@ -27,14 +27,16 @@ def async_info(hass: HomeAssistant) -> HardwareInfo: if not board.startswith("odroid"): raise HomeAssistantError - return HardwareInfo( - board=BoardInfo( - hassio_board_id=board, - manufacturer=DOMAIN, - model=board, - revision=None, - ), - dongles=None, - name=BOARD_NAMES.get(board, f"Unknown hardkernel Odroid model '{board}'"), - url=None, - ) + return [ + HardwareInfo( + board=BoardInfo( + hassio_board_id=board, + manufacturer=DOMAIN, + model=board, + revision=None, + ), + dongle=None, + name=BOARD_NAMES.get(board, f"Unknown hardkernel Odroid model '{board}'"), + url=None, + ) + ] diff --git a/homeassistant/components/hardware/models.py b/homeassistant/components/hardware/models.py index 8f9819a853d..8ce5e7be7f3 100644 --- a/homeassistant/components/hardware/models.py +++ b/homeassistant/components/hardware/models.py @@ -34,7 +34,7 @@ class HardwareInfo: name: str | None board: BoardInfo | None - dongles: list[USBInfo] | None + dongle: USBInfo | None url: str | None @@ -42,5 +42,5 @@ class HardwareProtocol(Protocol): """Define the format of hardware platforms.""" @callback - def async_info(self, hass: HomeAssistant) -> HardwareInfo: + def async_info(self, hass: HomeAssistant) -> list[HardwareInfo]: """Return info.""" diff --git a/homeassistant/components/hardware/websocket_api.py b/homeassistant/components/hardware/websocket_api.py index 388b9597481..df3b8868053 100644 --- a/homeassistant/components/hardware/websocket_api.py +++ b/homeassistant/components/hardware/websocket_api.py @@ -42,6 +42,6 @@ async def ws_info( for platform in hardware_platform.values(): if hasattr(platform, "async_info"): with contextlib.suppress(HomeAssistantError): - hardware_info.append(asdict(platform.async_info(hass))) + hardware_info.extend([asdict(hw) for hw in platform.async_info(hass)]) connection.send_result(msg["id"], {"hardware": hardware_info}) diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index 3c1993bfd8b..6eceb746756 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -10,24 +10,22 @@ DONGLE_NAME = "Home Assistant Sky Connect" @callback -def async_info(hass: HomeAssistant) -> HardwareInfo: +def async_info(hass: HomeAssistant) -> list[HardwareInfo]: """Return board info.""" entries = hass.config_entries.async_entries(DOMAIN) - dongles = [ - USBInfo( - vid=entry.data["vid"], - pid=entry.data["pid"], - serial_number=entry.data["serial_number"], - manufacturer=entry.data["manufacturer"], - description=entry.data["description"], + return [ + HardwareInfo( + board=None, + dongle=USBInfo( + vid=entry.data["vid"], + pid=entry.data["pid"], + serial_number=entry.data["serial_number"], + manufacturer=entry.data["manufacturer"], + description=entry.data["description"], + ), + name=DONGLE_NAME, + url=None, ) for entry in entries ] - - return HardwareInfo( - board=None, - dongles=dongles, - name=DONGLE_NAME, - url=None, - ) diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py index 01aee032a22..ad17eccfe7f 100644 --- a/homeassistant/components/homeassistant_yellow/hardware.py +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -12,7 +12,7 @@ MODEL = "yellow" @callback -def async_info(hass: HomeAssistant) -> HardwareInfo: +def async_info(hass: HomeAssistant) -> list[HardwareInfo]: """Return board info.""" if (os_info := get_os_info(hass)) is None: raise HomeAssistantError @@ -22,14 +22,16 @@ def async_info(hass: HomeAssistant) -> HardwareInfo: if not board == "yellow": raise HomeAssistantError - return HardwareInfo( - board=BoardInfo( - hassio_board_id=board, - manufacturer=MANUFACTURER, - model=MODEL, - revision=None, - ), - dongles=None, - name=BOARD_NAME, - url=None, - ) + return [ + HardwareInfo( + board=BoardInfo( + hassio_board_id=board, + manufacturer=MANUFACTURER, + model=MODEL, + revision=None, + ), + dongle=None, + name=BOARD_NAME, + url=None, + ) + ] diff --git a/homeassistant/components/raspberry_pi/hardware.py b/homeassistant/components/raspberry_pi/hardware.py index cd1b56ba789..6433b15adb5 100644 --- a/homeassistant/components/raspberry_pi/hardware.py +++ b/homeassistant/components/raspberry_pi/hardware.py @@ -32,7 +32,7 @@ MODELS = { @callback -def async_info(hass: HomeAssistant) -> HardwareInfo: +def async_info(hass: HomeAssistant) -> list[HardwareInfo]: """Return board info.""" if (os_info := get_os_info(hass)) is None: raise HomeAssistantError @@ -42,14 +42,16 @@ def async_info(hass: HomeAssistant) -> HardwareInfo: if not board.startswith("rpi"): raise HomeAssistantError - return HardwareInfo( - board=BoardInfo( - hassio_board_id=board, - manufacturer=DOMAIN, - model=MODELS.get(board), - revision=None, - ), - dongles=None, - name=BOARD_NAMES.get(board, f"Unknown Raspberry Pi model '{board}'"), - url=None, - ) + return [ + HardwareInfo( + board=BoardInfo( + hassio_board_id=board, + manufacturer=DOMAIN, + model=MODELS.get(board), + revision=None, + ), + dongle=None, + name=BOARD_NAMES.get(board, f"Unknown Raspberry Pi model '{board}'"), + url=None, + ) + ] diff --git a/tests/components/hardkernel/test_hardware.py b/tests/components/hardkernel/test_hardware.py index 5f33cb417f2..33602f92e3f 100644 --- a/tests/components/hardkernel/test_hardware.py +++ b/tests/components/hardkernel/test_hardware.py @@ -48,7 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "odroid-n2", "revision": None, }, - "dongles": None, + "dongle": None, "name": "Home Assistant Blue / Hardkernel Odroid-N2", "url": None, } diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index f4e48d56a67..6226651133a 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -62,24 +62,27 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "hardware": [ { "board": None, - "dongles": [ - { - "vid": "bla_vid", - "pid": "bla_pid", - "serial_number": "bla_serial_number", - "manufacturer": "bla_manufacturer", - "description": "bla_description", - }, - { - "vid": "bla_vid_2", - "pid": "bla_pid_2", - "serial_number": "bla_serial_number_2", - "manufacturer": "bla_manufacturer_2", - "description": "bla_description_2", - }, - ], + "dongle": { + "vid": "bla_vid", + "pid": "bla_pid", + "serial_number": "bla_serial_number", + "manufacturer": "bla_manufacturer", + "description": "bla_description", + }, "name": "Home Assistant Sky Connect", "url": None, - } + }, + { + "board": None, + "dongle": { + "vid": "bla_vid_2", + "pid": "bla_pid_2", + "serial_number": "bla_serial_number_2", + "manufacturer": "bla_manufacturer_2", + "description": "bla_description_2", + }, + "name": "Home Assistant Sky Connect", + "url": None, + }, ] } diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 295e44c6ce7..45d6fcabdfe 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -48,7 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "yellow", "revision": None, }, - "dongles": None, + "dongle": None, "name": "Home Assistant Yellow", "url": None, } diff --git a/tests/components/raspberry_pi/test_hardware.py b/tests/components/raspberry_pi/test_hardware.py index ad9533e8af5..c36fcbd1642 100644 --- a/tests/components/raspberry_pi/test_hardware.py +++ b/tests/components/raspberry_pi/test_hardware.py @@ -48,7 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "1", "revision": None, }, - "dongles": None, + "dongle": None, "name": "Raspberry Pi", "url": None, } From 8ed689fedee0a3b923b1689b98f2b0c8101d01e8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Aug 2022 08:55:32 +0200 Subject: [PATCH 711/903] Add new rule to enforce relative imports in pylint (#77358) * Add new rule to enforce relative imports in pylint * Early return * Adjust components --- homeassistant/components/ads/sensor.py | 2 +- homeassistant/components/ios/notify.py | 3 ++- homeassistant/components/ios/sensor.py | 2 +- homeassistant/components/mysensors/binary_sensor.py | 2 +- homeassistant/components/mysensors/climate.py | 2 +- homeassistant/components/mysensors/cover.py | 2 +- homeassistant/components/mysensors/device_tracker.py | 2 +- homeassistant/components/mysensors/light.py | 2 +- homeassistant/components/mysensors/notify.py | 2 +- homeassistant/components/mysensors/sensor.py | 2 +- homeassistant/components/mysensors/switch.py | 2 +- homeassistant/components/pilight/binary_sensor.py | 3 ++- homeassistant/components/pilight/sensor.py | 3 ++- homeassistant/components/recorder/history.py | 2 +- homeassistant/components/tellduslive/binary_sensor.py | 3 ++- homeassistant/components/tellduslive/cover.py | 3 ++- homeassistant/components/tellduslive/light.py | 3 ++- homeassistant/components/tellduslive/sensor.py | 3 ++- homeassistant/components/tellduslive/switch.py | 3 ++- homeassistant/components/trace/websocket_api.py | 4 +++- homeassistant/components/zabbix/sensor.py | 3 ++- pylint/plugins/hass_imports.py | 8 +++++++- tests/pylint/test_imports.py | 8 +++++++- 23 files changed, 46 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 172f8ee70df..76d73f75a8b 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components import ads from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -19,6 +18,7 @@ from . import ( STATE_KEY_STATE, AdsEntity, ) +from .. import ads DEFAULT_NAME = "ADS sensor" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 2e27271841a..12fe5ba5f5e 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -4,7 +4,6 @@ import logging import requests -from homeassistant.components import ios from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, @@ -15,6 +14,8 @@ from homeassistant.components.notify import ( ) import homeassistant.util.dt as dt_util +from .. import ios + _LOGGER = logging.getLogger(__name__) PUSH_URL = "https://ios-push.home-assistant.io/push" diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index a97510d364f..397d829f36d 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,7 +1,6 @@ """Support for Home Assistant iOS app sensors.""" from __future__ import annotations -from homeassistant.components import ios from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE @@ -12,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .. import ios from .const import DOMAIN SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index fc924aa4873..07d03c3debd 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,7 +1,6 @@ """Support for MySensors binary sensors.""" from __future__ import annotations -from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, BinarySensorDeviceClass, @@ -13,6 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .. import mysensors from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 6a6640f7bdd..3e540bd5714 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any -from homeassistant.components import mysensors from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, @@ -22,6 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .. import mysensors from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index f1d7a4cdbf4..a1b2cb303ed 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -4,7 +4,6 @@ from __future__ import annotations from enum import Enum, unique from typing import Any -from homeassistant.components import mysensors from homeassistant.components.cover import ATTR_POSITION, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, Platform @@ -12,6 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .. import mysensors from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index 3c204776b7d..a3fd86d1b44 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any, cast -from homeassistant.components import mysensors from homeassistant.components.device_tracker import AsyncSeeCallback from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -11,6 +10,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify +from .. import mysensors from .const import ATTR_GATEWAY_ID, DevId, DiscoveryInfo, GatewayId from .helpers import on_unload diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index e2c676220fa..e7ccdf8c569 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any, cast -from homeassistant.components import mysensors from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_RGB_COLOR, @@ -18,6 +17,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import rgb_hex_to_rgb_list +from .. import mysensors from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType from .device import MySensorsDevice from .helpers import on_unload diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py index 042abe1515a..43f4779604d 100644 --- a/homeassistant/components/mysensors/notify.py +++ b/homeassistant/components/mysensors/notify.py @@ -3,11 +3,11 @@ from __future__ import annotations from typing import Any -from homeassistant.components import mysensors from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from .. import mysensors from .const import DevId, DiscoveryInfo diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index ecc42c5a0bf..6f940c5d625 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -5,7 +5,6 @@ from typing import Any from awesomeversion import AwesomeVersion -from homeassistant.components import mysensors from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -37,6 +36,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .. import mysensors from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index d8492e5e4a6..dd79c6819ec 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -5,7 +5,6 @@ from typing import Any import voluptuous as vol -from homeassistant.components import mysensors from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform @@ -14,6 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .. import mysensors from .const import ( DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, diff --git a/homeassistant/components/pilight/binary_sensor.py b/homeassistant/components/pilight/binary_sensor.py index 0ee6ff1b38e..303c755a035 100644 --- a/homeassistant/components/pilight/binary_sensor.py +++ b/homeassistant/components/pilight/binary_sensor.py @@ -5,7 +5,6 @@ import datetime import voluptuous as vol -from homeassistant.components import pilight from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ( CONF_DISARM_AFTER_TRIGGER, @@ -21,6 +20,8 @@ from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util +from .. import pilight + CONF_VARIABLE = "variable" CONF_RESET_DELAY_SEC = "reset_delay_sec" diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 0f707489e9a..21036a94210 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -5,7 +5,6 @@ import logging import voluptuous as vol -from homeassistant.components import pilight from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -13,6 +12,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .. import pilight + _LOGGER = logging.getLogger(__name__) CONF_VARIABLE = "variable" diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index e1eca282a3a..7e875a5ff93 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -17,7 +17,6 @@ from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Subquery -from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE, @@ -25,6 +24,7 @@ from homeassistant.components.websocket_api.const import ( from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util +from .. import recorder from .db_schema import RecorderRuns, StateAttributes, States from .filters import Filters from .models import ( diff --git a/homeassistant/components/tellduslive/binary_sensor.py b/homeassistant/components/tellduslive/binary_sensor.py index e8e24f05c89..1e7a30d6174 100644 --- a/homeassistant/components/tellduslive/binary_sensor.py +++ b/homeassistant/components/tellduslive/binary_sensor.py @@ -1,11 +1,12 @@ """Support for binary sensors using Tellstick Net.""" -from homeassistant.components import binary_sensor, tellduslive +from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .. import tellduslive from .entry import TelldusLiveEntity diff --git a/homeassistant/components/tellduslive/cover.py b/homeassistant/components/tellduslive/cover.py index 829478fc990..57da852a356 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -1,7 +1,7 @@ """Support for Tellstick covers using Tellstick Net.""" from typing import Any -from homeassistant.components import cover, tellduslive +from homeassistant.components import cover from homeassistant.components.cover import CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -9,6 +9,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TelldusLiveClient +from .. import tellduslive from .entry import TelldusLiveEntity diff --git a/homeassistant/components/tellduslive/light.py b/homeassistant/components/tellduslive/light.py index 205fff840d6..3b69b58966c 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -2,13 +2,14 @@ import logging from typing import Any -from homeassistant.components import light, tellduslive +from homeassistant.components import light from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .. import tellduslive from .entry import TelldusLiveEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 00949fc41b8..8d02763d428 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -1,7 +1,7 @@ """Support for Tellstick Net/Telstick Live sensors.""" from __future__ import annotations -from homeassistant.components import sensor, tellduslive +from homeassistant.components import sensor from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -23,6 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .. import tellduslive from .entry import TelldusLiveEntity SENSOR_TYPE_TEMPERATURE = "temp" diff --git a/homeassistant/components/tellduslive/switch.py b/homeassistant/components/tellduslive/switch.py index 78b31e8fea7..a1bb2ffc9e1 100644 --- a/homeassistant/components/tellduslive/switch.py +++ b/homeassistant/components/tellduslive/switch.py @@ -1,11 +1,12 @@ """Support for Tellstick switches using Tellstick Net.""" -from homeassistant.components import switch, tellduslive +from homeassistant.components import switch from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .. import tellduslive from .entry import TelldusLiveEntity diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py index d45265c2989..fb9357cc067 100644 --- a/homeassistant/components/trace/websocket_api.py +++ b/homeassistant/components/trace/websocket_api.py @@ -3,7 +3,7 @@ import json import voluptuous as vol -from homeassistant.components import trace, websocket_api +from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import ( @@ -24,6 +24,8 @@ from homeassistant.helpers.script import ( debug_stop, ) +from .. import trace + # mypy: allow-untyped-calls, allow-untyped-defs TRACE_DOMAINS = ("automation", "script") diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 6d1b0b186d1..3e72e4d71f1 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -5,7 +5,6 @@ import logging import voluptuous as vol -from homeassistant.components import zabbix from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -13,6 +12,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .. import zabbix + _LOGGER = logging.getLogger(__name__) _CONF_TRIGGERS = "triggers" diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 62fd262eacc..6068e1c2baf 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -329,7 +329,13 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] f"{self.current_package}." ): self.add_message("hass-relative-import", node=node) - elif obsolete_imports := _OBSOLETE_IMPORT.get(node.modname): + return + if self.current_package.startswith("homeassistant.components") and node.modname == "homeassistant.components": + for name in node.names: + if name[0] == self.current_package.split(".")[2]: + self.add_message("hass-relative-import", node=node) + return + if obsolete_imports := _OBSOLETE_IMPORT.get(node.modname): for name_tuple in node.names: for obsolete_import in obsolete_imports: if import_match := obsolete_import.constant.match(name_tuple[0]): diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py index 6367427eea7..d1ba7fe4a7f 100644 --- a/tests/pylint/test_imports.py +++ b/tests/pylint/test_imports.py @@ -84,6 +84,12 @@ def test_good_import( "CONSTANT", "hass-absolute-import", ), + ( + "homeassistant.components.pylint_test.api.hub", + "homeassistant.components", + "pylint_test", + "hass-relative-import", + ), ], ) def test_bad_import( @@ -111,7 +117,7 @@ def test_bad_import( line=1, col_offset=0, end_line=1, - end_col_offset=len(import_from) + 21, + end_col_offset=len(import_from) + len(import_what) + 13, ), ): imports_checker.visit_importfrom(import_node) From 0c401bcab23123829dd4f5f6592c4d8ea916a222 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Aug 2022 10:20:55 +0200 Subject: [PATCH 712/903] Use _attr_temperature_unit in climate entities (#77472) --- homeassistant/components/daikin/climate.py | 7 ++----- homeassistant/components/esphome/climate.py | 7 ++----- homeassistant/components/fritzbox/climate.py | 6 +----- homeassistant/components/homematic/climate.py | 6 +----- .../components/homematicip_cloud/climate.py | 6 +----- homeassistant/components/incomfort/climate.py | 6 +----- homeassistant/components/intesishome/climate.py | 6 +----- homeassistant/components/izone/climate.py | 12 ++---------- homeassistant/components/melissa/climate.py | 6 +----- homeassistant/components/oem/climate.py | 6 +----- homeassistant/components/opentherm_gw/climate.py | 6 +----- homeassistant/components/proliphix/climate.py | 6 +----- homeassistant/components/schluter/climate.py | 6 +----- homeassistant/components/smarttub/climate.py | 6 +----- homeassistant/components/spider/climate.py | 7 ++----- homeassistant/components/stiebel_eltron/climate.py | 5 +---- homeassistant/components/tado/climate.py | 7 ++----- homeassistant/components/tfiac/climate.py | 6 +----- homeassistant/components/touchline/climate.py | 6 +----- homeassistant/components/vicare/climate.py | 6 +----- homeassistant/components/zha/climate.py | 7 ++----- homeassistant/components/zhong_hong/climate.py | 6 +----- 22 files changed, 28 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index d7bfb8cf7a0..2f07c5a0bdc 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -115,6 +115,8 @@ def format_target_temperature(target_temperature): class DaikinClimate(ClimateEntity): """Representation of a Daikin HVAC.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, api: DaikinApi) -> None: """Initialize the climate device.""" @@ -180,11 +182,6 @@ class DaikinClimate(ClimateEntity): """Return a unique ID.""" return self._api.device.mac - @property - def temperature_unit(self) -> str: - """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS - @property def current_temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 1552bd3775b..aba51a47a9d 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -136,6 +136,8 @@ _PRESETS: EsphomeEnumMapper[ClimatePreset, str] = EsphomeEnumMapper( class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEntity): """A climate implementation for ESPHome.""" + _attr_temperature_unit = TEMP_CELSIUS + @property def precision(self) -> float: """Return the precision of the climate device.""" @@ -146,11 +148,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti # Fall back to highest precision, tenths return PRECISION_TENTHS - @property - def temperature_unit(self) -> str: - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def hvac_modes(self) -> list[str]: """Return the list of available operation modes.""" diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 5d96cb95c15..20331459c3e 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -67,11 +67,7 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement that is used.""" - return TEMP_CELSIUS + _attr_temperature_unit = TEMP_CELSIUS @property def precision(self) -> float: diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 67a36284c58..78a03a28a4a 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -56,11 +56,7 @@ class HMThermostat(HMDevice, ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) - - @property - def temperature_unit(self): - """Return the unit of measurement that is used.""" - return TEMP_CELSIUS + _attr_temperature_unit = TEMP_CELSIUS @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 23da237331b..802cacb1d76 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -66,6 +66,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: """Initialize heating group.""" @@ -86,11 +87,6 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): via_device=(HMIPC_DOMAIN, self._device.homeId), ) - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def target_temperature(self) -> float: """Return the temperature we try to reach.""" diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index c76f16093ee..aaeab394f75 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -37,6 +37,7 @@ class InComfortClimate(IncomfortChild, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, client, heater, room) -> None: """Initialize the climate device.""" @@ -54,11 +55,6 @@ class InComfortClimate(IncomfortChild, ClimateEntity): """Return the device state attributes.""" return {"status": self._room.status} - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def current_temperature(self) -> float | None: """Return the current temperature.""" diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 61b171ea8cc..925147e82ad 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -144,6 +144,7 @@ class IntesisAC(ClimateEntity): """Represents an Intesishome air conditioning device.""" _attr_should_poll = False + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, ih_device_id, ih_device, controller): """Initialize the thermostat.""" @@ -218,11 +219,6 @@ class IntesisAC(ClimateEntity): """Return the name of the AC device.""" return self._device_name - @property - def temperature_unit(self): - """Intesishome API uses celsius on the backend.""" - return TEMP_CELSIUS - @property def extra_state_attributes(self): """Return the device specific state attributes.""" diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index df7b8af4fa3..8ff1593d5ff 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -127,6 +127,7 @@ class ControllerDevice(ClimateEntity): """Representation of iZone Controller.""" _attr_should_poll = False + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, controller: Controller) -> None: """Initialise ControllerDevice.""" @@ -252,11 +253,6 @@ class ControllerDevice(ClimateEntity): """Return the name of the entity.""" return f"iZone Controller {self._controller.device_uid}" - @property - def temperature_unit(self) -> str: - """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS - @property def precision(self) -> float: """Return the precision of the system.""" @@ -443,6 +439,7 @@ class ZoneDevice(ClimateEntity): """Representation of iZone Zone.""" _attr_should_poll = False + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, controller: ControllerDevice, zone: Zone) -> None: """Initialise ZoneDevice.""" @@ -529,11 +526,6 @@ class ZoneDevice(ClimateEntity): return self._attr_supported_features return self._attr_supported_features & ~ClimateEntityFeature.TARGET_TEMPERATURE - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS - @property def precision(self): """Return the precision of the system.""" diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 81ffd9a5d1e..7dae7c2dad6 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -58,6 +58,7 @@ class MelissaClimate(ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, api, serial_number, init_data): """Initialize the climate device.""" @@ -124,11 +125,6 @@ class MelissaClimate(ClimateEntity): return None return self._cur_settings[self._api.TEMP] - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS - @property def min_temp(self): """Return the minimum supported temperature for the thermostat.""" diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index 846113fb586..298ae965e17 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -64,6 +64,7 @@ class ThermostatDevice(ClimateEntity): _attr_hvac_modes = SUPPORT_HVAC _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, thermostat, name): """Initialize the device.""" @@ -93,11 +94,6 @@ class ThermostatDevice(ClimateEntity): """Return the name of this Thermostat.""" return self._name - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def hvac_action(self) -> HVACAction: """Return current hvac i.e. heat, cool, idle.""" diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index fecc99a4cca..a805cbacba0 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -66,6 +66,7 @@ class OpenThermClimate(ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, gw_dev, options): """Initialize the device.""" @@ -202,11 +203,6 @@ class OpenThermClimate(ClimateEntity): return PRECISION_HALVES return PRECISION_WHOLE - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def hvac_action(self) -> HVACAction | None: """Return current HVAC operation.""" diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index e47aa0fc8f0..52ed3bb8bcd 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -55,6 +55,7 @@ class ProliphixThermostat(ClimateEntity): """Representation a Proliphix thermostat.""" _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_temperature_unit = TEMP_FAHRENHEIT def __init__(self, pdp): """Initialize the thermostat.""" @@ -85,11 +86,6 @@ class ProliphixThermostat(ClimateEntity): """Return the device specific state attributes.""" return {ATTR_FAN: self._pdp.fan_state} - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - @property def current_temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index 5c848d7f7ea..3ccea458960 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -82,6 +82,7 @@ class SchluterThermostat(CoordinatorEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, coordinator, serial_number, api, session_id): """Initialize the thermostat.""" @@ -100,11 +101,6 @@ class SchluterThermostat(CoordinatorEntity, ClimateEntity): """Return the name of the thermostat.""" return self.coordinator.data[self._serial_number].name - @property - def temperature_unit(self): - """Schluter API always uses celsius.""" - return TEMP_CELSIUS - @property def current_temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index 8789c33bc9a..78d1d7b2495 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -61,16 +61,12 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, coordinator, spa): """Initialize the entity.""" super().__init__(coordinator, spa, "Thermostat") - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def hvac_action(self) -> HVACAction | None: """Return the current running hvac operation.""" diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index e80ec77592b..fa04cbbe058 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -35,6 +35,8 @@ async def async_setup_entry( class SpiderThermostat(ClimateEntity): """Representation of a thermostat.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, api, thermostat): """Initialize the thermostat.""" self.api = api @@ -75,11 +77,6 @@ class SpiderThermostat(ClimateEntity): """Return the name of the thermostat, if any.""" return self.thermostat.name - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def current_temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index 1aab76813ee..4d63820899e 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -74,6 +74,7 @@ class StiebelEltron(ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, name, ste_data): """Initialize the unit.""" @@ -112,10 +113,6 @@ class StiebelEltron(ClimateEntity): return self._name # Handle ClimateEntityFeature.TARGET_TEMPERATURE - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS @property def current_temperature(self): diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 87ca302f029..ae9c2097f75 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -213,6 +213,8 @@ def create_climate_entity(tado, name: str, zone_id: int, device_info: dict): class TadoClimate(TadoZoneEntity, ClimateEntity): """Representation of a Tado climate entity.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__( self, tado, @@ -367,11 +369,6 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): """Set new preset mode.""" self._tado.set_presence(preset_mode) - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def target_temperature_step(self): """Return the supported step of target temperature.""" diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 937723440f5..58f16d1cc99 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -82,6 +82,7 @@ class TfiacClimate(ClimateEntity): | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) + _attr_temperature_unit = TEMP_FAHRENHEIT def __init__(self, hass, client): """Init class.""" @@ -121,11 +122,6 @@ class TfiacClimate(ClimateEntity): """Return the temperature we try to reach.""" return self._client.status["target_temp"] - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - @property def current_temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 8c5eca09d3d..78fd00b734c 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -64,6 +64,7 @@ class Touchline(ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, touchline_thermostat): """Initialize the Touchline device.""" @@ -89,11 +90,6 @@ class Touchline(ClimateEntity): """Return the name of the climate device.""" return self._name - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def current_temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 8f00f9e6c3b..94f7b8d4e5d 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -145,6 +145,7 @@ class ViCareClimate(ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, name, api, circuit, device_config, heating_type): """Initialize the climate device.""" @@ -249,11 +250,6 @@ class ViCareClimate(ClimateEntity): """Return the name of the climate device.""" return self._name - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def current_temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index d8b2f0db3af..416d0c81483 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -137,6 +137,8 @@ class Thermostat(ZhaEntity, ClimateEntity): DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_TEMP = 7 + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize ZHA Thermostat instance.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -334,11 +336,6 @@ class Thermostat(ZhaEntity, ClimateEntity): return temp return round(temp / ZCL_TEMP, 1) - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def max_temp(self) -> float: """Return the maximum temperature.""" diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 9ceae1fea72..0877e19834f 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -128,6 +128,7 @@ class ZhongHongClimate(ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, hub, addr_out, addr_in): """Set up the ZhongHong climate devices.""" @@ -171,11 +172,6 @@ class ZhongHongClimate(ClimateEntity): """Return the unique ID of the HVAC.""" return f"zhong_hong_hvac_{self._device.addr_out}_{self._device.addr_in}" - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def hvac_mode(self) -> HVACMode: """Return current operation ie. heat, cool, idle.""" From 3846efecc594ec89a261178fa8593622e48fc2a4 Mon Sep 17 00:00:00 2001 From: Anil Daoud Date: Mon, 29 Aug 2022 17:40:24 +0900 Subject: [PATCH 713/903] Handle kaiterra ClientConnectorError exception (#77428) * Update api_data.py add ClientConnectorError exception * Update api_data.py fix ClientConnectorError exception handling * Update api_data.py import in alphabetical order and better exception logging --- homeassistant/components/kaiterra/api_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index 53cc89e708e..980c01d02a1 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -2,7 +2,7 @@ import asyncio from logging import getLogger -from aiohttp.client_exceptions import ClientResponseError +from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError import async_timeout from kaiterra_async_client import AQIStandard, KaiterraAPIClient, Units @@ -55,8 +55,8 @@ class KaiterraApiData: try: async with async_timeout.timeout(10): data = await self._api.get_latest_sensor_readings(self._devices) - except (ClientResponseError, asyncio.TimeoutError): - _LOGGER.debug("Couldn't fetch data from Kaiterra API") + except (ClientResponseError, ClientConnectorError, asyncio.TimeoutError) as err: + _LOGGER.debug("Couldn't fetch data from Kaiterra API: %s", err) self.data = {} async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) return From bf01b5a466a9b84827d92ae2b0851c6ce2d0d7fa Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 29 Aug 2022 11:40:47 +0300 Subject: [PATCH 714/903] Import issue_registry from helpers for speedtestdotnet (#77467) import issue_registry from helpers for speedtestdotnet --- homeassistant/components/speedtestdotnet/__init__.py | 3 +-- homeassistant/components/speedtestdotnet/manifest.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 0f236402eb4..17238f74810 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -6,12 +6,11 @@ import logging import speedtest -from homeassistant.components.repairs.issue_handler import async_create_issue -from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index 04400c14781..be2aaad7e02 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -3,7 +3,7 @@ "name": "Speedtest.net", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", - "dependencies": ["repairs"], + "dependencies": [], "requirements": ["speedtest-cli==2.1.3"], "codeowners": ["@rohankapoorcom", "@engrbm87"], "iot_class": "cloud_polling" From 8c41d0d3d712954eae142f84ae3b03730418537f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Aug 2022 04:02:41 -0500 Subject: [PATCH 715/903] Ensure LIFX connection is cleaned up on failure (#77465) Fixes #77464 --- homeassistant/components/lifx/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index ec54382ec40..6af30b91d28 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -193,10 +193,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await connection.async_setup() except socket.gaierror as ex: + connection.async_stop() raise ConfigEntryNotReady(f"Could not resolve {host}: {ex}") from ex coordinator = LIFXUpdateCoordinator(hass, connection, entry.title) coordinator.async_setup() - await coordinator.async_config_entry_first_refresh() + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + connection.async_stop() + raise domain_data[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) From 01c200e11d4609381cd124d06fc7d2c1390ec06c Mon Sep 17 00:00:00 2001 From: Doug Hoffman Date: Mon, 29 Aug 2022 06:19:44 -0400 Subject: [PATCH 716/903] Fix issue caused by restoring datetime value from mobile app (#77462) * Only pass strings to dt_util.parse_datetime() * Update homeassistant/components/mobile_app/sensor.py * Update sensor.py Co-authored-by: Erik Montnemery --- homeassistant/components/mobile_app/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index ef7dd122496..d802dc92b52 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -110,6 +110,9 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): SensorDeviceClass.DATE, SensorDeviceClass.TIMESTAMP, ) + # Only parse strings: if the sensor's state is restored, the state is a + # native date or datetime, not str + and isinstance(state, str) and (timestamp := dt_util.parse_datetime(state)) is not None ): if self.device_class == SensorDeviceClass.DATE: From d47edd5a34ac044655c41263c24d94bbc7a6b2e2 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Mon, 29 Aug 2022 21:30:35 +0800 Subject: [PATCH 717/903] Bump pizone version (#77257) --- homeassistant/components/izone/discovery.py | 4 +--- homeassistant/components/izone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index 04b0760261e..eb6e7d4a190 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -59,9 +59,7 @@ async def async_start_discovery_service(hass: HomeAssistant): # Start the pizone discovery service, disco is the listener session = aiohttp_client.async_get_clientsession(hass) - loop = hass.loop - - disco.pi_disco = pizone.discovery(disco, loop=loop, session=session) + disco.pi_disco = pizone.discovery(disco, session=session) await disco.pi_disco.start_discovery() async def shutdown_event(event): diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index b86e86e2b58..4a225ab7cdb 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -2,7 +2,7 @@ "domain": "izone", "name": "iZone", "documentation": "https://www.home-assistant.io/integrations/izone", - "requirements": ["python-izone==1.2.3"], + "requirements": ["python-izone==1.2.9"], "codeowners": ["@Swamp-Ig"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index d3246a5aa83..37c9595143a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1939,7 +1939,7 @@ python-homewizard-energy==1.1.0 python-hpilo==4.3 # homeassistant.components.izone -python-izone==1.2.3 +python-izone==1.2.9 # homeassistant.components.joaoapps_join python-join-api==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fa0fe132f0..60caeaf7c6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1332,7 +1332,7 @@ python-fullykiosk==0.0.11 python-homewizard-energy==1.1.0 # homeassistant.components.izone -python-izone==1.2.3 +python-izone==1.2.9 # homeassistant.components.juicenet python-juicenet==1.1.0 From 2e3a2d29e528fe95287c28d78df1aa24c1d59133 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Aug 2022 15:52:17 +0200 Subject: [PATCH 718/903] Finish update of integrations to import issue_registry from helpers (#77473) --- homeassistant/components/automation/manifest.json | 2 +- homeassistant/components/repairs/issue_handler.py | 3 +-- homeassistant/components/repairs/models.py | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 6d1e4ee6027..9dd0130ee2f 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -2,7 +2,7 @@ "domain": "automation", "name": "Automation", "documentation": "https://www.home-assistant.io/integrations/automation", - "dependencies": ["blueprint", "repairs", "trace"], + "dependencies": ["blueprint", "trace"], "after_dependencies": ["device_automation", "webhook"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 1201497f0c1..35a8d9cc49d 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -13,8 +13,7 @@ from homeassistant.helpers.integration_platform import ( ) # pylint: disable-next=unused-import -from homeassistant.helpers.issue_registry import ( # noqa: F401; Remove when integrations have been updated - async_create_issue, +from homeassistant.helpers.issue_registry import ( async_delete_issue, async_get as async_get_issue_registry, ) diff --git a/homeassistant/components/repairs/models.py b/homeassistant/components/repairs/models.py index 045b7bd55dc..6ae175b29e9 100644 --- a/homeassistant/components/repairs/models.py +++ b/homeassistant/components/repairs/models.py @@ -6,11 +6,6 @@ from typing import Protocol from homeassistant import data_entry_flow from homeassistant.core import HomeAssistant -# pylint: disable-next=unused-import -from homeassistant.helpers.issue_registry import ( # noqa: F401; Remove when integrations have been updated - IssueSeverity, -) - class RepairsFlow(data_entry_flow.FlowHandler): """Handle a flow for fixing an issue.""" From 6b9c4c7ec19e581a6b6855b2987f15ec6d2fd656 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Aug 2022 15:57:40 +0200 Subject: [PATCH 719/903] Tweak comment about humidity sensors (#77482) --- homeassistant/components/sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 6d35c2a4635..562b50bbc61 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -113,7 +113,7 @@ class SensorDeviceClass(StrEnum): # gas (m³ or ft³) GAS = "gas" - # % of humidity in the air + # Relative humidity (%) HUMIDITY = "humidity" # current light level (lx/lm) From 40e8979951f8e9bca9d153349642f95bacc8d0da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Aug 2022 09:21:30 -0500 Subject: [PATCH 720/903] Add bluetooth api to get the count of connectable and non-connectable scanners (#77427) --- homeassistant/components/bluetooth/__init__.py | 7 +++++++ homeassistant/components/bluetooth/manager.py | 16 +++++++++++++--- tests/components/bluetooth/test_init.py | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 632635f7dbc..8ba3a503a30 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -60,6 +60,7 @@ __all__ = [ "async_rediscover_address", "async_register_callback", "async_track_unavailable", + "async_scanner_count", "BaseHaScanner", "BluetoothServiceInfo", "BluetoothServiceInfoBleak", @@ -86,6 +87,12 @@ def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: return HaBleakScannerWrapper() +@hass_callback +def async_scanner_count(hass: HomeAssistant, connectable: bool = True) -> int: + """Return the number of scanners currently in use.""" + return _get_manager(hass).async_scanner_count(connectable) + + @hass_callback def async_discovered_service_info( hass: HomeAssistant, connectable: bool = True diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index b1193c47245..d2b59469bd9 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -144,7 +144,7 @@ class BluetoothManager: ] = [] self._history: dict[str, BluetoothServiceInfoBleak] = {} self._connectable_history: dict[str, BluetoothServiceInfoBleak] = {} - self._scanners: list[BaseHaScanner] = [] + self._non_connectable_scanners: list[BaseHaScanner] = [] self._connectable_scanners: list[BaseHaScanner] = [] self._adapters: dict[str, AdapterDetails] = {} @@ -153,13 +153,19 @@ class BluetoothManager: """Return if passive scan is supported.""" return any(adapter[ADAPTER_PASSIVE_SCAN] for adapter in self._adapters.values()) + def async_scanner_count(self, connectable: bool = True) -> int: + """Return the number of scanners.""" + if connectable: + return len(self._connectable_scanners) + return len(self._connectable_scanners) + len(self._non_connectable_scanners) + async def async_diagnostics(self) -> dict[str, Any]: """Diagnostics for the manager.""" scanner_diagnostics = await asyncio.gather( *[ scanner.async_diagnostics() for scanner in itertools.chain( - self._scanners, self._connectable_scanners + self._non_connectable_scanners, self._connectable_scanners ) ] ) @@ -408,7 +414,11 @@ class BluetoothManager: def _get_scanners_by_type(self, connectable: bool) -> list[BaseHaScanner]: """Return the scanners by type.""" - return self._connectable_scanners if connectable else self._scanners + return ( + self._connectable_scanners + if connectable + else self._non_connectable_scanners + ) def _get_unavailable_callbacks_by_type( self, connectable: bool diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a005a71f048..ade68fdb94d 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -2333,6 +2333,22 @@ async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_blu assert isinstance(scanner, models.HaBleakScannerWrapper) +async def test_scanner_count_connectable(hass, enable_bluetooth): + """Test getting the connectable scanner count.""" + scanner = models.BaseHaScanner() + cancel = bluetooth.async_register_scanner(hass, scanner, False) + assert bluetooth.async_scanner_count(hass, connectable=True) == 1 + cancel() + + +async def test_scanner_count(hass, enable_bluetooth): + """Test getting the connectable and non-connectable scanner count.""" + scanner = models.BaseHaScanner() + cancel = bluetooth.async_register_scanner(hass, scanner, False) + assert bluetooth.async_scanner_count(hass, connectable=False) == 2 + cancel() + + async def test_migrate_single_entry_macos( hass, mock_bleak_scanner_start, macos_adapter ): From 795691038f3d7c6e0977f593ed7de8acfa12f236 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Aug 2022 09:30:09 -0500 Subject: [PATCH 721/903] Add light platform to switchbot (#77430) --- .coveragerc | 1 + .../components/switchbot/__init__.py | 10 +- homeassistant/components/switchbot/const.py | 12 ++- .../components/switchbot/coordinator.py | 9 +- homeassistant/components/switchbot/entity.py | 33 ++++++- homeassistant/components/switchbot/light.py | 98 +++++++++++++++++++ .../components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/switchbot/light.py diff --git a/.coveragerc b/.coveragerc index af27bb86d66..365f64076b3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1205,6 +1205,7 @@ omit = homeassistant/components/switchbot/const.py homeassistant/components/switchbot/entity.py homeassistant/components/switchbot/cover.py + homeassistant/components/switchbot/light.py homeassistant/components/switchbot/sensor.py homeassistant/components/switchbot/coordinator.py homeassistant/components/switchmate/switch.py diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 3f63a507e52..59ed071f325 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -23,12 +23,14 @@ from .const import ( CONNECTABLE_SUPPORTED_MODEL_TYPES, DEFAULT_RETRY_COUNT, DOMAIN, + HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL, SupportedModels, ) from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { - SupportedModels.BULB.value: [Platform.SENSOR], + SupportedModels.BULB.value: [Platform.SENSOR, Platform.LIGHT], + SupportedModels.LIGHT_STRIP.value: [Platform.SENSOR, Platform.LIGHT], SupportedModels.BOT.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.PLUG.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.CURTAIN.value: [ @@ -44,6 +46,8 @@ CLASS_BY_DEVICE = { SupportedModels.CURTAIN.value: switchbot.SwitchbotCurtain, SupportedModels.BOT.value: switchbot.Switchbot, SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini, + SupportedModels.BULB.value: switchbot.SwitchbotBulb, + SupportedModels.LIGHT_STRIP.value: switchbot.SwitchbotLightStrip, } @@ -72,8 +76,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) sensor_type: str = entry.data[CONF_SENSOR_TYPE] + switchbot_model = HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL[sensor_type] # connectable means we can make connections to the device - connectable = sensor_type in CONNECTABLE_SUPPORTED_MODEL_TYPES.values() + connectable = switchbot_model in CONNECTABLE_SUPPORTED_MODEL_TYPES address: str = entry.data[CONF_ADDRESS] ble_device = bluetooth.async_ble_device_from_address( hass, address.upper(), connectable @@ -97,6 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.unique_id, entry.data.get(CONF_NAME, entry.title), connectable, + switchbot_model, ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index ad06dc7efcf..aa334120b85 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -18,6 +18,7 @@ class SupportedModels(StrEnum): BULB = "bulb" CURTAIN = "curtain" HYGROMETER = "hygrometer" + LIGHT_STRIP = "light_strip" CONTACT = "contact" PLUG = "plug" MOTION = "motion" @@ -28,6 +29,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.CURTAIN: SupportedModels.CURTAIN, SwitchbotModel.PLUG_MINI: SupportedModels.PLUG, SwitchbotModel.COLOR_BULB: SupportedModels.BULB, + SwitchbotModel.LIGHT_STRIP: SupportedModels.LIGHT_STRIP, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -36,9 +38,13 @@ NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION, } -SUPPORTED_MODEL_TYPES = { - **CONNECTABLE_SUPPORTED_MODEL_TYPES, - **NON_CONNECTABLE_SUPPORTED_MODEL_TYPES, +SUPPORTED_MODEL_TYPES = ( + CONNECTABLE_SUPPORTED_MODEL_TYPES | NON_CONNECTABLE_SUPPORTED_MODEL_TYPES +) + + +HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = { + str(v): k for k, v in SUPPORTED_MODEL_TYPES.items() } # Config Defaults diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 8b56b2f282f..103e9d67c58 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -21,6 +21,8 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +DEVICE_STARTUP_TIMEOUT = 30 + def flatten_sensors_data(sensor): """Deconstruct SwitchBot library temp object C/Fº readings from dictionary.""" @@ -42,6 +44,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): base_unique_id: str, device_name: str, connectable: bool, + model: str, ) -> None: """Initialize global switchbot data updater.""" super().__init__( @@ -56,6 +59,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): self.data: dict[str, Any] = {} self.device_name = device_name self.base_unique_id = base_unique_id + self.model = model self._ready_event = asyncio.Event() @callback @@ -65,7 +69,6 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): change: bluetooth.BluetoothChange, ) -> None: """Handle a Bluetooth event.""" - super()._async_handle_bluetooth_event(service_info, change) if adv := switchbot.parse_advertisement_data( service_info.device, service_info.advertisement ): @@ -74,12 +77,12 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): self._ready_event.set() _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) self.device.update_from_advertisement(adv) - self.async_update_listeners() + super()._async_handle_bluetooth_event(service_info, change) async def async_wait_ready(self) -> bool: """Wait for the device to be ready.""" with contextlib.suppress(asyncio.TimeoutError): - async with async_timeout.timeout(55): + async with async_timeout.timeout(DEVICE_STARTUP_TIMEOUT): await self._ready_event.wait() return True return False diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index 2e5ba78dcc8..b8d08e74f5f 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -1,13 +1,17 @@ """An abstract class common to all Switchbot entities.""" from __future__ import annotations +from abc import abstractmethod from collections.abc import Mapping from typing import Any +from switchbot import SwitchbotDevice + from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, ) from homeassistant.const import ATTR_CONNECTIONS +from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo @@ -19,6 +23,7 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): """Generic entity encapsulating common features of Switchbot device.""" coordinator: SwitchbotDataUpdateCoordinator + _device: SwitchbotDevice def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the entity.""" @@ -31,7 +36,7 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_BLUETOOTH, self._address)}, manufacturer=MANUFACTURER, - model=self.data["modelName"], + model=coordinator.model, # Sometimes the modelName is missing from the advertisement data name=coordinator.device_name, ) if ":" not in self._address: @@ -54,3 +59,29 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): def extra_state_attributes(self) -> Mapping[Any, Any]: """Return the state attributes.""" return {"last_run_success": self._last_run_success} + + +class SwitchbotSubscribeEntity(SwitchbotEntity): + """Base class for Switchbot entities that use subscribe.""" + + @abstractmethod + def _async_update_attrs(self) -> None: + """Update the entity attributes.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle data update.""" + self._async_update_attrs() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove(self._device.subscribe(self._handle_coordinator_update)) + return await super().async_added_to_hass() + + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self._device.update() diff --git a/homeassistant/components/switchbot/light.py b/homeassistant/components/switchbot/light.py new file mode 100644 index 00000000000..e55f5fff9b1 --- /dev/null +++ b/homeassistant/components/switchbot/light.py @@ -0,0 +1,98 @@ +"""Switchbot integration light platform.""" +from __future__ import annotations + +from typing import Any + +from switchbot import ColorMode as SwitchBotColorMode, SwitchbotBaseLight + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, + ColorMode, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) + +from .const import DOMAIN +from .coordinator import SwitchbotDataUpdateCoordinator +from .entity import SwitchbotSubscribeEntity + +SWITCHBOT_COLOR_MODE_TO_HASS = { + SwitchBotColorMode.RGB: ColorMode.RGB, + SwitchBotColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP, +} + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the switchbot light.""" + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([SwitchbotLightEntity(coordinator)]) + + +class SwitchbotLightEntity(SwitchbotSubscribeEntity, LightEntity): + """Representation of switchbot light bulb.""" + + _device: SwitchbotBaseLight + + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: + """Initialize the Switchbot light.""" + super().__init__(coordinator) + device = self._device + self._attr_min_mireds = color_temperature_kelvin_to_mired(device.max_temp) + self._attr_max_mireds = color_temperature_kelvin_to_mired(device.min_temp) + self._attr_supported_color_modes = { + SWITCHBOT_COLOR_MODE_TO_HASS[mode] for mode in device.color_modes + } + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + device = self._device + self._attr_is_on = self._device.on + self._attr_brightness = max(0, min(255, round(device.brightness * 2.55))) + if device.color_mode == SwitchBotColorMode.COLOR_TEMP: + self._attr_color_temp = color_temperature_kelvin_to_mired(device.color_temp) + self._attr_color_mode = ColorMode.COLOR_TEMP + return + self._attr_rgb_color = device.rgb + self._attr_color_mode = ColorMode.RGB + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the light to turn on.""" + brightness = round(kwargs.get(ATTR_BRIGHTNESS, self.brightness) / 255 * 100) + + if ( + self.supported_color_modes + and ColorMode.COLOR_TEMP in self.supported_color_modes + and ATTR_COLOR_TEMP in kwargs + ): + color_temp = kwargs[ATTR_COLOR_TEMP] + kelvin = max(2700, min(6500, color_temperature_mired_to_kelvin(color_temp))) + await self._device.set_color_temp(brightness, kelvin) + return + if ATTR_RGB_COLOR in kwargs: + rgb = kwargs[ATTR_RGB_COLOR] + await self._device.set_rgb(brightness, rgb[0], rgb[1], rgb[2]) + return + if ATTR_BRIGHTNESS in kwargs: + await self._device.set_brightness(brightness) + return + await self._device.turn_on() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the light to turn off.""" + await self._device.turn_off() diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 5011ed7e306..cda3f958f5c 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.14"], + "requirements": ["PySwitchbot==0.18.21"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 37c9595143a..3a52b694167 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.14 +PySwitchbot==0.18.21 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60caeaf7c6a..244adebff53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.14 +PySwitchbot==0.18.21 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 2e8d598795aa80cab697f0116dfe36199de2433a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:42:01 -0400 Subject: [PATCH 722/903] Allow ZHA startup to fail instead of raising `ConfigEntryNotReady` (#77417) * Retry startup within ZHA instead of raising `ConfigEntryNotReady` * Add unit tests * Disable pylint warning for intentional broad except --- homeassistant/components/zha/core/const.py | 4 ++ homeassistant/components/zha/core/gateway.py | 36 +++++++++----- tests/components/zha/test_gateway.py | 50 +++++++++++++++++++- 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index dfa5f608cfe..4a48c254b40 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -408,3 +408,7 @@ class Strobe(t.enum8): No_Strobe = 0x00 Strobe = 0x01 + + +STARTUP_FAILURE_DELAY_S = 3 +STARTUP_RETRIES = 3 diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 14fbf2cf701..5261396c794 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -13,7 +13,6 @@ import time import traceback from typing import TYPE_CHECKING, Any, NamedTuple, Union -from serial import SerialException from zigpy.application import ControllerApplication from zigpy.config import CONF_DEVICE import zigpy.device @@ -25,7 +24,6 @@ from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo @@ -62,6 +60,8 @@ from .const import ( SIGNAL_ADD_ENTITIES, SIGNAL_GROUP_MEMBERSHIP_CHANGE, SIGNAL_REMOVE, + STARTUP_FAILURE_DELAY_S, + STARTUP_RETRIES, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA_GW_MSG, @@ -166,17 +166,27 @@ class ZHAGateway: app_config[CONF_DEVICE] = self.config_entry.data[CONF_DEVICE] app_config = app_controller_cls.SCHEMA(app_config) - try: - self.application_controller = await app_controller_cls.new( - app_config, auto_form=True, start_radio=True - ) - except (asyncio.TimeoutError, SerialException, OSError) as exception: - _LOGGER.error( - "Couldn't start %s coordinator", - self.radio_description, - exc_info=exception, - ) - raise ConfigEntryNotReady from exception + + for attempt in range(STARTUP_RETRIES): + try: + self.application_controller = await app_controller_cls.new( + app_config, auto_form=True, start_radio=True + ) + except Exception as exc: # pylint: disable=broad-except + _LOGGER.warning( + "Couldn't start %s coordinator (attempt %s of %s)", + self.radio_description, + attempt + 1, + STARTUP_RETRIES, + exc_info=exc, + ) + + if attempt == STARTUP_RETRIES - 1: + raise exc + + await asyncio.sleep(STARTUP_FAILURE_DELAY_S) + else: + break self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 3c8c3e78c0e..bc49b04d86a 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,6 +1,6 @@ """Test ZHA Gateway.""" import asyncio -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest import zigpy.profiles.zha as zha @@ -211,3 +211,51 @@ async def test_gateway_create_group_with_id(hass, device_light_1, coordinator): assert len(zha_group.members) == 1 assert zha_group.members[0].device is device_light_1 assert zha_group.group_id == 0x1234 + + +@patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", + MagicMock(), +) +@patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups", + MagicMock(), +) +@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) +@pytest.mark.parametrize( + "startup", + [ + [asyncio.TimeoutError(), FileNotFoundError(), MagicMock()], + [asyncio.TimeoutError(), MagicMock()], + [MagicMock()], + ], +) +async def test_gateway_initialize_success(startup, hass, device_light_1, coordinator): + """Test ZHA initializing the gateway successfully.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + + zha_gateway.shutdown = AsyncMock() + + with patch( + "bellows.zigbee.application.ControllerApplication.new", side_effect=startup + ) as mock_new: + await zha_gateway.async_initialize() + + assert mock_new.call_count == len(startup) + + +@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) +async def test_gateway_initialize_failure(hass, device_light_1, coordinator): + """Test ZHA failing to initialize the gateway.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + side_effect=[asyncio.TimeoutError(), FileNotFoundError(), RuntimeError()], + ) as mock_new: + with pytest.raises(RuntimeError): + await zha_gateway.async_initialize() + + assert mock_new.call_count == 3 From d4ae81d2bb7e94dc364433072da26eec430c5f0b Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 29 Aug 2022 09:48:24 -0600 Subject: [PATCH 723/903] Add support for Feeder-Robot sensors (#77395) --- .../components/litterrobot/button.py | 1 + .../components/litterrobot/entity.py | 19 ++++----- homeassistant/components/litterrobot/hub.py | 8 +++- .../components/litterrobot/sensor.py | 42 ++++++++++++++----- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 5cb65596ec6..7c7990edf07 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -35,6 +35,7 @@ async def async_setup_entry( class LitterRobotResetWasteDrawerButton(LitterRobotEntity, ButtonEntity): """Litter-Robot reset waste drawer button.""" + robot: LitterRobot3 _attr_icon = "mdi:delete-variant" _attr_entity_category = EntityCategory.CONFIG diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index b169e075455..c81e4ea4c79 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -6,7 +6,7 @@ from datetime import time import logging from typing import Any -from pylitterbot import LitterRobot +from pylitterbot import LitterRobot, Robot from pylitterbot.exceptions import InvalidCommandException from typing_extensions import ParamSpec @@ -23,7 +23,6 @@ from .const import DOMAIN from .hub import LitterRobotHub _P = ParamSpec("_P") - _LOGGER = logging.getLogger(__name__) REFRESH_WAIT_TIME_SECONDS = 8 @@ -32,9 +31,7 @@ REFRESH_WAIT_TIME_SECONDS = 8 class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): """Generic Litter-Robot entity representing common data and methods.""" - def __init__( - self, robot: LitterRobot, entity_type: str, hub: LitterRobotHub - ) -> None: + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.robot = robot @@ -60,15 +57,16 @@ class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): manufacturer="Litter-Robot", model=self.robot.model, name=self.robot.name, + sw_version=getattr(self.robot, "firmware", None), ) class LitterRobotControlEntity(LitterRobotEntity): """A Litter-Robot entity that can control the unit.""" - def __init__( - self, robot: LitterRobot, entity_type: str, hub: LitterRobotHub - ) -> None: + robot: LitterRobot + + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._refresh_callback: CALLBACK_TYPE | None = None @@ -135,11 +133,10 @@ class LitterRobotControlEntity(LitterRobotEntity): class LitterRobotConfigEntity(LitterRobotControlEntity): """A Litter-Robot entity that can control configuration of the unit.""" + robot: LitterRobot _attr_entity_category = EntityCategory.CONFIG - def __init__( - self, robot: LitterRobot, entity_type: str, hub: LitterRobotHub - ) -> None: + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._assumed_state: bool | None = None diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 627075208cc..8fab3346cec 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging from typing import Any -from pylitterbot import Account, LitterRobot +from pylitterbot import Account, FeederRobot, LitterRobot from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -62,3 +62,9 @@ class LitterRobotHub: return ( robot for robot in self.account.robots if isinstance(robot, LitterRobot) ) + + def feeder_robots(self) -> Generator[FeederRobot, Any, Any]: + """Get Feeder-Robots from the account.""" + return ( + robot for robot in self.account.robots if isinstance(robot, FeederRobot) + ) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 53ed3605c68..5f6579e83c5 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Any, Union, cast -from pylitterbot import LitterRobot +from pylitterbot import FeederRobot, LitterRobot, Robot from homeassistant.components.sensor import ( SensorDeviceClass, @@ -36,23 +36,30 @@ def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str @dataclass -class LitterRobotSensorEntityDescription(SensorEntityDescription): - """A class that describes Litter-Robot sensor entities.""" +class RobotSensorEntityDescription(SensorEntityDescription): + """A class that describes robot sensor entities.""" icon_fn: Callable[[Any], str | None] = lambda _: None + should_report: Callable[[Robot], bool] = lambda _: True + + +@dataclass +class LitterRobotSensorEntityDescription(RobotSensorEntityDescription): + """A class that describes Litter-Robot sensor entities.""" + should_report: Callable[[LitterRobot], bool] = lambda _: True class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): """Litter-Robot sensor entity.""" - entity_description: LitterRobotSensorEntityDescription + entity_description: RobotSensorEntityDescription def __init__( self, - robot: LitterRobot, + robot: LitterRobot | FeederRobot, hub: LitterRobotHub, - description: LitterRobotSensorEntityDescription, + description: RobotSensorEntityDescription, ) -> None: """Initialize a Litter-Robot sensor entity.""" assert description.name @@ -76,7 +83,7 @@ class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): return super().icon -ROBOT_SENSORS = [ +LITTER_ROBOT_SENSORS = [ LitterRobotSensorEntityDescription( name="Waste Drawer", key="waste_drawer_level", @@ -109,6 +116,13 @@ ROBOT_SENSORS = [ ), ] +FEEDER_ROBOT_SENSOR = RobotSensorEntityDescription( + name="Food Level", + key="food_level", + native_unit_of_measurement=PERCENTAGE, + icon_fn=lambda state: icon_for_gauge_level(state, 10), +) + async def async_setup_entry( hass: HomeAssistant, @@ -118,7 +132,15 @@ async def async_setup_entry( """Set up Litter-Robot sensors using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] async_add_entities( - LitterRobotSensorEntity(robot=robot, hub=hub, description=description) - for description in ROBOT_SENSORS - for robot in hub.litter_robots() + [ + LitterRobotSensorEntity(robot=robot, hub=hub, description=description) + for description in LITTER_ROBOT_SENSORS + for robot in hub.litter_robots() + ] + + [ + LitterRobotSensorEntity( + robot=robot, hub=hub, description=FEEDER_ROBOT_SENSOR + ) + for robot in hub.feeder_robots() + ] ) From af9910d143fd5228e6d5fa89126f5ac735a8a836 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Aug 2022 17:58:36 +0200 Subject: [PATCH 724/903] Use _attr_native_value in glances sensor (#77494) --- homeassistant/components/glances/sensor.py | 68 ++++++++++------------ 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 0b2ce1801e1..f6ae6b6ec17 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -329,7 +329,6 @@ class GlancesSensor(SensorEntity): """Initialize the sensor.""" self.glances_data = glances_data self._sensor_name_prefix = sensor_name_prefix - self._state = None self.unsub_update: CALLBACK_TYPE | None = None self.entity_description = description @@ -346,11 +345,6 @@ class GlancesSensor(SensorEntity): """Could the device be accessed during the last update call.""" return self.glances_data.available - @property - def native_value(self): - """Return the state of the resources.""" - return self._state - async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" self.unsub_update = async_dispatcher_connect( @@ -358,16 +352,16 @@ class GlancesSensor(SensorEntity): ) @callback - def _schedule_immediate_update(self): + def _schedule_immediate_update(self) -> None: self.async_schedule_update_ha_state(True) - async def will_remove_from_hass(self): + async def will_remove_from_hass(self) -> None: """Unsubscribe from update dispatcher.""" if self.unsub_update: self.unsub_update() self.unsub_update = None - async def async_update(self): # noqa: C901 + async def async_update(self) -> None: # noqa: C901 """Get the latest data from REST API.""" if (value := self.glances_data.api.data) is None: return @@ -379,100 +373,100 @@ class GlancesSensor(SensorEntity): break if self.entity_description.key == "disk_free": try: - self._state = round(disk["free"] / 1024**3, 1) + self._attr_native_value = round(disk["free"] / 1024**3, 1) except KeyError: - self._state = round( + self._attr_native_value = round( (disk["size"] - disk["used"]) / 1024**3, 1, ) elif self.entity_description.key == "disk_use": - self._state = round(disk["used"] / 1024**3, 1) + self._attr_native_value = round(disk["used"] / 1024**3, 1) elif self.entity_description.key == "disk_use_percent": - self._state = disk["percent"] + self._attr_native_value = disk["percent"] elif self.entity_description.key == "battery": for sensor in value["sensors"]: if ( sensor["type"] == "battery" and sensor["label"] == self._sensor_name_prefix ): - self._state = sensor["value"] + self._attr_native_value = sensor["value"] elif self.entity_description.key == "fan_speed": for sensor in value["sensors"]: if ( sensor["type"] == "fan_speed" and sensor["label"] == self._sensor_name_prefix ): - self._state = sensor["value"] + self._attr_native_value = sensor["value"] elif self.entity_description.key == "temperature_core": for sensor in value["sensors"]: if ( sensor["type"] == "temperature_core" and sensor["label"] == self._sensor_name_prefix ): - self._state = sensor["value"] + self._attr_native_value = sensor["value"] elif self.entity_description.key == "temperature_hdd": for sensor in value["sensors"]: if ( sensor["type"] == "temperature_hdd" and sensor["label"] == self._sensor_name_prefix ): - self._state = sensor["value"] + self._attr_native_value = sensor["value"] elif self.entity_description.key == "memory_use_percent": - self._state = value["mem"]["percent"] + self._attr_native_value = value["mem"]["percent"] elif self.entity_description.key == "memory_use": - self._state = round(value["mem"]["used"] / 1024**2, 1) + self._attr_native_value = round(value["mem"]["used"] / 1024**2, 1) elif self.entity_description.key == "memory_free": - self._state = round(value["mem"]["free"] / 1024**2, 1) + self._attr_native_value = round(value["mem"]["free"] / 1024**2, 1) elif self.entity_description.key == "swap_use_percent": - self._state = value["memswap"]["percent"] + self._attr_native_value = value["memswap"]["percent"] elif self.entity_description.key == "swap_use": - self._state = round(value["memswap"]["used"] / 1024**3, 1) + self._attr_native_value = round(value["memswap"]["used"] / 1024**3, 1) elif self.entity_description.key == "swap_free": - self._state = round(value["memswap"]["free"] / 1024**3, 1) + self._attr_native_value = round(value["memswap"]["free"] / 1024**3, 1) elif self.entity_description.key == "processor_load": # Windows systems don't provide load details try: - self._state = value["load"]["min15"] + self._attr_native_value = value["load"]["min15"] except KeyError: - self._state = value["cpu"]["total"] + self._attr_native_value = value["cpu"]["total"] elif self.entity_description.key == "process_running": - self._state = value["processcount"]["running"] + self._attr_native_value = value["processcount"]["running"] elif self.entity_description.key == "process_total": - self._state = value["processcount"]["total"] + self._attr_native_value = value["processcount"]["total"] elif self.entity_description.key == "process_thread": - self._state = value["processcount"]["thread"] + self._attr_native_value = value["processcount"]["thread"] elif self.entity_description.key == "process_sleeping": - self._state = value["processcount"]["sleeping"] + self._attr_native_value = value["processcount"]["sleeping"] elif self.entity_description.key == "cpu_use_percent": - self._state = value["quicklook"]["cpu"] + self._attr_native_value = value["quicklook"]["cpu"] elif self.entity_description.key == "docker_active": count = 0 try: for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: count += 1 - self._state = count + self._attr_native_value = count except KeyError: - self._state = count + self._attr_native_value = count elif self.entity_description.key == "docker_cpu_use": cpu_use = 0.0 try: for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: cpu_use += container["cpu"]["total"] - self._state = round(cpu_use, 1) + self._attr_native_value = round(cpu_use, 1) except KeyError: - self._state = STATE_UNAVAILABLE + self._attr_native_value = STATE_UNAVAILABLE elif self.entity_description.key == "docker_memory_use": mem_use = 0.0 try: for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: mem_use += container["memory"]["usage"] - self._state = round(mem_use / 1024**2, 1) + self._attr_native_value = round(mem_use / 1024**2, 1) except KeyError: - self._state = STATE_UNAVAILABLE + self._attr_native_value = STATE_UNAVAILABLE elif self.entity_description.type == "raid": for raid_device, raid in value["raid"].items(): if raid_device == self._sensor_name_prefix: - self._state = raid[self.entity_description.key] + self._attr_native_value = raid[self.entity_description.key] From 8e0c26bf8697d5dae554efc2285129bd2aae47ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Aug 2022 11:38:18 -0500 Subject: [PATCH 725/903] Add LED BLE integration (#77489) Co-authored-by: Paulus Schoutsen --- .coveragerc | 3 + CODEOWNERS | 2 + homeassistant/components/led_ble/__init__.py | 119 +++++++++ .../components/led_ble/config_flow.py | 117 +++++++++ homeassistant/components/led_ble/const.py | 10 + homeassistant/components/led_ble/light.py | 99 ++++++++ .../components/led_ble/manifest.json | 17 ++ homeassistant/components/led_ble/models.py | 17 ++ homeassistant/components/led_ble/strings.json | 23 ++ .../components/led_ble/translations/en.json | 23 ++ homeassistant/components/led_ble/util.py | 51 ++++ homeassistant/generated/bluetooth.py | 20 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/led_ble/__init__.py | 51 ++++ tests/components/led_ble/conftest.py | 8 + tests/components/led_ble/test_config_flow.py | 229 ++++++++++++++++++ 18 files changed, 796 insertions(+) create mode 100644 homeassistant/components/led_ble/__init__.py create mode 100644 homeassistant/components/led_ble/config_flow.py create mode 100644 homeassistant/components/led_ble/const.py create mode 100644 homeassistant/components/led_ble/light.py create mode 100644 homeassistant/components/led_ble/manifest.json create mode 100644 homeassistant/components/led_ble/models.py create mode 100644 homeassistant/components/led_ble/strings.json create mode 100644 homeassistant/components/led_ble/translations/en.json create mode 100644 homeassistant/components/led_ble/util.py create mode 100644 tests/components/led_ble/__init__.py create mode 100644 tests/components/led_ble/conftest.py create mode 100644 tests/components/led_ble/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 365f64076b3..9301edcec52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -659,6 +659,9 @@ omit = homeassistant/components/lcn/helpers.py homeassistant/components/lcn/scene.py homeassistant/components/lcn/services.py + homeassistant/components/led_ble/__init__.py + homeassistant/components/led_ble/light.py + homeassistant/components/led_ble/util.py homeassistant/components/lg_netcast/media_player.py homeassistant/components/lg_soundbar/media_player.py homeassistant/components/life360/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 004bc365d89..fe634225124 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -601,6 +601,8 @@ build.json @home-assistant/supervisor /tests/components/laundrify/ @xLarry /homeassistant/components/lcn/ @alengwenus /tests/components/lcn/ @alengwenus +/homeassistant/components/led_ble/ @bdraco +/tests/components/led_ble/ @bdraco /homeassistant/components/lg_netcast/ @Drafteed /homeassistant/components/life360/ @pnbruckner /tests/components/life360/ @pnbruckner diff --git a/homeassistant/components/led_ble/__init__.py b/homeassistant/components/led_ble/__init__.py new file mode 100644 index 00000000000..d885b3eb950 --- /dev/null +++ b/homeassistant/components/led_ble/__init__.py @@ -0,0 +1,119 @@ +"""The LED BLE integration.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging + +import async_timeout +from led_ble import BLEAK_EXCEPTIONS, LEDBLE + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEVICE_TIMEOUT, DOMAIN, UPDATE_SECONDS +from .models import LEDBLEData + +PLATFORMS: list[Platform] = [Platform.LIGHT] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up LED BLE from a config entry.""" + address: str = entry.data[CONF_ADDRESS] + ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True) + if not ble_device: + raise ConfigEntryNotReady( + f"Could not find LED BLE device with address {address}" + ) + + led_ble = LEDBLE(ble_device) + + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a ble callback.""" + led_ble.set_ble_device(service_info.device) + + entry.async_on_unload( + bluetooth.async_register_callback( + hass, + _async_update_ble, + BluetoothCallbackMatcher({ADDRESS: address}), + bluetooth.BluetoothScanningMode.PASSIVE, + ) + ) + + async def _async_update(): + """Update the device state.""" + try: + await led_ble.update() + except BLEAK_EXCEPTIONS as ex: + raise UpdateFailed(str(ex)) from ex + + startup_event = asyncio.Event() + cancel_first_update = led_ble.register_callback(lambda *_: startup_event.set()) + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=led_ble.name, + update_method=_async_update, + update_interval=timedelta(seconds=UPDATE_SECONDS), + ) + + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + cancel_first_update() + raise + + try: + async with async_timeout.timeout(DEVICE_TIMEOUT): + await startup_event.wait() + except asyncio.TimeoutError as ex: + raise ConfigEntryNotReady( + "Unable to communicate with the device; " + f"Try moving the Bluetooth adapter closer to {led_ble.name}" + ) from ex + finally: + cancel_first_update() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LEDBLEData( + entry.title, led_ble, coordinator + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + + async def _async_stop(event: Event) -> None: + """Close the connection.""" + await led_ble.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) + ) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + data: LEDBLEData = hass.data[DOMAIN][entry.entry_id] + if entry.title != data.title: + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + data: LEDBLEData = hass.data[DOMAIN].pop(entry.entry_id) + await data.device.stop() + + return unload_ok diff --git a/homeassistant/components/led_ble/config_flow.py b/homeassistant/components/led_ble/config_flow.py new file mode 100644 index 00000000000..19be92f6647 --- /dev/null +++ b/homeassistant/components/led_ble/config_flow.py @@ -0,0 +1,117 @@ +"""Config flow for LEDBLE integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from led_ble import BLEAK_EXCEPTIONS, LEDBLE +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN, LOCAL_NAMES, UNSUPPORTED_SUB_MODEL +from .util import human_readable_name + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Yale Access Bluetooth.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + if discovery_info.name.startswith(UNSUPPORTED_SUB_MODEL): + # These versions speak a different protocol + # that we do not support yet. + return self.async_abort(reason="not_supported") + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + self._discovery_info = discovery_info + self.context["title_placeholders"] = { + "name": human_readable_name( + None, discovery_info.name, discovery_info.address + ) + } + return await self.async_step_user() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + errors: dict[str, str] = {} + + if user_input is not None: + address = user_input[CONF_ADDRESS] + discovery_info = self._discovered_devices[address] + local_name = discovery_info.name + await self.async_set_unique_id( + discovery_info.address, raise_on_progress=False + ) + self._abort_if_unique_id_configured() + led_ble = LEDBLE(discovery_info.device) + try: + await led_ble.update() + except BLEAK_EXCEPTIONS: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error") + errors["base"] = "unknown" + else: + await led_ble.stop() + return self.async_create_entry( + title=local_name, + data={ + CONF_ADDRESS: discovery_info.address, + }, + ) + + if discovery := self._discovery_info: + self._discovered_devices[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + for discovery in async_discovered_service_info(self.hass): + if ( + discovery.address in current_addresses + or discovery.address in self._discovered_devices + or not any( + discovery.name.startswith(local_name) + and not discovery.name.startswith(UNSUPPORTED_SUB_MODEL) + for local_name in LOCAL_NAMES + ) + ): + continue + self._discovered_devices[discovery.address] = discovery + + if not self._discovered_devices: + return self.async_abort(reason="no_unconfigured_devices") + + data_schema = vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + service_info.address: f"{service_info.name} ({service_info.address})" + for service_info in self._discovered_devices.values() + } + ), + } + ) + return self.async_show_form( + step_id="user", + data_schema=data_schema, + errors=errors, + ) diff --git a/homeassistant/components/led_ble/const.py b/homeassistant/components/led_ble/const.py new file mode 100644 index 00000000000..ad3ea8f6707 --- /dev/null +++ b/homeassistant/components/led_ble/const.py @@ -0,0 +1,10 @@ +"""Constants for the LED BLE integration.""" + +DOMAIN = "led_ble" + +DEVICE_TIMEOUT = 30 +LOCAL_NAMES = {"LEDnet", "BLE-LED", "LEDBLE", "Triones", "LEDBlue"} + +UNSUPPORTED_SUB_MODEL = "LEDnetWF" + +UPDATE_SECONDS = 15 diff --git a/homeassistant/components/led_ble/light.py b/homeassistant/components/led_ble/light.py new file mode 100644 index 00000000000..a18ab812b19 --- /dev/null +++ b/homeassistant/components/led_ble/light.py @@ -0,0 +1,99 @@ +"""LED BLE integration light platform.""" +from __future__ import annotations + +from typing import Any + +from led_ble import LEDBLE + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_RGB_COLOR, + ATTR_WHITE, + ColorMode, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN +from .models import LEDBLEData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the light platform for LEDBLE.""" + data: LEDBLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities([LEDBLEEntity(data.coordinator, data.device, entry.title)]) + + +class LEDBLEEntity(CoordinatorEntity, LightEntity): + """Representation of LEDBLE device.""" + + _attr_supported_color_modes = {ColorMode.RGB, ColorMode.WHITE} + _attr_has_entity_name = True + + def __init__( + self, coordinator: DataUpdateCoordinator, device: LEDBLE, name: str + ) -> None: + """Initialize an ledble light.""" + super().__init__(coordinator) + self._device = device + self._attr_unique_id = device._address + self._attr_device_info = DeviceInfo( + name=name, + model=hex(device.model_num), + sw_version=hex(device.version_num), + connections={(dr.CONNECTION_BLUETOOTH, device._address)}, + ) + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + device = self._device + self._attr_color_mode = ColorMode.WHITE if device.w else ColorMode.RGB + self._attr_brightness = device.brightness + self._attr_rgb_color = device.rgb_unscaled + self._attr_is_on = device.on + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the light to turn on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) + if ATTR_RGB_COLOR in kwargs: + rgb = kwargs[ATTR_RGB_COLOR] + await self._device.set_rgb(rgb, brightness) + return + if ATTR_BRIGHTNESS in kwargs: + await self._device.set_brightness(brightness) + return + if ATTR_WHITE in kwargs: + await self._device.set_white(kwargs[ATTR_WHITE]) + return + await self._device.turn_on() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the light to turn off.""" + await self._device.turn_off() + + @callback + def _handle_coordinator_update(self, *args: Any) -> None: + """Handle data update.""" + self._async_update_attrs() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + self._device.register_callback(self._handle_coordinator_update) + ) + return await super().async_added_to_hass() diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json new file mode 100644 index 00000000000..376fadcb3be --- /dev/null +++ b/homeassistant/components/led_ble/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "led_ble", + "name": "LED BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ble_ble", + "requirements": ["led-ble==0.5.4"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "bluetooth": [ + { "local_name": "LEDnet*" }, + { "local_name": "BLE-LED*" }, + { "local_name": "LEDBLE*" }, + { "local_name": "Triones*" }, + { "local_name": "LEDBlue*" } + ], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/led_ble/models.py b/homeassistant/components/led_ble/models.py new file mode 100644 index 00000000000..611d484ea61 --- /dev/null +++ b/homeassistant/components/led_ble/models.py @@ -0,0 +1,17 @@ +"""The led ble integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from led_ble import LEDBLE + +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +@dataclass +class LEDBLEData: + """Data for the led ble integration.""" + + title: str + device: LEDBLE + coordinator: DataUpdateCoordinator diff --git a/homeassistant/components/led_ble/strings.json b/homeassistant/components/led_ble/strings.json new file mode 100644 index 00000000000..79540552575 --- /dev/null +++ b/homeassistant/components/led_ble/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth address" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "not_supported": "Device not supported", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_unconfigured_devices": "No unconfigured devices found.", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/led_ble/translations/en.json b/homeassistant/components/led_ble/translations/en.json new file mode 100644 index 00000000000..75356b78460 --- /dev/null +++ b/homeassistant/components/led_ble/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "no_unconfigured_devices": "No unconfigured devices found.", + "not_supported": "Device not supported" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth address" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/util.py b/homeassistant/components/led_ble/util.py new file mode 100644 index 00000000000..e43655e2905 --- /dev/null +++ b/homeassistant/components/led_ble/util.py @@ -0,0 +1,51 @@ +"""The yalexs_ble integration models.""" +from __future__ import annotations + +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_discovered_service_info, + async_process_advertisements, +) +from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher +from homeassistant.core import HomeAssistant, callback + +from .const import DEVICE_TIMEOUT + + +@callback +def async_find_existing_service_info( + hass: HomeAssistant, local_name: str, address: str +) -> BluetoothServiceInfoBleak | None: + """Return the service info for the given local_name and address.""" + for service_info in async_discovered_service_info(hass): + device = service_info.device + if device.address == address: + return service_info + return None + + +async def async_get_service_info( + hass: HomeAssistant, local_name: str, address: str +) -> BluetoothServiceInfoBleak: + """Wait for the service info for the given local_name and address.""" + if service_info := async_find_existing_service_info(hass, local_name, address): + return service_info + return await async_process_advertisements( + hass, + lambda service_info: True, + BluetoothCallbackMatcher({ADDRESS: address}), + BluetoothScanningMode.ACTIVE, + DEVICE_TIMEOUT, + ) + + +def short_address(address: str) -> str: + """Convert a Bluetooth address to a short address.""" + split_address = address.replace("-", ":").split(":") + return f"{split_address[-2].upper()}{split_address[-1].upper()}"[-4:] + + +def human_readable_name(name: str | None, local_name: str, address: str) -> str: + """Return a human readable name for the given name, local_name, and address.""" + return f"{name or local_name} ({short_address(address)})" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index bda76859688..320c4c296da 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -118,6 +118,26 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "local_name": "tps", "connectable": False }, + { + "domain": "led_ble", + "local_name": "LEDnet*" + }, + { + "domain": "led_ble", + "local_name": "BLE-LED*" + }, + { + "domain": "led_ble", + "local_name": "LEDBLE*" + }, + { + "domain": "led_ble", + "local_name": "Triones*" + }, + { + "domain": "led_ble", + "local_name": "LEDBlue*" + }, { "domain": "moat", "local_name": "Moat_S*", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 07a5cdce04f..a9b303eabea 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -201,6 +201,7 @@ FLOWS = { "landisgyr_heat_meter", "launch_library", "laundrify", + "led_ble", "lg_soundbar", "life360", "lifx", diff --git a/requirements_all.txt b/requirements_all.txt index 3a52b694167..8cb1e2fcf63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -961,6 +961,9 @@ lakeside==0.12 # homeassistant.components.laundrify laundrify_aio==1.1.2 +# homeassistant.components.led_ble +led-ble==0.5.4 + # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 244adebff53..c9ee9666ff2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -699,6 +699,9 @@ lacrosse-view==0.0.9 # homeassistant.components.laundrify laundrify_aio==1.1.2 +# homeassistant.components.led_ble +led-ble==0.5.4 + # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/tests/components/led_ble/__init__.py b/tests/components/led_ble/__init__.py new file mode 100644 index 00000000000..702b793f57a --- /dev/null +++ b/tests/components/led_ble/__init__.py @@ -0,0 +1,51 @@ +"""Tests for the LED BLE Bluetooth integration.""" +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + +LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="Triones:F30200000152C", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={}, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Triones:F30200000152C"), + advertisement=AdvertisementData(), + time=0, + connectable=True, +) + +UNSUPPORTED_LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="LEDnetWFF30200000152C", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={}, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="LEDnetWFF30200000152C"), + advertisement=AdvertisementData(), + time=0, + connectable=True, +) + + +NOT_LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="Not", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={ + 33: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 21: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"), + advertisement=AdvertisementData(), + time=0, + connectable=True, +) diff --git a/tests/components/led_ble/conftest.py b/tests/components/led_ble/conftest.py new file mode 100644 index 00000000000..280eb0d6f17 --- /dev/null +++ b/tests/components/led_ble/conftest.py @@ -0,0 +1,8 @@ +"""led_ble session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/led_ble/test_config_flow.py b/tests/components/led_ble/test_config_flow.py new file mode 100644 index 00000000000..6767302af50 --- /dev/null +++ b/tests/components/led_ble/test_config_flow.py @@ -0,0 +1,229 @@ +"""Test the LED BLE Bluetooth config flow.""" +from unittest.mock import patch + +from bleak import BleakError + +from homeassistant import config_entries +from homeassistant.components.led_ble.const import DOMAIN +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + LED_BLE_DISCOVERY_INFO, + NOT_LED_BLE_DISCOVERY_INFO, + UNSUPPORTED_LED_BLE_DISCOVERY_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_user_step_success(hass: HomeAssistant) -> None: + """Test user step success path.""" + with patch( + "homeassistant.components.led_ble.config_flow.async_discovered_service_info", + return_value=[NOT_LED_BLE_DISCOVERY_INFO, LED_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch("homeassistant.components.led_ble.config_flow.LEDBLE.update",), patch( + "homeassistant.components.led_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == LED_BLE_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + } + assert result2["result"].unique_id == LED_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: + """Test user step with no devices found.""" + with patch( + "homeassistant.components.led_ble.config_flow.async_discovered_service_info", + return_value=[NOT_LED_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: + """Test user step with only existing devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + }, + unique_id=LED_BLE_DISCOVERY_INFO.address, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.led_ble.config_flow.async_discovered_service_info", + return_value=[LED_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: + """Test user step and we cannot connect.""" + with patch( + "homeassistant.components.led_ble.config_flow.async_discovered_service_info", + return_value=[LED_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + side_effect=BleakError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + with patch("homeassistant.components.led_ble.config_flow.LEDBLE.update",), patch( + "homeassistant.components.led_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == LED_BLE_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + } + assert result3["result"].unique_id == LED_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: + """Test user step with an unknown exception.""" + with patch( + "homeassistant.components.led_ble.config_flow.async_discovered_service_info", + return_value=[NOT_LED_BLE_DISCOVERY_INFO, LED_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + side_effect=RuntimeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + with patch("homeassistant.components.led_ble.config_flow.LEDBLE.update",), patch( + "homeassistant.components.led_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == LED_BLE_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + } + assert result3["result"].unique_id == LED_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_step_success(hass: HomeAssistant) -> None: + """Test bluetooth step success path.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LED_BLE_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch("homeassistant.components.led_ble.config_flow.LEDBLE.update",), patch( + "homeassistant.components.led_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == LED_BLE_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + } + assert result2["result"].unique_id == LED_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_unsupported_model(hass: HomeAssistant) -> None: + """Test bluetooth step with an unsupported model path.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=UNSUPPORTED_LED_BLE_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" From 4ba8fb645779b3cd74205a6a665ae552227c9759 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 29 Aug 2022 13:39:05 -0400 Subject: [PATCH 726/903] Add basic media_player to Fully Kiosk Browser integration (#77266) --- .../components/fully_kiosk/__init__.py | 1 + homeassistant/components/fully_kiosk/const.py | 11 ++ .../components/fully_kiosk/media_player.py | 82 +++++++++++ .../fully_kiosk/test_media_player.py | 132 ++++++++++++++++++ 4 files changed, 226 insertions(+) create mode 100644 homeassistant/components/fully_kiosk/media_player.py create mode 100644 tests/components/fully_kiosk/test_media_player.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index ebd9af1134a..86ab769e0ec 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -9,6 +9,7 @@ from .coordinator import FullyKioskDataUpdateCoordinator PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.MEDIA_PLAYER, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/fully_kiosk/const.py b/homeassistant/components/fully_kiosk/const.py index f21906bae73..56248544b81 100644 --- a/homeassistant/components/fully_kiosk/const.py +++ b/homeassistant/components/fully_kiosk/const.py @@ -5,9 +5,20 @@ from datetime import timedelta import logging from typing import Final +from homeassistant.components.media_player.const import MediaPlayerEntityFeature + DOMAIN: Final = "fully_kiosk" LOGGER = logging.getLogger(__package__) UPDATE_INTERVAL = timedelta(seconds=30) DEFAULT_PORT = 2323 + +AUDIOMANAGER_STREAM_MUSIC = 3 + +MEDIA_SUPPORT_FULLYKIOSK = ( + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.BROWSE_MEDIA +) diff --git a/homeassistant/components/fully_kiosk/media_player.py b/homeassistant/components/fully_kiosk/media_player.py new file mode 100644 index 00000000000..732f88170e1 --- /dev/null +++ b/homeassistant/components/fully_kiosk/media_player.py @@ -0,0 +1,82 @@ +"""Fully Kiosk Browser media player.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components import media_source +from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + BrowseMedia, + async_process_play_media_url, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_IDLE, STATE_PLAYING +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import AUDIOMANAGER_STREAM_MUSIC, DOMAIN, MEDIA_SUPPORT_FULLYKIOSK +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser media player entity.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([FullyMediaPlayer(coordinator)]) + + +class FullyMediaPlayer(FullyKioskEntity, MediaPlayerEntity): + """Representation of a Fully Kiosk Browser media player entity.""" + + _attr_supported_features = MEDIA_SUPPORT_FULLYKIOSK + _attr_assumed_state = True + _attr_state = STATE_IDLE + + def __init__(self, coordinator: FullyKioskDataUpdateCoordinator) -> None: + """Initialize the media player entity.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.data['deviceID']}-mediaplayer" + + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: + """Play a piece of media.""" + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) + media_id = async_process_play_media_url(self.hass, play_item.url) + + await self.coordinator.fully.playSound(media_id, AUDIOMANAGER_STREAM_MUSIC) + self._attr_state = STATE_PLAYING + self.async_write_ha_state() + + async def async_media_stop(self) -> None: + """Stop playing media.""" + await self.coordinator.fully.stopSound() + self._attr_state = STATE_IDLE + self.async_write_ha_state() + + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1.""" + await self.coordinator.fully.setAudioVolume( + int(volume * 100), AUDIOMANAGER_STREAM_MUSIC + ) + self._attr_volume_level = volume + self.async_write_ha_state() + + async def async_browse_media( + self, + media_content_type: str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Implement the WebSocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/tests/components/fully_kiosk/test_media_player.py b/tests/components/fully_kiosk/test_media_player.py new file mode 100644 index 00000000000..d423d809fbe --- /dev/null +++ b/tests/components/fully_kiosk/test_media_player.py @@ -0,0 +1,132 @@ +"""Test the Fully Kiosk Browser media player.""" +from unittest.mock import MagicMock, Mock, patch + +from aiohttp import ClientSession + +from homeassistant.components.fully_kiosk.const import DOMAIN, MEDIA_SUPPORT_FULLYKIOSK +import homeassistant.components.media_player as media_player +from homeassistant.components.media_source.const import DOMAIN as MS_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_media_player( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk media player.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("media_player.amazon_fire") + assert state + + entry = entity_registry.async_get("media_player.amazon_fire") + assert entry + assert entry.unique_id == "abcdef-123456-mediaplayer" + assert entry.supported_features == MEDIA_SUPPORT_FULLYKIOSK + + await hass.services.async_call( + media_player.DOMAIN, + "play_media", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + "media_content_type": "music", + "media_content_id": "test.mp3", + }, + blocking=True, + ) + assert len(mock_fully_kiosk.playSound.mock_calls) == 1 + + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=Mock(url="http://example.com/test.mp3"), + ): + await hass.services.async_call( + "media_player", + "play_media", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + "media_content_id": "media-source://some_source/some_id", + "media_content_type": "audio/mpeg", + }, + blocking=True, + ) + + assert len(mock_fully_kiosk.playSound.mock_calls) == 2 + assert ( + mock_fully_kiosk.playSound.mock_calls[1].args[0] + == "http://example.com/test.mp3" + ) + + await hass.services.async_call( + media_player.DOMAIN, + "media_stop", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + }, + blocking=True, + ) + assert len(mock_fully_kiosk.stopSound.mock_calls) == 1 + + await hass.services.async_call( + media_player.DOMAIN, + "volume_set", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + "volume_level": 0.5, + }, + blocking=True, + ) + assert len(mock_fully_kiosk.setAudioVolume.mock_calls) == 1 + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + +async def test_browse_media( + hass: HomeAssistant, + hass_ws_client: ClientSession, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test Fully Kiosk browse media.""" + + await async_setup_component(hass, MS_DOMAIN, {MS_DOMAIN: {}}) + await hass.async_block_till_done() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.amazon_fire", + } + ) + response = await client.receive_json() + assert response["success"] + expected_child_audio = { + "title": "test.mp3", + "media_class": "music", + "media_content_type": "audio/mpeg", + "media_content_id": "media-source://media_source/local/test.mp3", + "can_play": True, + "can_expand": False, + "thumbnail": None, + "children_media_class": None, + } + assert expected_child_audio in response["result"]["children"] From 7d9ae0784ead986480cd9910bb513ddb20799e77 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Aug 2022 13:59:00 -0400 Subject: [PATCH 727/903] Allow searching for person (#77339) --- homeassistant/components/person/__init__.py | 41 ++++++++++++++- homeassistant/components/search/__init__.py | 17 ++++++- tests/components/person/test_init.py | 56 +++++++++++++++++++++ tests/components/search/test_init.py | 30 +++++++++++ 4 files changed, 140 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 2eb80ed69cc..09851d70384 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -125,6 +125,38 @@ async def async_add_user_device_tracker( break +@callback +def persons_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all persons that reference the entity.""" + if ( + DOMAIN not in hass.data + or split_entity_id(entity_id)[0] != DEVICE_TRACKER_DOMAIN + ): + return [] + + component: EntityComponent = hass.data[DOMAIN][2] + + return [ + person_entity.entity_id + for person_entity in component.entities + if entity_id in cast(Person, person_entity).device_trackers + ] + + +@callback +def entities_in_person(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all entities belonging to a person.""" + if DOMAIN not in hass.data: + return [] + + component: EntityComponent = hass.data[DOMAIN][2] + + if (person_entity := component.get_entity(entity_id)) is None: + return [] + + return cast(Person, person_entity).device_trackers + + CREATE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_USER_ID): vol.Any(str, None), @@ -318,7 +350,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) await storage_collection.async_load() - hass.data[DOMAIN] = (yaml_collection, storage_collection) + hass.data[DOMAIN] = (yaml_collection, storage_collection, entity_component) collection.StorageCollectionWebsocket( storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS @@ -412,6 +444,11 @@ class Person(RestoreEntity): """Return a unique ID for the person.""" return self._config[CONF_ID] + @property + def device_trackers(self): + """Return the device trackers for the person.""" + return self._config[CONF_DEVICE_TRACKERS] + async def async_added_to_hass(self): """Register device trackers.""" await super().async_added_to_hass() @@ -506,7 +543,7 @@ def ws_list_person( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg ): """List persons.""" - yaml, storage = hass.data[DOMAIN] + yaml, storage, _ = hass.data[DOMAIN] connection.send_result( msg[ATTR_ID], {"storage": storage.async_items(), "config": yaml.async_items()} ) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index c951122d195..bfde6f38a73 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -6,7 +6,7 @@ import logging import voluptuous as vol -from homeassistant.components import automation, group, script, websocket_api +from homeassistant.components import automation, group, person, script, websocket_api from homeassistant.components.homeassistant import scene from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import device_registry, entity_registry @@ -36,6 +36,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "group", "scene", "script", + "person", ) ), vol.Required("item_id"): str, @@ -67,7 +68,7 @@ class Searcher: # These types won't be further explored. Config entries + Output types. DONT_RESOLVE = {"scene", "automation", "script", "group", "config_entry", "area"} # These types exist as an entity and so need cleanup in results - EXIST_AS_ENTITY = {"script", "scene", "automation", "group"} + EXIST_AS_ENTITY = {"script", "scene", "automation", "group", "person"} def __init__( self, @@ -183,6 +184,9 @@ class Searcher: for entity in script.scripts_with_entity(self.hass, entity_id): self._add_or_resolve("entity", entity) + for entity in person.persons_with_entity(self.hass, entity_id): + self._add_or_resolve("entity", entity) + # Find devices entity_entry = self._entity_reg.async_get(entity_id) if entity_entry is not None: @@ -251,6 +255,15 @@ class Searcher: for entity in scene.entities_in_scene(self.hass, scene_entity_id): self._add_or_resolve("entity", entity) + @callback + def _resolve_person(self, person_entity_id) -> None: + """Resolve a person. + + Will only be called if person is an entry point. + """ + for entity in person.entities_in_person(self.hass, person_entity_id): + self._add_or_resolve("entity", entity) + @callback def _resolve_config_entry(self, config_entry_id) -> None: """Resolve a config entry. diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 4e6793c07bb..981343ea3a5 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -783,3 +783,59 @@ async def test_person_storage_fixing_device_trackers(storage_collection): await storage_collection.async_load() assert storage_collection.data["bla"]["device_trackers"] == [] + + +async def test_persons_with_entity(hass): + """Test finding persons with an entity.""" + assert await async_setup_component( + hass, + "person", + { + "person": [ + { + "id": "abcd", + "name": "Paulus", + "device_trackers": [ + "device_tracker.paulus_iphone", + "device_tracker.paulus_ipad", + ], + }, + { + "id": "efgh", + "name": "Anne Therese", + "device_trackers": [ + "device_tracker.at_pixel", + ], + }, + ] + }, + ) + + assert person.persons_with_entity(hass, "device_tracker.paulus_iphone") == [ + "person.paulus" + ] + + +async def test_entities_in_person(hass): + """Test finding entities tracked by person.""" + assert await async_setup_component( + hass, + "person", + { + "person": [ + { + "id": "abcd", + "name": "Paulus", + "device_trackers": [ + "device_tracker.paulus_iphone", + "device_tracker.paulus_ipad", + ], + } + ] + }, + ) + + assert person.entities_in_person(hass, "person.paulus") == [ + "device_tracker.paulus_iphone", + "device_tracker.paulus_ipad", + ] diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index e9d320aa9ef..a728ef9b8c4 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -368,6 +368,36 @@ async def test_area_lookup(hass): } +async def test_person_lookup(hass): + """Test searching persons.""" + assert await async_setup_component( + hass, + "person", + { + "person": [ + { + "id": "abcd", + "name": "Paulus", + "device_trackers": ["device_tracker.paulus_iphone"], + } + ] + }, + ) + + device_reg = dr.async_get(hass) + entity_reg = er.async_get(hass) + + searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) + assert searcher.async_search("entity", "device_tracker.paulus_iphone") == { + "person": {"person.paulus"}, + } + + searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) + assert searcher.async_search("entity", "person.paulus") == { + "entity": {"device_tracker.paulus_iphone"}, + } + + async def test_ws_api(hass, hass_ws_client): """Test WS API.""" assert await async_setup_component(hass, "search", {}) From b7244da3ab35ff222b8384ae1285daefbae4f9c5 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 29 Aug 2022 14:39:04 -0400 Subject: [PATCH 728/903] Bump version of pyunifiprotect to 4.1.9 (#77498) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 78278993178..0eb07560624 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.1.8", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.1.9", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 8cb1e2fcf63..382e7d1982d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2025,7 +2025,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.8 +pyunifiprotect==4.1.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9ee9666ff2..ccc940837f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1388,7 +1388,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.8 +pyunifiprotect==4.1.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 58d4172decc1b441c74d2abe232965ead2181a3a Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 29 Aug 2022 12:41:07 -0600 Subject: [PATCH 729/903] Bump pylitterbot to 2022.8.2 (#77504) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 5b2f0f106b9..f25b4525877 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.8.0"], + "requirements": ["pylitterbot==2022.8.2"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 382e7d1982d..a17bac8c905 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1653,7 +1653,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.8.0 +pylitterbot==2022.8.2 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccc940837f2..553dfd7eb58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1154,7 +1154,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.8.0 +pylitterbot==2022.8.2 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 From bb1e6bf2098aeda4468da3ac101647bfd0bea05c Mon Sep 17 00:00:00 2001 From: Samuel Dumont Date: Mon, 29 Aug 2022 21:12:09 +0200 Subject: [PATCH 730/903] Fix oauth2 in Toon (#77480) --- homeassistant/components/toon/oauth2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/toon/oauth2.py b/homeassistant/components/toon/oauth2.py index 64abeb26992..95cde386215 100644 --- a/homeassistant/components/toon/oauth2.py +++ b/homeassistant/components/toon/oauth2.py @@ -33,6 +33,7 @@ def register_oauth2_implementations( client_secret=client_secret, name="Engie Electrabel Boxx", tenant_id="electrabel", + issuer="identity.toon.eu", ), ) config_flow.ToonFlowHandler.async_register_implementation( From d4c020b6753f8e87c6ca37868d03f237af68afdf Mon Sep 17 00:00:00 2001 From: Simon Engelhardt <360816+simonengelhardt@users.noreply.github.com> Date: Mon, 29 Aug 2022 21:14:12 +0200 Subject: [PATCH 731/903] Fix Tuya mc device support (#77346) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/binary_sensor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 5641a18022e..44d37050229 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -28,8 +28,8 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): # DPCode, to use. If None, the key will be used as DPCode dpcode: DPCode | None = None - # Value to consider binary sensor to be "on" - on_value: bool | float | int | str = True + # Value or values to consider binary sensor to be "on" + on_value: bool | float | int | str | set[bool | float | int | str] = True # Commonly used sensors @@ -187,8 +187,9 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { # https://developer.tuya.com/en/docs/iot/s?id=K9gf48r5zjsy9 "mc": ( TuyaBinarySensorEntityDescription( - key=DPCode.DOORCONTACT_STATE, + key=DPCode.STATUS, device_class=BinarySensorDeviceClass.DOOR, + on_value={"open", "opened"}, ), ), # Door Window Sensor @@ -393,4 +394,8 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity): dpcode = self.entity_description.dpcode or self.entity_description.key if dpcode not in self.device.status: return False + + if isinstance(self.entity_description.on_value, set): + return self.device.status[dpcode] in self.entity_description.on_value + return self.device.status[dpcode] == self.entity_description.on_value From e19e65908a7d2d030438bbd4549e858256f361dc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Aug 2022 22:02:29 +0200 Subject: [PATCH 732/903] Use _attr_precision in entities (#77477) --- homeassistant/components/elkm1/climate.py | 6 +----- homeassistant/components/fritzbox/climate.py | 6 +----- homeassistant/components/gree/climate.py | 6 +----- homeassistant/components/hisense_aehw4a1/climate.py | 6 +----- homeassistant/components/isy994/climate.py | 6 +----- homeassistant/components/izone/climate.py | 12 ++---------- homeassistant/components/proliphix/climate.py | 10 +--------- homeassistant/components/venstar/climate.py | 10 +--------- homeassistant/components/vicare/climate.py | 6 +----- homeassistant/components/vicare/water_heater.py | 6 +----- homeassistant/components/zha/climate.py | 6 +----- homeassistant/components/zwave_js/climate.py | 7 ++----- 12 files changed, 14 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 8bbf776c475..6eca3083b3a 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -74,6 +74,7 @@ async def async_setup_entry( class ElkThermostat(ElkEntity, ClimateEntity): """Representation of an Elk-M1 Thermostat.""" + _attr_precision = PRECISION_WHOLE _attr_supported_features = ( ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.AUX_HEAT @@ -138,11 +139,6 @@ class ElkThermostat(ElkEntity, ClimateEntity): """Return the list of available operation modes.""" return SUPPORT_HVAC - @property - def precision(self) -> int: - """Return the precision of the system.""" - return PRECISION_WHOLE - @property def is_aux_heat(self) -> bool: """Return if aux heater is on.""" diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 20331459c3e..10fd3cf0177 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -64,16 +64,12 @@ async def async_setup_entry( class FritzboxThermostat(FritzBoxEntity, ClimateEntity): """The thermostat class for FRITZ!SmartHome thermostats.""" + _attr_precision = PRECISION_HALVES _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) _attr_temperature_unit = TEMP_CELSIUS - @property - def precision(self) -> float: - """Return precision 0.5.""" - return PRECISION_HALVES - @property def current_temperature(self) -> float: """Return the current temperature.""" diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 6d8f32aa21c..4096f58e4cb 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -113,6 +113,7 @@ async def async_setup_entry( class GreeClimateEntity(CoordinatorEntity, ClimateEntity): """Representation of a Gree HVAC device.""" + _attr_precision = PRECISION_WHOLE _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE @@ -152,11 +153,6 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): units = self.coordinator.device.temperature_units return TEMP_CELSIUS if units == TemperatureUnits.C else TEMP_FAHRENHEIT - @property - def precision(self) -> float: - """Return the precision of temperature for the device.""" - return PRECISION_WHOLE - @property def current_temperature(self) -> float: """Return the reported current temperature for the device.""" diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index 246a04df9a8..3213c5f9414 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -142,6 +142,7 @@ class ClimateAehW4a1(ClimateEntity): """Representation of a Hisense AEH-W4A1 module for climate device.""" _attr_hvac_modes = HVAC_MODES + _attr_precision = PRECISION_WHOLE _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE @@ -294,11 +295,6 @@ class ClimateAehW4a1(ClimateEntity): return MAX_TEMP_C return MAX_TEMP_F - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_WHOLE - @property def target_temperature_step(self): """Return the supported step of target temperature.""" diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 9f4c52258a7..bc1f0353455 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -76,6 +76,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Representation of an ISY994 thermostat entity.""" _attr_hvac_modes = ISY_HVAC_MODES + _attr_precision = PRECISION_TENTHS _attr_supported_features = ( ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE @@ -96,11 +97,6 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): self._target_temp_low = 0 self._target_temp_high = 0 - @property - def precision(self) -> float: - """Return the precision of the system.""" - return PRECISION_TENTHS - @property def temperature_unit(self) -> str: """Return the unit of measurement.""" diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 8ff1593d5ff..2c181d90fda 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -126,6 +126,7 @@ def _return_on_connection_error(ret=None): class ControllerDevice(ClimateEntity): """Representation of iZone Controller.""" + _attr_precision = PRECISION_TENTHS _attr_should_poll = False _attr_temperature_unit = TEMP_CELSIUS @@ -253,11 +254,6 @@ class ControllerDevice(ClimateEntity): """Return the name of the entity.""" return f"iZone Controller {self._controller.device_uid}" - @property - def precision(self) -> float: - """Return the precision of the system.""" - return PRECISION_TENTHS - @property def extra_state_attributes(self): """Return the optional state attributes.""" @@ -438,6 +434,7 @@ class ControllerDevice(ClimateEntity): class ZoneDevice(ClimateEntity): """Representation of iZone Zone.""" + _attr_precision = PRECISION_TENTHS _attr_should_poll = False _attr_temperature_unit = TEMP_CELSIUS @@ -526,11 +523,6 @@ class ZoneDevice(ClimateEntity): return self._attr_supported_features return self._attr_supported_features & ~ClimateEntityFeature.TARGET_TEMPERATURE - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_TENTHS - @property def hvac_mode(self) -> HVACMode | None: """Return current operation ie. heat, cool, idle.""" diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 52ed3bb8bcd..1952a6f186a 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -54,6 +54,7 @@ def setup_platform( class ProliphixThermostat(ClimateEntity): """Representation a Proliphix thermostat.""" + _attr_precision = PRECISION_TENTHS _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = TEMP_FAHRENHEIT @@ -72,15 +73,6 @@ class ProliphixThermostat(ClimateEntity): """Return the name of the thermostat.""" return self._name - @property - def precision(self): - """Return the precision of the system. - - Proliphix temperature values are passed back and forth in the - API as tenths of degrees F (i.e. 690 for 69 degrees). - """ - return PRECISION_TENTHS - @property def extra_state_attributes(self): """Return the device specific state attributes.""" diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index efa1c56ccbc..798ffe5b6d3 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -106,6 +106,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _attr_fan_modes = [FAN_ON, FAN_AUTO] _attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF, HVACMode.AUTO] + _attr_precision = PRECISION_HALVES def __init__( self, @@ -139,15 +140,6 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): return features - @property - def precision(self): - """Return the precision of the system. - - Venstar temperature values are passed back and forth in the - API in C or F, with half-degree accuracy. - """ - return PRECISION_HALVES - @property def temperature_unit(self): """Return the unit of measurement, as defined by the API.""" diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 94f7b8d4e5d..53759b42243 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -142,6 +142,7 @@ async def async_setup_entry( class ViCareClimate(ClimateEntity): """Representation of the ViCare heating climate device.""" + _attr_precision = PRECISION_TENTHS _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) @@ -320,11 +321,6 @@ class ViCareClimate(ClimateEntity): """Return the maximum temperature.""" return VICARE_TEMP_HEATING_MAX - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_TENTHS - @property def target_temperature_step(self) -> float: """Set target temperature step to wholes.""" diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index ae8456cac6f..878f2ac47a5 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -100,6 +100,7 @@ async def async_setup_entry( class ViCareWater(WaterHeaterEntity): """Representation of the ViCare domestic hot water device.""" + _attr_precision = PRECISION_TENTHS _attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE def __init__(self, name, api, circuit, device_config, heating_type): @@ -197,11 +198,6 @@ class ViCareWater(WaterHeaterEntity): """Set target temperature step to wholes.""" return PRECISION_WHOLE - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_TENTHS - @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 416d0c81483..573b3df44fa 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -137,6 +137,7 @@ class Thermostat(ZhaEntity, ClimateEntity): DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_TEMP = 7 + _attr_precision = PRECISION_TENTHS _attr_temperature_unit = TEMP_CELSIUS def __init__(self, unique_id, zha_device, channels, **kwargs): @@ -264,11 +265,6 @@ class Thermostat(ZhaEntity, ClimateEntity): """Return the list of available HVAC operation modes.""" return SEQ_OF_OPERATION.get(self._thrm.ctrl_sequence_of_oper, [HVACMode.OFF]) - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_TENTHS - @property def preset_mode(self) -> str: """Return current preset mode.""" diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index d8037643488..b0a5cdfe295 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -126,6 +126,8 @@ async def async_setup_entry( class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): """Representation of a Z-Wave climate.""" + _attr_precision = PRECISION_TENTHS + def __init__( self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: @@ -251,11 +253,6 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return TEMP_FAHRENHEIT return TEMP_CELSIUS - @property - def precision(self) -> float: - """Return the precision of 0.1.""" - return PRECISION_TENTHS - @property def hvac_mode(self) -> HVACMode: """Return hvac operation ie. heat, cool mode.""" From a6c61cf339c9d5eb610591d8fc3a20667faaad25 Mon Sep 17 00:00:00 2001 From: simeon-simsoft <61541002+simeon-simsoft@users.noreply.github.com> Date: Mon, 29 Aug 2022 21:07:48 +0100 Subject: [PATCH 733/903] Wallbox switch entity state incorrect while discharging (#76530) Switch entity state incorrect while discharging --- homeassistant/components/wallbox/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/wallbox/switch.py b/homeassistant/components/wallbox/switch.py index 7ef6f1e97ed..3d046d5d241 100644 --- a/homeassistant/components/wallbox/switch.py +++ b/homeassistant/components/wallbox/switch.py @@ -56,6 +56,7 @@ class WallboxSwitch(WallboxEntity, SwitchEntity): """Return the availability of the switch.""" return self.coordinator.data[CHARGER_STATUS_DESCRIPTION_KEY] in { ChargerStatus.CHARGING, + ChargerStatus.DISCHARGING, ChargerStatus.PAUSED, ChargerStatus.SCHEDULED, } @@ -65,6 +66,7 @@ class WallboxSwitch(WallboxEntity, SwitchEntity): """Return the status of pause/resume.""" return self.coordinator.data[CHARGER_STATUS_DESCRIPTION_KEY] in { ChargerStatus.CHARGING, + ChargerStatus.DISCHARGING, ChargerStatus.WAITING_FOR_CAR, ChargerStatus.WAITING, } From 2224d0f43a048052cfc4572df95c7afcccdf3a57 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 29 Aug 2022 16:25:34 -0400 Subject: [PATCH 734/903] Add a callback for data flow handler removal (#77394) * Add a callback for when data flows are removed * Call `async_remove` at the very end * Handle and log exceptions caught during flow removal * Log the error as an exception, with a traceback * Adjust test's expected logging output to match updated format specifier --- homeassistant/data_entry_flow.py | 12 +++++++++ tests/test_data_entry_flow.py | 42 +++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 64750b2ff50..cdc4023f32c 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -5,6 +5,7 @@ import abc import asyncio from collections.abc import Iterable, Mapping from dataclasses import dataclass +import logging from types import MappingProxyType from typing import Any, TypedDict @@ -16,6 +17,8 @@ from .exceptions import HomeAssistantError from .helpers.frame import report from .util import uuid as uuid_util +_LOGGER = logging.getLogger(__name__) + class FlowResultType(StrEnum): """Result type for a data entry flow.""" @@ -337,6 +340,11 @@ class FlowManager(abc.ABC): if not self._handler_progress_index[handler]: del self._handler_progress_index[handler] + try: + flow.async_remove() + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error removing %s config flow: %s", flow.handler, err) + async def _async_handle_step( self, flow: Any, @@ -568,6 +576,10 @@ class FlowHandler: description_placeholders=description_placeholders, ) + @callback + def async_remove(self) -> None: + """Notification that the config flow has been removed.""" + @callback def _create_abort_data( diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 136c97808d3..1d60e20a3f0 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -1,6 +1,7 @@ """Test the flow classes.""" import asyncio -from unittest.mock import patch +import logging +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -149,6 +150,45 @@ async def test_abort_removes_instance(manager): assert len(manager.mock_created_entries) == 0 +async def test_abort_calls_async_remove(manager): + """Test abort calling the async_remove FlowHandler method.""" + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + async def async_step_init(self, user_input=None): + return self.async_abort(reason="reason") + + async_remove = Mock() + + await manager.async_init("test") + + TestFlow.async_remove.assert_called_once() + + assert len(manager.async_progress()) == 0 + assert len(manager.mock_created_entries) == 0 + + +async def test_abort_calls_async_remove_with_exception(manager, caplog): + """Test abort calling the async_remove FlowHandler method, with an exception.""" + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + async def async_step_init(self, user_input=None): + return self.async_abort(reason="reason") + + async_remove = Mock(side_effect=[RuntimeError("error")]) + + with caplog.at_level(logging.ERROR): + await manager.async_init("test") + + assert "Error removing test config flow: error" in caplog.text + + TestFlow.async_remove.assert_called_once() + + assert len(manager.async_progress()) == 0 + assert len(manager.mock_created_entries) == 0 + + async def test_create_saves_data(manager): """Test creating a config entry.""" From 14f68ec1a92ac7e41a1340f76255d6596affdd15 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Aug 2022 19:28:42 -0400 Subject: [PATCH 735/903] Store redirect URI in context instead of asking each time (#77380) * Store redirect URI in context instead of asking each time * Fix tests --- homeassistant/components/auth/login_flow.py | 18 +++++++------- .../components/config/config_entries.py | 1 + homeassistant/data_entry_flow.py | 1 + homeassistant/helpers/data_entry_flow.py | 1 + tests/components/auth/test_init.py | 2 -- tests/components/auth/test_init_link_user.py | 1 - tests/components/auth/test_login_flow.py | 24 ++++++++++++------- .../components/philips_js/test_config_flow.py | 1 + tests/components/subaru/test_config_flow.py | 2 ++ 9 files changed, 30 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index bb13431bfa7..df076a1b4c8 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -193,7 +193,6 @@ class LoginFlowBaseView(HomeAssistantView): self, request: web.Request, client_id: str, - redirect_uri: str, result: data_entry_flow.FlowResult, ) -> web.Response: """Convert the flow result to a response.""" @@ -214,10 +213,13 @@ class LoginFlowBaseView(HomeAssistantView): hass: HomeAssistant = request.app["hass"] - if not await indieauth.verify_redirect_uri(hass, client_id, redirect_uri): + if not await indieauth.verify_redirect_uri( + hass, client_id, result["context"]["redirect_uri"] + ): return self.json_message("Invalid redirect URI", HTTPStatus.FORBIDDEN) result.pop("data") + result.pop("context") result_obj: Credentials = result.pop("result") @@ -278,6 +280,7 @@ class LoginFlowIndexView(LoginFlowBaseView): context={ "ip_address": ip_address(request.remote), # type: ignore[arg-type] "credential_only": data.get("type") == "link_user", + "redirect_uri": redirect_uri, }, ) except data_entry_flow.UnknownHandler: @@ -287,9 +290,7 @@ class LoginFlowIndexView(LoginFlowBaseView): "Handler does not support init", HTTPStatus.BAD_REQUEST ) - return await self._async_flow_result_to_response( - request, client_id, redirect_uri, result - ) + return await self._async_flow_result_to_response(request, client_id, result) class LoginFlowResourceView(LoginFlowBaseView): @@ -304,7 +305,7 @@ class LoginFlowResourceView(LoginFlowBaseView): @RequestDataValidator( vol.Schema( - {vol.Required("client_id"): str, vol.Required("redirect_uri"): str}, + {vol.Required("client_id"): str}, extra=vol.ALLOW_EXTRA, ) ) @@ -314,7 +315,6 @@ class LoginFlowResourceView(LoginFlowBaseView): ) -> web.Response: """Handle progressing a login flow request.""" client_id: str = data.pop("client_id") - redirect_uri: str = data.pop("redirect_uri") if not indieauth.verify_client_id(client_id): return self.json_message("Invalid client id", HTTPStatus.BAD_REQUEST) @@ -330,9 +330,7 @@ class LoginFlowResourceView(LoginFlowBaseView): except vol.Invalid: return self.json_message("User input malformed", HTTPStatus.BAD_REQUEST) - return await self._async_flow_result_to_response( - request, client_id, redirect_uri, result - ) + return await self._async_flow_result_to_response(request, client_id, result) async def delete(self, request: web.Request, flow_id: str) -> web.Response: """Cancel a flow in progress.""" diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index ac452666103..54132080f08 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -121,6 +121,7 @@ def _prepare_config_flow_result_json(result, prepare_result_json): data = result.copy() data["result"] = entry_json(result["result"]) data.pop("data") + data.pop("context") return data diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index cdc4023f32c..629258e01d1 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -484,6 +484,7 @@ class FlowHandler: data=data, description=description, description_placeholders=description_placeholders, + context=self.context, ) @callback diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 428a62f0c9d..e3e4b4f0de8 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -30,6 +30,7 @@ class _BaseFlowManagerView(HomeAssistantView): data = result.copy() data.pop("result") data.pop("data") + data.pop("context") return data if "data_schema" not in result: diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 09a74cf9bc9..6854bb92052 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -64,7 +64,6 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, - "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "test-pass", }, @@ -133,7 +132,6 @@ async def test_auth_code_checks_local_only_user(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, - "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "test-pass", }, diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index bad6e3bfefe..882371a458f 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -48,7 +48,6 @@ async def async_get_code(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, - "redirect_uri": CLIENT_REDIRECT_URI, "username": "2nd-user", "password": "2nd-pass", }, diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index ce547149786..b3adfb93afb 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -61,7 +61,6 @@ async def test_invalid_username_password(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, - "redirect_uri": CLIENT_REDIRECT_URI, "username": "wrong-user", "password": "test-pass", }, @@ -82,7 +81,6 @@ async def test_invalid_username_password(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, - "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "wrong-pass", }, @@ -103,7 +101,6 @@ async def test_invalid_username_password(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, - "redirect_uri": "http://some-other-domain.com", "username": "wrong-user", "password": "test-pass", }, @@ -116,7 +113,21 @@ async def test_invalid_username_password(hass, aiohttp_client): assert step["step_id"] == "init" assert step["errors"]["base"] == "invalid_auth" - # Incorrect redirect URI + +async def test_invalid_redirect_uri(hass, aiohttp_client): + """Test invalid redirect URI.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.post( + "/auth/login_flow", + json={ + "client_id": CLIENT_ID, + "handler": ["insecure_example", None], + "redirect_uri": "https://some-other-domain.com", + }, + ) + assert resp.status == HTTPStatus.OK + step = await resp.json() + with patch( "homeassistant.components.auth.indieauth.fetch_redirect_uris", return_value=[] ), patch( @@ -126,7 +137,6 @@ async def test_invalid_username_password(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, - "redirect_uri": "http://some-other-domain.com", "username": "test-user", "password": "test-pass", }, @@ -165,7 +175,6 @@ async def test_login_exist_user(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, - "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "test-pass", }, @@ -206,14 +215,13 @@ async def test_login_local_only_user(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, - "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "test-pass", }, ) - assert len(mock_not_allowed_do_auth.mock_calls) == 1 assert resp.status == HTTPStatus.FORBIDDEN + assert len(mock_not_allowed_do_auth.mock_calls) == 1 assert await resp.json() == {"message": "Login blocked: User is local only"} diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index c6bade94ea4..284c7e7541e 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -120,6 +120,7 @@ async def test_pairing(hass, mock_tv_pairable, mock_setup_entry): ) assert result == { + "context": {"source": "user", "unique_id": "ABCDEFGHIJKLF"}, "flow_id": ANY, "type": "create_entry", "description": None, diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index e14a62d432d..62f69017a82 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -117,6 +117,7 @@ async def test_user_form_pin_not_required(hass, two_factor_verify_form): assert len(mock_setup_entry.mock_calls) == 1 expected = { + "context": {"source": "user"}, "title": TEST_USERNAME, "description": None, "description_placeholders": None, @@ -286,6 +287,7 @@ async def test_pin_form_success(hass, pin_form): assert len(mock_update_saved_pin.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 expected = { + "context": {"source": "user"}, "title": TEST_USERNAME, "description": None, "description_placeholders": None, From 3bddd6cf962984dbfca9103e8e28cdd4e8da069b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Aug 2022 01:30:48 +0200 Subject: [PATCH 736/903] Correct device class for tasmota apparent and reactive power sensors (#77519) --- homeassistant/components/tasmota/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 34526904f17..a9ab994b299 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -71,7 +71,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_APPARENT_POWERUSAGE: { - ICON: "mdi:flash", + DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_BATTERY: { @@ -162,7 +162,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL}, hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL}, hc.SENSOR_REACTIVE_POWERUSAGE: { - ICON: "mdi:flash", + DEVICE_CLASS: SensorDeviceClass.REACTIVE_POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, From 035cd16a9521faebd10c843119dad784fb699598 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 30 Aug 2022 00:30:17 +0000 Subject: [PATCH 737/903] [ci skip] Translation update --- .../ambiclimate/translations/de.json | 2 +- .../automation/translations/de.json | 2 +- .../automation/translations/it.json | 2 +- .../components/awair/translations/de.json | 2 +- .../components/bluetooth/translations/de.json | 2 +- .../components/bluetooth/translations/it.json | 2 +- .../components/bluetooth/translations/no.json | 2 +- .../bluetooth/translations/pt-BR.json | 2 +- .../components/broadlink/translations/de.json | 2 +- .../components/bthome/translations/de.json | 32 +++++++++++++++++++ .../components/bthome/translations/no.json | 32 +++++++++++++++++++ .../components/bthome/translations/pt-BR.json | 32 +++++++++++++++++++ .../components/deconz/translations/de.json | 8 ++--- .../components/dlna_dmr/translations/de.json | 2 +- .../components/dsmr/translations/pt-BR.json | 2 ++ .../flunearyou/translations/de.json | 2 +- .../components/freebox/translations/de.json | 2 +- .../components/google/translations/de.json | 2 +- .../components/guardian/translations/de.json | 2 +- .../components/hyperion/translations/de.json | 2 +- .../components/iotawatt/translations/de.json | 2 +- .../components/kraken/translations/pt-BR.json | 4 +++ .../components/led_ble/translations/ca.json | 23 +++++++++++++ .../components/led_ble/translations/de.json | 23 +++++++++++++ .../components/led_ble/translations/es.json | 23 +++++++++++++ .../components/led_ble/translations/fr.json | 23 +++++++++++++ .../components/led_ble/translations/it.json | 23 +++++++++++++ .../led_ble/translations/pt-BR.json | 23 +++++++++++++ .../litterrobot/translations/ca.json | 10 +++++- .../litterrobot/translations/de.json | 10 +++++- .../litterrobot/translations/el.json | 10 +++++- .../litterrobot/translations/en.json | 12 +++---- .../litterrobot/translations/es.json | 10 +++++- .../litterrobot/translations/et.json | 10 +++++- .../litterrobot/translations/fr.json | 10 +++++- .../litterrobot/translations/it.json | 10 +++++- .../litterrobot/translations/ja.json | 9 +++++- .../litterrobot/translations/no.json | 10 +++++- .../litterrobot/translations/pt-BR.json | 10 +++++- .../litterrobot/translations/zh-Hant.json | 10 +++++- .../logi_circle/translations/de.json | 4 +-- .../components/mqtt/translations/de.json | 8 ++--- .../nam/translations/sensor.de.json | 11 +++++++ .../nam/translations/sensor.el.json | 11 +++++++ .../nam/translations/sensor.es.json | 11 +++++++ .../nam/translations/sensor.et.json | 11 +++++++ .../nam/translations/sensor.fr.json | 11 +++++++ .../nam/translations/sensor.it.json | 11 +++++++ .../nam/translations/sensor.no.json | 11 +++++++ .../nam/translations/sensor.pl.json | 11 +++++++ .../nam/translations/sensor.pt-BR.json | 11 +++++++ .../nam/translations/sensor.zh-Hant.json | 11 +++++++ .../components/nanoleaf/translations/de.json | 2 +- .../components/nest/translations/de.json | 10 +++--- .../components/octoprint/translations/de.json | 2 +- .../components/onvif/translations/de.json | 2 +- .../components/point/translations/de.json | 2 +- .../components/ps4/translations/de.json | 4 +-- .../components/rachio/translations/de.json | 2 +- .../components/renault/translations/ca.json | 2 +- .../components/risco/translations/pt-BR.json | 4 +-- .../components/roon/translations/de.json | 2 +- .../components/roon/translations/pt-BR.json | 4 +++ .../components/scrape/translations/pt-BR.json | 4 +-- .../components/shelly/translations/de.json | 2 +- .../simplisafe/translations/de.json | 2 +- .../speedtestdotnet/translations/de.json | 13 ++++++++ .../speedtestdotnet/translations/it.json | 13 ++++++++ .../speedtestdotnet/translations/no.json | 13 ++++++++ .../speedtestdotnet/translations/pt-BR.json | 6 ++-- .../switchbot/translations/pt-BR.json | 4 +++ .../tellduslive/translations/de.json | 2 +- .../thermobeacon/translations/de.json | 22 +++++++++++++ .../thermobeacon/translations/no.json | 22 +++++++++++++ .../thermobeacon/translations/pt-BR.json | 4 +-- .../thermopro/translations/pt-BR.json | 2 +- .../unifiprotect/translations/ca.json | 4 +++ .../unifiprotect/translations/de.json | 4 +++ .../unifiprotect/translations/el.json | 4 +++ .../unifiprotect/translations/es.json | 4 +++ .../unifiprotect/translations/et.json | 4 +++ .../unifiprotect/translations/fr.json | 4 +++ .../unifiprotect/translations/it.json | 4 +++ .../unifiprotect/translations/no.json | 4 +++ .../unifiprotect/translations/pt-BR.json | 4 +++ .../unifiprotect/translations/zh-Hant.json | 4 +++ .../components/upnp/translations/pt-BR.json | 4 +++ .../volvooncall/translations/pt-BR.json | 2 +- .../components/webostv/translations/de.json | 4 +-- .../xiaomi_ble/translations/de.json | 8 ++--- .../components/zha/translations/de.json | 16 +++++----- 91 files changed, 657 insertions(+), 85 deletions(-) create mode 100644 homeassistant/components/bthome/translations/de.json create mode 100644 homeassistant/components/bthome/translations/no.json create mode 100644 homeassistant/components/bthome/translations/pt-BR.json create mode 100644 homeassistant/components/led_ble/translations/ca.json create mode 100644 homeassistant/components/led_ble/translations/de.json create mode 100644 homeassistant/components/led_ble/translations/es.json create mode 100644 homeassistant/components/led_ble/translations/fr.json create mode 100644 homeassistant/components/led_ble/translations/it.json create mode 100644 homeassistant/components/led_ble/translations/pt-BR.json create mode 100644 homeassistant/components/nam/translations/sensor.de.json create mode 100644 homeassistant/components/nam/translations/sensor.el.json create mode 100644 homeassistant/components/nam/translations/sensor.es.json create mode 100644 homeassistant/components/nam/translations/sensor.et.json create mode 100644 homeassistant/components/nam/translations/sensor.fr.json create mode 100644 homeassistant/components/nam/translations/sensor.it.json create mode 100644 homeassistant/components/nam/translations/sensor.no.json create mode 100644 homeassistant/components/nam/translations/sensor.pl.json create mode 100644 homeassistant/components/nam/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/nam/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/thermobeacon/translations/de.json create mode 100644 homeassistant/components/thermobeacon/translations/no.json diff --git a/homeassistant/components/ambiclimate/translations/de.json b/homeassistant/components/ambiclimate/translations/de.json index 3f4537a5d5c..4e8d987a8ce 100644 --- a/homeassistant/components/ambiclimate/translations/de.json +++ b/homeassistant/components/ambiclimate/translations/de.json @@ -9,7 +9,7 @@ "default": "Erfolgreich authentifiziert" }, "error": { - "follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst", + "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden dr\u00fcckst", "no_token": "Nicht authentifiziert mit Ambiclimate" }, "step": { diff --git a/homeassistant/components/automation/translations/de.json b/homeassistant/components/automation/translations/de.json index 9cadf8c51f8..6de031e3db8 100644 --- a/homeassistant/components/automation/translations/de.json +++ b/homeassistant/components/automation/translations/de.json @@ -4,7 +4,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Die Automatisierung \"{name}\" (`{entity_id}`) hat eine Aktion, die einen unbekannten Dienst aufruft: `{service}`.\n\nDieser Fehler verhindert, dass die Automatisierung ordnungsgem\u00e4\u00df ausgef\u00fchrt wird. Vielleicht ist dieser Dienst nicht mehr verf\u00fcgbar oder vielleicht hat ein Tippfehler ihn verursacht.\n\nUm diesen Fehler zu beheben, [bearbeite die Automatisierung]({edit}) und entferne die Aktion, die diesen Dienst aufruft.\n\nKlicke unten auf SENDEN, um zu best\u00e4tigen, dass du diese Automatisierung korrigiert hast.", + "description": "Die Automatisierung \"{name}\" (`{entity_id}`) hat eine Aktion, die einen unbekannten Dienst aufruft: `{service}`.\n\nDieser Fehler verhindert, dass die Automatisierung ordnungsgem\u00e4\u00df ausgef\u00fchrt wird. Vielleicht ist dieser Dienst nicht mehr verf\u00fcgbar oder vielleicht hat ein Tippfehler ihn verursacht.\n\nUm diesen Fehler zu beheben, [bearbeite die Automatisierung]({edit}) und entferne die Aktion, die diesen Dienst aufruft.\n\nDr\u00fccke unten auf SENDEN, um zu best\u00e4tigen, dass du diese Automatisierung korrigiert hast.", "title": "{name} verwendet einen unbekannten Dienst" } } diff --git a/homeassistant/components/automation/translations/it.json b/homeassistant/components/automation/translations/it.json index 72987f29477..7a8a4d572a9 100644 --- a/homeassistant/components/automation/translations/it.json +++ b/homeassistant/components/automation/translations/it.json @@ -4,7 +4,7 @@ "fix_flow": { "step": { "confirm": { - "description": "L'automazione \"{name}\" (`{entity_id}`) ha un'azione che chiama un servizio sconosciuto: `{service}`. \n\nQuesto errore impedisce il corretto funzionamento dell'automazione. Forse questo servizio non \u00e8 pi\u00f9 disponibile, o forse un errore di battitura lo ha causato. \n\nPer correggere questo errore, [modifica l'automazione]({edit}) e rimuovi l'azione che chiama questo servizio. \n\nFai clic su INVIA di seguito per confermare di aver corretto questa automazione.", + "description": "L'automazione \"{name}\" (`{entity_id}`) ha un'azione che chiama un servizio sconosciuto: `{service}`. \n\nQuesto errore impedisce il corretto funzionamento dell'automazione. Forse questo servizio non \u00e8 pi\u00f9 disponibile, o forse lo ha causato un errore di battitura. \n\nPer correggere questo errore, [modifica l'automazione]({edit}) e rimuovi l'azione che chiama questo servizio. \n\nFai clic su INVIA di seguito per confermare di aver corretto questa automazione.", "title": "{name} utilizza un servizio sconosciuto" } } diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index cba100b899c..af7aaaafff7 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -29,7 +29,7 @@ "data": { "host": "IP-Adresse" }, - "description": "Befolge [diese Anweisungen]({url}), um die Awair Local API zu aktivieren. \n\nKlicke abschlie\u00dfend auf Senden." + "description": "Befolge [diese Anweisungen]({url}), um die Awair Local API zu aktivieren. \n\nDr\u00fccke abschlie\u00dfend auf Senden." }, "local_pick": { "data": { diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index be3d0fa074a..1f8f48e05ee 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "Der zum Scannen zu verwendende Bluetooth-Adapter", - "passive": "Passives Mith\u00f6ren" + "passive": "Passives Scannen" }, "description": "Passives Mith\u00f6ren erfordert BlueZ 5.63 oder h\u00f6her mit aktivierten experimentellen Funktionen." } diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index 244a0e84d59..fc9ea431d29 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "L'adattatore Bluetooth da utilizzare per la scansione", - "passive": "Ascolto passivo" + "passive": "Scansione passiva" }, "description": "L'ascolto passivo richiede BlueZ 5.63 o successivo con funzionalit\u00e0 sperimentali abilitate." } diff --git a/homeassistant/components/bluetooth/translations/no.json b/homeassistant/components/bluetooth/translations/no.json index 761e3fd0a84..5ab1050a849 100644 --- a/homeassistant/components/bluetooth/translations/no.json +++ b/homeassistant/components/bluetooth/translations/no.json @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "Bluetooth-adapteren som skal brukes til skanning", - "passive": "Passiv lytting" + "passive": "Passiv skanning" }, "description": "Passiv lytting krever BlueZ 5.63 eller nyere med eksperimentelle funksjoner aktivert." } diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index daa1f2bc091..11c802b5023 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "O adaptador Bluetooth a ser usado para escaneamento", - "passive": "Escuta passiva" + "passive": "Varredura passiva" }, "description": "A escuta passiva requer BlueZ 5.63 ou posterior com recursos experimentais ativados." } diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index 1e5635b3145..98a5fe4bd2a 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -25,7 +25,7 @@ "title": "W\u00e4hle einen Namen f\u00fcr das Ger\u00e4t" }, "reset": { - "description": "{name} ({model} unter {host}) ist gesperrt. Du musst das Ger\u00e4t entsperren, um dich zu authentifizieren und die Konfiguration abzuschlie\u00dfen. Anweisungen:\n1. \u00d6ffne die Broadlink-App.\n2. Klicke auf auf das Ger\u00e4t.\n3. Klicke oben rechts auf `...`.\n4. Scrolle zum unteren Ende der Seite.\n5. Deaktiviere die Sperre.", + "description": "{name} ({model} unter {host}) ist gesperrt. Du musst das Ger\u00e4t entsperren, um dich zu authentifizieren und die Konfiguration abzuschlie\u00dfen. Anweisungen:\n1. \u00d6ffne die Broadlink-App.\n2. Dr\u00fccke auf auf das Ger\u00e4t.\n3. Dr\u00fccke oben rechts auf `...`.\n4. Scrolle zum unteren Ende der Seite.\n5. Deaktiviere die Sperre.", "title": "Entsperren des Ger\u00e4ts" }, "unlock": { diff --git a/homeassistant/components/bthome/translations/de.json b/homeassistant/components/bthome/translations/de.json new file mode 100644 index 00000000000..00c32b1a831 --- /dev/null +++ b/homeassistant/components/bthome/translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "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", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "decryption_failed": "Der bereitgestellte Bindkey funktionierte nicht, Sensordaten konnten nicht entschl\u00fcsselt werden. Bitte \u00fcberpr\u00fcfe es und versuche es erneut.", + "expected_32_characters": "Erwartet wird ein 32-stelliger hexadezimaler Bindkey." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 32-stelligen hexadezimalen Bindkey." + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/no.json b/homeassistant/components/bthome/translations/no.json new file mode 100644 index 00000000000..ba68150db4c --- /dev/null +++ b/homeassistant/components/bthome/translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "decryption_failed": "Den oppgitte bindingsn\u00f8kkelen fungerte ikke, sensordata kunne ikke dekrypteres. Vennligst sjekk det og pr\u00f8v igjen.", + "expected_32_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 32 tegn." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sensordataene som sendes av sensoren er kryptert. For \u00e5 dekryptere den trenger vi en heksadesimal bindn\u00f8kkel p\u00e5 32 tegn." + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/pt-BR.json b/homeassistant/components/bthome/translations/pt-BR.json new file mode 100644 index 00000000000..4a54b9f8a62 --- /dev/null +++ b/homeassistant/components/bthome/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "decryption_failed": "A chave de liga\u00e7\u00e3o fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", + "expected_32_characters": "Esperava-se uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Chave de liga\u00e7\u00e3o para descriptografia (bindkey)" + }, + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index b8b260709e8..28308f10f74 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Ger\u00e4t aufgeweckt", - "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", + "remote_button_double_press": "\"{subtype}\" Taste doppelt angedr\u00fcckt", "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", - "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt", - "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt", + "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach gedr\u00fcckt", + "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach gedr\u00fcckt", "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", - "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt", + "remote_button_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt", "remote_double_tap": "Ger\u00e4t \"{subtype}\" doppelt getippt", "remote_double_tap_any_side": "Ger\u00e4t auf beliebiger Seite doppelt angetippt", "remote_falling": "Ger\u00e4t im freien Fall", diff --git a/homeassistant/components/dlna_dmr/translations/de.json b/homeassistant/components/dlna_dmr/translations/de.json index 64cae60c13e..f9f7ce997fe 100644 --- a/homeassistant/components/dlna_dmr/translations/de.json +++ b/homeassistant/components/dlna_dmr/translations/de.json @@ -19,7 +19,7 @@ "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" }, "import_turn_on": { - "description": "Bitte schalte das Ger\u00e4t ein und klicke auf Senden, um die Migration fortzusetzen" + "description": "Bitte schalte das Ger\u00e4t ein und dr\u00fccke auf Senden, um die Migration fortzusetzen" }, "manual": { "data": { diff --git a/homeassistant/components/dsmr/translations/pt-BR.json b/homeassistant/components/dsmr/translations/pt-BR.json index 911a93db1b8..97722b24982 100644 --- a/homeassistant/components/dsmr/translations/pt-BR.json +++ b/homeassistant/components/dsmr/translations/pt-BR.json @@ -11,6 +11,8 @@ "cannot_connect": "Falha ao conectar" }, "step": { + "one": "", + "other": "", "setup_network": { "data": { "dsmr_version": "Selecione a vers\u00e3o do DSMR", diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json index 72109df534c..4e2287ad9ca 100644 --- a/homeassistant/components/flunearyou/translations/de.json +++ b/homeassistant/components/flunearyou/translations/de.json @@ -22,7 +22,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Die externe Datenquelle, aus der die Integration von Flu Near You gespeist wird, ist nicht mehr verf\u00fcgbar; daher funktioniert die Integration nicht mehr.\n\nDr\u00fccke SUBMIT, um Flu Near You aus deiner Home Assistant-Instanz zu entfernen.", + "description": "Die externe Datenquelle, aus der die Integration von Flu Near You gespeist wird, ist nicht mehr verf\u00fcgbar; daher funktioniert die Integration nicht mehr.\n\nDr\u00fccke SENDEN, um Flu Near You aus deiner Home Assistant-Instanz zu entfernen.", "title": "Flu Near You entfernen" } } diff --git a/homeassistant/components/freebox/translations/de.json b/homeassistant/components/freebox/translations/de.json index 864ce5f0c99..4fe44e2198d 100644 --- a/homeassistant/components/freebox/translations/de.json +++ b/homeassistant/components/freebox/translations/de.json @@ -10,7 +10,7 @@ }, "step": { "link": { - "description": "Klicke auf \"Senden\" und ber\u00fchre dann den Pfeil nach rechts auf dem Router, um Freebox bei Home Assistant zu registrieren. \n\n ![Position der Schaltfl\u00e4che am Router](/static/images/config_freebox.png)", + "description": "Dr\u00fccke auf \"Senden\" und ber\u00fchre dann den Pfeil nach rechts auf dem Router, um Freebox bei Home Assistant zu registrieren. \n\n ![Position der Schaltfl\u00e4che am Router](/static/images/config_freebox.png)", "title": "Link Freebox Router" }, "user": { diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index 2e81b2357c8..ec3c6d15035 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Folge den [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um Home Assistant Zugriff auf deinen Google-Kalender zu geben. Du musst auch Anwendungsnachweise erstellen, die mit deinem Kalender verkn\u00fcpft sind:\n1. Gehe zu [Credentials]({oauth_creds_url}) und klicke auf **Create Credentials**.\n1. W\u00e4hle in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **TV und eingeschr\u00e4nkte Eingabeger\u00e4te** f\u00fcr den Anwendungstyp.\n\n" + "description": "Folge den [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um Home Assistant Zugriff auf deinen Google-Kalender zu geben. Du musst auch Anwendungsnachweise erstellen, die mit deinem Kalender verkn\u00fcpft sind:\n1. Gehe zu [Credentials]({oauth_creds_url}) und dr\u00fccke auf **Create Credentials**.\n1. W\u00e4hle in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **TV und eingeschr\u00e4nkte Eingabeger\u00e4te** f\u00fcr den Anwendungstyp.\n\n" }, "config": { "abort": { diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 9b042a7a1c4..33d2973317f 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -23,7 +23,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Zielentit\u00e4ts-ID von `{alternate_target}` zu verwenden. Klicke dann unten auf SUBMIT, um dieses Problem als behoben zu markieren.", + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Zielentit\u00e4ts-ID von `{alternate_target}` zu verwenden. Dr\u00fccke dann unten auf SENDEN, um dieses Problem als behoben zu markieren.", "title": "Der Dienst {deprecated_service} wird entfernt" } } diff --git a/homeassistant/components/hyperion/translations/de.json b/homeassistant/components/hyperion/translations/de.json index a27abb7ff5f..22a1aba2bb6 100644 --- a/homeassistant/components/hyperion/translations/de.json +++ b/homeassistant/components/hyperion/translations/de.json @@ -27,7 +27,7 @@ "title": "Best\u00e4tige das Hinzuf\u00fcgen des Hyperion-Ambilight-Dienstes" }, "create_token": { - "description": "W\u00e4hle **Submit**, um einen neuen Authentifizierungs-Token anzufordern. Du wirst zur Hyperion-Benutzeroberfl\u00e4che weitergeleitet, um die Anforderung zu best\u00e4tigen. Bitte \u00fcberpr\u00fcfe, ob die angezeigte ID \"{auth_id}\" lautet.", + "description": "W\u00e4hle **Senden**, um einen neuen Authentifizierungs-Token anzufordern. Du wirst zur Hyperion-Benutzeroberfl\u00e4che weitergeleitet, um die Anforderung zu best\u00e4tigen. Bitte \u00fcberpr\u00fcfe, ob die angezeigte ID \"{auth_id}\" lautet.", "title": "Automatisch neuen Authentifizierungs-Token erstellen" }, "create_token_external": { diff --git a/homeassistant/components/iotawatt/translations/de.json b/homeassistant/components/iotawatt/translations/de.json index b1dda29414b..d5626c7e135 100644 --- a/homeassistant/components/iotawatt/translations/de.json +++ b/homeassistant/components/iotawatt/translations/de.json @@ -11,7 +11,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Das IoTawatt-Ger\u00e4t erfordert eine Authentifizierung. Bitte gib den Benutzernamen und das Passwort ein und klicke auf die Schaltfl\u00e4che Senden." + "description": "Das IoTawatt-Ger\u00e4t erfordert eine Authentifizierung. Bitte gib den Benutzernamen und das Passwort ein und dr\u00fccke auf die Schaltfl\u00e4che Senden." }, "user": { "data": { diff --git a/homeassistant/components/kraken/translations/pt-BR.json b/homeassistant/components/kraken/translations/pt-BR.json index 955386f2098..6351c298062 100644 --- a/homeassistant/components/kraken/translations/pt-BR.json +++ b/homeassistant/components/kraken/translations/pt-BR.json @@ -5,6 +5,10 @@ }, "step": { "user": { + "data": { + "one": "", + "other": "" + }, "description": "Deseja iniciar a configura\u00e7\u00e3o?" } } diff --git a/homeassistant/components/led_ble/translations/ca.json b/homeassistant/components/led_ble/translations/ca.json new file mode 100644 index 00000000000..8ec41bb97db --- /dev/null +++ b/homeassistant/components/led_ble/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "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", + "no_unconfigured_devices": "No s'han trobat dispositius no configurats.", + "not_supported": "Dispositiu no compatible" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adre\u00e7a Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/de.json b/homeassistant/components/led_ble/translations/de.json new file mode 100644 index 00000000000..d7ffdfb67c2 --- /dev/null +++ b/homeassistant/components/led_ble/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "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", + "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden.", + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-Adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/es.json b/homeassistant/components/led_ble/translations/es.json new file mode 100644 index 00000000000..2fc60e86dd5 --- /dev/null +++ b/homeassistant/components/led_ble/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "no_unconfigured_devices": "No se encontraron dispositivos no configurados.", + "not_supported": "Dispositivo no compatible" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Direcci\u00f3n Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/fr.json b/homeassistant/components/led_ble/translations/fr.json new file mode 100644 index 00000000000..d0e00344152 --- /dev/null +++ b/homeassistant/components/led_ble/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9.", + "not_supported": "Appareil non pris en charge" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adresse Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/it.json b/homeassistant/components/led_ble/translations/it.json new file mode 100644 index 00000000000..ef919547573 --- /dev/null +++ b/homeassistant/components/led_ble/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "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", + "no_unconfigured_devices": "Non sono stati trovati dispositivi non configurati.", + "not_supported": "Dispositivo non supportato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Indirizzo Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/pt-BR.json b/homeassistant/components/led_ble/translations/pt-BR.json new file mode 100644 index 00000000000..6b58b4193a5 --- /dev/null +++ b/homeassistant/components/led_ble/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado.", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Endere\u00e7o bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/ca.json b/homeassistant/components/litterrobot/translations/ca.json index 5165473860a..10506ab95d6 100644 --- a/homeassistant/components/litterrobot/translations/ca.json +++ b/homeassistant/components/litterrobot/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Si us plau, actualitza la contrasenya de {username}", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/litterrobot/translations/de.json b/homeassistant/components/litterrobot/translations/de.json index 14f319fb4d3..18bb6458cf3 100644 --- a/homeassistant/components/litterrobot/translations/de.json +++ b/homeassistant/components/litterrobot/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Bitte \u00e4ndere Dein Passwort f\u00fcr {username}", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/litterrobot/translations/el.json b/homeassistant/components/litterrobot/translations/el.json index cdc7ae85736..d5f7cabb2df 100644 --- a/homeassistant/components/litterrobot/translations/el.json +++ b/homeassistant/components/litterrobot/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -9,6 +10,13 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username}", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json index 2ca3d2f0dc2..3d6ab4dfaaa 100644 --- a/homeassistant/components/litterrobot/translations/en.json +++ b/homeassistant/components/litterrobot/translations/en.json @@ -10,18 +10,18 @@ "unknown": "Unexpected error" }, "step": { - "user": { - "data": { - "password": "Password", - "username": "Username" - } - }, "reauth_confirm": { "data": { "password": "Password" }, "description": "Please update your password for {username}", "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + } } } } diff --git a/homeassistant/components/litterrobot/translations/es.json b/homeassistant/components/litterrobot/translations/es.json index f92417d76a0..003a715f8a7 100644 --- a/homeassistant/components/litterrobot/translations/es.json +++ b/homeassistant/components/litterrobot/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -9,6 +10,13 @@ "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Por favor, actualiza tu contrase\u00f1a para {username}", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/litterrobot/translations/et.json b/homeassistant/components/litterrobot/translations/et.json index c3881a20337..8bbd26ee4c4 100644 --- a/homeassistant/components/litterrobot/translations/et.json +++ b/homeassistant/components/litterrobot/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kasutaja on juba seadistatud" + "already_configured": "Kasutaja on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -9,6 +10,13 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Uuenda kasutaja {username} salas\u00f5na", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/litterrobot/translations/fr.json b/homeassistant/components/litterrobot/translations/fr.json index 744b9c6a862..04e230589f9 100644 --- a/homeassistant/components/litterrobot/translations/fr.json +++ b/homeassistant/components/litterrobot/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -9,6 +10,13 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Veuillez mettre \u00e0 jour votre mot de passe pour {username}", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/litterrobot/translations/it.json b/homeassistant/components/litterrobot/translations/it.json index aee18749ab0..8b8ea9c03bb 100644 --- a/homeassistant/components/litterrobot/translations/it.json +++ b/homeassistant/components/litterrobot/translations/it.json @@ -1,7 +1,8 @@ { "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": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Aggiorna la tua password per {username}", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/litterrobot/translations/ja.json b/homeassistant/components/litterrobot/translations/ja.json index b4c39a6b251..6972cf2318a 100644 --- a/homeassistant/components/litterrobot/translations/ja.json +++ b/homeassistant/components/litterrobot/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -9,6 +10,12 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/litterrobot/translations/no.json b/homeassistant/components/litterrobot/translations/no.json index 4ea7b2401c3..40e3013cf45 100644 --- a/homeassistant/components/litterrobot/translations/no.json +++ b/homeassistant/components/litterrobot/translations/no.json @@ -1,7 +1,8 @@ { "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", @@ -9,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Vennligst oppdater passordet ditt for {username}", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/litterrobot/translations/pt-BR.json b/homeassistant/components/litterrobot/translations/pt-BR.json index d86aef5d51d..9b204c74f07 100644 --- a/homeassistant/components/litterrobot/translations/pt-BR.json +++ b/homeassistant/components/litterrobot/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 foi configurada" + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", @@ -9,6 +10,13 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Atualize sua senha para {username}", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Senha", diff --git a/homeassistant/components/litterrobot/translations/zh-Hant.json b/homeassistant/components/litterrobot/translations/zh-Hant.json index b07b7115b07..d83d49912ea 100644 --- a/homeassistant/components/litterrobot/translations/zh-Hant.json +++ b/homeassistant/components/litterrobot/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u66f4\u65b0 {username} \u5bc6\u78bc", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/logi_circle/translations/de.json b/homeassistant/components/logi_circle/translations/de.json index b1f318a8e5f..bed8328c92e 100644 --- a/homeassistant/components/logi_circle/translations/de.json +++ b/homeassistant/components/logi_circle/translations/de.json @@ -8,12 +8,12 @@ }, "error": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden klickst.", + "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden dr\u00fcckst.", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "auth": { - "description": "Folge dem Link unten und klicke **Akzeptieren** um auf dein Logi Circle-Konto zuzugreifen. Kehre dann zur\u00fcck und dr\u00fccke unten auf **Senden** . \n\n [Link] ({authorization_url})", + "description": "Folge dem Link unten und dr\u00fccke **Akzeptieren** um auf dein Logi Circle-Konto zuzugreifen. Kehre dann zur\u00fcck und dr\u00fccke unten auf **Senden** . \n\n [Link] ({authorization_url})", "title": "Authentifizierung mit Logi Circle" }, "user": { diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index ad92ed995d6..8a0171a6f85 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -39,14 +39,14 @@ "turn_on": "Einschalten" }, "trigger_type": { - "button_double_press": "\"{subtype}\" doppelt angeklickt", + "button_double_press": "\"{subtype}\" doppelt angedr\u00fcckt", "button_long_press": "\"{subtype}\" kontinuierlich gedr\u00fcckt", "button_long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen", - "button_quadruple_press": "\"{subtype}\" Vierfach geklickt", - "button_quintuple_press": "\"{subtype}\" f\u00fcnffach geklickt", + "button_quadruple_press": "\"{subtype}\" Vierfach gedr\u00fcckt", + "button_quintuple_press": "\"{subtype}\" f\u00fcnffach gedr\u00fcckt", "button_short_press": "\"{subtype}\" gedr\u00fcckt", "button_short_release": "\"{subtype}\" losgelassen", - "button_triple_press": "\"{subtype}\" dreifach geklickt" + "button_triple_press": "\"{subtype}\" dreifach gedr\u00fcckt" } }, "issues": { diff --git a/homeassistant/components/nam/translations/sensor.de.json b/homeassistant/components/nam/translations/sensor.de.json new file mode 100644 index 00000000000..ac352ac1c03 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.de.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Hoch", + "low": "Niedrig", + "medium": "Mittel", + "very high": "Sehr hoch", + "very low": "Sehr niedrig" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.el.json b/homeassistant/components/nam/translations/sensor.el.json new file mode 100644 index 00000000000..ce5c31d8d7b --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.el.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", + "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", + "medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf", + "very high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", + "very low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.es.json b/homeassistant/components/nam/translations/sensor.es.json new file mode 100644 index 00000000000..8a4a66f04fa --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.es.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Alto", + "low": "Bajo", + "medium": "Medio", + "very high": "Muy alto", + "very low": "Muy bajo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.et.json b/homeassistant/components/nam/translations/sensor.et.json new file mode 100644 index 00000000000..d608652071f --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.et.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "K\u00f5rge", + "low": "Madal", + "medium": "Keskmine", + "very high": "V\u00e4ga k\u00f5rge", + "very low": "V\u00e4ga madal" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.fr.json b/homeassistant/components/nam/translations/sensor.fr.json new file mode 100644 index 00000000000..475bc0ba61c --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.fr.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "\u00c9lev\u00e9", + "low": "Faible", + "medium": "Moyen", + "very high": "Tr\u00e8s \u00e9lev\u00e9", + "very low": "Tr\u00e8s faible" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.it.json b/homeassistant/components/nam/translations/sensor.it.json new file mode 100644 index 00000000000..dfa784f8e12 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.it.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Alto", + "low": "Basso", + "medium": "Medio", + "very high": "Molto alto", + "very low": "Molto basso" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.no.json b/homeassistant/components/nam/translations/sensor.no.json new file mode 100644 index 00000000000..6ddb76e2a99 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.no.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "H\u00f8y", + "low": "Lav", + "medium": "Medium", + "very high": "Veldig h\u00f8y", + "very low": "Veldig lav" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.pl.json b/homeassistant/components/nam/translations/sensor.pl.json new file mode 100644 index 00000000000..3c3a9961308 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.pl.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "wysoki", + "low": "niski", + "medium": "\u015bredni", + "very high": "bardzo wysoki", + "very low": "bardzo niski" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.pt-BR.json b/homeassistant/components/nam/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..239de3e055a --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.pt-BR.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Alto", + "low": "Baixo", + "medium": "M\u00e9dio", + "very high": "Muito alto", + "very low": "Muito baixo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.zh-Hant.json b/homeassistant/components/nam/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..d0e61f0e1d2 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.zh-Hant.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d", + "very high": "\u6975\u9ad8", + "very low": "\u6975\u4f4e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/de.json b/homeassistant/components/nanoleaf/translations/de.json index e29b3b76a46..2eb309fd71c 100644 --- a/homeassistant/components/nanoleaf/translations/de.json +++ b/homeassistant/components/nanoleaf/translations/de.json @@ -15,7 +15,7 @@ "flow_title": "{name}", "step": { "link": { - "description": "Halte die Ein-/Aus-Taste an deinem Nanoleaf 5 Sekunden lang gedr\u00fcckt, bis die LEDs der Tasten zu blinken beginnen, und klicke dann innerhalb von 30 Sekunden auf **SENDEN**.", + "description": "Halte die Ein-/Aus-Taste an deinem Nanoleaf 5 Sekunden lang gedr\u00fcckt, bis die LEDs der Tasten zu blinken beginnen, und dr\u00fccke dann innerhalb von 30 Sekunden auf **SENDEN**.", "title": "Nanoleaf verkn\u00fcpfen" }, "user": { diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 0836cc974bc..18ecafda58e 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Folge den [Anweisungen]({more_info_url}), um die Cloud-Konsole zu konfigurieren:\n\n1. Gehe zum [OAuth-Zustimmungsbildschirm]({oauth_consent_url}) und konfiguriere\n1. Gehe zu [Credentials]({oauth_creds_url}) und klicke auf **Create Credentials**.\n1. W\u00e4hle in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **Webanwendung** f\u00fcr den Anwendungstyp.\n1. F\u00fcge `{redirect_url}` unter *Authorized redirect URI* hinzu." + "description": "Folge den [Anweisungen]({more_info_url}), um die Cloud-Konsole zu konfigurieren:\n\n1. Gehe zum [OAuth-Zustimmungsbildschirm]({oauth_consent_url}) und konfiguriere\n1. Gehe zu [Credentials]({oauth_creds_url}) und dr\u00fccke auf **Create Credentials**.\n1. W\u00e4hle in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **Webanwendung** f\u00fcr den Anwendungstyp.\n1. F\u00fcge `{redirect_url}` unter *Authorized redirect URI* hinzu." }, "config": { "abort": { @@ -45,18 +45,18 @@ "title": "Nest: Cloud-Projekt-ID eingeben" }, "create_cloud_project": { - "description": "Die Nest-Integration erm\u00f6glicht es dir, deine Nest-Thermostate, -Kameras und -T\u00fcrklingeln \u00fcber die Smart Device Management API zu integrieren. Die SDM API **erfordert eine einmalige Einrichtungsgeb\u00fchr von US $5**. Siehe Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).\n\n1. Rufe die [Google Cloud Console]({cloud_console_url}) auf.\n1. Wenn dies dein erstes Projekt ist, klicke auf **Projekt erstellen** und dann auf **Neues Projekt**.\n1. Gib deinem Cloud-Projekt einen Namen und klicke dann auf **Erstellen**.\n1. Speichere die Cloud Project ID, z. B. *example-project-12345*, da du diese sp\u00e4ter ben\u00f6tigst.\n1. Gehe zur API-Bibliothek f\u00fcr [Smart Device Management API]({sdm_api_url}) und klicke auf **Aktivieren**.\n1. Wechsele zur API-Bibliothek f\u00fcr [Cloud Pub/Sub API]({pubsub_api_url}) und klicke auf **Aktivieren**.\n\nFahre fort, wenn dein Cloud-Projekt eingerichtet ist.", + "description": "Die Nest-Integration erm\u00f6glicht es dir, deine Nest-Thermostate, -Kameras und -T\u00fcrklingeln \u00fcber die Smart Device Management API zu integrieren. Die SDM API **erfordert eine einmalige Einrichtungsgeb\u00fchr von US $5**. Siehe Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).\n\n1. Rufe die [Google Cloud Console]({cloud_console_url}) auf.\n1. Wenn dies dein erstes Projekt ist, dr\u00fccke auf **Projekt erstellen** und dann auf **Neues Projekt**.\n1. Gib deinem Cloud-Projekt einen Namen und dr\u00fccke dann auf **Erstellen**.\n1. Speichere die Cloud Project ID, z. B. *example-project-12345*, da du diese sp\u00e4ter ben\u00f6tigst.\n1. Gehe zur API-Bibliothek f\u00fcr [Smart Device Management API]({sdm_api_url}) und dr\u00fccke auf **Aktivieren**.\n1. Wechsele zur API-Bibliothek f\u00fcr [Cloud Pub/Sub API]({pubsub_api_url}) und dr\u00fccke auf **Aktivieren**.\n\nFahre fort, wenn dein Cloud-Projekt eingerichtet ist.", "title": "Nest: Cloud-Projekt erstellen und konfigurieren" }, "device_project": { "data": { "project_id": "Ger\u00e4tezugriffsprojekt ID" }, - "description": "Erstelle ein Nest Ger\u00e4tezugriffsprojekt, f\u00fcr dessen Einrichtung **eine Geb\u00fchr von 5 US-Dollar** anf\u00e4llt.\n1. Gehe zur [Device Access Console]({device_access_console_url}) und durchlaufe den Zahlungsablauf.\n1. Klicke auf **Projekt erstellen**.\n1. Gib deinem Device Access-Projekt einen Namen und klicke auf **Weiter**.\n1. Gib deine OAuth-Client-ID ein\n1. Aktiviere Ereignisse, indem du auf **Aktivieren** und **Projekt erstellen** klickst.\n\nGib unten deine Ger\u00e4tezugriffsprojekt ID ein ([more info]({more_info_url})).", + "description": "Erstelle ein Nest Ger\u00e4tezugriffsprojekt, f\u00fcr dessen Einrichtung **eine Geb\u00fchr von 5 US-Dollar** anf\u00e4llt.\n1. Gehe zur [Device Access Console]({device_access_console_url}) und durchlaufe den Zahlungsablauf.\n1. Dr\u00fccke auf **Projekt erstellen**.\n1. Gib deinem Device Access-Projekt einen Namen und dr\u00fccke auf **Weiter**.\n1. Gib deine OAuth-Client-ID ein\n1. Aktiviere Ereignisse, indem du auf **Aktivieren** und **Projekt erstellen** dr\u00fcckst.\n\nGib unten deine Ger\u00e4tezugriffsprojekt ID ein ([more info]({more_info_url})).", "title": "Nest: Erstelle ein Ger\u00e4tezugriffsprojekt" }, "device_project_upgrade": { - "description": "Aktualisiere das Nest Ger\u00e4tezugriffsprojekt mit deiner neuen OAuth Client ID ([more info]({more_info_url}))\n1. Gehe zur [Ger\u00e4tezugriffskonsole]({device_access_console_url}).\n1. Klicke auf das Papierkorbsymbol neben *OAuth Client ID*.\n1. Klicke auf das \u00dcberlaufmen\u00fc und *Client ID hinzuf\u00fcgen*.\n1. Gib deine neue OAuth-Client-ID ein und klicke auf **Hinzuf\u00fcgen**.\n\nDeine OAuth-Client-ID lautet: `{client_id}`", + "description": "Aktualisiere das Nest Ger\u00e4tezugriffsprojekt mit deiner neuen OAuth Client ID ([more info]({more_info_url}))\n1. Gehe zur [Ger\u00e4tezugriffskonsole]({device_access_console_url}).\n1. Dr\u00fccke auf das Papierkorbsymbol neben *OAuth Client ID*.\n1. Dr\u00fccke auf das \u00dcberlaufmen\u00fc und *Client ID hinzuf\u00fcgen*.\n1. Gib deine neue OAuth-Client-ID ein und dr\u00fccke auf **Hinzuf\u00fcgen**.\n\nDeine OAuth-Client-ID lautet: `{client_id}`", "title": "Nest: Aktualisiere das Ger\u00e4tezugriffsprojekt" }, "init": { @@ -103,7 +103,7 @@ "title": "Die Nest-YAML-Konfiguration wird entfernt" }, "removed_app_auth": { - "description": "Um die Sicherheit zu verbessern und das Phishing-Risiko zu verringern, hat Google die von Home Assistant verwendete Authentifizierungsmethode eingestellt. \n\n **Zur L\u00f6sung sind Ma\u00dfnahmen deinerseits erforderlich** ([more info]( {more_info_url} )) \n\n 1. Besuche die Integrationsseite\n 1. Klicke in der Nest-Integration auf Neu konfigurieren.\n 1. Home Assistant f\u00fchrt dich durch die Schritte zum Upgrade auf die Webauthentifizierung. \n\n Informationen zur Fehlerbehebung findest du in der Nest [Integrationsanleitung]( {documentation_url} ).", + "description": "Um die Sicherheit zu verbessern und das Phishing-Risiko zu verringern, hat Google die von Home Assistant verwendete Authentifizierungsmethode eingestellt. \n\n **Zur L\u00f6sung sind Ma\u00dfnahmen deinerseits erforderlich** ([more info]( {more_info_url} )) \n\n 1. Besuche die Integrationsseite\n 1. Dr\u00fccke in der Nest-Integration auf Neu konfigurieren.\n 1. Home Assistant f\u00fchrt dich durch die Schritte zum Upgrade auf die Webauthentifizierung. \n\n Informationen zur Fehlerbehebung findest du in der Nest [Integrationsanleitung]( {documentation_url} ).", "title": "Nest-Authentifizierungsdaten m\u00fcssen aktualisiert werden" } } diff --git a/homeassistant/components/octoprint/translations/de.json b/homeassistant/components/octoprint/translations/de.json index 9ad0ff5f617..8cbe6846950 100644 --- a/homeassistant/components/octoprint/translations/de.json +++ b/homeassistant/components/octoprint/translations/de.json @@ -12,7 +12,7 @@ }, "flow_title": "OctoPrint-Drucker: {host}", "progress": { - "get_api_key": "\u00d6ffne die OctoPrint-Benutzeroberfl\u00e4che und klicke bei der Zugriffsanfrage f\u00fcr \"Home Assistant\" auf \"Zulassen\"." + "get_api_key": "\u00d6ffne die OctoPrint-Benutzeroberfl\u00e4che und dr\u00fccke bei der Zugriffsanfrage f\u00fcr \"Home Assistant\" auf \"Zulassen\"." }, "step": { "user": { diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json index c4c745f1766..10a7ec2ed53 100644 --- a/homeassistant/components/onvif/translations/de.json +++ b/homeassistant/components/onvif/translations/de.json @@ -38,7 +38,7 @@ "data": { "auto": "Automatisch suchen" }, - "description": "Wenn du auf Senden klickst, durchsuchen wir dein Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stelle sicher, dass ONVIF in der Konfiguration deiner Kamera aktiviert ist.", + "description": "Wenn du auf Senden dr\u00fcckst, durchsuchen wir dein Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stelle sicher, dass ONVIF in der Konfiguration deiner Kamera aktiviert ist.", "title": "ONVIF-Ger\u00e4tekonfiguration" } } diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index f0c2eee923b..e902da5ab4c 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -11,7 +11,7 @@ "default": "Erfolgreich authentifiziert" }, "error": { - "follow_link": "Bitte folgen dem Link und authentifiziere dich, bevor du auf Senden klickst", + "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden dr\u00fcckst", "no_token": "Ung\u00fcltiger Zugriffs-Token" }, "step": { diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index 8a5d700f2fc..cb3b7c328bb 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -9,13 +9,13 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Klicken zum Neustarten auf Senden.", + "credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Dr\u00fccke zum Neustarten auf Senden.", "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": { "creds": { - "description": "Anmeldeinformationen ben\u00f6tigt. Klicke auf \"Senden\" und dann in der PS4 2nd Screen App, aktualisiere die Ger\u00e4te und w\u00e4hle das \"Home-Assistant\"-Ger\u00e4t aus, um fortzufahren." + "description": "Anmeldeinformationen ben\u00f6tigt. Dr\u00fccke auf \"Senden\" und dann in der PS4 2nd Screen App, aktualisiere die Ger\u00e4te und w\u00e4hle das \"Home-Assistant\"-Ger\u00e4t aus, um fortzufahren." }, "link": { "data": { diff --git a/homeassistant/components/rachio/translations/de.json b/homeassistant/components/rachio/translations/de.json index 02a61e8f573..3a75dc969e6 100644 --- a/homeassistant/components/rachio/translations/de.json +++ b/homeassistant/components/rachio/translations/de.json @@ -13,7 +13,7 @@ "data": { "api_key": "API-Schl\u00fcssel" }, - "description": "Du ben\u00f6tigst den API-Schl\u00fcssel von https://app.rach.io/. Gehe in die Einstellungen und klicke auf \"API-SCHL\u00dcSSEL ANFORDERN\".", + "description": "Du ben\u00f6tigst den API-Schl\u00fcssel von https://app.rach.io/. Gehe in die Einstellungen und dr\u00fccke auf \"API-SCHL\u00dcSSEL ANFORDERN\".", "title": "Stelle eine Verbindung zu deinem Rachio-Ger\u00e4t her" } } diff --git a/homeassistant/components/renault/translations/ca.json b/homeassistant/components/renault/translations/ca.json index e16cb333acf..694b84e7c1b 100644 --- a/homeassistant/components/renault/translations/ca.json +++ b/homeassistant/components/renault/translations/ca.json @@ -19,7 +19,7 @@ "data": { "password": "Contrasenya" }, - "description": "Actualitza la contrasenya de l'usuari {username}", + "description": "Si us plau, actualitza la contrasenya de {username}", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, "user": { diff --git a/homeassistant/components/risco/translations/pt-BR.json b/homeassistant/components/risco/translations/pt-BR.json index b312b29c255..7e3a9ef6818 100644 --- a/homeassistant/components/risco/translations/pt-BR.json +++ b/homeassistant/components/risco/translations/pt-BR.json @@ -13,12 +13,12 @@ "data": { "password": "Senha", "pin": "C\u00f3digo PIN", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } }, "local": { "data": { - "host": "Host", + "host": "Nome do host", "pin": "C\u00f3digo PIN", "port": "Porta" } diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 377cae8946f..8d7c53a28ee 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -16,7 +16,7 @@ "description": "Der Roon-Server konnte nicht gefunden werden, bitte gib deinen Hostnamen und Port ein." }, "link": { - "description": "Du musst den Home Assistant in Roon autorisieren. Nachdem du auf \"Submit\" geklickt hast, gehe zur Roon Core-Anwendung, \u00f6ffne die Einstellungen und aktiviere HomeAssistant auf der Registerkarte \"Extensions\".", + "description": "Du musst den Home Assistant in Roon autorisieren. Nachdem du auf \"Senden\" gedr\u00fcckt hast, gehe zur Roon Core-Anwendung, \u00f6ffne die Einstellungen und aktiviere HomeAssistant auf der Registerkarte \"Extensions\".", "title": "HomeAssistant in Roon autorisieren" } } diff --git a/homeassistant/components/roon/translations/pt-BR.json b/homeassistant/components/roon/translations/pt-BR.json index 6875841568d..c5e060f4f77 100644 --- a/homeassistant/components/roon/translations/pt-BR.json +++ b/homeassistant/components/roon/translations/pt-BR.json @@ -18,6 +18,10 @@ "link": { "description": "Voc\u00ea deve autorizar o Home Assistant no Roon. Depois de clicar em enviar, v\u00e1 para o aplicativo Roon principal, abra Configura\u00e7\u00f5es e habilite o HomeAssistant na aba Extens\u00f5es.", "title": "Autorizar HomeAssistant no Roon" + }, + "user": { + "one": "", + "other": "" } } } diff --git a/homeassistant/components/scrape/translations/pt-BR.json b/homeassistant/components/scrape/translations/pt-BR.json index 9876157182e..84d7aaf6807 100644 --- a/homeassistant/components/scrape/translations/pt-BR.json +++ b/homeassistant/components/scrape/translations/pt-BR.json @@ -29,7 +29,7 @@ "index": "Define qual dos elementos retornados pelo seletor CSS usar", "resource": "A URL para o site que cont\u00e9m o valor", "select": "Define qual tag pesquisar. Verifique os seletores CSS da Beautiful Soup para obter detalhes", - "state_class": "O classe de estado do sensor", + "state_class": "A classe de estado do sensor", "value_template": "Define um modelo para obter o estado do sensor", "verify_ssl": "Ativa/desativa a verifica\u00e7\u00e3o do certificado SSL/TLS, por exemplo, se for autoassinado" } @@ -63,7 +63,7 @@ "index": "Define qual dos elementos retornados pelo seletor CSS usar", "resource": "A URL para o site que cont\u00e9m o valor", "select": "Define qual tag pesquisar. Verifique os seletores CSS da Beautiful Soup para obter detalhes", - "state_class": "O classe de estado do sensor", + "state_class": "A classe de estado do sensor", "value_template": "Define um modelo para obter o estado do sensor", "verify_ssl": "Ativa/desativa a verifica\u00e7\u00e3o do certificado SSL/TLS, por exemplo, se for autoassinado" } diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 6d4c0e92110..19a5108a752 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -42,7 +42,7 @@ "btn_up": "{subtype} Taste nach oben", "double": "{subtype} zweifach bet\u00e4tigt", "double_push": "{subtype} Doppel-Druck", - "long": "{subtype} lange angeklickt", + "long": "{subtype} lange angedr\u00fcckt", "long_push": "{subtype} langer Druck", "long_single": "{subtype} gehalten und dann einfach bet\u00e4tigt", "single": "{subtype} einfach bet\u00e4tigt", diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 3f9eae5f187..8f0bdda6baa 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -35,7 +35,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "SimpliSafe authentifiziert die Benutzer \u00fcber seine Web-App. Aufgrund technischer Beschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; bitte stelle sicher, dass du die [Dokumentation] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) liest, bevor du beginnst.\n\nWenn du bereit bist, klicke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben. Wenn du dich bereits bei SimpliSafe in deinem Browser angemeldet hast, kannst du eine neue Registerkarte \u00f6ffnen und dann die oben genannte URL in diese Registerkarte kopieren/einf\u00fcgen.\n\nWenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gib den Autorisierungscode von der URL \"com.simplisafe.mobile\" ein." + "description": "SimpliSafe authentifiziert die Benutzer \u00fcber seine Web-App. Aufgrund technischer Beschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; bitte stelle sicher, dass du die [Dokumentation] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) liest, bevor du beginnst.\n\nWenn du bereit bist, dr\u00fccke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben. Wenn du dich bereits bei SimpliSafe in deinem Browser angemeldet hast, kannst du eine neue Registerkarte \u00f6ffnen und dann die oben genannte URL in diese Registerkarte kopieren/einf\u00fcgen.\n\nWenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gib den Autorisierungscode von der URL \"com.simplisafe.mobile\" ein." } } }, diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index 81910cb9c70..f33c088998f 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst \"homeassistant.update_entity\" mit einer Speedtest-Entity_id zu verwenden. Dr\u00fccke dann unten auf SENDEN, um dieses Problem als behoben zu markieren.", + "title": "Der Speedtest-Dienst wird entfernt" + } + } + }, + "title": "Der Speedtest-Dienst wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/it.json b/homeassistant/components/speedtestdotnet/translations/it.json index 07615fe093c..c9e02758bb2 100644 --- a/homeassistant/components/speedtestdotnet/translations/it.json +++ b/homeassistant/components/speedtestdotnet/translations/it.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `homeassistant.update_entity` con un entity_id Speedtest di destinazione. Quindi, fai clic su INVIA di seguito per contrassegnare questo problema come risolto.", + "title": "Il servizio speedtest \u00e8 stato rimosso" + } + } + }, + "title": "Il servizio speedtest \u00e8 stato rimosso" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/no.json b/homeassistant/components/speedtestdotnet/translations/no.json index 01909d39f06..127541b5926 100644 --- a/homeassistant/components/speedtestdotnet/translations/no.json +++ b/homeassistant/components/speedtestdotnet/translations/no.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten for i stedet \u00e5 bruke `homeassistant.update_entity`-tjenesten med en m\u00e5l Speedtest-entity_id. Klikk deretter SEND nedenfor for \u00e5 merke dette problemet som l\u00f8st.", + "title": "Speedtest-tjenesten fjernes" + } + } + }, + "title": "Speedtest-tjenesten blir fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/pt-BR.json b/homeassistant/components/speedtestdotnet/translations/pt-BR.json index 1259d18bc18..7a159970a7f 100644 --- a/homeassistant/components/speedtestdotnet/translations/pt-BR.json +++ b/homeassistant/components/speedtestdotnet/translations/pt-BR.json @@ -14,12 +14,12 @@ "fix_flow": { "step": { "confirm": { - "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `homeassistant.update_entity` com um ID de entidade do Speedtest de destino. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", - "title": "O servi\u00e7o Speedtest est\u00e1 sendo removido" + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `homeassistant.update_entity` com um ID de entidade do Speedtest. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", + "title": "O servi\u00e7o speedtest est\u00e1 sendo removido" } } }, - "title": "O servi\u00e7o Speedtest est\u00e1 sendo removido" + "title": "O servi\u00e7o speedtest est\u00e1 sendo removido" } }, "options": { diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index bcc97a120af..8508185870c 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -7,6 +7,10 @@ "switchbot_unsupported_type": "Tipo de Switchbot sem suporte.", "unknown": "Erro inesperado" }, + "error": { + "one": "", + "other": "" + }, "flow_title": "{name} ({address})", "step": { "confirm": { diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json index adb5f0e2542..d77265bb351 100644 --- a/homeassistant/components/tellduslive/translations/de.json +++ b/homeassistant/components/tellduslive/translations/de.json @@ -11,7 +11,7 @@ }, "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})", + "description": "So verkn\u00fcpfest du dein TelldusLive-Konto: \n 1. Dr\u00fccke auf den Link unten \n 2. Melde dich bei Telldus Live an \n 3. Autorisiere ** {app_name} ** (dr\u00fccke auf ** Yes **). \n 4. Komme hierher zur\u00fcck und kdr\u00fccke auf **SENDEN**. \n\n [Link TelldusLive-Konto]({auth_url})", "title": "Authentifiziere dich gegen TelldusLive" }, "user": { diff --git a/homeassistant/components/thermobeacon/translations/de.json b/homeassistant/components/thermobeacon/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/no.json b/homeassistant/components/thermobeacon/translations/no.json new file mode 100644 index 00000000000..0bf8b1695ec --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Enheten st\u00f8ttes ikke" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/pt-BR.json b/homeassistant/components/thermobeacon/translations/pt-BR.json index 0da7639fa2a..5b654163201 100644 --- a/homeassistant/components/thermobeacon/translations/pt-BR.json +++ b/homeassistant/components/thermobeacon/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "not_supported": "Dispositivo n\u00e3o suportado" }, diff --git a/homeassistant/components/thermopro/translations/pt-BR.json b/homeassistant/components/thermopro/translations/pt-BR.json index e600b7b6bcf..3f93e65c087 100644 --- a/homeassistant/components/thermopro/translations/pt-BR.json +++ b/homeassistant/components/thermopro/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index 830b73d1eee..1d43956510a 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Ha de ser una llista d'adreces MAC separades per comes" + }, "step": { "init": { "data": { "all_updates": "M\u00e8triques en temps real (ALERTA: augmenta considerablement l'\u00fas de CPU)", "disable_rtsp": "Desactiva el flux RTSP", + "ignored_devices": "Llista d'adreces MAC dels dispositius a ignorar, separades per comes", "max_media": "Nombre m\u00e0xim d'esdeveniments a carregar al navegador multim\u00e8dia (augmenta l'\u00fas de RAM)", "override_connection_host": "Substitueix l'amfitri\u00f3 de connexi\u00f3" }, diff --git a/homeassistant/components/unifiprotect/translations/de.json b/homeassistant/components/unifiprotect/translations/de.json index 01867c2b76d..f44ad32a2b3 100644 --- a/homeassistant/components/unifiprotect/translations/de.json +++ b/homeassistant/components/unifiprotect/translations/de.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Muss eine durch Kommas getrennte Liste von MAC-Adressen sein" + }, "step": { "init": { "data": { "all_updates": "Echtzeitmetriken (WARNUNG: Erh\u00f6ht die CPU-Auslastung erheblich)", "disable_rtsp": "RTSP-Stream deaktivieren", + "ignored_devices": "Kommagetrennte Liste von MAC-Adressen von Ger\u00e4ten, die ignoriert werden sollen", "max_media": "Maximale Anzahl von Ereignissen, die f\u00fcr den Medienbrowser geladen werden (erh\u00f6ht die RAM-Nutzung)", "override_connection_host": "Verbindungshost \u00fcberschreiben" }, diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index 4ad61ad47bb..58da67d9383 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03c9\u03bd MAC \u03c0\u03bf\u03c5 \u03c7\u03c9\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1." + }, "step": { "init": { "data": { "all_updates": "\u039c\u03b5\u03c4\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf (\u03a0\u03a1\u039f\u0395\u0399\u0394\u039f\u03a0\u039f\u0399\u0397\u03a3\u0397: \u0391\u03c5\u03be\u03ac\u03bd\u03b5\u03b9 \u03c3\u03b7\u03bc\u03b1\u03bd\u03c4\u03b9\u03ba\u03ac \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 CPU)", "disable_rtsp": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03bf\u03ae RTSP", + "ignored_devices": "\u039a\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03c9\u03bd MAC \u03c4\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03b3\u03bd\u03bf\u03b7\u03b8\u03bf\u03cd\u03bd \u03bc\u03b5 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03cc \u03ba\u03cc\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2", "max_media": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd \u03c0\u03c1\u03bf\u03c2 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd (\u03b1\u03c5\u03be\u03ac\u03bd\u03b5\u03b9 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 RAM)", "override_connection_host": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index d12744bf841..e278fb6ecf0 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Debe ser una lista de direcciones MAC separadas por comas" + }, "step": { "init": { "data": { "all_updates": "M\u00e9tricas en tiempo real (ADVERTENCIA: aumenta considerablemente el uso de la CPU)", "disable_rtsp": "Deshabilitar la transmisi\u00f3n RTSP", + "ignored_devices": "Lista separada por comas de direcciones MAC de dispositivos para ignorar", "max_media": "N\u00famero m\u00e1ximo de eventos a cargar para el Navegador de Medios (aumenta el uso de RAM)", "override_connection_host": "Anular la conexi\u00f3n del host" }, diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json index 31b70f41dec..4bd402fa1c2 100644 --- a/homeassistant/components/unifiprotect/translations/et.json +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Peab olema komadega eraldatud MAC-aadresside loend" + }, "step": { "init": { "data": { "all_updates": "Reaalajas m\u00f5\u00f5dikud (HOIATUS: suurendab oluliselt CPU kasutust)", "disable_rtsp": "Keela RTSP voog", + "ignored_devices": "Komaga eraldatud loend nende seadmete MAC-aadressidest mida eirata", "max_media": "Meediumibrauserisse laaditavate s\u00fcndmuste maksimaalne arv (suurendab RAM-i kasutamist)", "override_connection_host": "\u00dchenduse hosti alistamine" }, diff --git a/homeassistant/components/unifiprotect/translations/fr.json b/homeassistant/components/unifiprotect/translations/fr.json index c6527d39a39..8cb4b819715 100644 --- a/homeassistant/components/unifiprotect/translations/fr.json +++ b/homeassistant/components/unifiprotect/translations/fr.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Doit \u00eatre une liste d'adresses MAC s\u00e9par\u00e9es par des virgules" + }, "step": { "init": { "data": { "all_updates": "M\u00e9triques en temps r\u00e9el (AVERTISSEMENT\u00a0: augmente consid\u00e9rablement l'utilisation du processeur)", "disable_rtsp": "D\u00e9sactiver le flux RTSP", + "ignored_devices": "Liste s\u00e9par\u00e9e par des virgules des adresses MAC des appareils \u00e0 ignorer", "max_media": "Nombre maximal d'\u00e9v\u00e9nements \u00e0 charger pour le navigateur multim\u00e9dia (augmente l'utilisation de la RAM)", "override_connection_host": "Ignorer l'h\u00f4te de connexion" }, diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index 00592e72ea1..712df7b7f2a 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Deve essere un elenco di indirizzi MAC separati da virgole" + }, "step": { "init": { "data": { "all_updates": "Metriche in tempo reale (ATTENZIONE: aumenta notevolmente l'utilizzo della CPU)", "disable_rtsp": "Disabilita il flusso RTSP", + "ignored_devices": "Elenco separato da virgole di indirizzi MAC di dispositivi da ignorare", "max_media": "Numero massimo di eventi da caricare per Media Browser (aumenta l'utilizzo della RAM)", "override_connection_host": "Sostituisci host di connessione" }, diff --git a/homeassistant/components/unifiprotect/translations/no.json b/homeassistant/components/unifiprotect/translations/no.json index 947d5c76887..7eac0b2dca1 100644 --- a/homeassistant/components/unifiprotect/translations/no.json +++ b/homeassistant/components/unifiprotect/translations/no.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "M\u00e5 v\u00e6re en liste over MAC-adresser atskilt med komma" + }, "step": { "init": { "data": { "all_updates": "Sanntidsm\u00e5linger (ADVARSEL: \u00d8ker CPU-bruken betraktelig)", "disable_rtsp": "Deaktiver RTSP-str\u00f8mmen", + "ignored_devices": "Kommadelt liste over MAC-adresser til enheter som skal ignoreres", "max_media": "Maks antall hendelser som skal lastes for medienettleseren (\u00f8ker RAM-bruken)", "override_connection_host": "Overstyr tilkoblingsvert" }, diff --git a/homeassistant/components/unifiprotect/translations/pt-BR.json b/homeassistant/components/unifiprotect/translations/pt-BR.json index e2951d47938..3bc780b57b7 100644 --- a/homeassistant/components/unifiprotect/translations/pt-BR.json +++ b/homeassistant/components/unifiprotect/translations/pt-BR.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Deve ser uma lista de endere\u00e7os MAC separados por v\u00edrgulas" + }, "step": { "init": { "data": { "all_updates": "M\u00e9tricas em tempo real (AVISO: aumenta muito o uso da CPU)", "disable_rtsp": "Desativar o fluxo RTSP", + "ignored_devices": "Lista separada por v\u00edrgulas de endere\u00e7os MAC de dispositivos a serem ignorados", "max_media": "N\u00famero m\u00e1ximo de eventos a serem carregados para o Media Browser (aumenta o uso de RAM)", "override_connection_host": "Anular o host de conex\u00e3o" }, diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json index ba996c5e123..0688a40d0c8 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hant.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "\u5fc5\u9808\u70ba\u4ee5\u9017\u865f\uff08\uff1a\uff09\u5206\u9694\u958b\u7684 MAC \u5730\u5740\u5217\u8868" + }, "step": { "init": { "data": { "all_updates": "\u5373\u6642\u6307\u6a19\uff08\u8b66\u544a\uff1a\u5927\u91cf\u63d0\u5347 CPU \u4f7f\u7528\u7387\uff09", "disable_rtsp": "\u95dc\u9589 RTSP \u4e32\u6d41", + "ignored_devices": "\u4ee5\u9017\u865f\u5206\u9694\u7684\u5ffd\u7565 MAC \u4f4d\u5740\u5217\u8868", "max_media": "\u5a92\u9ad4\u700f\u89bd\u5668\u6700\u9ad8\u8f09\u5165\u4e8b\u4ef6\u6578\uff08\u589e\u52a0\u8a18\u61b6\u9ad4\u4f7f\u7528\uff09", "override_connection_host": "\u7f6e\u63db\u9023\u7dda\u4e3b\u6a5f\u7aef" }, diff --git a/homeassistant/components/upnp/translations/pt-BR.json b/homeassistant/components/upnp/translations/pt-BR.json index 3070be8a1d0..ff5bd8aaa11 100644 --- a/homeassistant/components/upnp/translations/pt-BR.json +++ b/homeassistant/components/upnp/translations/pt-BR.json @@ -5,6 +5,10 @@ "incomplete_discovery": "Descoberta incompleta", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, + "error": { + "one": "", + "other": "" + }, "flow_title": "{name}", "step": { "ssdp_confirm": { diff --git a/homeassistant/components/volvooncall/translations/pt-BR.json b/homeassistant/components/volvooncall/translations/pt-BR.json index f6e4849b602..f1e66ecd29e 100644 --- a/homeassistant/components/volvooncall/translations/pt-BR.json +++ b/homeassistant/components/volvooncall/translations/pt-BR.json @@ -14,7 +14,7 @@ "password": "Senha", "region": "Regi\u00e3o", "scandinavian_miles": "Usar milhas escandinavas", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/webostv/translations/de.json b/homeassistant/components/webostv/translations/de.json index 6586ed63900..f88412a1d67 100644 --- a/homeassistant/components/webostv/translations/de.json +++ b/homeassistant/components/webostv/translations/de.json @@ -11,7 +11,7 @@ "flow_title": "LG webOS Smart TV", "step": { "pairing": { - "description": "Klicke auf Senden und akzeptiere die Kopplungsanfrage auf deinem Fernsehger\u00e4t.\n\n![Bild](/static/images/config_webos.png)", + "description": "Dr\u00fccke auf Senden und akzeptiere die Kopplungsanfrage auf deinem Fernsehger\u00e4t.\n\n![Bild](/static/images/config_webos.png)", "title": "webOS TV-Kopplung" }, "user": { @@ -19,7 +19,7 @@ "host": "Host", "name": "Name" }, - "description": "Schalte den TV ein, f\u00fclle die folgenden Felder aus und klicke auf Senden", + "description": "Schalte den TV ein, f\u00fclle die folgenden Felder aus und dr\u00fccke auf Senden", "title": "Mit webOS TV verbinden" } } diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index c21a653c4dc..448a160c3ab 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -24,15 +24,15 @@ }, "get_encryption_key_4_5": { "data": { - "bindkey": "Bindungsschl\u00fcssel" + "bindkey": "Bindkey" }, - "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 32-stelligen hexadezimalen Bindungsschl\u00fcssel." + "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 32-stelligen hexadezimalen Bindkey." }, "get_encryption_key_legacy": { "data": { - "bindkey": "Bindungsschl\u00fcssel" + "bindkey": "Bindkey" }, - "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindungsschl\u00fcssel." + "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindkey." }, "slow_confirm": { "description": "Von diesem Ger\u00e4t wurde in der letzten Minute kein Broadcast gesendet, so dass wir nicht sicher sind, ob dieses Ger\u00e4t Verschl\u00fcsselung verwendet oder nicht. Dies kann daran liegen, dass das Ger\u00e4t ein langsames Sendeintervall verwendet. Best\u00e4tige, dass du das Ger\u00e4t trotzdem hinzuf\u00fcgen m\u00f6chtest. Wenn das n\u00e4chste Mal ein Broadcast empfangen wird, wirst du aufgefordert, den Bindkey einzugeben, falls er ben\u00f6tigt wird." diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 5ac32a1df1f..c85b94a632d 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -97,22 +97,22 @@ "device_shaken": "Ger\u00e4t ersch\u00fcttert", "device_slid": "Ger\u00e4t gerutscht \"{subtype}\"", "device_tilted": "Ger\u00e4t gekippt", - "remote_button_alt_double_press": "\"{subtype}\" Taste doppelt geklickt (Alternativer Modus)", + "remote_button_alt_double_press": "\"{subtype}\" Taste doppelt gedr\u00fcckt (Alternativer Modus)", "remote_button_alt_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt (Alternativer Modus)", "remote_button_alt_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen (Alternativer Modus)", - "remote_button_alt_quadruple_press": "\"{subtype}\" Taste vierfach geklickt (Alternativer Modus)", - "remote_button_alt_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt (Alternativer Modus)", + "remote_button_alt_quadruple_press": "\"{subtype}\" Taste vierfach gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach gedr\u00fcckt (Alternativer Modus)", "remote_button_alt_short_press": "\"{subtype}\" Taste gedr\u00fcckt (Alternativer Modus)", "remote_button_alt_short_release": "\"{subtype}\" Taste losgelassen (Alternativer Modus)", - "remote_button_alt_triple_press": "\"{subtype}\" Taste dreimal geklickt (Alternativer Modus)", - "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", + "remote_button_alt_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt (Alternativer Modus)", + "remote_button_double_press": "\"{subtype}\" Taste doppelt angedr\u00fcckt", "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", - "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt", - "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt", + "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach gedr\u00fcckt", + "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach gedr\u00fcckt", "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", "remote_button_short_release": "\"{subtype}\" Taste losgelassen", - "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt" + "remote_button_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt" } } } \ No newline at end of file From 481205535c3230385573df12ec2814be2d7468dd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Aug 2022 20:45:27 -0400 Subject: [PATCH 738/903] Add PrusaLink integration (#77429) Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/prusalink/__init__.py | 120 ++++++++++++ homeassistant/components/prusalink/camera.py | 54 ++++++ .../components/prusalink/config_flow.py | 106 +++++++++++ homeassistant/components/prusalink/const.py | 3 + .../components/prusalink/manifest.json | 14 ++ homeassistant/components/prusalink/sensor.py | 173 ++++++++++++++++++ .../components/prusalink/strings.json | 18 ++ .../components/prusalink/strings.sensor.json | 11 ++ .../components/prusalink/translations/en.json | 17 ++ .../prusalink/translations/sensor.en.json | 11 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 1 + homeassistant/util/variance.py | 49 +++++ mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../config_flow/integration/__init__.py | 2 +- tests/components/prusalink/__init__.py | 1 + tests/components/prusalink/conftest.py | 109 +++++++++++ tests/components/prusalink/test_camera.py | 51 ++++++ .../components/prusalink/test_config_flow.py | 125 +++++++++++++ tests/components/prusalink/test_init.py | 23 +++ tests/components/prusalink/test_sensor.py | 114 ++++++++++++ tests/util/test_variance.py | 40 ++++ 26 files changed, 1061 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/prusalink/__init__.py create mode 100644 homeassistant/components/prusalink/camera.py create mode 100644 homeassistant/components/prusalink/config_flow.py create mode 100644 homeassistant/components/prusalink/const.py create mode 100644 homeassistant/components/prusalink/manifest.json create mode 100644 homeassistant/components/prusalink/sensor.py create mode 100644 homeassistant/components/prusalink/strings.json create mode 100644 homeassistant/components/prusalink/strings.sensor.json create mode 100644 homeassistant/components/prusalink/translations/en.json create mode 100644 homeassistant/components/prusalink/translations/sensor.en.json create mode 100644 homeassistant/util/variance.py create mode 100644 tests/components/prusalink/__init__.py create mode 100644 tests/components/prusalink/conftest.py create mode 100644 tests/components/prusalink/test_camera.py create mode 100644 tests/components/prusalink/test_config_flow.py create mode 100644 tests/components/prusalink/test_init.py create mode 100644 tests/components/prusalink/test_sensor.py create mode 100644 tests/util/test_variance.py diff --git a/.strict-typing b/.strict-typing index 1e6a47f508b..f36439a66d3 100644 --- a/.strict-typing +++ b/.strict-typing @@ -201,6 +201,7 @@ homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* homeassistant.components.powerwall.* homeassistant.components.proximity.* +homeassistant.components.prusalink.* homeassistant.components.pvoutput.* homeassistant.components.pure_energie.* homeassistant.components.qnap_qsw.* diff --git a/CODEOWNERS b/CODEOWNERS index fe634225124..9a6578d8fd5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -846,6 +846,8 @@ build.json @home-assistant/supervisor /homeassistant/components/prosegur/ @dgomes /tests/components/prosegur/ @dgomes /homeassistant/components/proxmoxve/ @jhollowe @Corbeno +/homeassistant/components/prusalink/ @balloob +/tests/components/prusalink/ @balloob /homeassistant/components/ps4/ @ktnrg45 /tests/components/ps4/ @ktnrg45 /homeassistant/components/pure_energie/ @klaasnicolaas diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py new file mode 100644 index 00000000000..cbc77b92e8a --- /dev/null +++ b/homeassistant/components/prusalink/__init__.py @@ -0,0 +1,120 @@ +"""The PrusaLink integration.""" +from __future__ import annotations + +from abc import abstractmethod +from datetime import timedelta +import logging +from typing import Generic, TypeVar + +import async_timeout +from pyprusalink import InvalidAuth, JobInfo, PrinterInfo, PrusaLink, PrusaLinkError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.CAMERA] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up PrusaLink from a config entry.""" + api = PrusaLink( + async_get_clientsession(hass), + entry.data["host"], + entry.data["api_key"], + ) + + coordinators = { + "printer": PrinterUpdateCoordinator(hass, api), + "job": JobUpdateCoordinator(hass, api), + } + for coordinator in coordinators.values(): + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +T = TypeVar("T", PrinterInfo, JobInfo) + + +class PrusaLinkUpdateCoordinator(DataUpdateCoordinator, Generic[T]): + """Update coordinator for the printer.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, api: PrusaLink) -> None: + """Initialize the update coordinator.""" + self.api = api + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) + ) + + async def _async_update_data(self) -> T: + """Update the data.""" + try: + with async_timeout.timeout(5): + return await self._fetch_data() + except InvalidAuth: + raise UpdateFailed("Invalid authentication") from None + except PrusaLinkError as err: + raise UpdateFailed(str(err)) from err + + @abstractmethod + async def _fetch_data(self) -> T: + """Fetch the actual data.""" + raise NotImplementedError + + +class PrinterUpdateCoordinator(PrusaLinkUpdateCoordinator[PrinterInfo]): + """Printer update coordinator.""" + + async def _fetch_data(self) -> PrinterInfo: + """Fetch the printer data.""" + return await self.api.get_printer() + + +class JobUpdateCoordinator(PrusaLinkUpdateCoordinator[JobInfo]): + """Job update coordinator.""" + + async def _fetch_data(self) -> JobInfo: + """Fetch the printer data.""" + return await self.api.get_job() + + +class PrusaLinkEntity(CoordinatorEntity[PrusaLinkUpdateCoordinator]): + """Defines a base PrusaLink entity.""" + + _attr_has_entity_name = True + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this PrusaLink device.""" + return DeviceInfo( + identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, + name=self.coordinator.config_entry.title, + manufacturer="Prusa", + configuration_url=self.coordinator.api.host, + ) diff --git a/homeassistant/components/prusalink/camera.py b/homeassistant/components/prusalink/camera.py new file mode 100644 index 00000000000..a6c16e2f5f2 --- /dev/null +++ b/homeassistant/components/prusalink/camera.py @@ -0,0 +1,54 @@ +"""Camera entity for PrusaLink.""" +from __future__ import annotations + +from homeassistant.components.camera import Camera +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN, JobUpdateCoordinator, PrusaLinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up PrusaLink camera.""" + coordinator: JobUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]["job"] + async_add_entities([PrusaLinkJobPreviewEntity(coordinator)]) + + +class PrusaLinkJobPreviewEntity(PrusaLinkEntity, Camera): + """Defines a PrusaLink camera.""" + + last_path = "" + last_image: bytes + _attr_name = "Job Preview" + + def __init__(self, coordinator: JobUpdateCoordinator) -> None: + """Initialize a PrusaLink camera entity.""" + super().__init__(coordinator) + Camera.__init__(self) + self._attr_unique_id = f"{self.coordinator.config_entry.entry_id}_job_preview" + + @property + def available(self) -> bool: + """Get if camera is available.""" + return super().available and self.coordinator.data.get("job") is not None + + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return a still image from the camera.""" + if not self.available: + return None + + path = self.coordinator.data["job"]["file"]["path"] + + if self.last_path == path: + return self.last_image + + self.last_image = await self.coordinator.api.get_large_thumbnail(path) + self.last_path = path + return self.last_image diff --git a/homeassistant/components/prusalink/config_flow.py b/homeassistant/components/prusalink/config_flow.py new file mode 100644 index 00000000000..da21cca99de --- /dev/null +++ b/homeassistant/components/prusalink/config_flow.py @@ -0,0 +1,106 @@ +"""Config flow for PrusaLink integration.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +from aiohttp import ClientError +import async_timeout +from awesomeversion import AwesomeVersion, AwesomeVersionException +from pyprusalink import InvalidAuth, PrusaLink +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def add_protocol(value: str) -> str: + """Validate URL has a scheme.""" + value = value.rstrip("/") + if value.startswith(("http://", "https://")): + return value + + return f"http://{value}" + + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("host"): vol.All(str, add_protocol), + vol.Required("api_key"): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + api = PrusaLink(async_get_clientsession(hass), data["host"], data["api_key"]) + + try: + async with async_timeout.timeout(5): + version = await api.get_version() + + except (asyncio.TimeoutError, ClientError) as err: + _LOGGER.error("Could not connect to PrusaLink: %s", err) + raise CannotConnect from err + + try: + if AwesomeVersion(version["api"]) < AwesomeVersion("2.0.0"): + raise NotSupported + except AwesomeVersionException as err: + raise NotSupported from err + + return {"title": version["hostname"]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for PrusaLink.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except NotSupported: + errors["base"] = "not_supported" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class NotSupported(HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/prusalink/const.py b/homeassistant/components/prusalink/const.py new file mode 100644 index 00000000000..76f8d9f2693 --- /dev/null +++ b/homeassistant/components/prusalink/const.py @@ -0,0 +1,3 @@ +"""Constants for the PrusaLink integration.""" + +DOMAIN = "prusalink" diff --git a/homeassistant/components/prusalink/manifest.json b/homeassistant/components/prusalink/manifest.json new file mode 100644 index 00000000000..9efed0be74a --- /dev/null +++ b/homeassistant/components/prusalink/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "prusalink", + "name": "PrusaLink", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/prusalink", + "requirements": ["pyprusalink==1.0.1"], + "dhcp": [ + { + "macaddress": "109C70*" + } + ], + "codeowners": ["@balloob"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/prusalink/sensor.py b/homeassistant/components/prusalink/sensor.py new file mode 100644 index 00000000000..f2a4f2fec81 --- /dev/null +++ b/homeassistant/components/prusalink/sensor.py @@ -0,0 +1,173 @@ +"""PrusaLink sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta +from typing import Generic, TypeVar, cast + +from pyprusalink import JobInfo, PrinterInfo + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util.dt import utcnow +from homeassistant.util.variance import ignore_variance + +from . import DOMAIN, PrusaLinkEntity, PrusaLinkUpdateCoordinator + +T = TypeVar("T", PrinterInfo, JobInfo) + + +@dataclass +class PrusaLinkSensorEntityDescriptionMixin(Generic[T]): + """Mixin for required keys.""" + + value_fn: Callable[[T], datetime | StateType] + + +@dataclass +class PrusaLinkSensorEntityDescription( + SensorEntityDescription, PrusaLinkSensorEntityDescriptionMixin[T], Generic[T] +): + """Describes PrusaLink sensor entity.""" + + available_fn: Callable[[T], bool] = lambda _: True + + +SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { + "printer": ( + PrusaLinkSensorEntityDescription[PrinterInfo]( + key="printer.state", + icon="mdi:printer-3d", + value_fn=lambda data: ( + "pausing" + if (flags := data["state"]["flags"])["pausing"] + else "cancelling" + if flags["cancelling"] + else "paused" + if flags["paused"] + else "printing" + if flags["printing"] + else "idle" + ), + device_class="prusalink__printer_state", + ), + PrusaLinkSensorEntityDescription[PrinterInfo]( + key="printer.telemetry.temp-bed", + name="Heatbed", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: cast(float, data["telemetry"]["temp-bed"]), + entity_registry_enabled_default=False, + ), + PrusaLinkSensorEntityDescription[PrinterInfo]( + key="printer.telemetry.temp-nozzle", + name="Nozzle Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: cast(float, data["telemetry"]["temp-nozzle"]), + entity_registry_enabled_default=False, + ), + ), + "job": ( + PrusaLinkSensorEntityDescription[JobInfo]( + key="job.progress", + name="Progress", + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: cast(float, data["progress"]["completion"]) * 100, + available_fn=lambda data: data.get("progress") is not None, + ), + PrusaLinkSensorEntityDescription[JobInfo]( + key="job.filename", + name="Filename", + icon="mdi:file-image-outline", + value_fn=lambda data: cast(str, data["job"]["file"]["display"]), + available_fn=lambda data: data.get("job") is not None, + ), + PrusaLinkSensorEntityDescription[JobInfo]( + key="job.start", + name="Print Start", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=ignore_variance( + lambda data: ( + utcnow() - timedelta(seconds=data["progress"]["printTime"]) + ), + timedelta(minutes=2), + ), + available_fn=lambda data: data.get("progress") is not None, + ), + PrusaLinkSensorEntityDescription[JobInfo]( + key="job.finish", + name="Print Finish", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=ignore_variance( + lambda data: ( + utcnow() + timedelta(seconds=data["progress"]["printTimeLeft"]) + ), + timedelta(minutes=2), + ), + available_fn=lambda data: data.get("progress") is not None, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up PrusaLink sensor based on a config entry.""" + coordinators: dict[str, PrusaLinkUpdateCoordinator] = hass.data[DOMAIN][ + entry.entry_id + ] + + entities: list[PrusaLinkEntity] = [] + + for coordinator_type, sensors in SENSORS.items(): + coordinator = coordinators[coordinator_type] + entities.extend( + PrusaLinkSensorEntity(coordinator, sensor_description) + for sensor_description in sensors + ) + + async_add_entities(entities) + + +class PrusaLinkSensorEntity(PrusaLinkEntity, SensorEntity): + """Defines a PrusaLink sensor.""" + + entity_description: PrusaLinkSensorEntityDescription + + def __init__( + self, + coordinator: PrusaLinkUpdateCoordinator, + description: PrusaLinkSensorEntityDescription, + ) -> None: + """Initialize a PrusaLink sensor entity.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + + @property + def native_value(self) -> datetime | StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) + + @property + def available(self) -> bool: + """Return if sensor is available.""" + return super().available and self.entity_description.available_fn( + self.coordinator.data + ) diff --git a/homeassistant/components/prusalink/strings.json b/homeassistant/components/prusalink/strings.json new file mode 100644 index 00000000000..24835324e18 --- /dev/null +++ b/homeassistant/components/prusalink/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "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%]", + "not_supported": "Only PrusaLink API v2 is supported" + } + } +} diff --git a/homeassistant/components/prusalink/strings.sensor.json b/homeassistant/components/prusalink/strings.sensor.json new file mode 100644 index 00000000000..6e1fe62e7f5 --- /dev/null +++ b/homeassistant/components/prusalink/strings.sensor.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "pausing": "Pausing", + "cancelling": "Cancelling", + "paused": "Paused", + "printing": "Printing", + "idle": "Idle" + } + } +} diff --git a/homeassistant/components/prusalink/translations/en.json b/homeassistant/components/prusalink/translations/en.json new file mode 100644 index 00000000000..e9be6d8d96e --- /dev/null +++ b/homeassistant/components/prusalink/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.en.json b/homeassistant/components/prusalink/translations/sensor.en.json new file mode 100644 index 00000000000..98b9c3a9265 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.en.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Cancelling", + "idle": "Idle", + "paused": "Paused", + "pausing": "Pausing", + "printing": "Printing" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a9b303eabea..133c02fd210 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -290,6 +290,7 @@ FLOWS = { "profiler", "progettihwsw", "prosegur", + "prusalink", "ps4", "pure_energie", "pushover", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 77255577cc2..8ced5265136 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -80,6 +80,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'oncue', 'hostname': 'kohlergen*', 'macaddress': '00146F*'}, {'domain': 'overkiz', 'hostname': 'gateway*', 'macaddress': 'F8811A*'}, {'domain': 'powerwall', 'hostname': '1118431-*'}, + {'domain': 'prusalink', 'macaddress': '109C70*'}, {'domain': 'qnap_qsw', 'macaddress': '245EBE*'}, {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '009D6B*'}, {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': 'F0038C*'}, diff --git a/homeassistant/util/variance.py b/homeassistant/util/variance.py new file mode 100644 index 00000000000..626b111817f --- /dev/null +++ b/homeassistant/util/variance.py @@ -0,0 +1,49 @@ +"""Util functions to help filter out similar results.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import functools +from typing import Any, TypeVar, overload + +T = TypeVar("T", int, float, datetime) + + +@overload +def ignore_variance( + func: Callable[..., int], ignored_variance: int +) -> Callable[..., int]: + ... + + +@overload +def ignore_variance( + func: Callable[..., float], ignored_variance: float +) -> Callable[..., float]: + ... + + +@overload +def ignore_variance( + func: Callable[..., datetime], ignored_variance: timedelta +) -> Callable[..., datetime]: + ... + + +def ignore_variance(func: Callable[..., T], ignored_variance: Any) -> Callable[..., T]: + """Wrap a function that returns old result if new result does not vary enough.""" + last_value: T | None = None + + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + nonlocal last_value + + value = func(*args, **kwargs) + + if last_value is not None and abs(value - last_value) < ignored_variance: + return last_value + + last_value = value + return value + + return wrapper diff --git a/mypy.ini b/mypy.ini index 863e673401c..d6665cb40c8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1769,6 +1769,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.prusalink.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.pvoutput.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index a17bac8c905..4be9e231781 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1780,6 +1780,9 @@ pyprof2calltree==1.4.5 # homeassistant.components.prosegur pyprosegur==0.0.5 +# homeassistant.components.prusalink +pyprusalink==1.0.1 + # homeassistant.components.ps4 pyps4-2ndscreen==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 553dfd7eb58..08e4ef72be9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1251,6 +1251,9 @@ pyprof2calltree==1.4.5 # homeassistant.components.prosegur pyprosegur==0.0.5 +# homeassistant.components.prusalink +pyprusalink==1.0.1 + # homeassistant.components.ps4 pyps4-2ndscreen==1.3.1 diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index 704292a2e9b..4d74c5d41ff 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # TODO 3. Store an API object for your platforms to access # hass.data[DOMAIN][entry.entry_id] = MyApi(...) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/tests/components/prusalink/__init__.py b/tests/components/prusalink/__init__.py new file mode 100644 index 00000000000..a34a40b107f --- /dev/null +++ b/tests/components/prusalink/__init__.py @@ -0,0 +1 @@ +"""Tests for the PrusaLink integration.""" diff --git a/tests/components/prusalink/conftest.py b/tests/components/prusalink/conftest.py new file mode 100644 index 00000000000..9d968d615aa --- /dev/null +++ b/tests/components/prusalink/conftest.py @@ -0,0 +1,109 @@ +"""Fixtures for PrusaLink.""" + +from unittest.mock import patch + +import pytest + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry(hass): + """Mock a PrusaLink config entry.""" + entry = MockConfigEntry( + domain="prusalink", data={"host": "http://example.com", "api_key": "abcdefgh"} + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +def mock_version_api(hass): + """Mock PrusaLink version API.""" + resp = { + "api": "2.0.0", + "server": "2.1.2", + "text": "PrusaLink MINI", + "hostname": "PrusaMINI", + } + with patch("pyprusalink.PrusaLink.get_version", return_value=resp): + yield resp + + +@pytest.fixture +def mock_printer_api(hass): + """Mock PrusaLink printer API.""" + resp = { + "telemetry": { + "temp-bed": 41.9, + "temp-nozzle": 47.8, + "print-speed": 100, + "z-height": 1.8, + "material": "PLA", + }, + "temperature": { + "tool0": {"actual": 47.8, "target": 0.0, "display": 0.0, "offset": 0}, + "bed": {"actual": 41.9, "target": 0.0, "offset": 0}, + }, + "state": { + "text": "Operational", + "flags": { + "operational": True, + "paused": False, + "printing": False, + "cancelling": False, + "pausing": False, + "sdReady": False, + "error": False, + "closedOnError": False, + "ready": True, + "busy": False, + }, + }, + } + with patch("pyprusalink.PrusaLink.get_printer", return_value=resp): + yield resp + + +@pytest.fixture +def mock_job_api_idle(hass): + """Mock PrusaLink job API having no job.""" + with patch( + "pyprusalink.PrusaLink.get_job", + return_value={ + "state": "Operational", + "job": None, + "progress": None, + }, + ): + yield + + +@pytest.fixture +def mock_job_api_active(hass): + """Mock PrusaLink job API having no job.""" + with patch( + "pyprusalink.PrusaLink.get_job", + return_value={ + "state": "Printing", + "job": { + "estimatedPrintTime": 117007, + "file": { + "name": "TabletStand3.gcode", + "path": "/usb/TABLET~1.GCO", + "display": "TabletStand3.gcode", + }, + }, + "progress": { + "completion": 0.37, + "printTime": 43987, + "printTimeLeft": 73020, + }, + }, + ): + yield + + +@pytest.fixture +def mock_api(mock_version_api, mock_printer_api, mock_job_api_idle): + """Mock PrusaLink API.""" diff --git a/tests/components/prusalink/test_camera.py b/tests/components/prusalink/test_camera.py new file mode 100644 index 00000000000..36ec8ec3700 --- /dev/null +++ b/tests/components/prusalink/test_camera.py @@ -0,0 +1,51 @@ +"""Test Prusalink camera.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def setup_camera_platform_only(): + """Only setup camera platform.""" + with patch("homeassistant.components.prusalink.PLATFORMS", [Platform.CAMERA]): + yield + + +async def test_camera_no_job( + hass: HomeAssistant, + mock_config_entry, + mock_api, +) -> None: + """Test sensors while no job active.""" + assert await async_setup_component(hass, "prusalink", {}) + state = hass.states.get("camera.mock_title_job_preview") + assert state is not None + assert state.state == "unavailable" + + +async def test_camera_active_job( + hass: HomeAssistant, mock_config_entry, mock_api, mock_job_api_active, hass_client +): + """Test sensors while no job active.""" + assert await async_setup_component(hass, "prusalink", {}) + state = hass.states.get("camera.mock_title_job_preview") + assert state is not None + assert state.state == "idle" + + client = await hass_client() + + with patch("pyprusalink.PrusaLink.get_large_thumbnail", return_value=b"hello"): + resp = await client.get("/api/camera_proxy/camera.mock_title_job_preview") + assert resp.status == 200 + assert await resp.read() == b"hello" + + # Make sure we hit cached value. + with patch("pyprusalink.PrusaLink.get_large_thumbnail", side_effect=ValueError): + resp = await client.get("/api/camera_proxy/camera.mock_title_job_preview") + assert resp.status == 200 + assert await resp.read() == b"hello" diff --git a/tests/components/prusalink/test_config_flow.py b/tests/components/prusalink/test_config_flow.py new file mode 100644 index 00000000000..78cd652f5eb --- /dev/null +++ b/tests/components/prusalink/test_config_flow.py @@ -0,0 +1,125 @@ +"""Test the PrusaLink config flow.""" +import asyncio +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.prusalink.config_flow import InvalidAuth +from homeassistant.components.prusalink.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_form(hass: HomeAssistant, mock_version_api) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.prusalink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "http://1.1.1.1/", + "api_key": "abcdefg", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "PrusaMINI" + assert result2["data"] == { + "host": "http://1.1.1.1", + "api_key": "abcdefg", + } + 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} + ) + + with patch( + "homeassistant.components.prusalink.config_flow.PrusaLink.get_version", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "api_key": "abcdefg", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_unknown(hass: HomeAssistant) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.prusalink.config_flow.PrusaLink.get_version", + side_effect=ValueError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "api_key": "abcdefg", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_invalid_version(hass: HomeAssistant, mock_version_api) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_version_api["api"] = "1.2.0" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "api_key": "abcdefg", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "not_supported"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """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.prusalink.config_flow.PrusaLink.get_version", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "api_key": "abcdefg", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/prusalink/test_init.py b/tests/components/prusalink/test_init.py new file mode 100644 index 00000000000..a36c70bb882 --- /dev/null +++ b/tests/components/prusalink/test_init.py @@ -0,0 +1,23 @@ +"""Test setting up and unloading PrusaLink.""" + + +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant + + +async def test_sensors_no_job( + hass: HomeAssistant, + mock_config_entry: ConfigEntry, + mock_api, +): + """Test sensors while no job active.""" + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state == ConfigEntryState.LOADED + + assert hass.states.async_entity_ids_count() > 0 + + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + assert mock_config_entry.state == ConfigEntryState.NOT_LOADED + + for state in hass.states.async_all(): + assert state.state == "unavailable" diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py new file mode 100644 index 00000000000..2ce62cf3990 --- /dev/null +++ b/tests/components/prusalink/test_sensor.py @@ -0,0 +1,114 @@ +"""Test Prusalink sensors.""" + +from datetime import datetime, timezone +from unittest.mock import PropertyMock, patch + +import pytest + +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def setup_sensor_platform_only(): + """Only setup sensor platform.""" + with patch( + "homeassistant.components.prusalink.PLATFORMS", [Platform.SENSOR] + ), patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + PropertyMock(return_value=True), + ): + yield + + +async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api): + """Test sensors while no job active.""" + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("sensor.mock_title") + assert state is not None + assert state.state == "idle" + + state = hass.states.get("sensor.mock_title_heatbed") + assert state is not None + assert state.state == "41.9" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.mock_title_nozzle_temperature") + assert state is not None + assert state.state == "47.8" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.mock_title_progress") + assert state is not None + assert state.state == "unavailable" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + + state = hass.states.get("sensor.mock_title_filename") + assert state is not None + assert state.state == "unavailable" + + state = hass.states.get("sensor.mock_title_print_start") + assert state is not None + assert state.state == "unavailable" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP + + state = hass.states.get("sensor.mock_title_print_finish") + assert state is not None + assert state.state == "unavailable" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP + + +async def test_sensors_active_job( + hass: HomeAssistant, + mock_config_entry, + mock_api, + mock_printer_api, + mock_job_api_active, +): + """Test sensors while active job.""" + mock_printer_api["state"]["flags"]["printing"] = True + + with patch( + "homeassistant.components.prusalink.sensor.utcnow", + return_value=datetime(2022, 8, 27, 14, 0, 0, tzinfo=timezone.utc), + ): + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("sensor.mock_title") + assert state is not None + assert state.state == "printing" + + state = hass.states.get("sensor.mock_title_progress") + assert state is not None + assert state.state == "37.0" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + + state = hass.states.get("sensor.mock_title_filename") + assert state is not None + assert state.state == "TabletStand3.gcode" + + state = hass.states.get("sensor.mock_title_print_start") + assert state is not None + assert state.state == "2022-08-27T01:46:53+00:00" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP + + state = hass.states.get("sensor.mock_title_print_finish") + assert state is not None + assert state.state == "2022-08-28T10:17:00+00:00" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP diff --git a/tests/util/test_variance.py b/tests/util/test_variance.py new file mode 100644 index 00000000000..c100d8b04d0 --- /dev/null +++ b/tests/util/test_variance.py @@ -0,0 +1,40 @@ +"""Test variance method.""" +from datetime import datetime, timedelta + +import pytest + +from homeassistant.util.variance import ignore_variance + + +@pytest.mark.parametrize( + "value_1, value_2, variance, expected", + [ + (1, 1, 1, 1), + (1, 2, 2, 1), + (1, 2, 0, 2), + (2, 1, 0, 1), + ( + datetime(2020, 1, 1, 0, 0), + datetime(2020, 1, 2, 0, 0), + timedelta(days=2), + datetime(2020, 1, 1, 0, 0), + ), + ( + datetime(2020, 1, 2, 0, 0), + datetime(2020, 1, 1, 0, 0), + timedelta(days=2), + datetime(2020, 1, 2, 0, 0), + ), + ( + datetime(2020, 1, 1, 0, 0), + datetime(2020, 1, 2, 0, 0), + timedelta(days=1), + datetime(2020, 1, 2, 0, 0), + ), + ], +) +def test_ignore_variance(value_1, value_2, variance, expected): + """Test ignore_variance.""" + with_ignore = ignore_variance(lambda x: x, variance) + assert with_ignore(value_1) == value_1 + assert with_ignore(value_2) == expected From 79b5147b46a16b65404c74df5dd9a10ce16ea216 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Aug 2022 20:46:03 -0400 Subject: [PATCH 739/903] Awair local use config entry name + add measurement state class (#77383) --- homeassistant/components/awair/__init__.py | 11 ++++++++ homeassistant/components/awair/const.py | 15 ++++++++++- homeassistant/components/awair/sensor.py | 29 ++++++++-------------- tests/components/awair/__init__.py | 21 ++++++++++++++++ tests/components/awair/test_init.py | 26 +++++++++++++++++++ tests/components/awair/test_sensor.py | 16 ++---------- 6 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 tests/components/awair/test_init.py diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index 359d0d6d853..fd964328c4d 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -37,6 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if CONF_HOST in config_entry.data: coordinator = AwairLocalDataUpdateCoordinator(hass, config_entry, session) + config_entry.async_on_unload( + config_entry.add_update_listener(_async_update_listener) + ) else: coordinator = AwairCloudDataUpdateCoordinator(hass, config_entry, session) @@ -50,6 +53,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + coordinator: AwairLocalDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + if entry.title != coordinator.title: + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Awair configuration.""" unload_ok = await hass.config_entries.async_unload_platforms( @@ -73,6 +83,7 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): ) -> None: """Set up the AwairDataUpdateCoordinator class.""" self._config_entry = config_entry + self.title = config_entry.title super().__init__(hass, LOGGER, name=DOMAIN, update_interval=update_interval) diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 133cf03fdbe..4de912c9fd9 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -8,7 +8,11 @@ import logging from python_awair.air_data import AirData from python_awair.devices import AwairBaseDevice -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, @@ -61,6 +65,7 @@ SENSOR_TYPE_SCORE = AwairSensorEntityDescription( native_unit_of_measurement=PERCENTAGE, name="Awair score", unique_id_tag="score", # matches legacy format + state_class=SensorStateClass.MEASUREMENT, ) SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( @@ -70,6 +75,7 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, name="Humidity", unique_id_tag="HUMID", # matches legacy format + state_class=SensorStateClass.MEASUREMENT, ), AwairSensorEntityDescription( key=API_LUX, @@ -77,6 +83,7 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( native_unit_of_measurement=LIGHT_LUX, name="Illuminance", unique_id_tag="illuminance", + state_class=SensorStateClass.MEASUREMENT, ), AwairSensorEntityDescription( key=API_SPL_A, @@ -84,6 +91,7 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA, name="Sound level", unique_id_tag="sound_level", + state_class=SensorStateClass.MEASUREMENT, ), AwairSensorEntityDescription( key=API_VOC, @@ -91,6 +99,7 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, name="Volatile organic compounds", unique_id_tag="VOC", # matches legacy format + state_class=SensorStateClass.MEASUREMENT, ), AwairSensorEntityDescription( key=API_TEMP, @@ -98,6 +107,7 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, name="Temperature", unique_id_tag="TEMP", # matches legacy format + state_class=SensorStateClass.MEASUREMENT, ), AwairSensorEntityDescription( key=API_CO2, @@ -105,6 +115,7 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, name="Carbon dioxide", unique_id_tag="CO2", # matches legacy format + state_class=SensorStateClass.MEASUREMENT, ), ) @@ -115,6 +126,7 @@ SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = ( native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name="PM2.5", unique_id_tag="PM25", # matches legacy format + state_class=SensorStateClass.MEASUREMENT, ), AwairSensorEntityDescription( key=API_PM10, @@ -122,6 +134,7 @@ SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = ( native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name="PM10", unique_id_tag="PM10", # matches legacy format + state_class=SensorStateClass.MEASUREMENT, ), ) diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 00d5c929409..18805154283 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -1,17 +1,14 @@ """Support for Awair sensors.""" from __future__ import annotations +from typing import cast + from python_awair.air_data import AirData from python_awair.devices import AwairBaseDevice, AwairLocalDevice from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_CONNECTIONS, - ATTR_NAME, - ATTR_SW_VERSION, -) +from homeassistant.const import ATTR_CONNECTIONS, ATTR_SW_VERSION from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo @@ -78,6 +75,8 @@ class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): """Defines an Awair sensor entity.""" entity_description: AwairSensorEntityDescription + _attr_has_entity_name = True + _attr_attribution = ATTRIBUTION def __init__( self, @@ -90,14 +89,6 @@ class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): self.entity_description = description self._device = device - @property - def name(self) -> str | None: - """Return the name of the sensor.""" - if self._device.name: - return f"{self._device.name} {self.entity_description.name}" - - return self.entity_description.name - @property def unique_id(self) -> str: """Return the uuid as the unique_id.""" @@ -187,7 +178,7 @@ class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): https://docs.developer.getawair.com/?version=latest#awair-score-and-index """ sensor_type = self.entity_description.key - attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + attrs: dict = {} if not self._air_data: return attrs if sensor_type in self._air_data.indices: @@ -204,11 +195,13 @@ class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): identifiers={(DOMAIN, self._device.uuid)}, manufacturer="Awair", model=self._device.model, + name=( + self._device.name + or cast(ConfigEntry, self.coordinator.config_entry).title + or f"{self._device.model} ({self._device.device_id})" + ), ) - if self._device.name: - info[ATTR_NAME] = self._device.name - if self._device.mac_address: info[ATTR_CONNECTIONS] = { (dr.CONNECTION_NETWORK_MAC, self._device.mac_address) diff --git a/tests/components/awair/__init__.py b/tests/components/awair/__init__.py index 5331ae5492a..e8b93e47fd7 100644 --- a/tests/components/awair/__init__.py +++ b/tests/components/awair/__init__.py @@ -1 +1,22 @@ """Tests for the awair component.""" + + +from unittest.mock import patch + +from homeassistant.components.awair import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_awair(hass: HomeAssistant, fixtures, unique_id, data) -> ConfigEntry: + """Add Awair devices to hass, using specified fixtures for data.""" + + entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=data) + with patch("python_awair.AwairClient.query", side_effect=fixtures): + 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/awair/test_init.py b/tests/components/awair/test_init.py new file mode 100644 index 00000000000..82a9f18597b --- /dev/null +++ b/tests/components/awair/test_init.py @@ -0,0 +1,26 @@ +"""Test Awair init.""" +from unittest.mock import patch + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_awair +from .const import LOCAL_CONFIG, LOCAL_UNIQUE_ID + + +async def test_local_awair_sensors(hass: HomeAssistant, local_devices, local_data): + """Test expected sensors on a local Awair.""" + fixtures = [local_devices, local_data] + entry = await setup_awair(hass, fixtures, LOCAL_UNIQUE_ID, LOCAL_CONFIG) + + dev_reg = dr.async_get(hass) + device_entry = dr.async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + + assert device_entry.name == "Mock Title" + + with patch("python_awair.AwairClient.query", side_effect=fixtures): + hass.config_entries.async_update_entry(entry, title="Hello World") + await hass.async_block_till_done() + + device_entry = dev_reg.async_get(device_entry.id) + assert device_entry.name == "Hello World" diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 87b931a3f7f..1a17f812d4d 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -11,7 +11,6 @@ from homeassistant.components.awair.const import ( API_SPL_A, API_TEMP, API_VOC, - DOMAIN, SENSOR_TYPE_SCORE, SENSOR_TYPES, SENSOR_TYPES_DUST, @@ -30,6 +29,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity +from . import setup_awair from .const import ( AWAIR_UUID, CLOUD_CONFIG, @@ -38,23 +38,11 @@ from .const import ( LOCAL_UNIQUE_ID, ) -from tests.common import MockConfigEntry - SENSOR_TYPES_MAP = { desc.key: desc for desc in (SENSOR_TYPE_SCORE, *SENSOR_TYPES, *SENSOR_TYPES_DUST) } -async def setup_awair(hass: HomeAssistant, fixtures, unique_id, data): - """Add Awair devices to hass, using specified fixtures for data.""" - - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=data) - with patch("python_awair.AwairClient.query", side_effect=fixtures): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - def assert_expected_properties( hass: HomeAssistant, registry: er.RegistryEntry, @@ -209,7 +197,7 @@ async def test_local_awair_sensors(hass: HomeAssistant, local_devices, local_dat assert_expected_properties( hass, registry, - "sensor.awair_score", + "sensor.mock_title_awair_score", f"{local_devices['device_uuid']}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "94", {}, From fa0dfd812c76e7689ccc8e0d974f61895f870494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Tue, 30 Aug 2022 03:52:10 +0200 Subject: [PATCH 740/903] Update allowlisted OAuth redirect URIs for Wear OS (#77411) --- homeassistant/components/auth/indieauth.py | 12 +++++++++--- tests/components/auth/test_indieauth.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index fc4c298ca6c..478f7ab2831 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -38,9 +38,15 @@ async def verify_redirect_uri( # Whitelist the iOS and Android callbacks so that people can link apps # without being connected to the internet. - if redirect_uri == "homeassistant://auth-callback" and client_id in ( - "https://home-assistant.io/android", - "https://home-assistant.io/iOS", + if ( + client_id == "https://home-assistant.io/iOS" + and redirect_uri == "homeassistant://auth-callback" + ): + return True + + if client_id == "https://home-assistant.io/android" and redirect_uri in ( + "homeassistant://auth-callback", + "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", ): return True diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 4cf7402725d..17d1fa927a0 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -183,3 +183,16 @@ async def test_verify_redirect_uri_android_ios(client_id): assert not await indieauth.verify_redirect_uri( None, "https://incorrect.com", "homeassistant://auth-callback" ) + + if client_id == "https://home-assistant.io/android": + assert await indieauth.verify_redirect_uri( + None, + client_id, + "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", + ) + else: + assert not await indieauth.verify_redirect_uri( + None, + client_id, + "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", + ) From cf5a11a1e7c7ef49a439753f3bb708d10b2c22b9 Mon Sep 17 00:00:00 2001 From: Simon Hansen <67142049+DurgNomis-drol@users.noreply.github.com> Date: Tue, 30 Aug 2022 08:11:53 +0200 Subject: [PATCH 741/903] Use DataUpdateCoordinator in ISS (#65178) * Move update method to coordinator * Add missing type annotations * Simplify update function * Add missing type annotation for coordinates * Forgot to extend with CoordinatorEntity * ... * Tweaks * ... * Fix linting and conflicts * import coordinatorentity * ... * Hopefully fixed linting * ... * Fix suggestions --- homeassistant/components/iss/__init__.py | 53 ++++++++++ homeassistant/components/iss/binary_sensor.py | 98 +++++++------------ 2 files changed, 88 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py index 476a944be81..d6065fd4f78 100644 --- a/homeassistant/components/iss/__init__.py +++ b/homeassistant/components/iss/__init__.py @@ -1,18 +1,71 @@ """The iss component.""" from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime, timedelta +import logging + +import pyiss +import requests +from requests.exceptions import HTTPError + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + PLATFORMS = [Platform.BINARY_SENSOR] +@dataclass +class IssData: + """Dataclass representation of data returned from pyiss.""" + + number_of_people_in_space: int + current_location: dict[str, str] + is_above: bool + next_rise: datetime + + +def update(iss: pyiss.ISS, latitude: float, longitude: float) -> IssData: + """Retrieve data from the pyiss API.""" + return IssData( + number_of_people_in_space=iss.number_of_people_in_space(), + current_location=iss.current_location(), + is_above=iss.is_ISS_above(latitude, longitude), + next_rise=iss.next_rise(latitude, longitude), + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up this integration using UI.""" hass.data.setdefault(DOMAIN, {}) + latitude = hass.config.latitude + longitude = hass.config.longitude + + iss = pyiss.ISS() + + async def async_update() -> IssData: + try: + return await hass.async_add_executor_job(update, iss, latitude, longitude) + except (HTTPError, requests.exceptions.ConnectionError) as ex: + raise UpdateFailed("Unable to retrieve data") from ex + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update, + update_interval=timedelta(seconds=60), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN] = coordinator entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index e2034fb48f9..77cb86fc45a 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -1,19 +1,21 @@ """Support for iss binary sensor.""" from __future__ import annotations -from datetime import timedelta import logging - -import pyiss -import requests -from requests.exceptions import HTTPError +from typing import Any from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import IssData +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -23,8 +25,6 @@ ATTR_ISS_NUMBER_PEOPLE_SPACE = "number_of_people_in_space" DEFAULT_NAME = "ISS" DEFAULT_DEVICE_CLASS = "visible" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - async def async_setup_entry( hass: HomeAssistant, @@ -32,27 +32,26 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor platform.""" + coordinator: DataUpdateCoordinator[IssData] = hass.data[DOMAIN] + name = entry.title show_on_map = entry.options.get(CONF_SHOW_ON_MAP, False) - try: - iss_data = IssData(hass.config.latitude, hass.config.longitude) - await hass.async_add_executor_job(iss_data.update) - except HTTPError as error: - _LOGGER.error(error) - return - - async_add_entities([IssBinarySensor(iss_data, name, show_on_map)], True) + async_add_entities([IssBinarySensor(coordinator, name, show_on_map)]) -class IssBinarySensor(BinarySensorEntity): +class IssBinarySensor( + CoordinatorEntity[DataUpdateCoordinator[IssData]], BinarySensorEntity +): """Implementation of the ISS binary sensor.""" _attr_device_class = DEFAULT_DEVICE_CLASS - def __init__(self, iss_data, name, show): + def __init__( + self, coordinator: DataUpdateCoordinator[IssData], name: str, show: bool + ) -> None: """Initialize the sensor.""" - self.iss_data = iss_data + super().__init__(coordinator) self._state = None self._attr_name = name self._show_on_map = show @@ -60,51 +59,24 @@ class IssBinarySensor(BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.iss_data.is_above if self.iss_data else False + return self.coordinator.data.is_above is True @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - if self.iss_data: - attrs = { - ATTR_ISS_NUMBER_PEOPLE_SPACE: self.iss_data.number_of_people_in_space, - ATTR_ISS_NEXT_RISE: self.iss_data.next_rise, - } - if self._show_on_map: - attrs[ATTR_LONGITUDE] = self.iss_data.position.get("longitude") - attrs[ATTR_LATITUDE] = self.iss_data.position.get("latitude") - else: - attrs["long"] = self.iss_data.position.get("longitude") - attrs["lat"] = self.iss_data.position.get("latitude") + attrs = { + ATTR_ISS_NUMBER_PEOPLE_SPACE: self.coordinator.data.number_of_people_in_space, + ATTR_ISS_NEXT_RISE: self.coordinator.data.next_rise, + } + if self._show_on_map: + attrs[ATTR_LONGITUDE] = self.coordinator.data.current_location.get( + "longitude" + ) + attrs[ATTR_LATITUDE] = self.coordinator.data.current_location.get( + "latitude" + ) + else: + attrs["long"] = self.coordinator.data.current_location.get("longitude") + attrs["lat"] = self.coordinator.data.current_location.get("latitude") - return attrs - - def update(self): - """Get the latest data from ISS API and updates the states.""" - self.iss_data.update() - - -class IssData: - """Get data from the ISS API.""" - - def __init__(self, latitude, longitude): - """Initialize the data object.""" - self.is_above = None - self.next_rise = None - self.number_of_people_in_space = None - self.position = None - self.latitude = latitude - self.longitude = longitude - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from the ISS API.""" - try: - iss = pyiss.ISS() - self.is_above = iss.is_ISS_above(self.latitude, self.longitude) - self.next_rise = iss.next_rise(self.latitude, self.longitude) - self.number_of_people_in_space = iss.number_of_people_in_space() - self.position = iss.current_location() - except (HTTPError, requests.exceptions.ConnectionError): - _LOGGER.error("Unable to retrieve data") - return False + return attrs From 14717951c375d72442b6929c8bc580aa86364e5a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Aug 2022 12:47:35 +0200 Subject: [PATCH 742/903] Support configuring the mode of MQTT number entities (#77478) * Support configuring the mode of MQTT number entities * Use modern schema for tests Co-authored-by: jbouwh --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/number.py | 8 ++ tests/components/mqtt/test_number.py | 74 ++++++++++++++++++- 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 6f2eeeeedd0..758e978bb46 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -99,6 +99,7 @@ ABBREVIATIONS = { "min_mirs": "min_mireds", "max_temp": "max_temp", "min_temp": "min_temp", + "mode": "mode", "mode_cmd_tpl": "mode_command_template", "mode_cmd_t": "mode_command_topic", "mode_stat_t": "mode_state_topic", diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 4e9f237431a..eeac406f668 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -13,11 +13,13 @@ from homeassistant.components.number import ( DEFAULT_STEP, DEVICE_CLASSES_SCHEMA, NumberDeviceClass, + NumberMode, RestoreNumber, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, + CONF_MODE, CONF_NAME, CONF_OPTIMISTIC, CONF_UNIT_OF_MEASUREMENT, @@ -83,6 +85,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), + vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string, @@ -276,6 +279,11 @@ class MqttNumber(MqttEntity, RestoreNumber): """Return the current value.""" return self._current_number + @property + def mode(self) -> NumberMode: + """Return the mode of the entity.""" + return self._config[CONF_MODE] + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" current_number = value diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 458f1f740e1..603984cffad 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import number +from homeassistant.components import mqtt, number from homeassistant.components.mqtt.number import ( CONF_MAX, CONF_MIN, @@ -24,6 +24,7 @@ from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_MODE, ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, Platform, @@ -702,6 +703,77 @@ async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock_entry_no_yaml_ assert f"'{CONF_MAX}' must be > '{CONF_MIN}'" in caplog.text +async def test_default_mode(hass, mqtt_mock_entry_with_yaml_config): + """Test default mode.""" + topic = "test/number" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("number.test_number") + assert state.attributes.get(ATTR_MODE) == "auto" + + +@pytest.mark.parametrize("mode", ("auto", "box", "slider")) +async def test_mode(hass, mqtt_mock_entry_with_yaml_config, mode): + """Test mode.""" + topic = "test/number" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "mode": mode, + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("number.test_number") + assert state.attributes.get(ATTR_MODE) == mode + + +@pytest.mark.parametrize("mode,valid", [("bleh", False), ("auto", True)]) +async def test_invalid_mode(hass, mode, valid): + """Test invalid mode.""" + topic = "test/number" + assert ( + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "mode": mode, + } + } + }, + ) + is valid + ) + + async def test_mqtt_payload_not_a_number_warning( hass, caplog, mqtt_mock_entry_with_yaml_config ): From 8bb182dffb3f9937a5a20171c768f966eeef65c0 Mon Sep 17 00:00:00 2001 From: Wagner Sartori Junior Date: Tue, 30 Aug 2022 13:55:52 +0200 Subject: [PATCH 743/903] Sync supported locales from alexa official documentation into alexa smart home integration (#77536) --- .../components/alexa/capabilities.py | 176 +++++++++++++++++- 1 file changed, 169 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 1456221e20e..dfac5c89ffd 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -269,6 +269,7 @@ class Alexa(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -277,10 +278,13 @@ class Alexa(AlexaCapability): "en-US", "es-ES", "es-MX", + "es-US", "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def name(self): @@ -295,6 +299,7 @@ class AlexaEndpointHealth(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -302,9 +307,14 @@ class AlexaEndpointHealth(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def __init__(self, hass, entity): @@ -345,6 +355,7 @@ class AlexaPowerController(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -352,9 +363,14 @@ class AlexaPowerController(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def name(self): @@ -400,6 +416,7 @@ class AlexaLockController(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -461,9 +478,14 @@ class AlexaSceneController(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def __init__(self, entity, supports_deactivation): @@ -483,6 +505,7 @@ class AlexaBrightnessController(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -490,6 +513,9 @@ class AlexaBrightnessController(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", "fr-FR", "hi-IN", "it-IT", @@ -536,6 +562,9 @@ class AlexaColorController(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", "fr-FR", "hi-IN", "it-IT", @@ -587,6 +616,9 @@ class AlexaColorTemperatureController(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", "fr-FR", "hi-IN", "it-IT", @@ -635,9 +667,13 @@ class AlexaPercentageController(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-US", + "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def name(self): @@ -758,7 +794,24 @@ class AlexaPlaybackController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html """ - supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US", "fr-FR"} + supported_locales = { + "ar-SA", + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } def name(self): """Return the Alexa API name of this interface.""" @@ -792,7 +845,24 @@ class AlexaInputController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html """ - supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"} + supported_locales = { + "ar-SA", + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } def name(self): """Return the Alexa API name of this interface.""" @@ -830,6 +900,7 @@ class AlexaTemperatureSensor(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -837,9 +908,14 @@ class AlexaTemperatureSensor(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def __init__(self, hass, entity): @@ -901,12 +977,18 @@ class AlexaContactSensor(AlexaCapability): "de-DE", "en-AU", "en-CA", + "en-GB", "en-IN", "en-US", - "en-GB", "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def __init__(self, hass, entity): @@ -950,10 +1032,15 @@ class AlexaMotionSensor(AlexaCapability): "de-DE", "en-AU", "en-CA", + "en-GB", "en-IN", "en-US", - "en-GB", "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", "it-IT", "ja-JP", "pt-BR", @@ -997,6 +1084,7 @@ class AlexaThermostatController(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -1004,7 +1092,11 @@ class AlexaThermostatController(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", "pt-BR", @@ -1127,6 +1219,8 @@ class AlexaPowerLevelController(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "fr-CA", "fr-FR", "it-IT", "ja-JP", @@ -1260,10 +1354,13 @@ class AlexaModeController(AlexaCapability): "en-US", "es-ES", "es-MX", + "es-US", "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def __init__(self, entity, instance, non_controllable=False): @@ -1446,10 +1543,13 @@ class AlexaRangeController(AlexaCapability): "en-US", "es-ES", "es-MX", + "es-US", "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def __init__(self, entity, instance, non_controllable=False): @@ -1691,8 +1791,10 @@ class AlexaToggleController(AlexaCapability): "en-US", "es-ES", "es-MX", + "es-US", "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", "pt-BR", @@ -1753,6 +1855,7 @@ class AlexaChannelController(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -1761,6 +1864,8 @@ class AlexaChannelController(AlexaCapability): "en-US", "es-ES", "es-MX", + "es-US", + "fr-CA", "fr-FR", "hi-IN", "it-IT", @@ -1780,6 +1885,7 @@ class AlexaDoorbellEventSource(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -1794,6 +1900,7 @@ class AlexaDoorbellEventSource(AlexaCapability): "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def name(self): @@ -1811,7 +1918,24 @@ class AlexaPlaybackStateReporter(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html """ - supported_locales = {"de-DE", "en-GB", "en-US", "es-MX", "fr-FR"} + supported_locales = { + "ar-SA", + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } def name(self): """Return the Alexa API name of this interface.""" @@ -1849,7 +1973,24 @@ class AlexaSeekController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html """ - supported_locales = {"de-DE", "en-GB", "en-US", "es-MX"} + supported_locales = { + "ar-SA", + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } def name(self): """Return the Alexa API name of this interface.""" @@ -1925,7 +2066,24 @@ class AlexaEqualizerController(AlexaCapability): https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html """ - supported_locales = {"de-DE", "en-IN", "en-US", "es-ES", "it-IT", "ja-JP", "pt-BR"} + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } + VALID_SOUND_MODES = { "MOVIE", "MUSIC", @@ -2017,6 +2175,7 @@ class AlexaCameraStreamController(AlexaCapability): """ supported_locales = { + "ar-SA", "de-DE", "en-AU", "en-CA", @@ -2024,6 +2183,9 @@ class AlexaCameraStreamController(AlexaCapability): "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", "fr-FR", "hi-IN", "it-IT", From 6ed095f000f64804d2b6e2ac146b733fdb9e93f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 30 Aug 2022 13:56:19 +0200 Subject: [PATCH 744/903] Revert dark_ image variants for add-ons (#77528) --- homeassistant/components/hassio/http.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 7d2e79956cc..8b99b4075ee 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -46,9 +46,7 @@ NO_TIMEOUT = re.compile( NO_AUTH_ONBOARDING = re.compile(r"^(?:" r"|supervisor/logs" r"|backups/[^/]+/.+" r")$") -NO_AUTH = re.compile( - r"^(?:" r"|app/.*" r"|[store\/]*addons/[^/]+/(logo|dark_logo|icon|dark_icon)" r")$" -) +NO_AUTH = re.compile(r"^(?:" r"|app/.*" r"|[store\/]*addons/[^/]+/(logo|icon)" r")$") NO_STORE = re.compile(r"^(?:" r"|app/entrypoint.js" r")$") # pylint: enable=implicit-str-concat From cac4015882a67aed0303b2493c368030fe49f49f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Aug 2022 14:14:46 +0200 Subject: [PATCH 745/903] Fix schedule during single weekday (#77543) --- homeassistant/components/schedule/__init__.py | 2 +- tests/components/schedule/test_init.py | 60 +++++++++++++++++-- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index 023cfef99e1..96d452469a5 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -271,7 +271,7 @@ class Schedule(Entity): # Find next event in the schedule, loop over each day (starting with # the current day) until the next event has been found. next_event = None - for day in range(7): + for day in range(8): # 8 because we need to search same weekday next week day_schedule = self._config.get( WEEKDAY_TO_CONF[(now.weekday() + day) % 7], [] ) diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index d559dc27a9a..a1161800e9e 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -6,7 +6,6 @@ from typing import Any from unittest.mock import patch from aiohttp import ClientWebSocketResponse -from freezegun import freeze_time import pytest from homeassistant.components.schedule import STORAGE_VERSION, STORAGE_VERSION_MINOR @@ -177,6 +176,55 @@ async def test_invalid_schedules( assert error in caplog.text +@pytest.mark.parametrize( + "schedule", + ({CONF_FROM: "07:00:00", CONF_TO: "11:00:00"},), +) +async def test_events_one_day( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + caplog: pytest.LogCaptureFixture, + schedule: list[dict[str, str]], + freezer, +) -> None: + """Test events only during one day of the week.""" + freezer.move_to("2022-08-30 13:20:00-07:00") + + assert await schedule_setup( + config={ + DOMAIN: { + "from_yaml": { + CONF_NAME: "from yaml", + CONF_ICON: "mdi:party-popper", + CONF_SUNDAY: schedule, + } + } + }, + items=[], + ) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T07:00:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T11:00:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T07:00:00-07:00" + + async def test_setup_no_config(hass: HomeAssistant, hass_admin_user: MockUser) -> None: """Test component setup with no config.""" count_start = len(hass.states.async_entity_ids()) @@ -224,19 +272,19 @@ async def test_load( async def test_schedule_updates( hass: HomeAssistant, schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + freezer, ) -> None: """Test the schedule updates when time changes.""" - with freeze_time("2022-08-10 20:10:00-07:00"): - assert await schedule_setup() + freezer.move_to("2022-08-10 20:10:00-07:00") + assert await schedule_setup() state = hass.states.get(f"{DOMAIN}.from_storage") assert state assert state.state == STATE_OFF assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-12T17:00:00-07:00" - with freeze_time(state.attributes[ATTR_NEXT_EVENT]): - async_fire_time_changed(hass, state.attributes[ATTR_NEXT_EVENT]) - await hass.async_block_till_done() + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) state = hass.states.get(f"{DOMAIN}.from_storage") assert state From 3e066e469aa34f4fb4e4f444878b3ea1c2a473b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 30 Aug 2022 08:23:39 -0400 Subject: [PATCH 746/903] Remove "Awair" from score entity name (#77522) --- homeassistant/components/awair/const.py | 2 +- tests/components/awair/test_sensor.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 4de912c9fd9..c117129dd2a 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -63,7 +63,7 @@ SENSOR_TYPE_SCORE = AwairSensorEntityDescription( key=API_SCORE, icon="mdi:blur", native_unit_of_measurement=PERCENTAGE, - name="Awair score", + name="Score", unique_id_tag="score", # matches legacy format state_class=SensorStateClass.MEASUREMENT, ) diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 1a17f812d4d..162b68d913b 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -72,7 +72,7 @@ async def test_awair_gen1_sensors(hass: HomeAssistant, user, cloud_devices, gen1 assert_expected_properties( hass, registry, - "sensor.living_room_awair_score", + "sensor.living_room_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "88", {}, @@ -164,7 +164,7 @@ async def test_awair_gen2_sensors(hass: HomeAssistant, user, cloud_devices, gen2 assert_expected_properties( hass, registry, - "sensor.living_room_awair_score", + "sensor.living_room_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "97", {}, @@ -197,7 +197,7 @@ async def test_local_awair_sensors(hass: HomeAssistant, local_devices, local_dat assert_expected_properties( hass, registry, - "sensor.mock_title_awair_score", + "sensor.mock_title_score", f"{local_devices['device_uuid']}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "94", {}, @@ -214,7 +214,7 @@ async def test_awair_mint_sensors(hass: HomeAssistant, user, cloud_devices, mint assert_expected_properties( hass, registry, - "sensor.living_room_awair_score", + "sensor.living_room_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "98", {}, @@ -255,7 +255,7 @@ async def test_awair_glow_sensors(hass: HomeAssistant, user, cloud_devices, glow assert_expected_properties( hass, registry, - "sensor.living_room_awair_score", + "sensor.living_room_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "93", {}, @@ -275,7 +275,7 @@ async def test_awair_omni_sensors(hass: HomeAssistant, user, cloud_devices, omni assert_expected_properties( hass, registry, - "sensor.living_room_awair_score", + "sensor.living_room_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "99", {}, @@ -315,7 +315,7 @@ async def test_awair_offline(hass: HomeAssistant, user, cloud_devices, awair_off # device *should* have if it's online. If we don't see it, # then we probably didn't set anything up. Which is correct, # in this case. - assert hass.states.get("sensor.living_room_awair_score") is None + assert hass.states.get("sensor.living_room_score") is None async def test_awair_unavailable( @@ -330,18 +330,18 @@ async def test_awair_unavailable( assert_expected_properties( hass, registry, - "sensor.living_room_awair_score", + "sensor.living_room_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "88", {}, ) with patch("python_awair.AwairClient.query", side_effect=awair_offline): - await async_update_entity(hass, "sensor.living_room_awair_score") + await async_update_entity(hass, "sensor.living_room_score") assert_expected_properties( hass, registry, - "sensor.living_room_awair_score", + "sensor.living_room_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", STATE_UNAVAILABLE, {}, From 5f31bdf2d73473142cd960dc95337dd8b75cc95e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 30 Aug 2022 09:53:40 -0400 Subject: [PATCH 747/903] Bump the ZHA quirks lib (#77545) --- 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 0648cbd86f7..779aee956a1 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.33.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.78", + "zha-quirks==0.0.79", "zigpy-deconz==0.18.0", "zigpy==0.50.2", "zigpy-xbee==0.15.0", diff --git a/requirements_all.txt b/requirements_all.txt index 4be9e231781..a51dfdcb86e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2554,7 +2554,7 @@ zengge==0.2 zeroconf==0.39.0 # homeassistant.components.zha -zha-quirks==0.0.78 +zha-quirks==0.0.79 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08e4ef72be9..3bca123fa86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1749,7 +1749,7 @@ youless-api==0.16 zeroconf==0.39.0 # homeassistant.components.zha -zha-quirks==0.0.78 +zha-quirks==0.0.79 # homeassistant.components.zha zigpy-deconz==0.18.0 From b47de426d8e281402089398054dc335b1b6e2ab8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 15:54:32 +0200 Subject: [PATCH 748/903] Adjust callback registration in harmony (#77533) --- homeassistant/components/harmony/remote.py | 22 +++++++++++---------- homeassistant/components/harmony/switch.py | 23 +++++++++++----------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 93d72f0ef7e..5dfd6d6290d 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -105,16 +105,18 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): if ATTR_ACTIVITY in data: self.default_activity = data[ATTR_ACTIVITY] - def _setup_callbacks(self): - callbacks = { - "connected": self.async_got_connected, - "disconnected": self.async_got_disconnected, - "config_updated": self.async_new_config, - "activity_starting": self.async_new_activity, - "activity_started": self.async_new_activity_finished, - } - - self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) + def _setup_callbacks(self) -> None: + self.async_on_remove( + self._data.async_subscribe( + HarmonyCallback( + connected=self.async_got_connected, + disconnected=self.async_got_disconnected, + config_updated=self.async_new_config, + activity_starting=self.async_new_activity, + activity_started=self.async_new_activity_finished, + ) + ) + ) @callback def async_new_activity_finished(self, activity_info: tuple) -> None: diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index 200c261b565..fe2238293da 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -58,18 +58,19 @@ class HarmonyActivitySwitch(HarmonyEntity, SwitchEntity): """Stop this activity.""" await self._data.async_power_off() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" - - callbacks = { - "connected": self.async_got_connected, - "disconnected": self.async_got_disconnected, - "activity_starting": self._async_activity_update, - "activity_started": self._async_activity_update, - "config_updated": None, - } - - self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) + self.async_on_remove( + self._data.async_subscribe( + HarmonyCallback( + connected=self.async_got_connected, + disconnected=self.async_got_disconnected, + activity_starting=self._async_activity_update, + activity_started=self._async_activity_update, + config_updated=None, + ) + ) + ) @callback def _async_activity_update(self, activity_info: tuple): From f9eee0e9d7e4f09c2610e4042b004e3764fa23ec Mon Sep 17 00:00:00 2001 From: guozi7788 <64695265+guozi7788@users.noreply.github.com> Date: Tue, 30 Aug 2022 21:58:21 +0800 Subject: [PATCH 749/903] Add the USB discovery for the Sonoff ZigBee dongle plus V2 (#77523) --- homeassistant/components/zha/manifest.json | 6 ++++++ homeassistant/generated/usb.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 779aee956a1..607fb351838 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,6 +21,12 @@ "description": "*2652*", "known_devices": ["slae.sh cc2652rb stick"] }, + { + "vid": "1A86", + "pid": "55D4", + "description": "*sonoff*plus*", + "known_devices": ["sonoff zigbee dongle plus v2"] + }, { "vid": "10C4", "pid": "EA60", diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 3583f96dc2c..2a87f33cf4f 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -41,6 +41,12 @@ USB = [ "pid": "EA60", "description": "*2652*" }, + { + "domain": "zha", + "vid": "1A86", + "pid": "55D4", + "description": "*sonoff*plus*" + }, { "domain": "zha", "vid": "10C4", From 640d8b40f009b2274af305d66ed06841cf39f804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Tue, 30 Aug 2022 16:27:42 +0200 Subject: [PATCH 750/903] Add hvac_action property to Senz (#77413) Add hvac_action property --- homeassistant/components/senz/climate.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/senz/climate.py b/homeassistant/components/senz/climate.py index c03ca2732dd..d47ae7a4a85 100644 --- a/homeassistant/components/senz/climate.py +++ b/homeassistant/components/senz/climate.py @@ -6,7 +6,11 @@ from typing import Any from aiosenz import MODE_AUTO, Thermostat from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate.const import ( + ClimateEntityFeature, + HVACAction, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback @@ -85,6 +89,11 @@ class SENZClimate(CoordinatorEntity, ClimateEntity): return HVACMode.AUTO return HVACMode.HEAT + @property + def hvac_action(self) -> HVACAction: + """Return current hvac action.""" + return HVACAction.HEATING if self._thermostat.is_heating else HVACAction.IDLE + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" if hvac_mode == HVACMode.AUTO: From b0a05530b0551ec05535e0fd8a33afc2c9445f29 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 30 Aug 2022 10:37:03 -0400 Subject: [PATCH 751/903] Migrate Litterrobot to new entity naming style (#77484) * Migrate Litterrobot to new entity naming style * uno mas --- homeassistant/components/litterrobot/entity.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index c81e4ea4c79..7248cbe9315 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -31,17 +31,15 @@ REFRESH_WAIT_TIME_SECONDS = 8 class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): """Generic Litter-Robot entity representing common data and methods.""" + _attr_has_entity_name = True + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.robot = robot self.entity_type = entity_type self.hub = hub - - @property - def name(self) -> str: - """Return the name of this entity.""" - return f"{self.robot.name} {self.entity_type}" + self._attr_name = entity_type.capitalize() @property def unique_id(self) -> str: From c11925f7a9e88e46b920b61f02a6c6b7e951c138 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 30 Aug 2022 11:04:14 -0400 Subject: [PATCH 752/903] Add prusalink test cases and fix config flow (#77544) --- .../components/prusalink/config_flow.py | 23 +++++++------ tests/components/prusalink/test_camera.py | 5 +++ .../components/prusalink/test_config_flow.py | 24 ++++++++++++-- tests/components/prusalink/test_init.py | 33 +++++++++++++++++-- 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/prusalink/config_flow.py b/homeassistant/components/prusalink/config_flow.py index da21cca99de..6b0e6189f41 100644 --- a/homeassistant/components/prusalink/config_flow.py +++ b/homeassistant/components/prusalink/config_flow.py @@ -22,18 +22,9 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def add_protocol(value: str) -> str: - """Validate URL has a scheme.""" - value = value.rstrip("/") - if value.startswith(("http://", "https://")): - return value - - return f"http://{value}" - - STEP_USER_DATA_SCHEMA = vol.Schema( { - vol.Required("host"): vol.All(str, add_protocol), + vol.Required("host"): str, vol.Required("api_key"): str, } ) @@ -77,10 +68,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA ) + host = user_input["host"].rstrip("/") + if not host.startswith(("http://", "https://")): + host = f"http://{host}" + + data = { + "host": host, + "api_key": user_input["api_key"], + } errors = {} try: - info = await validate_input(self.hass, user_input) + info = await validate_input(self.hass, data) except CannotConnect: errors["base"] = "cannot_connect" except NotSupported: @@ -91,7 +90,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - return self.async_create_entry(title=info["title"], data=user_input) + return self.async_create_entry(title=info["title"], data=data) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors diff --git a/tests/components/prusalink/test_camera.py b/tests/components/prusalink/test_camera.py index 36ec8ec3700..74354a75580 100644 --- a/tests/components/prusalink/test_camera.py +++ b/tests/components/prusalink/test_camera.py @@ -20,6 +20,7 @@ async def test_camera_no_job( hass: HomeAssistant, mock_config_entry, mock_api, + hass_client, ) -> None: """Test sensors while no job active.""" assert await async_setup_component(hass, "prusalink", {}) @@ -27,6 +28,10 @@ async def test_camera_no_job( assert state is not None assert state.state == "unavailable" + client = await hass_client() + resp = await client.get("/api/camera_proxy/camera.mock_title_job_preview") + assert resp.status == 500 + async def test_camera_active_job( hass: HomeAssistant, mock_config_entry, mock_api, mock_job_api_active, hass_client diff --git a/tests/components/prusalink/test_config_flow.py b/tests/components/prusalink/test_config_flow.py index 78cd652f5eb..4810ea82166 100644 --- a/tests/components/prusalink/test_config_flow.py +++ b/tests/components/prusalink/test_config_flow.py @@ -83,8 +83,8 @@ async def test_form_unknown(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "unknown"} -async def test_form_invalid_version(hass: HomeAssistant, mock_version_api) -> None: - """Test we handle invalid auth.""" +async def test_form_too_low_version(hass: HomeAssistant, mock_version_api) -> None: + """Test we handle too low API version.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -103,6 +103,26 @@ async def test_form_invalid_version(hass: HomeAssistant, mock_version_api) -> No assert result2["errors"] == {"base": "not_supported"} +async def test_form_invalid_version_2(hass: HomeAssistant, mock_version_api) -> None: + """Test we handle invalid version.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_version_api["api"] = "i am not a version" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "api_key": "abcdefg", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "not_supported"} + + async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/prusalink/test_init.py b/tests/components/prusalink/test_init.py index a36c70bb882..2e15ac19193 100644 --- a/tests/components/prusalink/test_init.py +++ b/tests/components/prusalink/test_init.py @@ -1,16 +1,23 @@ """Test setting up and unloading PrusaLink.""" +from datetime import timedelta +from unittest.mock import patch +from pyprusalink import InvalidAuth, PrusaLinkError +import pytest from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed -async def test_sensors_no_job( +async def test_unloading( hass: HomeAssistant, mock_config_entry: ConfigEntry, mock_api, ): - """Test sensors while no job active.""" + """Test unloading prusalink.""" assert await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.state == ConfigEntryState.LOADED @@ -21,3 +28,25 @@ async def test_sensors_no_job( for state in hass.states.async_all(): assert state.state == "unavailable" + + +@pytest.mark.parametrize("exception", [InvalidAuth, PrusaLinkError]) +async def test_failed_update( + hass: HomeAssistant, mock_config_entry: ConfigEntry, mock_api, exception +): + """Test failed update marks prusalink unavailable.""" + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.prusalink.PrusaLink.get_printer", + side_effect=exception, + ), patch( + "homeassistant.components.prusalink.PrusaLink.get_job", + side_effect=exception, + ): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30), fire_all=True) + await hass.async_block_till_done() + + for state in hass.states.async_all(): + assert state.state == "unavailable" From 110803f23afbdd07f9b38579eedaeec36399a7ce Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 30 Aug 2022 12:12:41 -0400 Subject: [PATCH 753/903] Bump AIOAladdinConnect 0.1.44 (#77542) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- homeassistant/components/aladdin_connect/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index f099694f309..3a3efa0f4a2 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.43"], + "requirements": ["AIOAladdinConnect==0.1.44"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 5fcc75fa27c..51ae5154302 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -83,7 +83,7 @@ async def async_setup_entry( [AladdinConnectSensor(acc, door, description) for description in SENSORS] ) - async_add_entities(entities) + async_add_entities(entities) class AladdinConnectSensor(SensorEntity): diff --git a/requirements_all.txt b/requirements_all.txt index a51dfdcb86e..f6385091df8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.43 +AIOAladdinConnect==0.1.44 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bca123fa86..ccad6a75f1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.43 +AIOAladdinConnect==0.1.44 # homeassistant.components.adax Adax-local==0.1.4 From a2f1b882279e96d05306d7aad0534e2501136433 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 18:14:06 +0200 Subject: [PATCH 754/903] Use generics in litterrobot (#77537) --- .../components/litterrobot/button.py | 3 +- .../components/litterrobot/entity.py | 22 ++++---- .../components/litterrobot/select.py | 4 +- .../components/litterrobot/sensor.py | 51 +++++++++---------- .../components/litterrobot/switch.py | 8 ++- .../components/litterrobot/vacuum.py | 3 +- 6 files changed, 46 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 7c7990edf07..b833500ec4c 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -32,10 +32,9 @@ async def async_setup_entry( ) -class LitterRobotResetWasteDrawerButton(LitterRobotEntity, ButtonEntity): +class LitterRobotResetWasteDrawerButton(LitterRobotEntity[LitterRobot3], ButtonEntity): """Litter-Robot reset waste drawer button.""" - robot: LitterRobot3 _attr_icon = "mdi:delete-variant" _attr_entity_category = EntityCategory.CONFIG diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 7248cbe9315..8471e007ce9 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -4,9 +4,9 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from datetime import time import logging -from typing import Any +from typing import Any, Generic, TypeVar -from pylitterbot import LitterRobot, Robot +from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException from typing_extensions import ParamSpec @@ -23,17 +23,20 @@ from .const import DOMAIN from .hub import LitterRobotHub _P = ParamSpec("_P") +_RobotT = TypeVar("_RobotT", bound=Robot) _LOGGER = logging.getLogger(__name__) REFRESH_WAIT_TIME_SECONDS = 8 -class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): +class LitterRobotEntity( + CoordinatorEntity[DataUpdateCoordinator[bool]], Generic[_RobotT] +): """Generic Litter-Robot entity representing common data and methods.""" _attr_has_entity_name = True - def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.robot = robot @@ -59,12 +62,10 @@ class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): ) -class LitterRobotControlEntity(LitterRobotEntity): +class LitterRobotControlEntity(LitterRobotEntity[_RobotT]): """A Litter-Robot entity that can control the unit.""" - robot: LitterRobot - - def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._refresh_callback: CALLBACK_TYPE | None = None @@ -128,13 +129,12 @@ class LitterRobotControlEntity(LitterRobotEntity): ) -class LitterRobotConfigEntity(LitterRobotControlEntity): +class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]): """A Litter-Robot entity that can control configuration of the unit.""" - robot: LitterRobot _attr_entity_category = EntityCategory.CONFIG - def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._assumed_state: bool | None = None diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index 403f7a8c257..2889499f1c4 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -1,6 +1,8 @@ """Support for Litter-Robot selects.""" from __future__ import annotations +from pylitterbot import LitterRobot + from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -29,7 +31,7 @@ async def async_setup_entry( ) -class LitterRobotSelect(LitterRobotConfigEntity, SelectEntity): +class LitterRobotSelect(LitterRobotConfigEntity[LitterRobot], SelectEntity): """Litter-Robot Select.""" _attr_icon = "mdi:timer-outline" diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 5f6579e83c5..386d0e04f3c 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,12 +1,13 @@ """Support for Litter-Robot sensors.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable from dataclasses import dataclass from datetime import datetime -from typing import Any, Union, cast +import itertools +from typing import Any, Generic, Union, cast -from pylitterbot import FeederRobot, LitterRobot, Robot +from pylitterbot import FeederRobot, LitterRobot from homeassistant.components.sensor import ( SensorDeviceClass, @@ -20,7 +21,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub @@ -36,30 +37,23 @@ def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str @dataclass -class RobotSensorEntityDescription(SensorEntityDescription): +class RobotSensorEntityDescription(SensorEntityDescription, Generic[_RobotT]): """A class that describes robot sensor entities.""" icon_fn: Callable[[Any], str | None] = lambda _: None - should_report: Callable[[Robot], bool] = lambda _: True + should_report: Callable[[_RobotT], bool] = lambda _: True -@dataclass -class LitterRobotSensorEntityDescription(RobotSensorEntityDescription): - """A class that describes Litter-Robot sensor entities.""" - - should_report: Callable[[LitterRobot], bool] = lambda _: True - - -class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): +class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): """Litter-Robot sensor entity.""" - entity_description: RobotSensorEntityDescription + entity_description: RobotSensorEntityDescription[_RobotT] def __init__( self, - robot: LitterRobot | FeederRobot, + robot: _RobotT, hub: LitterRobotHub, - description: RobotSensorEntityDescription, + description: RobotSensorEntityDescription[_RobotT], ) -> None: """Initialize a Litter-Robot sensor entity.""" assert description.name @@ -84,31 +78,31 @@ class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): LITTER_ROBOT_SENSORS = [ - LitterRobotSensorEntityDescription( + RobotSensorEntityDescription[LitterRobot]( name="Waste Drawer", key="waste_drawer_level", native_unit_of_measurement=PERCENTAGE, icon_fn=lambda state: icon_for_gauge_level(state, 10), ), - LitterRobotSensorEntityDescription( + RobotSensorEntityDescription[LitterRobot]( name="Sleep Mode Start Time", key="sleep_mode_start_time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), - LitterRobotSensorEntityDescription( + RobotSensorEntityDescription[LitterRobot]( name="Sleep Mode End Time", key="sleep_mode_end_time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), - LitterRobotSensorEntityDescription( + RobotSensorEntityDescription[LitterRobot]( name="Last Seen", key="last_seen", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, ), - LitterRobotSensorEntityDescription( + RobotSensorEntityDescription[LitterRobot]( name="Status Code", key="status_code", device_class="litterrobot__status_code", @@ -116,7 +110,7 @@ LITTER_ROBOT_SENSORS = [ ), ] -FEEDER_ROBOT_SENSOR = RobotSensorEntityDescription( +FEEDER_ROBOT_SENSOR = RobotSensorEntityDescription[FeederRobot]( name="Food Level", key="food_level", native_unit_of_measurement=PERCENTAGE, @@ -131,16 +125,17 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot sensors using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [ + entities: Iterable[LitterRobotSensorEntity] = itertools.chain( + ( LitterRobotSensorEntity(robot=robot, hub=hub, description=description) for description in LITTER_ROBOT_SENSORS for robot in hub.litter_robots() - ] - + [ + ), + ( LitterRobotSensorEntity( robot=robot, hub=hub, description=FEEDER_ROBOT_SENSOR ) for robot in hub.feeder_robots() - ] + ), ) + async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 69050057050..de401576a78 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from pylitterbot import LitterRobot + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -13,7 +15,9 @@ from .entity import LitterRobotConfigEntity from .hub import LitterRobotHub -class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): +class LitterRobotNightLightModeSwitch( + LitterRobotConfigEntity[LitterRobot], SwitchEntity +): """Litter-Robot Night Light Mode Switch.""" @property @@ -37,7 +41,7 @@ class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): await self.perform_action_and_assume_state(self.robot.set_night_light, False) -class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): +class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity[LitterRobot], SwitchEntity): """Litter-Robot Panel Lockout Switch.""" @property diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 11a437e893c..9a4b825045f 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any +from pylitterbot import LitterRobot from pylitterbot.enums import LitterBoxStatus import voluptuous as vol @@ -68,7 +69,7 @@ async def async_setup_entry( ) -class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): +class LitterRobotCleaner(LitterRobotControlEntity[LitterRobot], StateVacuumEntity): """Litter-Robot "Vacuum" Cleaner.""" _attr_supported_features = ( From e48d493db495d27427a44647e50eaa4e0f0c1919 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 30 Aug 2022 19:14:54 +0300 Subject: [PATCH 755/903] Bump `glances` library to 0.4.1 (#77540) --- homeassistant/components/glances/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 3c2906f9fd6..48e290c6360 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -3,7 +3,7 @@ "name": "Glances", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/glances", - "requirements": ["glances_api==0.3.5"], + "requirements": ["glances_api==0.4.1"], "codeowners": ["@engrbm87"], "iot_class": "local_polling", "loggers": ["glances_api"] diff --git a/requirements_all.txt b/requirements_all.txt index f6385091df8..3166dd4bbf6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -742,7 +742,7 @@ gios==2.1.0 gitterpy==0.1.7 # homeassistant.components.glances -glances_api==0.3.5 +glances_api==0.4.1 # homeassistant.components.goalzero goalzero==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccad6a75f1d..e1996e8955e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -549,7 +549,7 @@ getmac==0.8.2 gios==2.1.0 # homeassistant.components.glances -glances_api==0.3.5 +glances_api==0.4.1 # homeassistant.components.goalzero goalzero==0.2.1 From f78b39bdbfbe151e8bab72610b6fe03afc8c0747 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 30 Aug 2022 12:40:16 -0400 Subject: [PATCH 756/903] ZHA backup/restore config flow (#77044) --- homeassistant/components/zha/config_flow.py | 706 +++++++++-- homeassistant/components/zha/core/const.py | 12 +- homeassistant/components/zha/manifest.json | 1 + homeassistant/components/zha/strings.json | 122 +- .../components/zha/translations/en.json | 124 +- tests/components/zha/test_config_flow.py | 1085 ++++++++++++++--- 6 files changed, 1736 insertions(+), 314 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 9c7ec46a386..5684e784a6a 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,21 +1,39 @@ """Config flow for ZHA.""" from __future__ import annotations +import collections +import contextlib +import copy +import json +import logging +import os from typing import Any import serial.tools.list_ports import voluptuous as vol +from zigpy.application import ControllerApplication +import zigpy.backups from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH +from zigpy.exceptions import NetworkNotFormed from homeassistant import config_entries from homeassistant.components import onboarding, usb, zeroconf +from homeassistant.components.file_upload import process_uploaded_file from homeassistant.const import CONF_NAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowHandler, FlowResult +from homeassistant.helpers.selector import FileSelector, FileSelectorConfig +from homeassistant.util import dt from .core.const import ( CONF_BAUDRATE, + CONF_DATABASE, CONF_FLOWCONTROL, CONF_RADIO_TYPE, + CONF_ZIGPY, + DATA_ZHA, + DATA_ZHA_CONFIG, + DEFAULT_DATABASE_NAME, DOMAIN, RadioType, ) @@ -27,24 +45,184 @@ SUPPORTED_PORT_SETTINGS = ( ) DECONZ_DOMAIN = "deconz" +# Only the common radio types will be autoprobed, ordered by new device popularity. +# XBee takes too long to probe since it scans through all possible bauds and likely has +# very few users to begin with. +AUTOPROBE_RADIOS = ( + RadioType.ezsp, + RadioType.znp, + RadioType.deconz, + RadioType.zigate, +) -class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow.""" +FORMATION_STRATEGY = "formation_strategy" +FORMATION_FORM_NEW_NETWORK = "form_new_network" +FORMATION_REUSE_SETTINGS = "reuse_settings" +FORMATION_CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup" +FORMATION_UPLOAD_MANUAL_BACKUP = "upload_manual_backup" - VERSION = 3 +CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup" +OVERWRITE_COORDINATOR_IEEE = "overwrite_coordinator_ieee" - def __init__(self): +UPLOADED_BACKUP_FILE = "uploaded_backup_file" + +_LOGGER = logging.getLogger(__name__) + + +def _format_backup_choice( + backup: zigpy.backups.NetworkBackup, *, pan_ids: bool = True +) -> str: + """Format network backup info into a short piece of text.""" + if not pan_ids: + return dt.as_local(backup.backup_time).strftime("%c") + + identifier = ( + # PAN ID + f"{str(backup.network_info.pan_id)[2:]}" + # EPID + f":{str(backup.network_info.extended_pan_id).replace(':', '')}" + ).lower() + + return f"{dt.as_local(backup.backup_time).strftime('%c')} ({identifier})" + + +def _allow_overwrite_ezsp_ieee( + backup: zigpy.backups.NetworkBackup, +) -> zigpy.backups.NetworkBackup: + """Return a new backup with the flag to allow overwriting the EZSP EUI64.""" + new_stack_specific = copy.deepcopy(backup.network_info.stack_specific) + new_stack_specific.setdefault("ezsp", {})[ + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" + ] = True + + return backup.replace( + network_info=backup.network_info.replace(stack_specific=new_stack_specific) + ) + + +def _prevent_overwrite_ezsp_ieee( + backup: zigpy.backups.NetworkBackup, +) -> zigpy.backups.NetworkBackup: + """Return a new backup without the flag to allow overwriting the EZSP EUI64.""" + if "ezsp" not in backup.network_info.stack_specific: + return backup + + new_stack_specific = copy.deepcopy(backup.network_info.stack_specific) + new_stack_specific.setdefault("ezsp", {}).pop( + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it", None + ) + + return backup.replace( + network_info=backup.network_info.replace(stack_specific=new_stack_specific) + ) + + +class BaseZhaFlow(FlowHandler): + """Mixin for common ZHA flow steps and forms.""" + + def __init__(self) -> None: """Initialize flow instance.""" - self._device_path = None - self._device_settings = None - self._radio_type = None - self._title = None + super().__init__() - async def async_step_user(self, user_input=None): - """Handle a zha config flow start.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + self._device_path: str | None = None + self._device_settings: dict[str, Any] | None = None + self._radio_type: RadioType | None = None + self._title: str | None = None + self._current_settings: zigpy.backups.NetworkBackup | None = None + self._backups: list[zigpy.backups.NetworkBackup] = [] + self._chosen_backup: zigpy.backups.NetworkBackup | None = None + @contextlib.asynccontextmanager + async def _connect_zigpy_app(self) -> ControllerApplication: + """Connect to the radio with the current config and then clean up.""" + assert self._radio_type is not None + + config = self.hass.data.get(DATA_ZHA, {}).get(DATA_ZHA_CONFIG, {}) + app_config = config.get(CONF_ZIGPY, {}).copy() + + database_path = config.get( + CONF_DATABASE, + self.hass.config.path(DEFAULT_DATABASE_NAME), + ) + + # Don't create `zigbee.db` if it doesn't already exist + if not await self.hass.async_add_executor_job(os.path.exists, database_path): + database_path = None + + app_config[CONF_DATABASE] = database_path + app_config[CONF_DEVICE] = self._device_settings + app_config = self._radio_type.controller.SCHEMA(app_config) + + app = await self._radio_type.controller.new( + app_config, auto_form=False, start_radio=False + ) + + try: + await app.connect() + yield app + finally: + await app.disconnect() + + async def _restore_backup( + self, backup: zigpy.backups.NetworkBackup, **kwargs: Any + ) -> None: + """Restore the provided network backup, passing through kwargs.""" + if self._current_settings is not None and self._current_settings.supersedes( + self._chosen_backup + ): + return + + async with self._connect_zigpy_app() as app: + await app.backups.restore_backup(backup, **kwargs) + + async def _detect_radio_type(self) -> bool: + """Probe all radio types on the current port.""" + for radio in AUTOPROBE_RADIOS: + _LOGGER.debug("Attempting to probe radio type %s", radio) + + dev_config = radio.controller.SCHEMA_DEVICE( + {CONF_DEVICE_PATH: self._device_path} + ) + probe_result = await radio.controller.probe(dev_config) + + if not probe_result: + continue + + # Radio library probing can succeed and return new device settings + if isinstance(probe_result, dict): + dev_config = probe_result + + self._radio_type = radio + self._device_settings = dev_config + + return True + + return False + + async def _async_create_radio_entity(self) -> FlowResult: + """Create a config entity with the current flow state.""" + assert self._title is not None + assert self._radio_type is not None + assert self._device_path is not None + assert self._device_settings is not None + + device_settings = self._device_settings.copy() + device_settings[CONF_DEVICE_PATH] = await self.hass.async_add_executor_job( + usb.get_serial_by_id, self._device_path + ) + + return self.async_create_entry( + title=self._title, + data={ + CONF_DEVICE: device_settings, + CONF_RADIO_TYPE: self._radio_type.name, + }, + ) + + async def async_step_choose_serial_port( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Choose a serial port.""" ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) list_of_ports = [ f"{p}, s/n: {p.serial_number or 'n/a'}" @@ -53,48 +231,329 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ] if not list_of_ports: - return await self.async_step_pick_radio() + return await self.async_step_manual_pick_radio_type() list_of_ports.append(CONF_MANUAL_PATH) if user_input is not None: user_selection = user_input[CONF_DEVICE_PATH] + if user_selection == CONF_MANUAL_PATH: - return await self.async_step_pick_radio() + return await self.async_step_manual_pick_radio_type() port = ports[list_of_ports.index(user_selection)] - dev_path = await self.hass.async_add_executor_job( - usb.get_serial_by_id, port.device + self._device_path = port.device + + if not await self._detect_radio_type(): + # Did not autodetect anything, proceed to manual selection + return await self.async_step_manual_pick_radio_type() + + self._title = ( + f"{port.description}, s/n: {port.serial_number or 'n/a'}" + f" - {port.manufacturer}" + if port.manufacturer + else "" ) - auto_detected_data = await detect_radios(dev_path) - if auto_detected_data is not None: - title = f"{port.description}, s/n: {port.serial_number or 'n/a'}" - title += f" - {port.manufacturer}" if port.manufacturer else "" - return self.async_create_entry( - title=title, - data=auto_detected_data, + + return await self.async_step_choose_formation_strategy() + + # Pre-select the currently configured port + default_port = vol.UNDEFINED + + if self._device_path is not None: + for description, port in zip(list_of_ports, ports): + if port.device == self._device_path: + default_port = description + break + else: + default_port = CONF_MANUAL_PATH + + schema = vol.Schema( + { + vol.Required(CONF_DEVICE_PATH, default=default_port): vol.In( + list_of_ports ) + } + ) + return self.async_show_form(step_id="choose_serial_port", data_schema=schema) - # did not detect anything - self._device_path = dev_path - return await self.async_step_pick_radio() - - schema = vol.Schema({vol.Required(CONF_DEVICE_PATH): vol.In(list_of_ports)}) - return self.async_show_form(step_id="user", data_schema=schema) - - async def async_step_pick_radio(self, user_input=None): - """Select radio type.""" - + async def async_step_manual_pick_radio_type( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manually select the radio type.""" if user_input is not None: self._radio_type = RadioType.get_by_description(user_input[CONF_RADIO_TYPE]) - return await self.async_step_port_config() + return await self.async_step_manual_port_config() + + # Pre-select the current radio type + default = vol.UNDEFINED + + if self._radio_type is not None: + default = self._radio_type.description + + schema = { + vol.Required(CONF_RADIO_TYPE, default=default): vol.In(RadioType.list()) + } - schema = {vol.Required(CONF_RADIO_TYPE): vol.In(sorted(RadioType.list()))} return self.async_show_form( - step_id="pick_radio", + step_id="manual_pick_radio_type", data_schema=vol.Schema(schema), ) + async def async_step_manual_port_config( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Enter port settings specific for this type of radio.""" + assert self._radio_type is not None + errors = {} + + if user_input is not None: + self._title = user_input[CONF_DEVICE_PATH] + self._device_path = user_input[CONF_DEVICE_PATH] + self._device_settings = user_input.copy() + + if await self._radio_type.controller.probe(user_input): + return await self.async_step_choose_formation_strategy() + + errors["base"] = "cannot_connect" + + schema = { + vol.Required( + CONF_DEVICE_PATH, default=self._device_path or vol.UNDEFINED + ): str + } + + source = self.context.get("source") + for param, value in self._radio_type.controller.SCHEMA_DEVICE.schema.items(): + if param not in SUPPORTED_PORT_SETTINGS: + continue + + if source == config_entries.SOURCE_ZEROCONF and param == CONF_BAUDRATE: + value = 115200 + param = vol.Required(CONF_BAUDRATE, default=value) + elif self._device_settings is not None and param in self._device_settings: + param = vol.Required(str(param), default=self._device_settings[param]) + + schema[param] = value + + return self.async_show_form( + step_id="manual_port_config", + data_schema=vol.Schema(schema), + errors=errors, + ) + + async def _async_load_network_settings(self) -> None: + """Connect to the radio and load its current network settings.""" + async with self._connect_zigpy_app() as app: + # Check if the stick has any settings and load them + try: + await app.load_network_info() + except NetworkNotFormed: + pass + else: + self._current_settings = zigpy.backups.NetworkBackup( + network_info=app.state.network_info, + node_info=app.state.node_info, + ) + + # The list of backups will always exist + self._backups = app.backups.backups.copy() + + async def async_step_choose_formation_strategy( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Choose how to deal with the current radio's settings.""" + await self._async_load_network_settings() + + strategies = [] + + # Check if we have any automatic backups *and* if the backups differ from + # the current radio settings, if they exist (since restoring would be redundant) + if self._backups and ( + self._current_settings is None + or any( + not backup.is_compatible_with(self._current_settings) + for backup in self._backups + ) + ): + strategies.append(CHOOSE_AUTOMATIC_BACKUP) + + if self._current_settings is not None: + strategies.append(FORMATION_REUSE_SETTINGS) + + strategies.append(FORMATION_UPLOAD_MANUAL_BACKUP) + strategies.append(FORMATION_FORM_NEW_NETWORK) + + return self.async_show_menu( + step_id="choose_formation_strategy", + menu_options=strategies, + ) + + async def async_step_reuse_settings( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Reuse the existing network settings on the stick.""" + return await self._async_create_radio_entity() + + async def async_step_form_new_network( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Form a brand new network.""" + async with self._connect_zigpy_app() as app: + await app.form_network() + + return await self._async_create_radio_entity() + + def _parse_uploaded_backup( + self, uploaded_file_id: str + ) -> zigpy.backups.NetworkBackup: + """Read and parse an uploaded backup JSON file.""" + with process_uploaded_file(self.hass, uploaded_file_id) as file_path: + contents = file_path.read_text() + + return zigpy.backups.NetworkBackup.from_dict(json.loads(contents)) + + async def async_step_upload_manual_backup( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Upload and restore a coordinator backup JSON file.""" + errors = {} + + if user_input is not None: + try: + self._chosen_backup = await self.hass.async_add_executor_job( + self._parse_uploaded_backup, user_input[UPLOADED_BACKUP_FILE] + ) + except ValueError: + errors["base"] = "invalid_backup_json" + else: + return await self.async_step_maybe_confirm_ezsp_restore() + + return self.async_show_form( + step_id="upload_manual_backup", + data_schema=vol.Schema( + { + vol.Required(UPLOADED_BACKUP_FILE): FileSelector( + FileSelectorConfig(accept=".json,application/json") + ) + } + ), + errors=errors, + ) + + async def async_step_choose_automatic_backup( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Choose an automatic backup.""" + if self.show_advanced_options: + # Always show the PAN IDs when in advanced mode + choices = [ + _format_backup_choice(backup, pan_ids=True) for backup in self._backups + ] + else: + # Only show the PAN IDs for multiple backups taken on the same day + num_backups_on_date = collections.Counter( + backup.backup_time.date() for backup in self._backups + ) + choices = [ + _format_backup_choice( + backup, pan_ids=(num_backups_on_date[backup.backup_time.date()] > 1) + ) + for backup in self._backups + ] + + if user_input is not None: + index = choices.index(user_input[CHOOSE_AUTOMATIC_BACKUP]) + self._chosen_backup = self._backups[index] + + return await self.async_step_maybe_confirm_ezsp_restore() + + return self.async_show_form( + step_id="choose_automatic_backup", + data_schema=vol.Schema( + { + vol.Required(CHOOSE_AUTOMATIC_BACKUP, default=choices[0]): vol.In( + choices + ), + } + ), + ) + + async def async_step_maybe_confirm_ezsp_restore( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm restore for EZSP radios that require permanent IEEE writes.""" + assert self._chosen_backup is not None + + if self._radio_type != RadioType.ezsp: + await self._restore_backup(self._chosen_backup) + return await self._async_create_radio_entity() + + # We have no way to partially load network settings if no network is formed + if self._current_settings is None: + # Since we are going to be restoring the backup anyways, write it to the + # radio without overwriting the IEEE but don't take a backup with these + # temporary settings + temp_backup = _prevent_overwrite_ezsp_ieee(self._chosen_backup) + await self._restore_backup(temp_backup, create_new=False) + await self._async_load_network_settings() + + assert self._current_settings is not None + + if ( + self._current_settings.node_info.ieee == self._chosen_backup.node_info.ieee + or not self._current_settings.network_info.metadata["ezsp"][ + "can_write_custom_eui64" + ] + ): + # No point in prompting the user if the backup doesn't have a new IEEE + # address or if there is no way to overwrite the IEEE address a second time + await self._restore_backup(self._chosen_backup) + + return await self._async_create_radio_entity() + + if user_input is not None: + backup = self._chosen_backup + + if user_input[OVERWRITE_COORDINATOR_IEEE]: + backup = _allow_overwrite_ezsp_ieee(backup) + + # If the user declined to overwrite the IEEE *and* we wrote the backup to + # their empty radio above, restoring it again would be redundant. + await self._restore_backup(backup) + + return await self._async_create_radio_entity() + + return self.async_show_form( + step_id="maybe_confirm_ezsp_restore", + data_schema=vol.Schema( + {vol.Required(OVERWRITE_COORDINATOR_IEEE, default=True): bool} + ), + ) + + +class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 3 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Create the options flow.""" + return ZhaOptionsFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a zha config flow start.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_choose_serial_port(user_input) + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle usb discovery.""" vid = discovery_info.vid @@ -118,9 +577,8 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - # If they already have a discovery for deconz - # we ignore the usb discovery as they probably - # want to use it there instead + # If they already have a discovery for deconz we ignore the usb discovery as + # they probably want to use it there instead if self.hass.config_entries.flow.async_progress_by_handler(DECONZ_DOMAIN): return self.async_abort(reason="not_zha_device") for entry in self.hass.config_entries.async_entries(DECONZ_DOMAIN): @@ -140,19 +598,18 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {CONF_NAME: self._title} return await self.async_step_confirm() - async def async_step_confirm(self, user_input=None): - """Confirm a USB discovery.""" + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm a discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): - auto_detected_data = await detect_radios(self._device_path) - if auto_detected_data is None: + if not await self._detect_radio_type(): # This path probably will not happen now that we have # more precise USB matching unless there is a problem # with the device return self.async_abort(reason="usb_probe_failed") - return self.async_create_entry( - title=self._title, - data=auto_detected_data, - ) + + return await self.async_step_choose_formation_strategy() return self.async_show_form( step_id="confirm", @@ -188,61 +645,22 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - self.context["title_placeholders"] = { - CONF_NAME: node_name, - } - + self.context["title_placeholders"] = {CONF_NAME: node_name} + self._title = device_path self._device_path = device_path + if "efr32" in radio_type: - self._radio_type = RadioType.ezsp.name + self._radio_type = RadioType.ezsp elif "zigate" in radio_type: - self._radio_type = RadioType.zigate.name + self._radio_type = RadioType.zigate else: - self._radio_type = RadioType.znp.name + self._radio_type = RadioType.znp - return await self.async_step_port_config() + return await self.async_step_manual_port_config() - async def async_step_port_config(self, user_input=None): - """Enter port settings specific for this type of radio.""" - errors = {} - app_cls = RadioType[self._radio_type].controller - - if user_input is not None: - self._device_path = user_input.get(CONF_DEVICE_PATH) - if await app_cls.probe(user_input): - serial_by_id = await self.hass.async_add_executor_job( - usb.get_serial_by_id, user_input[CONF_DEVICE_PATH] - ) - user_input[CONF_DEVICE_PATH] = serial_by_id - return self.async_create_entry( - title=user_input[CONF_DEVICE_PATH], - data={CONF_DEVICE: user_input, CONF_RADIO_TYPE: self._radio_type}, - ) - errors["base"] = "cannot_connect" - - schema = { - vol.Required( - CONF_DEVICE_PATH, default=self._device_path or vol.UNDEFINED - ): str - } - radio_schema = app_cls.SCHEMA_DEVICE.schema - if isinstance(radio_schema, vol.Schema): - radio_schema = radio_schema.schema - - source = self.context.get("source") - for param, value in radio_schema.items(): - if param in SUPPORTED_PORT_SETTINGS: - schema[param] = value - if source == config_entries.SOURCE_ZEROCONF and param == CONF_BAUDRATE: - schema[param] = 115200 - - return self.async_show_form( - step_id="port_config", - data_schema=vol.Schema(schema), - errors=errors, - ) - - async def async_step_hardware(self, data=None): + async def async_step_hardware( + self, data: dict[str, Any] | None = None + ) -> FlowResult: """Handle hardware flow.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -250,40 +668,39 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="invalid_hardware_data") if data.get("radio_type") != "efr32": return self.async_abort(reason="invalid_hardware_data") - self._radio_type = RadioType.ezsp.name - app_cls = RadioType[self._radio_type].controller + self._radio_type = RadioType.ezsp schema = { vol.Required( CONF_DEVICE_PATH, default=self._device_path or vol.UNDEFINED ): str } - radio_schema = app_cls.SCHEMA_DEVICE.schema + + radio_schema = self._radio_type.controller.SCHEMA_DEVICE.schema assert not isinstance(radio_schema, vol.Schema) for param, value in radio_schema.items(): if param in SUPPORTED_PORT_SETTINGS: schema[param] = value + try: - self._device_settings = vol.Schema(schema)(data.get("port")) + device_settings = vol.Schema(schema)(data.get("port")) except vol.Invalid: return self.async_abort(reason="invalid_hardware_data") self._title = data.get("name", data["port"]["path"]) + self._device_path = device_settings.pop(CONF_DEVICE_PATH) + self._device_settings = device_settings self._set_confirm_only() return await self.async_step_confirm_hardware() - async def async_step_confirm_hardware(self, user_input=None): + async def async_step_confirm_hardware( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm a hardware discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): - return self.async_create_entry( - title=self._title, - data={ - CONF_DEVICE: self._device_settings, - CONF_RADIO_TYPE: self._radio_type, - }, - ) + return await self._async_create_radio_entity() return self.async_show_form( step_id="confirm_hardware", @@ -291,14 +708,65 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -async def detect_radios(dev_path: str) -> dict[str, Any] | None: - """Probe all radio types on the device port.""" - for radio in RadioType: - dev_config = radio.controller.SCHEMA_DEVICE({CONF_DEVICE_PATH: dev_path}) - probe_result = await radio.controller.probe(dev_config) - if probe_result: - if isinstance(probe_result, dict): - return {CONF_RADIO_TYPE: radio.name, CONF_DEVICE: probe_result} - return {CONF_RADIO_TYPE: radio.name, CONF_DEVICE: dev_config} +class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): + """Handle an options flow.""" - return None + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + super().__init__() + self.config_entry = config_entry + + self._device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + self._device_settings = config_entry.data[CONF_DEVICE] + self._radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]] + self._title = config_entry.title + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Launch the options flow.""" + if user_input is not None: + try: + await self.hass.config_entries.async_unload(self.config_entry.entry_id) + except config_entries.OperationNotAllowed: + # ZHA is not running + pass + + return await self.async_step_choose_serial_port() + + return self.async_show_form(step_id="init") + + async def _async_create_radio_entity(self): + """Re-implementation of the base flow's final step to update the config.""" + device_settings = self._device_settings.copy() + device_settings[CONF_DEVICE_PATH] = await self.hass.async_add_executor_job( + usb.get_serial_by_id, self._device_path + ) + + # Avoid creating both `.options` and `.data` by directly writing `data` here + self.hass.config_entries.async_update_entry( + entry=self.config_entry, + data={ + CONF_DEVICE: device_settings, + CONF_RADIO_TYPE: self._radio_type.name, + }, + options=self.config_entry.options, + ) + + # Reload ZHA after we finish + await self.hass.config_entries.async_setup(self.config_entry.entry_id) + + # Intentionally do not set `data` to avoid creating `options`, we set it above + return self.async_create_entry(title=self._title, data={}) + + def async_remove(self): + """Maybe reload ZHA if the flow is aborted.""" + if self.config_entry.state not in ( + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.NOT_LOADED, + ): + return + + self.hass.async_create_task( + self.hass.config_entries.async_setup(self.config_entry.entry_id) + ) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 4a48c254b40..fa8b7148c77 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -236,14 +236,14 @@ _ControllerClsType = type[zigpy.application.ControllerApplication] class RadioType(enum.Enum): """Possible options for radio type.""" - znp = ( - "ZNP = Texas Instruments Z-Stack ZNP protocol: CC253x, CC26x2, CC13x2", - zigpy_znp.zigbee.application.ControllerApplication, - ) ezsp = ( "EZSP = Silicon Labs EmberZNet protocol: Elelabs, HUSBZB-1, Telegesis", bellows.zigbee.application.ControllerApplication, ) + znp = ( + "ZNP = Texas Instruments Z-Stack ZNP protocol: CC253x, CC26x2, CC13x2", + zigpy_znp.zigbee.application.ControllerApplication, + ) deconz = ( "deCONZ = dresden elektronik deCONZ protocol: ConBee I/II, RaspBee I/II", zigpy_deconz.zigbee.application.ControllerApplication, @@ -263,11 +263,11 @@ class RadioType(enum.Enum): return [e.description for e in RadioType] @classmethod - def get_by_description(cls, description: str) -> str: + def get_by_description(cls, description: str) -> RadioType: """Get radio by description.""" for radio in cls: if radio.description == description: - return radio.name + return radio raise ValueError def __init__(self, description: str, controller_cls: _ControllerClsType) -> None: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 607fb351838..2e35427a70c 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -93,6 +93,7 @@ "name": "*zigate*" } ], + "dependencies": ["file_upload"], "after_dependencies": ["onboarding", "usb", "zeroconf"], "iot_class": "local_polling", "loggers": [ diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 37be80e9b56..1de5e164fee 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -2,10 +2,10 @@ "config": { "flow_title": "{name}", "step": { - "user": { - "title": "ZHA", + "choose_serial_port": { + "title": "Select a Serial Port", "data": { "path": "Serial Device Path" }, - "description": "Select serial port for Zigbee radio" + "description": "Select the serial port for your Zigbee radio" }, "confirm": { "description": "Do you want to setup {name}?" @@ -13,23 +13,55 @@ "confirm_hardware": { "description": "Do you want to setup {name}?" }, - "pick_radio": { + "manual_pick_radio_type": { "data": { "radio_type": "Radio Type" }, "title": "Radio Type", - "description": "Pick a type of your Zigbee radio" + "description": "Pick your Zigbee radio type" }, - "port_config": { - "title": "Settings", - "description": "Enter port specific settings", + "manual_port_config": { + "title": "Serial Port Settings", + "description": "Enter the serial port settings", "data": { "path": "Serial device path", "baudrate": "port speed", "flow_control": "data flow control" } + }, + "choose_formation_strategy": { + "title": "Network Formation", + "description": "Choose the network settings for your radio.", + "menu_options": { + "form_new_network": "Erase network settings and form a new network", + "reuse_settings": "Keep radio network settings", + "choose_automatic_backup": "Restore an automatic backup", + "upload_manual_backup": "Upload a manual backup" + } + }, + "choose_automatic_backup": { + "title": "Restore Automatic Backup", + "description": "Restore your network settings from an automatic backup", + "data": { + "choose_automatic_backup": "Choose an automatic backup" + } + }, + "upload_manual_backup": { + "title": "Upload a Manual Backup", + "description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.", + "data": { + "uploaded_backup_file": "Upload a file" + } + }, + "maybe_confirm_ezsp_restore": { + "title": "Overwrite Radio IEEE Address", + "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", + "data": { + "overwrite_coordinator_ieee": "Permanently replace the radio IEEE address" + } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_backup_json": "Invalid backup JSON" }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", @@ -37,6 +69,78 @@ "usb_probe_failed": "Failed to probe the usb device" } }, + "options": { + "flow_title": "[%key:component::zha::config::flow_title%]", + "step": { + "init": { + "title": "Reconfigure ZHA", + "description": "ZHA will be stopped. Do you wish to continue?" + }, + "choose_serial_port": { + "title": "[%key:component::zha::config::step::choose_serial_port::title%]", + "data": { + "path": "[%key:component::zha::config::step::choose_serial_port::data::path%]" + }, + "description": "[%key:component::zha::config::step::choose_serial_port::description%]" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "[%key:component::zha::config::step::manual_pick_radio_type::data::radio_type%]" + }, + "title": "[%key:component::zha::config::step::manual_pick_radio_type::title%]", + "description": "[%key:component::zha::config::step::manual_pick_radio_type::description%]" + }, + "manual_port_config": { + "title": "[%key:component::zha::config::step::manual_port_config::title%]", + "description": "[%key:component::zha::config::step::manual_port_config::description%]", + "data": { + "path": "[%key:component::zha::config::step::manual_port_config::data::path%]", + "baudrate": "[%key:component::zha::config::step::manual_port_config::data::baudrate%]", + "flow_control": "[%key:component::zha::config::step::manual_port_config::data::flow_control%]" + } + }, + "choose_formation_strategy": { + "title": "[%key:component::zha::config::step::choose_formation_strategy::title%]", + "description": "[%key:component::zha::config::step::choose_formation_strategy::description%]", + "menu_options": { + "form_new_network": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::form_new_network%]", + "reuse_settings": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::reuse_settings%]", + "choose_automatic_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::choose_automatic_backup%]", + "upload_manual_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::upload_manual_backup%]" + } + }, + "choose_automatic_backup": { + "title": "[%key:component::zha::config::step::choose_automatic_backup::title%]", + "description": "[%key:component::zha::config::step::choose_automatic_backup::description%]", + "data": { + "choose_automatic_backup": "[%key:component::zha::config::step::choose_automatic_backup::data::choose_automatic_backup%]" + } + }, + "upload_manual_backup": { + "title": "[%key:component::zha::config::step::upload_manual_backup::title%]", + "description": "[%key:component::zha::config::step::upload_manual_backup::description%]", + "data": { + "uploaded_backup_file": "[%key:component::zha::config::step::upload_manual_backup::data::uploaded_backup_file%]" + } + }, + "maybe_confirm_ezsp_restore": { + "title": "[%key:component::zha::config::step::maybe_confirm_ezsp_restore::title%]", + "description": "[%key:component::zha::config::step::maybe_confirm_ezsp_restore::description%]", + "data": { + "overwrite_coordinator_ieee": "[%key:component::zha::config::step::maybe_confirm_ezsp_restore::data::overwrite_coordinator_ieee%]" + } + } + }, + "error": { + "cannot_connect": "[%key:component::zha::config::error::cannot_connect%]", + "invalid_backup_json": "[%key:component::zha::config::error::invalid_backup_json%]" + }, + "abort": { + "single_instance_allowed": "[%key:component::zha::config::abort::single_instance_allowed%]", + "not_zha_device": "[%key:component::zha::config::abort::not_zha_device%]", + "usb_probe_failed": "[%key:component::zha::config::abort::usb_probe_failed%]" + } + }, "config_panel": { "zha_options": { "title": "Global Options", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 46e897167b0..27a4db9ef02 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -6,38 +6,70 @@ "usb_probe_failed": "Failed to probe the usb device" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_backup_json": "Invalid backup JSON" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Choose an automatic backup" + }, + "description": "Restore your network settings from an automatic backup", + "title": "Restore Automatic Backup" + }, + "choose_formation_strategy": { + "description": "Choose the network settings for your radio.", + "menu_options": { + "choose_automatic_backup": "Restore an automatic backup", + "form_new_network": "Erase network settings and form a new network", + "reuse_settings": "Keep radio network settings", + "upload_manual_backup": "Upload a manual backup" + }, + "title": "Network Formation" + }, + "choose_serial_port": { + "data": { + "path": "Serial Device Path" + }, + "description": "Select the serial port for your Zigbee radio", + "title": "Select a Serial Port" + }, "confirm": { "description": "Do you want to setup {name}?" }, "confirm_hardware": { "description": "Do you want to setup {name}?" }, - "pick_radio": { + "manual_pick_radio_type": { "data": { "radio_type": "Radio Type" }, - "description": "Pick a type of your Zigbee radio", + "description": "Pick your Zigbee radio type", "title": "Radio Type" }, - "port_config": { + "manual_port_config": { "data": { "baudrate": "port speed", "flow_control": "data flow control", "path": "Serial device path" }, - "description": "Enter port specific settings", - "title": "Settings" + "description": "Enter the serial port settings", + "title": "Serial Port Settings" }, - "user": { + "maybe_confirm_ezsp_restore": { "data": { - "path": "Serial Device Path" + "overwrite_coordinator_ieee": "Permanently replace the radio IEEE address" }, - "description": "Select serial port for Zigbee radio", - "title": "ZHA" + "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", + "title": "Overwrite Radio IEEE Address" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Upload a file" + }, + "description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.", + "title": "Upload a Manual Backup" } } }, @@ -114,5 +146,77 @@ "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked" } + }, + "options": { + "abort": { + "not_zha_device": "This device is not a zha device", + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "usb_probe_failed": "Failed to probe the usb device" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_backup_json": "Invalid backup JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Choose an automatic backup" + }, + "description": "Restore your network settings from an automatic backup", + "title": "Restore Automatic Backup" + }, + "choose_formation_strategy": { + "description": "Choose the network settings for your radio.", + "menu_options": { + "choose_automatic_backup": "Restore an automatic backup", + "form_new_network": "Erase network settings and form a new network", + "reuse_settings": "Keep radio network settings", + "upload_manual_backup": "Upload a manual backup" + }, + "title": "Network Formation" + }, + "choose_serial_port": { + "data": { + "path": "Serial Device Path" + }, + "description": "Select the serial port for your Zigbee radio", + "title": "Select a Serial Port" + }, + "init": { + "description": "ZHA will be stopped. Do you wish to continue?", + "title": "Reconfigure ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Radio Type" + }, + "description": "Pick your Zigbee radio type", + "title": "Radio Type" + }, + "manual_port_config": { + "data": { + "baudrate": "port speed", + "flow_control": "data flow control", + "path": "Serial device path" + }, + "description": "Enter the serial port settings", + "title": "Serial Port Settings" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Permanently replace the radio IEEE address" + }, + "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", + "title": "Overwrite Radio IEEE Address" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Upload a file" + }, + "description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.", + "title": "Upload a Manual Backup" + } + } } } \ No newline at end of file diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index a769303a4c4..12f5434abd4 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,11 +1,16 @@ """Tests for ZHA config flow.""" -from unittest.mock import AsyncMock, MagicMock, patch, sentinel +import copy +import json +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +import uuid import pytest import serial.tools.list_ports import zigpy.config from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH +from zigpy.exceptions import NetworkNotFormed +import zigpy.types from homeassistant import config_entries from homeassistant.components import ssdp, usb, zeroconf @@ -29,6 +34,8 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry +PROBE_FUNCTION_PATH = "zigbee.application.ControllerApplication.probe" + @pytest.fixture(autouse=True) def disable_platform_only(): @@ -37,20 +44,55 @@ def disable_platform_only(): yield -def com_port(): +@pytest.fixture(autouse=True) +def mock_app(): + """Mock zigpy app interface.""" + mock_app = AsyncMock() + mock_app.backups.backups = [] + + with patch( + "zigpy.application.ControllerApplication.new", AsyncMock(return_value=mock_app) + ): + yield mock_app + + +@pytest.fixture +def backup(): + """Zigpy network backup with non-default settings.""" + backup = zigpy.backups.NetworkBackup() + backup.node_info.ieee = zigpy.types.EUI64.convert("AA:BB:CC:DD:11:22:33:44") + + return backup + + +def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): + """Mock `_detect_radio_type` that just sets the appropriate attributes.""" + + async def detect(self): + self._radio_type = radio_type + self._device_settings = radio_type.controller.SCHEMA_DEVICE( + {CONF_DEVICE_PATH: self._device_path} + ) + + return ret + + return detect + + +def com_port(device="/dev/ttyUSB1234"): """Mock of a serial port.""" port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") port.serial_number = "1234" port.manufacturer = "Virtual serial port" - port.device = "/dev/ttyUSB1234" + port.device = device port.description = "Some serial port" return port @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_zeroconf_discovery_znp(hass): """Test zeroconf flow -- radio detected.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.200", @@ -62,15 +104,24 @@ async def test_discovery(detect_mock, hass): type="mock_type", ) flow = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) - result = await hass.config_entries.flow.async_configure( + result1 = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "socket://192.168.1.200:6638" - assert result["data"] == { + assert result1["type"] == FlowResultType.MENU + assert result1["step_id"] == "choose_formation_strategy" + + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "socket://192.168.1.200:6638" + assert result2["data"] == { CONF_DEVICE: { CONF_BAUDRATE: 115200, CONF_FLOWCONTROL: None, @@ -81,8 +132,8 @@ async def test_discovery(detect_mock, hass): @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -@patch("zigpy_zigate.zigbee.application.ControllerApplication.probe") -async def test_zigate_via_zeroconf(probe_mock, hass): +@patch(f"zigpy_zigate.{PROBE_FUNCTION_PATH}") +async def test_zigate_via_zeroconf(setup_entry_mock, hass): """Test zeroconf flow -- zigate radio detected.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.200", @@ -94,15 +145,24 @@ async def test_zigate_via_zeroconf(probe_mock, hass): type="mock_type", ) flow = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) - result = await hass.config_entries.flow.async_configure( + result1 = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "socket://192.168.1.200:1234" - assert result["data"] == { + assert result1["type"] == FlowResultType.MENU + assert result1["step_id"] == "choose_formation_strategy" + + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "socket://192.168.1.200:1234" + assert result2["data"] == { CONF_DEVICE: { CONF_DEVICE_PATH: "socket://192.168.1.200:1234", }, @@ -111,8 +171,8 @@ async def test_zigate_via_zeroconf(probe_mock, hass): @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -@patch("bellows.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_efr32_via_zeroconf(probe_mock, hass): +@patch(f"bellows.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_efr32_via_zeroconf(hass): """Test zeroconf flow -- efr32 radio detected.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.200", @@ -124,15 +184,24 @@ async def test_efr32_via_zeroconf(probe_mock, hass): type="mock_type", ) flow = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) - result = await hass.config_entries.flow.async_configure( - flow["flow_id"], user_input={"baudrate": 115200} + result1 = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "socket://192.168.1.200:6638" - assert result["data"] == { + assert result1["type"] == FlowResultType.MENU + assert result1["step_id"] == "choose_formation_strategy" + + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "socket://192.168.1.200:6638" + assert result2["data"] == { CONF_DEVICE: { CONF_DEVICE_PATH: "socket://192.168.1.200:6638", CONF_BAUDRATE: 115200, @@ -143,8 +212,8 @@ async def test_efr32_via_zeroconf(probe_mock, hass): @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery_via_zeroconf_ip_change(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_discovery_via_zeroconf_ip_change(hass): """Test zeroconf flow -- radio detected.""" entry = MockConfigEntry( domain=DOMAIN, @@ -169,7 +238,7 @@ async def test_discovery_via_zeroconf_ip_change(detect_mock, hass): type="mock_type", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) assert result["type"] == FlowResultType.ABORT @@ -182,8 +251,8 @@ async def test_discovery_via_zeroconf_ip_change(detect_mock, hass): @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_discovery_via_zeroconf_ip_change_ignored(hass): """Test zeroconf flow that was ignored gets updated.""" entry = MockConfigEntry( domain=DOMAIN, @@ -202,7 +271,7 @@ async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass): type="mock_type", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) assert result["type"] == FlowResultType.ABORT @@ -212,8 +281,8 @@ async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass): } -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery_via_usb(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_discovery_via_usb(hass): """Test usb flow -- radio detected.""" discovery_info = usb.UsbServiceInfo( device="/dev/ttyZIGBEE", @@ -224,21 +293,30 @@ async def test_discovery_via_usb(detect_mock, hass): manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" - with patch("homeassistant.components.zha.async_setup_entry"): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.MENU + assert result2["step_id"] == "choose_formation_strategy" + + with patch("homeassistant.components.zha.async_setup_entry", return_value=True): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "zigbee radio" - assert result2["data"] == { + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "zigbee radio" + assert result3["data"] == { "device": { "baudrate": 115200, "flow_control": None, @@ -248,8 +326,8 @@ async def test_discovery_via_usb(detect_mock, hass): } -@patch("zigpy_zigate.zigbee.application.ControllerApplication.probe") -async def test_zigate_discovery_via_usb(detect_mock, hass): +@patch(f"zigpy_zigate.{PROBE_FUNCTION_PATH}", return_value=True) +async def test_zigate_discovery_via_usb(probe_mock, hass): """Test zigate usb flow -- radio detected.""" discovery_info = usb.UsbServiceInfo( device="/dev/ttyZIGBEE", @@ -260,21 +338,30 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" - with patch("homeassistant.components.zha.async_setup_entry"): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.MENU + assert result2["step_id"] == "choose_formation_strategy" + + with patch("homeassistant.components.zha.async_setup_entry", return_value=True): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "zigate radio" - assert result2["data"] == { + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "zigate radio" + assert result3["data"] == { "device": { "path": "/dev/ttyZIGBEE", }, @@ -282,8 +369,8 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): } -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=False) -async def test_discovery_via_usb_no_radio(detect_mock, hass): +@patch(f"bellows.{PROBE_FUNCTION_PATH}", return_value=False) +async def test_discovery_via_usb_no_radio(probe_mock, hass): """Test usb flow -- no radio detected.""" discovery_info = usb.UsbServiceInfo( device="/dev/null", @@ -294,13 +381,13 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" - with patch("homeassistant.components.zha.async_setup_entry"): + with patch("homeassistant.components.zha.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -310,8 +397,8 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): assert result2["reason"] == "usb_probe_failed" -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery_via_usb_already_setup(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_discovery_via_usb_already_setup(hass): """Test usb flow -- already setup.""" MockConfigEntry( @@ -327,7 +414,7 @@ async def test_discovery_via_usb_already_setup(detect_mock, hass): manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() @@ -361,7 +448,7 @@ async def test_discovery_via_usb_path_changes(hass): manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() @@ -374,8 +461,8 @@ async def test_discovery_via_usb_path_changes(hass): } -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery_via_usb_deconz_already_discovered(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_discovery_via_usb_deconz_already_discovered(hass): """Test usb flow -- deconz discovered.""" result = await hass.config_entries.flow.async_init( "deconz", @@ -400,7 +487,7 @@ async def test_discovery_via_usb_deconz_already_discovered(detect_mock, hass): manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() @@ -408,8 +495,8 @@ async def test_discovery_via_usb_deconz_already_discovered(detect_mock, hass): assert result["reason"] == "not_zha_device" -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery_via_usb_deconz_already_setup(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_discovery_via_usb_deconz_already_setup(hass): """Test usb flow -- deconz setup.""" MockConfigEntry(domain="deconz", data={}).add_to_hass(hass) await hass.async_block_till_done() @@ -422,7 +509,7 @@ async def test_discovery_via_usb_deconz_already_setup(detect_mock, hass): manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() @@ -430,8 +517,8 @@ async def test_discovery_via_usb_deconz_already_setup(detect_mock, hass): assert result["reason"] == "not_zha_device" -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_discovery_via_usb_deconz_ignored(hass): """Test usb flow -- deconz ignored.""" MockConfigEntry( domain="deconz", source=config_entries.SOURCE_IGNORE, data={} @@ -446,7 +533,7 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() @@ -454,8 +541,8 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): assert result["step_id"] == "confirm" -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery_via_usb_zha_ignored_updates(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_discovery_via_usb_zha_ignored_updates(hass): """Test usb flow that was ignored gets updated.""" entry = MockConfigEntry( domain=DOMAIN, @@ -474,7 +561,7 @@ async def test_discovery_via_usb_zha_ignored_updates(detect_mock, hass): manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() @@ -486,8 +573,8 @@ async def test_discovery_via_usb_zha_ignored_updates(detect_mock, hass): @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) -async def test_discovery_already_setup(detect_mock, hass): +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_discovery_already_setup(hass): """Test zeroconf flow -- radio detected.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.200", @@ -504,7 +591,7 @@ async def test_discovery_already_setup(detect_mock, hass): ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "zha", context={"source": SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) await hass.async_block_till_done() @@ -512,12 +599,12 @@ async def test_discovery_already_setup(detect_mock, hass): assert result["reason"] == "single_instance_allowed" -@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @patch( - "homeassistant.components.zha.config_flow.detect_radios", - return_value={CONF_RADIO_TYPE: "test_radio"}, + "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._detect_radio_type", + mock_detect_radio_type(radio_type=RadioType.deconz), ) -async def test_user_flow(detect_mock, hass): +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) +async def test_user_flow(hass): """Test user flow -- radio detected.""" port = com_port() @@ -526,21 +613,36 @@ async def test_user_flow(detect_mock, hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, - data={zigpy.config.CONF_DEVICE_PATH: port_select}, + data={ + zigpy.config.CONF_DEVICE_PATH: port_select, + }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"].startswith(port.description) - assert result["data"] == {CONF_RADIO_TYPE: "test_radio"} - assert detect_mock.await_count == 1 - assert detect_mock.await_args[0][0] == port.device + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "choose_formation_strategy" + + with patch("homeassistant.components.zha.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"].startswith(port.description) + assert result2["data"] == { + "device": { + "path": port.device, + }, + CONF_RADIO_TYPE: "deconz", + } -@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @patch( - "homeassistant.components.zha.config_flow.detect_radios", - return_value=None, + "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._detect_radio_type", + mock_detect_radio_type(ret=False), ) -async def test_user_flow_not_detected(detect_mock, hass): +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) +async def test_user_flow_not_detected(hass): """Test user flow, radio not detected.""" port = com_port() @@ -553,9 +655,7 @@ async def test_user_flow_not_detected(detect_mock, hass): ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "pick_radio" - assert detect_mock.await_count == 1 - assert detect_mock.await_args[0][0] == port.device + assert result["step_id"] == "manual_pick_radio_type" @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @@ -567,7 +667,7 @@ async def test_user_flow_show_form(hass): ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "choose_serial_port" @patch("serial.tools.list_ports.comports", MagicMock(return_value=[])) @@ -579,7 +679,7 @@ async def test_user_flow_show_manual(hass): ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "pick_radio" + assert result["step_id"] == "manual_pick_radio_type" async def test_user_flow_manual(hass): @@ -591,7 +691,7 @@ async def test_user_flow_manual(hass): data={zigpy.config.CONF_DEVICE_PATH: config_flow.CONF_MANUAL_PATH}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "pick_radio" + assert result["step_id"] == "manual_pick_radio_type" @pytest.mark.parametrize("radio_type", RadioType.list()) @@ -599,10 +699,12 @@ async def test_pick_radio_flow(hass, radio_type): """Test radio picker.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: "pick_radio"}, data={CONF_RADIO_TYPE: radio_type} + DOMAIN, + context={CONF_SOURCE: "manual_pick_radio_type"}, + data={CONF_RADIO_TYPE: radio_type}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "port_config" + assert result["step_id"] == "manual_port_config" async def test_user_flow_existing_config_entry(hass): @@ -618,82 +720,62 @@ async def test_user_flow_existing_config_entry(hass): assert result["type"] == "abort" -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=False) +@patch(f"bellows.{PROBE_FUNCTION_PATH}", return_value=False) +@patch(f"zigpy_deconz.{PROBE_FUNCTION_PATH}", return_value=False) +@patch(f"zigpy_zigate.{PROBE_FUNCTION_PATH}", return_value=False) +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", return_value=True) +async def test_detect_radio_type_success( + znp_probe, zigate_probe, deconz_probe, bellows_probe, hass +): + """Test detect radios successfully.""" + + handler = config_flow.ZhaConfigFlowHandler() + handler._device_path = "/dev/null" + + await handler._detect_radio_type() + + assert handler._radio_type == RadioType.znp + assert handler._device_settings[zigpy.config.CONF_DEVICE_PATH] == "/dev/null" + + assert bellows_probe.await_count == 1 + assert znp_probe.await_count == 1 + assert deconz_probe.await_count == 0 + assert zigate_probe.await_count == 0 + + @patch( - "zigpy_deconz.zigbee.application.ControllerApplication.probe", return_value=False + f"bellows.{PROBE_FUNCTION_PATH}", + return_value={"new_setting": 123, zigpy.config.CONF_DEVICE_PATH: "/dev/null"}, ) -@patch( - "zigpy_zigate.zigbee.application.ControllerApplication.probe", return_value=False -) -@patch("zigpy_xbee.zigbee.application.ControllerApplication.probe", return_value=False) -async def test_probe_radios(xbee_probe, zigate_probe, deconz_probe, znp_probe, hass): - """Test detect radios.""" - app_ctrl_cls = MagicMock() - app_ctrl_cls.SCHEMA_DEVICE = zigpy.config.SCHEMA_DEVICE - app_ctrl_cls.probe = AsyncMock(side_effect=(True, False)) +@patch(f"zigpy_deconz.{PROBE_FUNCTION_PATH}", return_value=False) +@patch(f"zigpy_zigate.{PROBE_FUNCTION_PATH}", return_value=False) +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", return_value=False) +async def test_detect_radio_type_success_with_settings( + znp_probe, zigate_probe, deconz_probe, bellows_probe, hass +): + """Test detect radios successfully but probing returns new settings.""" - p1 = patch( - "bellows.zigbee.application.ControllerApplication.probe", - side_effect=(True, False), - ) - with p1 as probe_mock: - res = await config_flow.detect_radios("/dev/null") - assert probe_mock.await_count == 1 - assert znp_probe.await_count == 1 # ZNP appears earlier in the radio list - assert res[CONF_RADIO_TYPE] == "ezsp" - assert zigpy.config.CONF_DEVICE in res - assert ( - res[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH] == "/dev/null" - ) + handler = config_flow.ZhaConfigFlowHandler() + handler._device_path = "/dev/null" + await handler._detect_radio_type() - res = await config_flow.detect_radios("/dev/null") - assert res is None - assert xbee_probe.await_count == 1 - assert zigate_probe.await_count == 1 - assert deconz_probe.await_count == 1 - assert znp_probe.await_count == 2 + assert handler._radio_type == RadioType.ezsp + assert handler._device_settings["new_setting"] == 123 + assert handler._device_settings[zigpy.config.CONF_DEVICE_PATH] == "/dev/null" + + assert bellows_probe.await_count == 1 + assert znp_probe.await_count == 0 + assert deconz_probe.await_count == 0 + assert zigate_probe.await_count == 0 -@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=False) -@patch( - "zigpy_deconz.zigbee.application.ControllerApplication.probe", return_value=False -) -@patch( - "zigpy_zigate.zigbee.application.ControllerApplication.probe", return_value=False -) -@patch("zigpy_xbee.zigbee.application.ControllerApplication.probe", return_value=False) -async def test_probe_new_ezsp(xbee_probe, zigate_probe, deconz_probe, znp_probe, hass): - """Test detect radios.""" - app_ctrl_cls = MagicMock() - app_ctrl_cls.SCHEMA_DEVICE = zigpy.config.SCHEMA_DEVICE - app_ctrl_cls.probe = AsyncMock(side_efferct=(True, False)) - - p1 = patch( - "bellows.zigbee.application.ControllerApplication.probe", - return_value={ - zigpy.config.CONF_DEVICE_PATH: sentinel.usb_port, - "baudrate": 33840, - }, - ) - with p1 as probe_mock: - res = await config_flow.detect_radios("/dev/null") - assert probe_mock.await_count == 1 - assert res[CONF_RADIO_TYPE] == "ezsp" - assert zigpy.config.CONF_DEVICE in res - assert ( - res[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH] - is sentinel.usb_port - ) - assert res[zigpy.config.CONF_DEVICE]["baudrate"] == 33840 - - -@patch("bellows.zigbee.application.ControllerApplication.probe", return_value=False) +@patch(f"bellows.{PROBE_FUNCTION_PATH}", return_value=False) async def test_user_port_config_fail(probe_mock, hass): """Test port config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, - context={CONF_SOURCE: "pick_radio"}, + context={CONF_SOURCE: "manual_pick_radio_type"}, data={CONF_RADIO_TYPE: RadioType.ezsp.description}, ) @@ -702,19 +784,19 @@ async def test_user_port_config_fail(probe_mock, hass): user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "port_config" + assert result["step_id"] == "manual_port_config" assert result["errors"]["base"] == "cannot_connect" assert probe_mock.await_count == 1 @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -@patch("bellows.zigbee.application.ControllerApplication.probe", return_value=True) +@patch(f"bellows.{PROBE_FUNCTION_PATH}", return_value=True) async def test_user_port_config(probe_mock, hass): """Test port config.""" result = await hass.config_entries.flow.async_init( DOMAIN, - context={CONF_SOURCE: "pick_radio"}, + context={CONF_SOURCE: "manual_pick_radio_type"}, data={CONF_RADIO_TYPE: RadioType.ezsp.description}, ) @@ -723,13 +805,20 @@ async def test_user_port_config(probe_mock, hass): user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) - assert result["type"] == "create_entry" - assert result["title"].startswith("/dev/ttyUSB33") + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "choose_formation_strategy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + assert ( - result["data"][zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH] + result2["data"][zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH] == "/dev/ttyUSB33" ) - assert result["data"][CONF_RADIO_TYPE] == "ezsp" + assert result2["data"][CONF_RADIO_TYPE] == "ezsp" assert probe_mock.await_count == 1 @@ -784,7 +873,7 @@ async def test_hardware_not_onboarded(hass): "homeassistant.components.onboarding.async_is_onboarded", return_value=False ): result = await hass.config_entries.flow.async_init( - "zha", context={"source": "hardware"}, data=data + DOMAIN, context={"source": "hardware"}, data=data ) assert result["type"] == FlowResultType.CREATE_ENTRY @@ -814,7 +903,7 @@ async def test_hardware_onboarded(hass): "homeassistant.components.onboarding.async_is_onboarded", return_value=True ): result = await hass.config_entries.flow.async_init( - "zha", context={"source": "hardware"}, data=data + DOMAIN, context={"source": "hardware"}, data=data ) assert result["type"] == FlowResultType.FORM @@ -852,7 +941,7 @@ async def test_hardware_already_setup(hass): }, } result = await hass.config_entries.flow.async_init( - "zha", context={"source": "hardware"}, data=data + DOMAIN, context={"source": "hardware"}, data=data ) assert result["type"] == FlowResultType.ABORT @@ -866,8 +955,664 @@ async def test_hardware_invalid_data(hass, data): """Test onboarding flow -- invalid data.""" result = await hass.config_entries.flow.async_init( - "zha", context={"source": "hardware"}, data=data + DOMAIN, context={"source": "hardware"}, data=data ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_hardware_data" + + +def test_allow_overwrite_ezsp_ieee(): + """Test modifying the backup to allow bellows to override the IEEE address.""" + backup = zigpy.backups.NetworkBackup() + new_backup = config_flow._allow_overwrite_ezsp_ieee(backup) + + assert backup != new_backup + assert ( + new_backup.network_info.stack_specific["ezsp"][ + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" + ] + is True + ) + + +def test_prevent_overwrite_ezsp_ieee(): + """Test modifying the backup to prevent bellows from overriding the IEEE address.""" + backup = zigpy.backups.NetworkBackup() + backup.network_info.stack_specific["ezsp"] = { + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it": True + } + new_backup = config_flow._prevent_overwrite_ezsp_ieee(backup) + + assert backup != new_backup + assert not new_backup.network_info.stack_specific.get("ezsp", {}).get( + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" + ) + + +@pytest.fixture +def pick_radio(hass): + """Fixture for the first step of the config flow (where a radio is picked).""" + + async def wrapper(radio_type): + port = com_port() + port_select = f"{port}, s/n: {port.serial_number} - {port.manufacturer}" + + with patch( + "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._detect_radio_type", + mock_detect_radio_type(radio_type=radio_type), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={ + zigpy.config.CONF_DEVICE_PATH: port_select, + }, + ) + + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "choose_formation_strategy" + + return result, port + + p1 = patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) + p2 = patch("homeassistant.components.zha.async_setup_entry") + + with p1, p2: + yield wrapper + + +async def test_strategy_no_network_settings(pick_radio, mock_app, hass): + """Test formation strategy when no network settings are present.""" + mock_app.load_network_info = MagicMock(side_effect=NetworkNotFormed()) + + result, port = await pick_radio(RadioType.ezsp) + assert ( + config_flow.FORMATION_REUSE_SETTINGS + not in result["data_schema"].schema["next_step_id"].container + ) + + +async def test_formation_strategy_form_new_network(pick_radio, mock_app, hass): + """Test forming a new network.""" + result, port = await pick_radio(RadioType.ezsp) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_FORM_NEW_NETWORK}, + ) + await hass.async_block_till_done() + + # A new network will be formed + mock_app.form_network.assert_called_once() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + + +async def test_formation_strategy_reuse_settings(pick_radio, mock_app, hass): + """Test reusing existing network settings.""" + result, port = await pick_radio(RadioType.ezsp) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + # Nothing will be written when settings are reused + mock_app.write_network_info.assert_not_called() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + + +@patch("homeassistant.components.zha.config_flow.process_uploaded_file") +def test_parse_uploaded_backup(process_mock): + """Test parsing uploaded backup files.""" + backup = zigpy.backups.NetworkBackup() + + text = json.dumps(backup.as_dict()) + process_mock.return_value.__enter__.return_value.read_text.return_value = text + + handler = config_flow.ZhaConfigFlowHandler() + parsed_backup = handler._parse_uploaded_backup(str(uuid.uuid4())) + + assert backup == parsed_backup + + +@patch("homeassistant.components.zha.config_flow._allow_overwrite_ezsp_ieee") +async def test_formation_strategy_restore_manual_backup_non_ezsp( + allow_overwrite_ieee_mock, pick_radio, mock_app, hass +): + """Test restoring a manual backup on non-EZSP coordinators.""" + result, port = await pick_radio(RadioType.znp) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_UPLOAD_MANUAL_BACKUP}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "upload_manual_backup" + + with patch( + "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._parse_uploaded_backup", + return_value=zigpy.backups.NetworkBackup(), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={config_flow.UPLOADED_BACKUP_FILE: str(uuid.uuid4())}, + ) + + mock_app.backups.restore_backup.assert_called_once() + allow_overwrite_ieee_mock.assert_not_called() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["data"][CONF_RADIO_TYPE] == "znp" + + +@patch("homeassistant.components.zha.config_flow._allow_overwrite_ezsp_ieee") +async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( + allow_overwrite_ieee_mock, pick_radio, mock_app, backup, hass +): + """Test restoring a manual backup on EZSP coordinators (overwrite IEEE).""" + result, port = await pick_radio(RadioType.ezsp) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_UPLOAD_MANUAL_BACKUP}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "upload_manual_backup" + + with patch( + "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._parse_uploaded_backup", + return_value=backup, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={config_flow.UPLOADED_BACKUP_FILE: str(uuid.uuid4())}, + ) + + assert result3["type"] == FlowResultType.FORM + assert result3["step_id"] == "maybe_confirm_ezsp_restore" + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + user_input={config_flow.OVERWRITE_COORDINATOR_IEEE: True}, + ) + + allow_overwrite_ieee_mock.assert_called_once() + mock_app.backups.restore_backup.assert_called_once() + + assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["data"][CONF_RADIO_TYPE] == "ezsp" + + +@patch("homeassistant.components.zha.config_flow._allow_overwrite_ezsp_ieee") +async def test_formation_strategy_restore_manual_backup_ezsp( + allow_overwrite_ieee_mock, pick_radio, mock_app, hass +): + """Test restoring a manual backup on EZSP coordinators (don't overwrite IEEE).""" + result, port = await pick_radio(RadioType.ezsp) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_UPLOAD_MANUAL_BACKUP}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "upload_manual_backup" + + backup = zigpy.backups.NetworkBackup() + + with patch( + "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._parse_uploaded_backup", + return_value=backup, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={config_flow.UPLOADED_BACKUP_FILE: str(uuid.uuid4())}, + ) + + assert result3["type"] == FlowResultType.FORM + assert result3["step_id"] == "maybe_confirm_ezsp_restore" + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + user_input={config_flow.OVERWRITE_COORDINATOR_IEEE: False}, + ) + + allow_overwrite_ieee_mock.assert_not_called() + mock_app.backups.restore_backup.assert_called_once_with(backup) + + assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["data"][CONF_RADIO_TYPE] == "ezsp" + + +async def test_formation_strategy_restore_manual_backup_invalid_upload( + pick_radio, mock_app, hass +): + """Test restoring a manual backup but an invalid file is uploaded.""" + result, port = await pick_radio(RadioType.ezsp) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_UPLOAD_MANUAL_BACKUP}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "upload_manual_backup" + + with patch( + "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._parse_uploaded_backup", + side_effect=ValueError("Invalid backup JSON"), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={config_flow.UPLOADED_BACKUP_FILE: str(uuid.uuid4())}, + ) + + mock_app.backups.restore_backup.assert_not_called() + + assert result3["type"] == FlowResultType.FORM + assert result3["step_id"] == "upload_manual_backup" + assert result3["errors"]["base"] == "invalid_backup_json" + + +def test_format_backup_choice(): + """Test formatting zigpy NetworkBackup objects.""" + backup = zigpy.backups.NetworkBackup() + backup.network_info.pan_id = zigpy.types.PanId(0x1234) + backup.network_info.extended_pan_id = zigpy.types.EUI64.convert( + "aa:bb:cc:dd:ee:ff:00:11" + ) + + with_ids = config_flow._format_backup_choice(backup, pan_ids=True) + without_ids = config_flow._format_backup_choice(backup, pan_ids=False) + + assert with_ids.startswith(without_ids) + assert "1234:aabbccddeeff0011" in with_ids + assert "1234:aabbccddeeff0011" not in without_ids + + +@patch( + "homeassistant.components.zha.config_flow._format_backup_choice", + lambda s, **kwargs: "choice:" + repr(s), +) +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_formation_strategy_restore_automatic_backup_ezsp( + pick_radio, mock_app, hass +): + """Test restoring an automatic backup (EZSP radio).""" + mock_app.backups.backups = [ + MagicMock(), + MagicMock(), + MagicMock(), + ] + backup = mock_app.backups.backups[1] # pick the second one + backup.is_compatible_with = MagicMock(return_value=False) + + result, port = await pick_radio(RadioType.ezsp) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": (config_flow.FORMATION_CHOOSE_AUTOMATIC_BACKUP)}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "choose_automatic_backup" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + config_flow.CHOOSE_AUTOMATIC_BACKUP: "choice:" + repr(backup), + }, + ) + + assert result3["type"] == FlowResultType.FORM + assert result3["step_id"] == "maybe_confirm_ezsp_restore" + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + user_input={config_flow.OVERWRITE_COORDINATOR_IEEE: True}, + ) + + mock_app.backups.restore_backup.assert_called_once() + + assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["data"][CONF_RADIO_TYPE] == "ezsp" + + +@patch( + "homeassistant.components.zha.config_flow._format_backup_choice", + lambda s, **kwargs: "choice:" + repr(s), +) +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +@pytest.mark.parametrize("is_advanced", [True, False]) +async def test_formation_strategy_restore_automatic_backup_non_ezsp( + is_advanced, pick_radio, mock_app, hass +): + """Test restoring an automatic backup (non-EZSP radio).""" + mock_app.backups.backups = [ + MagicMock(), + MagicMock(), + MagicMock(), + ] + backup = mock_app.backups.backups[1] # pick the second one + backup.is_compatible_with = MagicMock(return_value=False) + + result, port = await pick_radio(RadioType.znp) + + with patch( + "homeassistant.config_entries.ConfigFlow.show_advanced_options", + new_callable=PropertyMock(return_value=is_advanced), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + "next_step_id": (config_flow.FORMATION_CHOOSE_AUTOMATIC_BACKUP) + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "choose_automatic_backup" + + # We must prompt for overwriting the IEEE address + assert config_flow.OVERWRITE_COORDINATOR_IEEE not in result2["data_schema"].schema + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + config_flow.CHOOSE_AUTOMATIC_BACKUP: "choice:" + repr(backup), + }, + ) + + mock_app.backups.restore_backup.assert_called_once_with(backup) + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["data"][CONF_RADIO_TYPE] == "znp" + + +@patch("homeassistant.components.zha.config_flow._allow_overwrite_ezsp_ieee") +async def test_ezsp_restore_without_settings_change_ieee( + allow_overwrite_ieee_mock, pick_radio, mock_app, backup, hass +): + """Test a manual backup on EZSP coordinators without settings (no IEEE write).""" + # Fail to load settings + with patch.object( + mock_app, "load_network_info", MagicMock(side_effect=NetworkNotFormed()) + ): + result, port = await pick_radio(RadioType.ezsp) + + # Set the network state, it'll be picked up later after the load "succeeds" + mock_app.state.node_info = backup.node_info + mock_app.state.network_info = copy.deepcopy(backup.network_info) + mock_app.state.network_info.network_key.tx_counter += 10000 + + # Include the overwrite option, just in case someone uploads a backup with it + backup.network_info.metadata["ezsp"] = { + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it": True + } + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_UPLOAD_MANUAL_BACKUP}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "upload_manual_backup" + + with patch( + "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._parse_uploaded_backup", + return_value=backup, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={config_flow.UPLOADED_BACKUP_FILE: str(uuid.uuid4())}, + ) + + # We wrote settings when connecting + allow_overwrite_ieee_mock.assert_not_called() + mock_app.backups.restore_backup.assert_called_once_with(backup, create_new=False) + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["data"][CONF_RADIO_TYPE] == "ezsp" + + +@pytest.mark.parametrize( + "async_unload_effect", [True, config_entries.OperationNotAllowed()] +) +@patch( + "serial.tools.list_ports.comports", + MagicMock( + return_value=[ + com_port("/dev/SomePort"), + com_port("/dev/ttyUSB0"), + com_port("/dev/SomeOtherPort"), + ] + ), +) +@patch("homeassistant.components.zha.async_setup_entry", return_value=True) +async def test_options_flow_defaults(async_setup_entry, async_unload_effect, hass): + """Test options flow defaults match radio defaults.""" + + entry = MockConfigEntry( + version=config_flow.ZhaConfigFlowHandler.VERSION, + domain=DOMAIN, + data={ + CONF_DEVICE: { + CONF_DEVICE_PATH: "/dev/ttyUSB0", + CONF_BAUDRATE: 12345, + CONF_FLOWCONTROL: None, + }, + CONF_RADIO_TYPE: "znp", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + flow = await hass.config_entries.options.async_init(entry.entry_id) + + async_setup_entry.reset_mock() + + # ZHA gets unloaded + with patch( + "homeassistant.config_entries.ConfigEntries.async_unload", + side_effect=[async_unload_effect], + ) as mock_async_unload: + result1 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + mock_async_unload.assert_called_once_with(entry.entry_id) + + # Unload it ourselves + entry.state = config_entries.ConfigEntryState.NOT_LOADED + + # Current path is the default + assert result1["step_id"] == "choose_serial_port" + assert "/dev/ttyUSB0" in result1["data_schema"]({})[CONF_DEVICE_PATH] + + # Autoprobing fails, we have to manually choose the radio type + result2 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + # Current radio type is the default + assert result2["step_id"] == "manual_pick_radio_type" + assert result2["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description + + # Continue on to port settings + result3 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={ + CONF_RADIO_TYPE: RadioType.znp.description, + }, + ) + + # The defaults match our current settings + assert result3["step_id"] == "manual_port_config" + assert result3["data_schema"]({}) == entry.data[CONF_DEVICE] + + with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)): + # Change the serial port path + result4 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={ + # Change everything + CONF_DEVICE_PATH: "/dev/new_serial_port", + CONF_BAUDRATE: 54321, + CONF_FLOWCONTROL: "software", + }, + ) + + # The radio has been detected, we can move on to creating the config entry + assert result4["step_id"] == "choose_formation_strategy" + + async_setup_entry.assert_not_called() + + result5 = await hass.config_entries.options.async_configure( + result1["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["data"] == {} + + # The updated entry contains correct settings + assert entry.data == { + CONF_DEVICE: { + CONF_DEVICE_PATH: "/dev/new_serial_port", + CONF_BAUDRATE: 54321, + CONF_FLOWCONTROL: "software", + }, + CONF_RADIO_TYPE: "znp", + } + + # ZHA was started again + assert async_setup_entry.call_count == 1 + + +@patch( + "serial.tools.list_ports.comports", + MagicMock( + return_value=[ + com_port("/dev/SomePort"), + com_port("/dev/SomeOtherPort"), + ] + ), +) +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_options_flow_defaults_socket(hass): + """Test options flow defaults work even for serial ports that can't be listed.""" + + entry = MockConfigEntry( + version=config_flow.ZhaConfigFlowHandler.VERSION, + domain=DOMAIN, + data={ + CONF_DEVICE: { + CONF_DEVICE_PATH: "socket://localhost:5678", + CONF_BAUDRATE: 12345, + CONF_FLOWCONTROL: None, + }, + CONF_RADIO_TYPE: "znp", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + flow = await hass.config_entries.options.async_init(entry.entry_id) + + # ZHA gets unloaded + with patch( + "homeassistant.config_entries.ConfigEntries.async_unload", return_value=True + ): + result1 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + # Radio path must be manually entered + assert result1["step_id"] == "choose_serial_port" + assert result1["data_schema"]({})[CONF_DEVICE_PATH] == config_flow.CONF_MANUAL_PATH + + result2 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + # Current radio type is the default + assert result2["step_id"] == "manual_pick_radio_type" + assert result2["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description + + # Continue on to port settings + result3 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + # The defaults match our current settings + assert result3["step_id"] == "manual_port_config" + assert result3["data_schema"]({}) == entry.data[CONF_DEVICE] + + with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)): + result4 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + assert result4["step_id"] == "choose_formation_strategy" + + +@patch("homeassistant.components.zha.async_setup_entry", return_value=True) +async def test_options_flow_restarts_running_zha_if_cancelled(async_setup_entry, hass): + """Test options flow restarts a previously-running ZHA if it's cancelled.""" + + entry = MockConfigEntry( + version=config_flow.ZhaConfigFlowHandler.VERSION, + domain=DOMAIN, + data={ + CONF_DEVICE: { + CONF_DEVICE_PATH: "socket://localhost:5678", + CONF_BAUDRATE: 12345, + CONF_FLOWCONTROL: None, + }, + CONF_RADIO_TYPE: "znp", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + flow = await hass.config_entries.options.async_init(entry.entry_id) + + # ZHA gets unloaded + with patch( + "homeassistant.config_entries.ConfigEntries.async_unload", return_value=True + ): + result1 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + entry.state = config_entries.ConfigEntryState.NOT_LOADED + + # Radio path must be manually entered + assert result1["step_id"] == "choose_serial_port" + + async_setup_entry.reset_mock() + + # Abort the flow + hass.config_entries.options.async_abort(result1["flow_id"]) + await hass.async_block_till_done() + + # ZHA was set up once more + async_setup_entry.assert_called_once_with(hass, entry) From df214c2d26cf8d2918d9d4b6b15f356b458dc864 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 30 Aug 2022 12:49:27 -0400 Subject: [PATCH 757/903] Add support for zwave_js firmware update service (#77401) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/zwave_js/__init__.py | 15 +- homeassistant/components/zwave_js/const.py | 4 + homeassistant/components/zwave_js/update.py | 190 ++++++++++++ tests/components/zwave_js/conftest.py | 2 + tests/components/zwave_js/test_fan.py | 4 + tests/components/zwave_js/test_init.py | 12 +- tests/components/zwave_js/test_update.py | 275 ++++++++++++++++++ 7 files changed, 491 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/zwave_js/update.py create mode 100644 tests/components/zwave_js/test_update.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 482f635da65..538fe911dd0 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -19,8 +19,6 @@ from zwave_js_server.model.notification import ( ) from zwave_js_server.model.value import Value, ValueNotification -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, @@ -28,6 +26,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_URL, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -244,7 +243,7 @@ async def setup_driver( # noqa: C901 registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) discovered_value_ids: dict[str, set[str]] = defaultdict(set) - async def async_setup_platform(platform: str) -> None: + async def async_setup_platform(platform: Platform) -> None: """Set up platform if needed.""" if platform not in platform_setup_tasks: platform_setup_tasks[platform] = hass.async_create_task( @@ -353,17 +352,23 @@ async def setup_driver( # noqa: C901 # No need for a ping button or node status sensor for controller nodes if not node.is_controller_node: # Create a node status sensor for each device - await async_setup_platform(SENSOR_DOMAIN) + await async_setup_platform(Platform.SENSOR) async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node ) # Create a ping button for each device - await async_setup_platform(BUTTON_DOMAIN) + await async_setup_platform(Platform.BUTTON) async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node ) + # Create a firmware update entity for each device + await async_setup_platform(Platform.UPDATE) + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_firmware_update_entity", node + ) + # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 3e0bdb9c3f6..cd10109bb3d 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -120,3 +120,7 @@ ENTITY_DESC_KEY_TEMPERATURE = "temperature" ENTITY_DESC_KEY_TARGET_TEMPERATURE = "target_temperature" ENTITY_DESC_KEY_MEASUREMENT = "measurement" ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" + +# This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for +# your own (https://github.com/zwave-js/firmware-updates/). +API_KEY_FIRMWARE_UPDATE_SERVICE = "b48e74337db217f44e1e003abb1e9144007d260a17e2b2422e0a45d0eaf6f4ad86f2a9943f17fee6dde343941f238a64" diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py new file mode 100644 index 00000000000..d179700c724 --- /dev/null +++ b/homeassistant/components/zwave_js/update.py @@ -0,0 +1,190 @@ +"""Representation of Z-Wave updates.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +from typing import Any + +from awesomeversion import AwesomeVersion +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import NodeStatus +from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.model.driver import Driver +from zwave_js_server.model.firmware import FirmwareUpdateInfo +from zwave_js_server.model.node import Node as ZwaveNode + +from homeassistant.components.update import UpdateDeviceClass, UpdateEntity +from homeassistant.components.update.const import UpdateEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER +from .helpers import get_device_id, get_valueless_base_unique_id + +PARALLEL_UPDATES = 1 +SCAN_INTERVAL = timedelta(days=1) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Z-Wave button from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_firmware_update_entity(node: ZwaveNode) -> None: + """Add firmware update entity.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. + async_add_entities([ZWaveNodeFirmwareUpdate(driver, node)], True) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_firmware_update_entity", + async_add_firmware_update_entity, + ) + ) + + +class ZWaveNodeFirmwareUpdate(UpdateEntity): + """Representation of a firmware update entity.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = ( + UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES + ) + _attr_has_entity_name = True + + def __init__(self, driver: Driver, node: ZwaveNode) -> None: + """Initialize a Z-Wave device firmware update entity.""" + self.driver = driver + self.node = node + self.available_firmware_updates: list[FirmwareUpdateInfo] = [] + self._latest_version_firmware: FirmwareUpdateInfo | None = None + self._status_unsub: Callable[[], None] | None = None + + # Entity class attributes + self._attr_name = "Firmware" + self._base_unique_id = get_valueless_base_unique_id(driver, node) + self._attr_unique_id = f"{self._base_unique_id}.firmware_update" + # device may not be precreated in main handler yet + self._attr_device_info = DeviceInfo( + identifiers={get_device_id(driver, 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, + suggested_area=node.location if node.location else None, + ) + + self._attr_installed_version = self._attr_latest_version = node.firmware_version + + def _update_on_wake_up(self, _: dict[str, Any]) -> None: + """Update the entity when node is awake.""" + self._status_unsub = None + self.hass.async_create_task(self.async_update(True)) + + async def async_update(self, write_state: bool = False) -> None: + """Update the entity.""" + if self.node.status == NodeStatus.ASLEEP: + if not self._status_unsub: + self._status_unsub = self.node.once("wake up", self._update_on_wake_up) + return + self.available_firmware_updates = ( + await self.driver.controller.async_get_available_firmware_updates( + self.node, API_KEY_FIRMWARE_UPDATE_SERVICE + ) + ) + self._async_process_available_updates(write_state) + + @callback + def _async_process_available_updates(self, write_state: bool = True) -> None: + """ + Process available firmware updates. + + Sets latest version attribute and FirmwareUpdateInfo instance. + """ + # If we have an available firmware update that is a higher version than what's + # on the node, we should advertise it, otherwise we are on the latest version + if self.available_firmware_updates and AwesomeVersion( + ( + firmware := max( + self.available_firmware_updates, + key=lambda x: AwesomeVersion(x.version), + ) + ).version + ) > AwesomeVersion(self.node.firmware_version): + self._latest_version_firmware = firmware + self._attr_latest_version = firmware.version + else: + self._latest_version_firmware = None + self._attr_latest_version = self._attr_installed_version + if write_state: + self.async_write_ha_state() + + async def async_release_notes(self) -> str | None: + """Get release notes.""" + if self._latest_version_firmware is None: + return None + return self._latest_version_firmware.changelog + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + firmware = self._latest_version_firmware + assert firmware + self._attr_in_progress = True + self.async_write_ha_state() + try: + for file in firmware.files: + await self.driver.controller.async_begin_ota_firmware_update( + self.node, file + ) + except BaseZwaveJSServerError as err: + raise HomeAssistantError(err) from err + else: + self._attr_installed_version = firmware.version + self.available_firmware_updates.remove(firmware) + self._async_process_available_updates() + finally: + self._attr_in_progress = False + + async def async_poll_value(self, _: bool) -> None: + """Poll a value.""" + LOGGER.error( + "There is no value to refresh for this entity so the zwave_js.refresh_value " + "service won't work for it" + ) + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self.unique_id}_poll_value", + self.async_poll_value, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self._base_unique_id}_remove_entity", + self.async_remove, + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed.""" + if self._status_unsub: + self._status_unsub() + self._status_unsub = None diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 1524aca719e..7131b1ade69 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -844,6 +844,8 @@ async def integration_fixture(hass, client): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + client.async_send_command.reset_mock() + return entry diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 140d9fb3d83..27e300286d0 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -513,6 +513,8 @@ async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() + client.async_send_command.reset_mock() + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -774,6 +776,8 @@ async def test_thermostat_fan_without_off( await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() + client.async_send_command.reset_mock() + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNKNOWN diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 202088bb481..57f552c9502 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -211,8 +211,8 @@ async def test_on_node_added_not_ready( client.driver.receive_event(event) await hass.async_block_till_done() - # the only entities are the node status sensor and ping button - assert len(hass.states.async_all()) == 2 + # the only entities are the node status sensor, ping button, and firmware update + assert len(hass.states.async_all()) == 3 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device @@ -254,8 +254,8 @@ async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integrati assert not device.model assert not device.sw_version - # the only entities are the node status sensor and ping button - assert len(hass.states.async_all()) == 2 + # the only entities are the node status sensor, ping button, and firmware update + assert len(hass.states.async_all()) == 3 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device @@ -817,7 +817,7 @@ async def test_removed_device( # Check how many entities there are ent_reg = er.async_get(hass) entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 29 + assert len(entity_entries) == 31 # Remove a node and reload the entry old_node = driver.controller.nodes.pop(13) @@ -829,7 +829,7 @@ async def test_removed_device( device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) assert len(device_entries) == 1 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 17 + assert len(entity_entries) == 18 assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py new file mode 100644 index 00000000000..852dcba5954 --- /dev/null +++ b/tests/components/zwave_js/test_update.py @@ -0,0 +1,275 @@ +"""Test the Z-Wave JS update entities.""" +from datetime import timedelta + +import pytest +from zwave_js_server.event import Event +from zwave_js_server.exceptions import FailedZWaveCommand + +from homeassistant.components.update.const import ( + ATTR_AUTO_UPDATE, + ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + ATTR_RELEASE_URL, + DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, +) +from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE +from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import datetime as dt_util + +from tests.common import async_fire_time_changed + +UPDATE_ENTITY = "update.z_wave_thermostat_firmware" + + +async def test_update_entity_success( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + controller_node, + integration, + caplog, + hass_ws_client, +): + """Test update entity.""" + ws_client = await hass_ws_client(hass) + await hass.async_block_till_done() + + assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF + + client.async_send_command.return_value = {"updates": []} + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + + await ws_client.send_json( + { + "id": 1, + "type": "update/release_notes", + "entity_id": UPDATE_ENTITY, + } + ) + result = await ws_client.receive_json() + assert result["result"] is None + + client.async_send_command.return_value = { + "updates": [ + { + "version": "10.11.1", + "changelog": "blah 1", + "files": [ + {"target": 0, "url": "https://example1.com", "integrity": "sha1"} + ], + }, + { + "version": "11.2.4", + "changelog": "blah 2", + "files": [ + {"target": 0, "url": "https://example2.com", "integrity": "sha2"} + ], + }, + { + "version": "11.1.5", + "changelog": "blah 3", + "files": [ + {"target": 0, "url": "https://example3.com", "integrity": "sha3"} + ], + }, + ] + } + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=2)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_ON + attrs = state.attributes + assert not attrs[ATTR_AUTO_UPDATE] + assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == "11.2.4" + assert attrs[ATTR_RELEASE_URL] is None + + await ws_client.send_json( + { + "id": 2, + "type": "update/release_notes", + "entity_id": UPDATE_ENTITY, + } + ) + result = await ws_client.receive_json() + assert result["result"] == "blah 2" + + # Refresh value should not be supported by this entity + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_VALUE, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + blocking=True, + ) + + assert "There is no value to refresh for this entity" in caplog.text + + # Assert a node firmware update entity is not created for the controller + driver = client.driver + node = driver.controller.nodes[1] + assert node.is_controller_node + assert ( + async_get(hass).async_get_entity_id( + DOMAIN, + "sensor", + f"{get_valueless_base_unique_id(driver, node)}.firmware_update", + ) + is None + ) + + client.async_send_command.reset_mock() + + # Test successful install call without a version + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + blocking=True, + ) + + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.begin_ota_firmware_update" + assert ( + args["nodeId"] + == climate_radio_thermostat_ct100_plus_different_endpoints.node_id + ) + assert args["update"] == { + "target": 0, + "url": "https://example2.com", + "integrity": "sha2", + } + + client.async_send_command.reset_mock() + + +async def test_update_entity_failure( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + controller_node, + integration, + caplog, + hass_ws_client, +): + """Test update entity failed install.""" + client.async_send_command.return_value = { + "updates": [ + { + "version": "10.11.1", + "changelog": "blah 1", + "files": [ + {"target": 0, "url": "https://example1.com", "integrity": "sha1"} + ], + }, + { + "version": "11.2.4", + "changelog": "blah 2", + "files": [ + {"target": 0, "url": "https://example2.com", "integrity": "sha2"} + ], + }, + { + "version": "11.1.5", + "changelog": "blah 3", + "files": [ + {"target": 0, "url": "https://example3.com", "integrity": "sha3"} + ], + }, + ] + } + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + # Test failed installation by driver + client.async_send_command.side_effect = FailedZWaveCommand("test", 12, "test") + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + blocking=True, + ) + + +async def test_update_entity_sleep( + hass, + client, + multisensor_6, + integration, +): + """Test update occurs when device is asleep after it wakes up.""" + event = Event( + "sleep", + data={"source": "node", "event": "sleep", "nodeId": multisensor_6.node_id}, + ) + multisensor_6.receive_event(event) + client.async_send_command.reset_mock() + + client.async_send_command.return_value = { + "updates": [ + { + "version": "10.11.1", + "changelog": "blah 1", + "files": [ + {"target": 0, "url": "https://example1.com", "integrity": "sha1"} + ], + }, + { + "version": "11.2.4", + "changelog": "blah 2", + "files": [ + {"target": 0, "url": "https://example2.com", "integrity": "sha2"} + ], + }, + { + "version": "11.1.5", + "changelog": "blah 3", + "files": [ + {"target": 0, "url": "https://example3.com", "integrity": "sha3"} + ], + }, + ] + } + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + # Because node is asleep we shouldn't attempt to check for firmware updates + assert len(client.async_send_command.call_args_list) == 0 + + event = Event( + "wake up", + data={"source": "node", "event": "wake up", "nodeId": multisensor_6.node_id}, + ) + multisensor_6.receive_event(event) + await hass.async_block_till_done() + + # Now that the node is up we can check for updates + assert len(client.async_send_command.call_args_list) > 0 + + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == multisensor_6.node_id From 9cdb7bba4c1fdbd4f1ff26c01912ba9a8afb3b8f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 18:57:42 +0200 Subject: [PATCH 758/903] Fix glances config-flow flaky test (#77549) --- .coveragerc | 1 - homeassistant/components/glances/const.py | 5 +---- tests/components/glances/test_config_flow.py | 13 ++++++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9301edcec52..770cfb978d9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -441,7 +441,6 @@ omit = homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py homeassistant/components/glances/__init__.py - homeassistant/components/glances/const.py homeassistant/components/glances/sensor.py homeassistant/components/goalfeed/* homeassistant/components/goodwe/__init__.py diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 92fe8ba91f6..efcc30c057b 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -13,7 +13,4 @@ DEFAULT_SCAN_INTERVAL = 60 DATA_UPDATED = "glances_data_updated" SUPPORTED_VERSIONS = [2, 3] -if sys.maxsize > 2**32: - CPU_ICON = "mdi:cpu-64-bit" -else: - CPU_ICON = "mdi:cpu-32-bit" +CPU_ICON = f"mdi:cpu-{64 if sys.maxsize > 2**32 else 32}-bit" diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 8ee669ae84e..7b2dee6429e 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -7,6 +7,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import glances from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -36,7 +37,7 @@ def glances_setup_fixture(): yield -async def test_form(hass): +async def test_form(hass: HomeAssistant) -> None: """Test config entry configured successfully.""" result = await hass.config_entries.flow.async_init( @@ -56,7 +57,7 @@ async def test_form(hass): assert result["data"] == DEMO_USER_INPUT -async def test_form_cannot_connect(hass): +async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test to return error if we cannot connect.""" with patch( @@ -74,7 +75,7 @@ async def test_form_cannot_connect(hass): assert result["errors"] == {"base": "cannot_connect"} -async def test_form_wrong_version(hass): +async def test_form_wrong_version(hass: HomeAssistant) -> None: """Test to check if wrong version is entered.""" user_input = DEMO_USER_INPUT.copy() @@ -90,7 +91,7 @@ async def test_form_wrong_version(hass): assert result["errors"] == {"version": "wrong_version"} -async def test_form_already_configured(hass): +async def test_form_already_configured(hass: HomeAssistant) -> None: """Test host is already configured.""" entry = MockConfigEntry( domain=glances.DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} @@ -107,12 +108,14 @@ async def test_form_already_configured(hass): assert result["reason"] == "already_configured" -async def test_options(hass): +async def test_options(hass: HomeAssistant) -> None: """Test options for Glances.""" entry = MockConfigEntry( domain=glances.DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} ) entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) From 50663bbc5d9af119ccde86a2ebd06794387c8df1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 19:19:36 +0200 Subject: [PATCH 759/903] Use _attr_available in denonavr (#77486) --- .../components/denonavr/media_player.py | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index bc3b4264d24..c06d5a939a3 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -157,32 +157,32 @@ def async_log_errors( return await func(self, *args, **kwargs) except AvrTimoutError: available = False - if self._available is True: + if self.available: _LOGGER.warning( "Timeout connecting to Denon AVR receiver at host %s. " "Device is unavailable", self._receiver.host, ) - self._available = False + self._attr_available = False except AvrNetworkError: available = False - if self._available is True: + if self.available: _LOGGER.warning( "Network error connecting to Denon AVR receiver at host %s. " "Device is unavailable", self._receiver.host, ) - self._available = False + self._attr_available = False except AvrForbiddenError: available = False - if self._available is True: + if self.available: _LOGGER.warning( "Denon AVR receiver at host %s responded with HTTP 403 error. " "Device is unavailable. Please consider power cycling your " "receiver", self._receiver.host, ) - self._available = False + self._attr_available = False except AvrCommandError as err: available = False _LOGGER.error( @@ -199,12 +199,12 @@ def async_log_errors( exc_info=True, ) finally: - if available is True and self._available is False: + if available and not self.available: _LOGGER.info( "Denon AVR receiver at host %s is available again", self._receiver.host, ) - self._available = True + self._attr_available = True return None return wrapper @@ -242,7 +242,6 @@ class DenonDevice(MediaPlayerEntity): self._receiver.support_sound_mode and MediaPlayerEntityFeature.SELECT_SOUND_MODE ) - self._available = True @async_log_errors async def async_update(self) -> None: @@ -251,11 +250,6 @@ class DenonDevice(MediaPlayerEntity): if self._update_audyssey: await self._receiver.async_update_audyssey() - @property - def available(self): - """Return True if entity is available.""" - return self._available - @property def state(self): """Return the state of the device.""" From 23090cb8a268b3f268aefa8477f30af88bf46051 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 19:21:08 +0200 Subject: [PATCH 760/903] Improve entity type hints [i] (#77529) --- homeassistant/components/iaqualink/climate.py | 3 +- homeassistant/components/iaqualink/switch.py | 6 ++-- homeassistant/components/ihc/switch.py | 6 ++-- homeassistant/components/imap/sensor.py | 8 ++--- .../components/imap_email_content/sensor.py | 2 +- homeassistant/components/incomfort/climate.py | 2 +- homeassistant/components/influxdb/sensor.py | 2 +- homeassistant/components/insteon/climate.py | 6 ++-- homeassistant/components/insteon/switch.py | 6 ++-- .../components/integration/sensor.py | 4 +-- .../components/intellifire/climate.py | 6 ++-- .../components/intesishome/climate.py | 16 +++++----- homeassistant/components/iperf3/sensor.py | 4 +-- homeassistant/components/ipma/weather.py | 2 +- .../components/irish_rail_transport/sensor.py | 2 +- .../components/islamic_prayer_times/sensor.py | 2 +- homeassistant/components/itach/remote.py | 10 ++++--- .../components/itunes/media_player.py | 30 ++++++++++--------- homeassistant/components/izone/climate.py | 15 +++++----- 19 files changed, 74 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 7ccf0510ae2..5a8cd0ce09f 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from iaqualink.const import ( AQUALINK_TEMP_CELSIUS_HIGH, @@ -105,7 +106,7 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): return float(self.dev.state) @refresh_system - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await await_or_reraise(self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE]))) diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index 146d30e2d04..8f482e8730f 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -1,6 +1,8 @@ """Support for Aqualink pool feature switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -52,11 +54,11 @@ class HassAqualinkSwitch(AqualinkEntity, SwitchEntity): return self.dev.is_on @refresh_system - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" await await_or_reraise(self.dev.turn_on()) @refresh_system - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await await_or_reraise(self.dev.turn_off()) diff --git a/homeassistant/components/ihc/switch.py b/homeassistant/components/ihc/switch.py index e33d3b6bb5e..8e8edb0b7f7 100644 --- a/homeassistant/components/ihc/switch.py +++ b/homeassistant/components/ihc/switch.py @@ -1,6 +1,8 @@ """Support for IHC switches.""" from __future__ import annotations +from typing import Any + from ihcsdk.ihccontroller import IHCController from homeassistant.components.switch import SwitchEntity @@ -64,14 +66,14 @@ class IHCSwitch(IHCDevice, SwitchEntity): """Return true if switch is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if self._ihc_on_id: await async_pulse(self.hass, self.ihc_controller, self._ihc_on_id) else: await async_set_bool(self.hass, self.ihc_controller, self.ihc_id, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._ihc_off_id: await async_pulse(self.hass, self.ihc_controller, self._ihc_off_id) diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index 43a2e3e82e7..fa5428ccc06 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -89,7 +89,7 @@ class ImapSensor(SensorEntity): self._does_push = None self._idle_loop_task = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle when an entity is about to be added to Home Assistant.""" if not self.should_poll: self._idle_loop_task = self.hass.loop.create_task(self.idle_loop()) @@ -110,12 +110,12 @@ class ImapSensor(SensorEntity): return self._email_count @property - def available(self): + def available(self) -> bool: """Return the availability of the device.""" return self._connection is not None @property - def should_poll(self): + def should_poll(self) -> bool: """Return if polling is needed.""" return not self._does_push @@ -151,7 +151,7 @@ class ImapSensor(SensorEntity): except (AioImapException, asyncio.TimeoutError): self.disconnected() - async def async_update(self): + async def async_update(self) -> None: """Periodic polling of state.""" try: if await self.connection(): diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index a8bd394a159..216a5a7cfe7 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -252,7 +252,7 @@ class EmailContentSensor(SensorEntity): return email_message.get_payload() - def update(self): + def update(self) -> None: """Read emails and publish state change.""" email_message = self._email_reader.read_next() diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index aaeab394f75..b7b66e2b25d 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -75,7 +75,7 @@ class InComfortClimate(IncomfortChild, ClimateEntity): """Return max valid temperature that can be set.""" return 30.0 - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature for this zone.""" temperature = kwargs.get(ATTR_TEMPERATURE) await self._room.set_override(temperature) diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index f9535292e69..dfb5ee57b6a 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -242,7 +242,7 @@ class InfluxSensor(SensorEntity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - def update(self): + def update(self) -> None: """Get the latest data from Influxdb and updates the states.""" self.data.update() if (value := self.data.value) is None: diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 29c127ba0c7..8806caf3999 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -1,6 +1,8 @@ """Support for Insteon thermostat.""" from __future__ import annotations +from typing import Any + from pyinsteon.config import CELSIUS from pyinsteon.constants import ThermostatMode @@ -182,7 +184,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): attr["humidifier"] = humidifier return attr - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) @@ -214,7 +216,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): await self._insteon_device.async_set_humidity_low_set_point(low) await self._insteon_device.async_set_humidity_high_set_point(high) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register INSTEON update events.""" await super().async_added_to_hass() await self._insteon_device.async_read_op_flags() diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index ec80254515e..d9a15d383c0 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -1,4 +1,6 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" +from typing import Any + from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -37,10 +39,10 @@ class InsteonSwitchEntity(InsteonEntity, SwitchEntity): """Return the boolean response if the node is on.""" return bool(self._insteon_device_group.value) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch on.""" await self._insteon_device.async_on(group=self._insteon_device_group.group) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn switch off.""" await self._insteon_device.async_off(group=self._insteon_device_group.group) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index b3b8a2a2b9d..b1b666af9aa 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -150,7 +150,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._attr_unique_id = unique_id self._sensor_source_id = source_entity self._round_digits = round_digits - self._state = None + self._state: Decimal | None = None self._method = integration_method self._attr_name = name if name is not None else f"{source_entity} integral" @@ -174,7 +174,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): return self._unit_template.format(integral_unit) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() if state := await self.async_get_last_state(): diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py index a00a20f64f1..1656be621e6 100644 --- a/homeassistant/components/intellifire/climate.py +++ b/homeassistant/components/intellifire/climate.py @@ -1,6 +1,8 @@ """Intellifire Climate Entities.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ( ClimateEntity, ClimateEntityDescription, @@ -70,7 +72,7 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity): return HVACMode.HEAT return HVACMode.OFF - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Turn on thermostat by setting a target temperature.""" raw_target_temp = kwargs[ATTR_TEMPERATURE] self.last_temp = int(raw_target_temp) @@ -93,7 +95,7 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity): """Return target temperature.""" return float(self.coordinator.read_api.data.thermostat_setpoint_c) - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set HVAC mode to normal or thermostat control.""" LOGGER.debug( "Setting mode to [%s] - using last temp: %s", hvac_mode, self.last_temp diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 925147e82ad..b85c976a928 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging from random import randrange -from typing import NamedTuple +from typing import Any, NamedTuple from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome import voluptuous as vol @@ -204,7 +204,7 @@ class IntesisAC(ClimateEntity): self._attr_hvac_modes.extend(mode_list) self._attr_hvac_modes.append(HVACMode.OFF) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to event updates.""" _LOGGER.debug("Added climate device with state: %s", repr(self._ih_device)) await self._controller.add_update_callback(self.async_update_callback) @@ -256,7 +256,7 @@ class IntesisAC(ClimateEntity): """Return the current preset mode.""" return self._preset - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if hvac_mode := kwargs.get(ATTR_HVAC_MODE): await self.async_set_hvac_mode(hvac_mode) @@ -295,7 +295,7 @@ class IntesisAC(ClimateEntity): self._hvac_mode = hvac_mode self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set fan mode (from quiet, low, medium, high, auto).""" await self._controller.set_fan_speed(self._device_id, fan_mode) @@ -303,12 +303,12 @@ class IntesisAC(ClimateEntity): self._fan_speed = fan_mode self.async_write_ha_state() - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" ih_preset_mode = MAP_PRESET_MODE_TO_IH.get(preset_mode) await self._controller.set_preset_mode(self._device_id, ih_preset_mode) - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set the vertical vane.""" if swing_settings := MAP_SWING_TO_IH.get(swing_mode): await self._controller.set_vertical_vane( @@ -318,7 +318,7 @@ class IntesisAC(ClimateEntity): self._device_id, swing_settings.hvane ) - async def async_update(self): + async def async_update(self) -> None: """Copy values from controller dictionary to climate device.""" # Update values from controller's device dictionary self._connected = self._controller.is_connected @@ -353,7 +353,7 @@ class IntesisAC(ClimateEntity): self._device_id ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Shutdown the controller when the device is being removed.""" await self._controller.stop() diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index efdaea5b4f5..2dffabeeb8c 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -62,7 +62,7 @@ class Iperf3Sensor(RestoreEntity, SensorEntity): ATTR_VERSION: self._iperf3_data.data[ATTR_VERSION], } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() @@ -76,7 +76,7 @@ class Iperf3Sensor(RestoreEntity, SensorEntity): return self._attr_native_value = state.state - def update(self): + def update(self) -> None: """Get the latest data and update the states.""" data = self._iperf3_data.data.get(self.entity_description.key) if data is not None: diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index dd585b88802..7a3a28b8bd0 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -190,7 +190,7 @@ class IPMAWeather(WeatherEntity): self._forecast = None @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): + async def async_update(self) -> None: """Update Condition and Forecast.""" async with async_timeout.timeout(10): new_observation = await self._location.observation(self._api) diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index 16016593cd4..2035080b96d 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -133,7 +133,7 @@ class IrishRailTransportSensor(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data and update the states.""" self.data.update() self._times = self.data.info diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 92ba82b6e15..a90a2c53c52 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -54,7 +54,7 @@ class IslamicPrayerTimeSensor(SensorEntity): dt_util.UTC ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" self.async_on_remove( async_dispatcher_connect(self.hass, DATA_UPDATED, self.async_write_ha_state) diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index 409bbe4868f..c0dddfa080e 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -1,7 +1,9 @@ """Support for iTach IR devices.""" from __future__ import annotations +from collections.abc import Iterable import logging +from typing import Any import pyitachip2ir import voluptuous as vol @@ -123,19 +125,19 @@ class ITachIP2IRRemote(remote.RemoteEntity): """Return true if device is on.""" return self._power - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._power = True self.itachip2ir.send(self._name, "ON", self._ir_count) self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._power = False self.itachip2ir.send(self._name, "OFF", self._ir_count) self.schedule_update_ha_state() - def send_command(self, command, **kwargs): + def send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" num_repeats = kwargs.get(ATTR_NUM_REPEATS, DEFAULT_NUM_REPEATS) for single_command in command: @@ -143,6 +145,6 @@ class ITachIP2IRRemote(remote.RemoteEntity): self._name, single_command, self._ir_count * num_repeats ) - def update(self): + def update(self) -> None: """Update the device.""" self.itachip2ir.update() diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index 56b47a5c515..ea2cad37c77 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -1,6 +1,8 @@ """Support for interfacing to iTunes API.""" from __future__ import annotations +from typing import Any + import requests import voluptuous as vol @@ -268,7 +270,7 @@ class ItunesDevice(MediaPlayerEntity): return STATE_PLAYING - def update(self): + def update(self) -> None: """Retrieve latest state.""" now_playing = self.client.now_playing() self.update_state(now_playing) @@ -354,48 +356,48 @@ class ItunesDevice(MediaPlayerEntity): """Boolean if shuffle is enabled.""" return self.shuffled - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" response = self.client.set_volume(int(volume * 100)) self.update_state(response) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" response = self.client.set_muted(mute) self.update_state(response) - def set_shuffle(self, shuffle): + def set_shuffle(self, shuffle: bool) -> None: """Shuffle (true) or no shuffle (false) media player.""" response = self.client.set_shuffle(shuffle) self.update_state(response) - def media_play(self): + def media_play(self) -> None: """Send media_play command to media player.""" response = self.client.play() self.update_state(response) - def media_pause(self): + def media_pause(self) -> None: """Send media_pause command to media player.""" response = self.client.pause() self.update_state(response) - def media_next_track(self): + def media_next_track(self) -> None: """Send media_next command to media player.""" response = self.client.next() self.update_state(response) - def media_previous_track(self): + def media_previous_track(self) -> None: """Send media_previous command media player.""" response = self.client.previous() self.update_state(response) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Send the play_media command to the media player.""" if media_type == MEDIA_TYPE_PLAYLIST: response = self.client.play_playlist(media_id) self.update_state(response) - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" response = self.client.stop() self.update_state(response) @@ -471,7 +473,7 @@ class AirPlayDevice(MediaPlayerEntity): return STATE_OFF - def update(self): + def update(self) -> None: """Retrieve latest state.""" @property @@ -484,20 +486,20 @@ class AirPlayDevice(MediaPlayerEntity): """Flag of media content that is supported.""" return MEDIA_TYPE_MUSIC - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" volume = int(volume * 100) response = self.client.set_volume_airplay_device(self._id, volume) self.update_state(response) - def turn_on(self): + def turn_on(self) -> None: """Select AirPlay.""" self.update_state({"selected": True}) self.schedule_update_ha_state() response = self.client.toggle_airplay_device(self._id, True) self.update_state(response) - def turn_off(self): + def turn_off(self) -> None: """Deselect AirPlay.""" self.update_state({"selected": False}) self.schedule_update_ha_state() diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 2c181d90fda..a80549f27fc 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pizone import Controller, Zone import voluptuous as vol @@ -170,7 +171,7 @@ class ControllerDevice(ClimateEntity): for zone in controller.zones: self.zones[zone] = ZoneDevice(self, zone) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call on adding to hass.""" # Register for connect/disconnect/update events @callback @@ -395,7 +396,7 @@ class ControllerDevice(ClimateEntity): else: self.set_available(True) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if not self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE: self.async_schedule_update_ha_state(True) @@ -468,7 +469,7 @@ class ZoneDevice(ClimateEntity): via_device=(IZONE, controller.unique_id), ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call on adding to hass.""" @callback @@ -517,7 +518,7 @@ class ZoneDevice(ClimateEntity): @property # type: ignore[misc] @_return_on_connection_error(0) - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" if self._zone.mode == Zone.Mode.AUTO: return self._attr_supported_features @@ -588,7 +589,7 @@ class ZoneDevice(ClimateEntity): ) self.async_write_ha_state() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if self._zone.mode != Zone.Mode.AUTO: return @@ -606,7 +607,7 @@ class ZoneDevice(ClimateEntity): """Return true if on.""" return self._zone.mode != Zone.Mode.CLOSE - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn device on (open zone).""" if self._zone.type == Zone.Type.AUTO: await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.AUTO)) @@ -614,7 +615,7 @@ class ZoneDevice(ClimateEntity): await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.OPEN)) self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn device off (close zone).""" await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.CLOSE)) self.async_write_ha_state() From 935274c2e775cb1096a887a59dedc495a081a97a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Aug 2022 12:55:48 -0500 Subject: [PATCH 761/903] Bump bluetooth-auto-recovery to 0.3.0 (#77555) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 8e4f0eb75de..5cd83a2d51a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.15.1", "bluetooth-adapters==0.3.2", - "bluetooth-auto-recovery==0.2.2" + "bluetooth-auto-recovery==0.3.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 91660db1367..1da07040c9b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.15.1 bluetooth-adapters==0.3.2 -bluetooth-auto-recovery==0.2.2 +bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 3166dd4bbf6..22dc87efc33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ blockchain==1.4.4 bluetooth-adapters==0.3.2 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.2.2 +bluetooth-auto-recovery==0.3.0 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1996e8955e..0af08e46034 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ blinkpy==0.19.0 bluetooth-adapters==0.3.2 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.2.2 +bluetooth-auto-recovery==0.3.0 # homeassistant.components.bond bond-async==0.1.22 From ba499dff25623a30e47e16c03fff0101a3fb3732 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Aug 2022 13:29:13 -0500 Subject: [PATCH 762/903] Add Nutrichef as a supported brand of inkbird (#77551) --- homeassistant/components/inkbird/manifest.json | 5 ++++- homeassistant/generated/supported_brands.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index 97234de9d6d..e8076576f6e 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -13,5 +13,8 @@ "requirements": ["inkbird-ble==0.5.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], - "iot_class": "local_push" + "iot_class": "local_push", + "supported_brands": { + "nutrichef": "Nutrichef" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 162c953b2b4..92c7b827855 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -8,6 +8,7 @@ To update, run python3 -m script.hassfest HAS_SUPPORTED_BRANDS = ( "denonavr", "hunterdouglas_powerview", + "inkbird", "motion_blinds", "overkiz", "renault", From 0c60e887e316c1d667a55fbf3f41154da8e06c5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Aug 2022 13:30:30 -0500 Subject: [PATCH 763/903] Bump unifi-discovery to 1.1.6 (#77557) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 0eb07560624..a01706337e0 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.1.9", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.1.9", "unifi-discovery==1.1.6"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 22dc87efc33..fca5683ce7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2405,7 +2405,7 @@ uasiren==0.0.1 ultraheat-api==0.4.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.5 +unifi-discovery==1.1.6 # homeassistant.components.unifiled unifiled==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0af08e46034..777c97d1083 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1639,7 +1639,7 @@ uasiren==0.0.1 ultraheat-api==0.4.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.5 +unifi-discovery==1.1.6 # homeassistant.components.upb upb_lib==0.4.12 From fe881230dbd7631441864ebb1aef2f19b7c832cd Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 30 Aug 2022 12:30:42 -0600 Subject: [PATCH 764/903] Add support for Feeder-Robot button (#77501) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/litterrobot/button.py | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index b833500ec4c..74c659fd474 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -1,20 +1,23 @@ """Support for Litter-Robot button.""" from __future__ import annotations -from pylitterbot import LitterRobot3 +from collections.abc import Callable, Coroutine, Iterable +from dataclasses import dataclass +import itertools +from typing import Any, Generic -from homeassistant.components.button import ButtonEntity +from pylitterbot import FeederRobot, LitterRobot3 + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub -TYPE_RESET_WASTE_DRAWER = "Reset Waste Drawer" - async def async_setup_entry( hass: HomeAssistant, @@ -23,22 +26,68 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - LitterRobotResetWasteDrawerButton( - robot=robot, entity_type=TYPE_RESET_WASTE_DRAWER, hub=hub - ) - for robot in hub.litter_robots() - if isinstance(robot, LitterRobot3) + entities: Iterable[LitterRobotButtonEntity] = itertools.chain( + ( + LitterRobotButtonEntity( + robot=robot, hub=hub, description=LITTER_ROBOT_BUTTON + ) + for robot in hub.litter_robots() + if isinstance(robot, LitterRobot3) + ), + ( + LitterRobotButtonEntity( + robot=robot, hub=hub, description=FEEDER_ROBOT_BUTTON + ) + for robot in hub.feeder_robots() + ), ) + async_add_entities(entities) -class LitterRobotResetWasteDrawerButton(LitterRobotEntity[LitterRobot3], ButtonEntity): - """Litter-Robot reset waste drawer button.""" +@dataclass +class RequiredKeysMixin(Generic[_RobotT]): + """A class that describes robot button entity required keys.""" - _attr_icon = "mdi:delete-variant" - _attr_entity_category = EntityCategory.CONFIG + press_fn: Callable[[_RobotT], Coroutine[Any, Any, bool]] + + +@dataclass +class RobotButtonEntityDescription(ButtonEntityDescription, RequiredKeysMixin[_RobotT]): + """A class that describes robot button entities.""" + + +LITTER_ROBOT_BUTTON = RobotButtonEntityDescription[LitterRobot3]( + key="reset_waste_drawer", + name="Reset Waste Drawer", + icon="mdi:delete-variant", + entity_category=EntityCategory.CONFIG, + press_fn=lambda robot: robot.reset_waste_drawer(), +) +FEEDER_ROBOT_BUTTON = RobotButtonEntityDescription[FeederRobot]( + key="give_snack", + name="Give snack", + icon="mdi:candy-outline", + press_fn=lambda robot: robot.give_snack(), +) + + +class LitterRobotButtonEntity(LitterRobotEntity[_RobotT], ButtonEntity): + """Litter-Robot button entity.""" + + entity_description: RobotButtonEntityDescription[_RobotT] + + def __init__( + self, + robot: _RobotT, + hub: LitterRobotHub, + description: RobotButtonEntityDescription[_RobotT], + ) -> None: + """Initialize a Litter-Robot button entity.""" + assert description.name + super().__init__(robot, description.name, hub) + self.entity_description = description async def async_press(self) -> None: """Press the button.""" - await self.robot.reset_waste_drawer() + await self.entity_description.press_fn(self.robot) self.coordinator.async_set_updated_data(True) From 8936c91f5059a95848c8d58aa8ee70128e676f63 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Aug 2022 20:45:52 +0200 Subject: [PATCH 765/903] Migrate smartthings light to color_mode (#70968) --- homeassistant/components/smartthings/light.py | 71 ++++++++++++------- tests/components/smartthings/test_light.py | 33 ++++----- 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 918e8b4258c..ccf63582e86 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -12,11 +12,10 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ColorMode, LightEntity, LightEntityFeature, + brightness_supported, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -74,26 +73,40 @@ def convert_scale(value, value_scale, target_scale, round_digits=4): class SmartThingsLight(SmartThingsEntity, LightEntity): """Define a SmartThings Light.""" + _attr_supported_color_modes: set[ColorMode] + def __init__(self, device): """Initialize a SmartThingsLight.""" super().__init__(device) self._brightness = None self._color_temp = None self._hs_color = None - self._supported_features = self._determine_features() + self._attr_supported_color_modes = self._determine_color_modes() + self._attr_supported_features = self._determine_features() + + def _determine_color_modes(self): + """Get features supported by the device.""" + color_modes = set() + # Color Temperature + if Capability.color_temperature in self._device.capabilities: + color_modes.add(ColorMode.COLOR_TEMP) + # Color + if Capability.color_control in self._device.capabilities: + color_modes.add(ColorMode.HS) + # Brightness + if not color_modes and Capability.switch_level in self._device.capabilities: + color_modes.add(ColorMode.BRIGHTNESS) + if not color_modes: + color_modes.add(ColorMode.ONOFF) + + return color_modes def _determine_features(self): """Get features supported by the device.""" features = 0 - # Brightness and transition + # Transition if Capability.switch_level in self._device.capabilities: - features |= SUPPORT_BRIGHTNESS | LightEntityFeature.TRANSITION - # Color Temperature - if Capability.color_temperature in self._device.capabilities: - features |= SUPPORT_COLOR_TEMP - # Color - if Capability.color_control in self._device.capabilities: - features |= SUPPORT_COLOR + features |= LightEntityFeature.TRANSITION return features @@ -101,17 +114,17 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): """Turn the light on.""" tasks = [] # Color temperature - if self._supported_features & SUPPORT_COLOR_TEMP and ATTR_COLOR_TEMP in kwargs: + if ATTR_COLOR_TEMP in kwargs: tasks.append(self.async_set_color_temp(kwargs[ATTR_COLOR_TEMP])) # Color - if self._supported_features & SUPPORT_COLOR and ATTR_HS_COLOR in kwargs: + if ATTR_HS_COLOR in kwargs: tasks.append(self.async_set_color(kwargs[ATTR_HS_COLOR])) if tasks: # Set temp/color first await asyncio.gather(*tasks) # Switch/brightness/transition - if self._supported_features & SUPPORT_BRIGHTNESS and ATTR_BRIGHTNESS in kwargs: + if ATTR_BRIGHTNESS in kwargs: await self.async_set_level( kwargs[ATTR_BRIGHTNESS], kwargs.get(ATTR_TRANSITION, 0) ) @@ -125,10 +138,7 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" # Switch/transition - if ( - self._supported_features & LightEntityFeature.TRANSITION - and ATTR_TRANSITION in kwargs - ): + if ATTR_TRANSITION in kwargs: await self.async_set_level(0, int(kwargs[ATTR_TRANSITION])) else: await self._device.switch_off(set_status=True) @@ -140,17 +150,17 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): async def async_update(self) -> None: """Update entity attributes when the device status has changed.""" # Brightness and transition - if self._supported_features & SUPPORT_BRIGHTNESS: + if brightness_supported(self._attr_supported_color_modes): self._brightness = int( convert_scale(self._device.status.level, 100, 255, 0) ) # Color Temperature - if self._supported_features & SUPPORT_COLOR_TEMP: + if ColorMode.COLOR_TEMP in self._attr_supported_color_modes: self._color_temp = color_util.color_temperature_kelvin_to_mired( self._device.status.color_temperature ) # Color - if self._supported_features & SUPPORT_COLOR: + if ColorMode.HS in self._attr_supported_color_modes: self._hs_color = ( convert_scale(self._device.status.hue, 100, 360), self._device.status.saturation, @@ -179,6 +189,18 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): duration = int(transition) await self._device.set_level(level, duration, set_status=True) + @property + def color_mode(self) -> ColorMode: + """Return the color mode of the light.""" + if len(self._attr_supported_color_modes) == 1: + # The light supports only a single color mode + return list(self._attr_supported_color_modes)[0] + + # The light supports hs + color temp, determine which one it is + if self._hs_color and self._hs_color[1]: + return ColorMode.HS + return ColorMode.COLOR_TEMP + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -214,8 +236,3 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): # implemented within each device-type handler. This value is the # highest kelvin found supported across 20+ handlers. return 111 # 9000K - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 0fff1403985..4bff370fb60 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -11,11 +11,10 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, DOMAIN as LIGHT_DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ColorMode, LightEntityFeature, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE @@ -66,7 +65,7 @@ def light_devices_fixture(device_factory): Attribute.switch: "on", Attribute.level: 100, Attribute.hue: 76.0, - Attribute.saturation: 55.0, + Attribute.saturation: 0.0, Attribute.color_temperature: 4500, }, ), @@ -80,33 +79,27 @@ async def test_entity_state(hass, light_devices): # Dimmer 1 state = hass.states.get("light.dimmer_1") assert state.state == "on" - assert ( - state.attributes[ATTR_SUPPORTED_FEATURES] - == SUPPORT_BRIGHTNESS | LightEntityFeature.TRANSITION - ) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION assert isinstance(state.attributes[ATTR_BRIGHTNESS], int) assert state.attributes[ATTR_BRIGHTNESS] == 255 # Color Dimmer 1 state = hass.states.get("light.color_dimmer_1") assert state.state == "off" - assert ( - state.attributes[ATTR_SUPPORTED_FEATURES] - == SUPPORT_BRIGHTNESS | LightEntityFeature.TRANSITION | SUPPORT_COLOR - ) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.HS] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION # Color Dimmer 2 state = hass.states.get("light.color_dimmer_2") assert state.state == "on" - assert ( - state.attributes[ATTR_SUPPORTED_FEATURES] - == SUPPORT_BRIGHTNESS - | LightEntityFeature.TRANSITION - | SUPPORT_COLOR - | SUPPORT_COLOR_TEMP - ) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION assert state.attributes[ATTR_BRIGHTNESS] == 255 - assert state.attributes[ATTR_HS_COLOR] == (273.6, 55.0) + assert ATTR_HS_COLOR not in state.attributes[ATTR_HS_COLOR] assert isinstance(state.attributes[ATTR_COLOR_TEMP], int) assert state.attributes[ATTR_COLOR_TEMP] == 222 From 05264cedfa4ec9e0c998b65f2aef5031139def65 Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Tue, 30 Aug 2022 14:46:54 -0400 Subject: [PATCH 766/903] Fix lutron_caseta handling of 'None' serials for RA3/QSX zones (#77553) Co-authored-by: J. Nick Koston --- .../components/lutron_caseta/__init__.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index bcbaedeb8d1..2041f4d65d6 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -343,7 +343,7 @@ class LutronCasetaDevice(Entity): area, name = _area_and_name_from_name(device["name"]) self._attr_name = full_name = f"{area} {name}" info = DeviceInfo( - identifiers={(DOMAIN, self.serial)}, + identifiers={(DOMAIN, self._handle_none_serial(self.serial))}, manufacturer=MANUFACTURER, model=f"{device['model']} ({device['type']})", name=full_name, @@ -358,6 +358,12 @@ class LutronCasetaDevice(Entity): """Register callbacks.""" self._smartbridge.add_subscriber(self.device_id, self.async_write_ha_state) + def _handle_none_serial(self, serial: str | None) -> str | int: + """Handle None serial returned by RA3 and QSX processors.""" + if serial is None: + return f"{self._bridge_unique_id}_{self.device_id}" + return serial + @property def device_id(self): """Return the device ID used for calling pylutron_caseta.""" @@ -369,9 +375,9 @@ class LutronCasetaDevice(Entity): return self._device["serial"] @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the device (serial).""" - return str(self.serial) + return str(self._handle_none_serial(self.serial)) @property def extra_state_attributes(self): @@ -387,13 +393,6 @@ class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): self._device = self._smartbridge.get_device_by_id(self.device_id) _LOGGER.debug(self._device) - @property - def unique_id(self): - """Return a unique identifier if serial number is None.""" - if self.serial is None: - return f"{self._bridge_unique_id}_{self.device_id}" - return super().unique_id - def _id_to_identifier(lutron_id: str) -> tuple[str, str]: """Convert a lutron caseta identifier to a device identifier.""" From a1374963d10fd1e2042b51243835fd8c05eddb00 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 20:55:01 +0200 Subject: [PATCH 767/903] Improve entity type hints [h] (#77468) --- homeassistant/components/habitica/sensor.py | 4 +- .../harman_kardon_avr/media_player.py | 14 +++--- homeassistant/components/harmony/remote.py | 10 +++-- homeassistant/components/harmony/switch.py | 5 ++- homeassistant/components/hassio/update.py | 4 +- .../components/haveibeenpwned/sensor.py | 4 +- homeassistant/components/hddtemp/sensor.py | 2 +- .../components/hdmi_cec/media_player.py | 33 +++++++------- homeassistant/components/hdmi_cec/switch.py | 5 ++- homeassistant/components/heatmiser/climate.py | 2 +- homeassistant/components/heos/media_player.py | 43 +++++++++++-------- .../components/hikvisioncam/switch.py | 7 +-- .../components/hisense_aehw4a1/climate.py | 15 ++++--- .../components/hive/alarm_control_panel.py | 2 +- .../components/hive/binary_sensor.py | 2 +- homeassistant/components/hive/climate.py | 5 ++- homeassistant/components/hive/sensor.py | 2 +- homeassistant/components/hive/switch.py | 7 +-- homeassistant/components/hive/water_heater.py | 4 +- homeassistant/components/hlk_sw16/switch.py | 6 ++- .../components/home_connect/binary_sensor.py | 2 +- .../components/home_connect/light.py | 3 +- .../components/home_connect/sensor.py | 2 +- .../components/home_connect/switch.py | 13 +++--- .../components/home_plus_control/switch.py | 5 ++- homeassistant/components/homematic/climate.py | 4 +- homeassistant/components/homematic/switch.py | 6 ++- .../components/homematicip_cloud/climate.py | 2 +- .../components/homematicip_cloud/switch.py | 8 ++-- homeassistant/components/honeywell/climate.py | 4 +- .../components/horizon/media_player.py | 19 ++++---- homeassistant/components/hp_ilo/sensor.py | 2 +- .../hunterdouglas_powerview/sensor.py | 6 +-- .../hvv_departures/binary_sensor.py | 11 +++-- .../components/hvv_departures/sensor.py | 3 +- .../components/hydrawise/binary_sensor.py | 2 +- homeassistant/components/hydrawise/sensor.py | 2 +- homeassistant/components/hydrawise/switch.py | 7 +-- 38 files changed, 153 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 3547c8ca6f9..5a7109df0b9 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -154,7 +154,7 @@ class HabitipySensor(SensorEntity): self._state = None self._updater = updater - async def async_update(self): + async def async_update(self) -> None: """Update Condition and Forecast.""" await self._updater.update() data = self._updater.data @@ -194,7 +194,7 @@ class HabitipyTaskSensor(SensorEntity): self._state = None self._updater = updater - async def async_update(self): + async def async_update(self) -> None: """Update Condition and Forecast.""" await self._updater.update() all_tasks = self._updater.tasks diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index 5b0c87cfbc5..c6272626f94 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -69,7 +69,7 @@ class HkAvrDevice(MediaPlayerEntity): self._muted = avr.muted self._current_source = avr.current_source - def update(self): + def update(self) -> None: """Update the state of this media_player.""" if self._avr.is_on(): self._state = STATE_ON @@ -106,26 +106,26 @@ class HkAvrDevice(MediaPlayerEntity): """Available sources.""" return self._source_list - def turn_on(self): + def turn_on(self) -> None: """Turn the AVR on.""" self._avr.power_on() - def turn_off(self): + def turn_off(self) -> None: """Turn off the AVR.""" self._avr.power_off() - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" return self._avr.select_source(source) - def volume_up(self): + def volume_up(self) -> None: """Volume up the AVR.""" return self._avr.volume_up() - def volume_down(self): + def volume_down(self) -> None: """Volume down AVR.""" return self._avr.volume_down() - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" return self._avr.mute(mute) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 5dfd6d6290d..1482c8aaa4d 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -1,6 +1,8 @@ """Support for Harmony Hub devices.""" +from collections.abc import Iterable import json import logging +from typing import Any import voluptuous as vol @@ -124,7 +126,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): self._activity_starting = None self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Complete the initialization.""" await super().async_added_to_hass() @@ -205,7 +207,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): self.async_new_activity(self._data.current_activity) await self.hass.async_add_executor_job(self.write_config_file) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Start an activity from the Harmony device.""" _LOGGER.debug("%s: Turn On", self.name) @@ -223,11 +225,11 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): else: _LOGGER.error("%s: No activity specified with turn_on service", self.name) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Start the PowerOff activity.""" await self._data.async_power_off() - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a list of commands to one device.""" _LOGGER.debug("%s: Send Command", self.name) if (device := kwargs.get(ATTR_DEVICE)) is None: diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index fe2238293da..acd04596bd5 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -1,5 +1,6 @@ """Support for Harmony Hub activities.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -50,11 +51,11 @@ class HarmonyActivitySwitch(HarmonyEntity, SwitchEntity): _, activity_name = self._data.current_activity return activity_name == self._activity_name - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Start this activity.""" await self._data.async_start_activity(self._activity_name) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Stop this activity.""" await self._data.async_power_off() diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index 8af3d88088a..e68dbece5b6 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -101,7 +101,7 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug] @property - def auto_update(self): + def auto_update(self) -> bool: """Return true if auto-update is enabled for the add-on.""" return self._addon_data[ATTR_AUTO_UPDATE] @@ -159,7 +159,7 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): async def async_install( self, version: str | None = None, - backup: bool | None = False, + backup: bool = False, **kwargs: Any, ) -> None: """Install an update.""" diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index d320cb2d911..400c280263a 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -100,7 +100,7 @@ class HaveIBeenPwnedSensor(SensorEntity): return val - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Get initial data.""" # To make sure we get initial data for the sensors ignoring the normal # throttle of 15 minutes but using an update throttle of 5 seconds @@ -126,7 +126,7 @@ class HaveIBeenPwnedSensor(SensorEntity): self._state = len(self._data.data[self._email]) self.schedule_update_ha_state() - def update(self): + def update(self) -> None: """Update data and see if it contains data for our email.""" self._data.update() diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index ff17abc257e..7ff8de90509 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -91,7 +91,7 @@ class HddTempSensor(SensorEntity): if self._details is not None: return {ATTR_DEVICE: self._details[0], ATTR_MODEL: self._details[1]} - def update(self): + def update(self) -> None: """Get the latest data from HDDTemp daemon and updates the state.""" self.hddtemp.update() diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index cc4b1972bea..e3ec00749c1 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand from pycec.const import ( @@ -83,69 +84,69 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): """Send playback status to CEC adapter.""" self._device.async_send_command(CecCommand(key, dst=self._logical_address)) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute volume.""" self.send_keypress(KEY_MUTE_TOGGLE) - def media_previous_track(self): + def media_previous_track(self) -> None: """Go to previous track.""" self.send_keypress(KEY_BACKWARD) - def turn_on(self): + def turn_on(self) -> None: """Turn device on.""" self._device.turn_on() self._state = STATE_ON - def clear_playlist(self): + def clear_playlist(self) -> None: """Clear players playlist.""" raise NotImplementedError() - def turn_off(self): + def turn_off(self) -> None: """Turn device off.""" self._device.turn_off() self._state = STATE_OFF - def media_stop(self): + def media_stop(self) -> None: """Stop playback.""" self.send_keypress(KEY_STOP) self._state = STATE_IDLE - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Not supported.""" raise NotImplementedError() - def media_next_track(self): + def media_next_track(self) -> None: """Skip to next track.""" self.send_keypress(KEY_FORWARD) - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Not supported.""" raise NotImplementedError() - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" raise NotImplementedError() - def media_pause(self): + def media_pause(self) -> None: """Pause playback.""" self.send_keypress(KEY_PAUSE) self._state = STATE_PAUSED - def select_source(self, source): + def select_source(self, source: str) -> None: """Not supported.""" raise NotImplementedError() - def media_play(self): + def media_play(self) -> None: """Start playback.""" self.send_keypress(KEY_PLAY) self._state = STATE_PLAYING - def volume_up(self): + def volume_up(self) -> None: """Increase volume.""" _LOGGER.debug("%s: volume up", self._logical_address) self.send_keypress(KEY_VOLUME_UP) - def volume_down(self): + def volume_down(self) -> None: """Decrease volume.""" _LOGGER.debug("%s: volume down", self._logical_address) self.send_keypress(KEY_VOLUME_DOWN) @@ -155,7 +156,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): """Cache state of device.""" return self._state - def update(self): + def update(self) -> None: """Update device status.""" device = self._device if device.power_status in [POWER_OFF, 3]: diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index 076bedde0b2..b44e5ce5c64 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON @@ -40,13 +41,13 @@ class CecSwitchEntity(CecEntity, SwitchEntity): CecEntity.__init__(self, device, logical) self.entity_id = f"{SWITCH_DOMAIN}.hdmi_{hex(self._logical_address)[2:]}" - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn device on.""" self._device.turn_on() self._state = STATE_ON self.schedule_update_ha_state(force_refresh=False) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn device off.""" self._device.turn_off() self._state = STATE_OFF diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index aa3f5223ebe..942bb673cf9 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -112,7 +112,7 @@ class HeatmiserV3Thermostat(ClimateEntity): self._target_temperature = int(temperature) self.therm.set_target_temp(self._target_temperature) - def update(self): + def update(self) -> None: """Get the latest data.""" self.uh1.reopen() if not self.uh1.status: diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 765fe2f79c5..b72e3ec73c1 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( + BrowseMedia, async_process_play_media_url, ) from homeassistant.components.media_player.const import ( @@ -136,11 +137,11 @@ class HeosMediaPlayer(MediaPlayerEntity): self._media_position_updated_at = utcnow() await self.async_update_ha_state(True) - async def _heos_updated(self): + async def _heos_updated(self) -> None: """Handle sources changed.""" await self.async_update_ha_state(True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Device added to hass.""" # Update state when attributes of the player change self._signals.append( @@ -159,7 +160,7 @@ class HeosMediaPlayer(MediaPlayerEntity): async_dispatcher_send(self.hass, SIGNAL_HEOS_PLAYER_ADDED) @log_command_error("clear playlist") - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" await self._player.clear_queue() @@ -169,37 +170,39 @@ class HeosMediaPlayer(MediaPlayerEntity): await self._group_manager.async_join_players(self.entity_id, group_members) @log_command_error("pause") - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._player.pause() @log_command_error("play") - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._player.play() @log_command_error("move to previous track") - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self._player.play_previous() @log_command_error("move to next track") - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._player.play_next() @log_command_error("stop") - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" await self._player.stop() @log_command_error("set mute") - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" await self._player.set_mute(mute) @log_command_error("play media") - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL @@ -218,7 +221,7 @@ class HeosMediaPlayer(MediaPlayerEntity): # media_id may be an int or a str selects = await self._player.get_quick_selects() try: - index = int(media_id) + index: int | None = int(media_id) except ValueError: # Try finding index by name index = next( @@ -262,21 +265,21 @@ class HeosMediaPlayer(MediaPlayerEntity): raise ValueError(f"Unsupported media type '{media_type}'") @log_command_error("select source") - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" await self._source_manager.play_source(source, self._player) @log_command_error("set shuffle") - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" await self._player.set_play_mode(self._player.repeat, shuffle) @log_command_error("set volume level") - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._player.set_volume(int(volume * 100)) - async def async_update(self): + async def async_update(self) -> None: """Update supported features of the player.""" controls = self._player.now_playing_media.supported_controls current_support = [CONTROL_TO_SUPPORT[control] for control in controls] @@ -291,11 +294,11 @@ class HeosMediaPlayer(MediaPlayerEntity): self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] @log_command_error("unjoin_player") - async def async_unjoin_player(self): + async def async_unjoin_player(self) -> None: """Remove this player from any group.""" await self._group_manager.async_unjoin_player(self.entity_id) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect the device when removed.""" for signal_remove in self._signals: signal_remove() @@ -318,7 +321,7 @@ class HeosMediaPlayer(MediaPlayerEntity): ) @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Get additional attribute about the state.""" return { "media_album_id": self._player.now_playing_media.album_id, @@ -434,7 +437,9 @@ class HeosMediaPlayer(MediaPlayerEntity): """Volume level of the media player (0..1).""" return self._player.volume / 100 - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( self.hass, diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index d6be7067e8a..ea521ec3a6b 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import hikvision.api from hikvision.error import HikvisionError, MissingParamError @@ -88,17 +89,17 @@ class HikvisionMotionSwitch(SwitchEntity): """Return true if device is on.""" return self._state == STATE_ON - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" _LOGGING.info("Turning on Motion Detection ") self._hikvision_cam.enable_motion_detection() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" _LOGGING.info("Turning off Motion Detection ") self._hikvision_cam.disable_motion_detection() - def update(self): + def update(self) -> None: """Update Motion Detection state.""" enabled = self._hikvision_cam.is_motion_detection_enabled() _LOGGING.info("enabled: %s", enabled) diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index 3213c5f9414..9612fd74f0b 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyaehw4a1.aehw4a1 import AehW4a1 import pyaehw4a1.exceptions @@ -168,7 +169,7 @@ class ClimateAehW4a1(ClimateEntity): self._preset_mode = None self._previous_state = None - async def async_update(self): + async def async_update(self) -> None: """Pull state from AEH-W4A1.""" try: status = await self._device.command("status_102_0") @@ -300,7 +301,7 @@ class ClimateAehW4a1(ClimateEntity): """Return the supported step of target temperature.""" return 1 - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if self._on != "1": _LOGGER.warning( @@ -316,7 +317,7 @@ class ClimateAehW4a1(ClimateEntity): else: await self._device.command(f"temp_{int(temp)}_F") - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" if self._on != "1": _LOGGER.warning("AC at %s is off, could not set fan mode", self._unique_id) @@ -327,7 +328,7 @@ class ClimateAehW4a1(ClimateEntity): _LOGGER.debug("Setting fan mode of %s to %s", self._unique_id, fan_mode) await self._device.command(HA_FAN_MODES_TO_AC[fan_mode]) - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" if self._on != "1": _LOGGER.warning( @@ -362,7 +363,7 @@ class ClimateAehW4a1(ClimateEntity): if swing_act in (SWING_OFF, SWING_VERTICAL): await self._device.command("hor_swing") - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if self._on != "1": if preset_mode == PRESET_NONE: @@ -408,12 +409,12 @@ class ClimateAehW4a1(ClimateEntity): if self._on != "1": await self.async_turn_on() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on.""" _LOGGER.debug("Turning %s on", self._unique_id) await self._device.command("on") - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off.""" _LOGGER.debug("Turning %s off", self._unique_id) await self._device.command("off") diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py index 5f0b3d8f03c..0b10130a88f 100644 --- a/homeassistant/components/hive/alarm_control_panel.py +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -65,7 +65,7 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity): """Send arm away command.""" await self.hive.alarm.setMode(self.device, "away") - async def async_alarm_trigger(self, code=None) -> None: + async def async_alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" await self.hive.alarm.setMode(self.device, "sos") diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 313c78275e7..6306b48f733 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -68,7 +68,7 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): super().__init__(hive, hive_device) self.entity_description = entity_description - async def async_update(self): + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.sensor.getSensor(self.device) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index d6dfcfa6b2c..b6f4f8270b4 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,6 +1,7 @@ """Support for the Hive climate devices.""" from datetime import timedelta import logging +from typing import Any import voluptuous as vol @@ -120,7 +121,7 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): await self.hive.heating.setMode(self.device, new_mode) @refresh_system - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" new_temperature = kwargs.get(ATTR_TEMPERATURE) if new_temperature is not None: @@ -153,7 +154,7 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): """Handle boost heating service call.""" await self.hive.heating.setBoostOff(self.device) - async def async_update(self): + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.heating.getClimate(self.device) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 809feb19d5e..0cc222c27c3 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -59,7 +59,7 @@ class HiveSensorEntity(HiveEntity, SensorEntity): super().__init__(hive, hive_device) self.entity_description = entity_description - async def async_update(self): + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.sensor.getSensor(self.device) diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index f72f228c595..d9d1caded8c 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -52,16 +53,16 @@ class HiveSwitch(HiveEntity, SwitchEntity): self.entity_description = entity_description @refresh_system - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.hive.switch.turnOn(self.device) @refresh_system - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.hive.switch.turnOff(self.device) - async def async_update(self): + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.switch.getSwitch(self.device) diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 0e7f2453c92..1860d7b092e 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -88,7 +88,7 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): await self.hive.hotwater.setMode(self.device, "OFF") @refresh_system - async def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode: str) -> None: """Set operation mode.""" new_mode = HASS_TO_HIVE_STATE[operation_mode] await self.hive.hotwater.setMode(self.device, new_mode) @@ -101,7 +101,7 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): elif on_off == "off": await self.hive.hotwater.setBoostOff(self.device) - async def async_update(self): + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.hotwater.getWaterHeater(self.device) diff --git a/homeassistant/components/hlk_sw16/switch.py b/homeassistant/components/hlk_sw16/switch.py index 44377093560..2189a285ad8 100644 --- a/homeassistant/components/hlk_sw16/switch.py +++ b/homeassistant/components/hlk_sw16/switch.py @@ -1,4 +1,6 @@ """Support for HLK-SW16 switches.""" +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -36,10 +38,10 @@ class SW16Switch(SW16Device, SwitchEntity): """Return true if device is on.""" return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._client.turn_on(self._device_port) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._client.turn_off(self._device_port) diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index 5f1948ea0ce..c00e8303b66 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -72,7 +72,7 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): """Return true if the binary sensor is available.""" return self._state is not None - async def async_update(self): + async def async_update(self) -> None: """Update the binary sensor's status.""" state = self.device.appliance.status.get(self._update_key, {}) if not state: diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py index d1586450881..c7418060eaf 100644 --- a/homeassistant/components/home_connect/light.py +++ b/homeassistant/components/home_connect/light.py @@ -1,6 +1,7 @@ """Provides a light for Home Connect.""" import logging from math import ceil +from typing import Any from homeconnect.api import HomeConnectError @@ -153,7 +154,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): self.async_entity_update() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Switch the light off.""" _LOGGER.debug("Switching light off for: %s", self.name) try: diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index f1d288f1f04..de409484f1e 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -57,7 +57,7 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): """Return true if the sensor is available.""" return self._state is not None - async def async_update(self): + async def async_update(self) -> None: """Update the sensor's status.""" status = self.device.appliance.status if self._key not in status: diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index 8e84081beb0..2278c2b1f2d 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -1,5 +1,6 @@ """Provides a switch for Home Connect.""" import logging +from typing import Any from homeconnect.api import HomeConnectError @@ -64,7 +65,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): """Return true if the entity is available.""" return True - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Start the program.""" _LOGGER.debug("Tried to turn on program %s", self.program_name) try: @@ -75,7 +76,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): _LOGGER.error("Error while trying to start program: %s", err) self.async_entity_update() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Stop the program.""" _LOGGER.debug("Tried to stop program %s", self.program_name) try: @@ -84,7 +85,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): _LOGGER.error("Error while trying to stop program: %s", err) self.async_entity_update() - async def async_update(self): + async def async_update(self) -> None: """Update the switch's status.""" state = self.device.appliance.status.get(BSH_ACTIVE_PROGRAM, {}) if state.get(ATTR_VALUE) == self.program_name: @@ -107,7 +108,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): """Return true if the switch is on.""" return bool(self._state) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Switch the device on.""" _LOGGER.debug("Tried to switch on %s", self.name) try: @@ -119,7 +120,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): self._state = False self.async_entity_update() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Switch the device off.""" _LOGGER.debug("tried to switch off %s", self.name) try: @@ -133,7 +134,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): self._state = True self.async_entity_update() - async def async_update(self): + async def async_update(self) -> None: """Update the switch's status.""" if ( self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE) diff --git a/homeassistant/components/home_plus_control/switch.py b/homeassistant/components/home_plus_control/switch.py index 9c0b12dd736..6e92fac3b72 100644 --- a/homeassistant/components/home_plus_control/switch.py +++ b/homeassistant/components/home_plus_control/switch.py @@ -1,5 +1,6 @@ """Legrand Home+ Control Switch Entity Module that uses the HomeAssistant DataUpdateCoordinator.""" from functools import partial +from typing import Any from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -118,14 +119,14 @@ class HomeControlSwitchEntity(CoordinatorEntity, SwitchEntity): """Return entity state.""" return self.module.status == "on" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" # Do the turning on. await self.module.turn_on() # Update the data await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.module.turn_off() # Update the data diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 78a03a28a4a..2d106b90072 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -1,6 +1,8 @@ """Support for Homematic thermostats.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( PRESET_BOOST, @@ -131,7 +133,7 @@ class HMThermostat(HMDevice, ClimateEntity): """Return the target temperature.""" return self._data.get(self._state) - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return None diff --git a/homeassistant/components/homematic/switch.py b/homeassistant/components/homematic/switch.py index 9d37b8a6322..7accb011ebf 100644 --- a/homeassistant/components/homematic/switch.py +++ b/homeassistant/components/homematic/switch.py @@ -1,6 +1,8 @@ """Support for HomeMatic switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -50,11 +52,11 @@ class HMSwitch(HMDevice, SwitchEntity): return None - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._hmdevice.on(self._channel) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._hmdevice.off(self._channel) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 802cacb1d76..ae3ecf9dc9d 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -194,7 +194,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): """Return the maximum temperature.""" return self._device.maxTemperature - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 82eafe1f212..1b39633bc15 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -106,11 +106,11 @@ class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity): """Return true if switch is on.""" return self._device.functionalChannels[self._channel].on - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._device.turn_on(self._channel) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.turn_off(self._channel) @@ -155,11 +155,11 @@ class HomematicipGroupSwitch(HomematicipGenericEntity, SwitchEntity): return state_attr - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the group on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the group off.""" await self._device.turn_off() diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 11b899dc0f5..abcd1d6f340 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -252,7 +252,7 @@ class HoneywellUSThermostat(ClimateEntity): except somecomfort.SomeComfortError: _LOGGER.error("Temperature %.1f out of range", temperature) - def set_temperature(self, **kwargs) -> None: + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map): self._set_temperature(**kwargs) @@ -352,6 +352,6 @@ class HoneywellUSThermostat(ClimateEntity): else: self.set_hvac_mode(HVACMode.OFF) - async def async_update(self): + async def async_update(self) -> None: """Get the latest state from the service.""" await self._data.async_update() diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 3cbe9ad3959..a19199eb5b3 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from horimote import Client, keys from horimote.exceptions import AuthenticationError @@ -105,7 +106,7 @@ class HorizonDevice(MediaPlayerEntity): return self._state @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update(self): + def update(self) -> None: """Update State using the media server running on the Horizon.""" try: if self._client.is_powered_on(): @@ -115,37 +116,37 @@ class HorizonDevice(MediaPlayerEntity): except OSError: self._state = STATE_OFF - def turn_on(self): + def turn_on(self) -> None: """Turn the device on.""" if self._state == STATE_OFF: self._send_key(self._keys.POWER) - def turn_off(self): + def turn_off(self) -> None: """Turn the device off.""" if self._state != STATE_OFF: self._send_key(self._keys.POWER) - def media_previous_track(self): + def media_previous_track(self) -> None: """Channel down.""" self._send_key(self._keys.CHAN_DOWN) self._state = STATE_PLAYING - def media_next_track(self): + def media_next_track(self) -> None: """Channel up.""" self._send_key(self._keys.CHAN_UP) self._state = STATE_PLAYING - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._send_key(self._keys.PAUSE) self._state = STATE_PLAYING - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._send_key(self._keys.PAUSE) self._state = STATE_PAUSED - def media_play_pause(self): + def media_play_pause(self) -> None: """Send play/pause command.""" self._send_key(self._keys.PAUSE) if self._state == STATE_PAUSED: @@ -153,7 +154,7 @@ class HorizonDevice(MediaPlayerEntity): else: self._state = STATE_PAUSED - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media / switch to channel.""" if MEDIA_TYPE_CHANNEL == media_type: try: diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 43fc8456281..fcb8788c646 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -157,7 +157,7 @@ class HpIloSensor(SensorEntity): """Return the device state attributes.""" return self._state_attributes - def update(self): + def update(self) -> None: """Get the latest data from HP iLO and updates the states.""" # Call the API for new data. Each sensor will re-trigger this # same exact call, but that's fine. Results should be cached for diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 6328ad63bc2..1887498e604 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -69,20 +69,20 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): return f"{self._shade_name} Battery" @property - def native_value(self): + def native_value(self) -> int: """Get the current value in percentage.""" return round( self._shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100 ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self.async_on_remove( self.coordinator.async_add_listener(self._async_update_shade_from_group) ) @callback - def _async_update_shade_from_group(self): + def _async_update_shade_from_group(self) -> None: """Update with new data from the coordinator.""" self._shade.raw_data = self.data.get_raw_data(self._shade.id) self.async_write_ha_state() diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index f9ab469a48d..6a2f1467b19 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -1,6 +1,9 @@ """Binary sensor platform for hvv_departures.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from aiohttp import ClientConnectorError import async_timeout @@ -136,7 +139,7 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): return self.coordinator.data[self.idx]["state"] @property - def available(self): + def available(self) -> bool: """Return if entity is available.""" return ( self.coordinator.last_update_success @@ -175,7 +178,7 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): return BinarySensorDeviceClass.PROBLEM @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if not ( self.coordinator.last_update_success @@ -188,13 +191,13 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): if v is not None } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self.async_on_remove( self.coordinator.async_add_listener(self.async_write_ha_state) ) - async def async_update(self): + async def async_update(self) -> None: """Update the entity. Only used by the generic entity update service. diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 54224bf5fb2..93e1002edf4 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for hvv.""" from datetime import timedelta import logging +from typing import Any from aiohttp import ClientConnectorError from pygti.exceptions import InvalidAuth @@ -66,7 +67,7 @@ class HVVDepartureSensor(SensorEntity): self.gti = hub.gti @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self, **kwargs): + async def async_update(self, **kwargs: Any) -> None: """Update the sensor.""" departure_time = utcnow() + timedelta( minutes=self.config_entry.options.get("offset", 0) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 51ee9cb24ec..3b496355a56 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -80,7 +80,7 @@ def setup_platform( class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): """A sensor implementation for Hydrawise device.""" - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" _LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name) mydata = self.hass.data[DATA_HYDRAWISE].data diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index cfa8bc84959..f9201cc0420 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -73,7 +73,7 @@ def setup_platform( class HydrawiseSensor(HydrawiseEntity, SensorEntity): """A sensor implementation for Hydrawise device.""" - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" mydata = self.hass.data[DATA_HYDRAWISE].data _LOGGER.debug("Updating Hydrawise sensor: %s", self.name) diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index 680b92ca3ee..ed4fa11317e 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -85,7 +86,7 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity): super().__init__(data, description) self._default_watering_timer = default_watering_timer - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" relay_data = self.data["relay"] - 1 if self.entity_description.key == "manual_watering": @@ -95,7 +96,7 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity): elif self.entity_description.key == "auto_watering": self.hass.data[DATA_HYDRAWISE].data.suspend_zone(0, relay_data) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" relay_data = self.data["relay"] - 1 if self.entity_description.key == "manual_watering": @@ -103,7 +104,7 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity): elif self.entity_description.key == "auto_watering": self.hass.data[DATA_HYDRAWISE].data.suspend_zone(365, relay_data) - def update(self): + def update(self) -> None: """Update device state.""" relay_data = self.data["relay"] - 1 mydata = self.hass.data[DATA_HYDRAWISE].data From 7f55e26cd143f77b8769f80d24e16e4311023a15 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 20:55:27 +0200 Subject: [PATCH 768/903] Improve type hints in icloud (#77531) --- homeassistant/components/icloud/device_tracker.py | 11 ++++++----- homeassistant/components/icloud/sensor.py | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 35a411ff11c..a273b7909e2 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -72,7 +72,7 @@ class IcloudTrackerEntity(TrackerEntity): """Set up the iCloud tracker entity.""" self._account = account self._device = device - self._unsub_dispatcher = None + self._unsub_dispatcher: CALLBACK_TYPE | None = None @property def unique_id(self) -> str: @@ -130,15 +130,16 @@ class IcloudTrackerEntity(TrackerEntity): name=self._device.name, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( self.hass, self._account.signal_device_update, self.async_write_ha_state ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Clean up after entity before removal.""" - self._unsub_dispatcher() + if self._unsub_dispatcher: + self._unsub_dispatcher() def icon_for_icloud_device(icloud_device: IcloudDevice) -> str: diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index e747d898dea..c3b23057ebc 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -62,7 +62,7 @@ class IcloudDeviceBatterySensor(SensorEntity): """Initialize the battery sensor.""" self._account = account self._device = device - self._unsub_dispatcher = None + self._unsub_dispatcher: CALLBACK_TYPE | None = None @property def unique_id(self) -> str: @@ -103,12 +103,13 @@ class IcloudDeviceBatterySensor(SensorEntity): name=self._device.name, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( self.hass, self._account.signal_device_update, self.async_write_ha_state ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Clean up after entity before removal.""" - self._unsub_dispatcher() + if self._unsub_dispatcher: + self._unsub_dispatcher() From b366354d55414a94cf1100fbab49fbb5dc5a1f02 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 20:56:10 +0200 Subject: [PATCH 769/903] Improve type hints in insteon (#77532) --- homeassistant/components/insteon/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 8806caf3999..922ef141350 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -208,9 +208,9 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): mode = list(HVAC_MODES)[list(HVAC_MODES.values()).index(hvac_mode)] await self._insteon_device.async_set_mode(mode) - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Set new humidity level.""" - change = humidity - self.target_humidity + change = humidity - (self.target_humidity or 0) high = self._insteon_device.groups[HUMIDITY_HIGH].value + change low = self._insteon_device.groups[HUMIDITY_LOW].value + change await self._insteon_device.async_set_humidity_low_set_point(low) From c8c9a4b09f7fca532993ee0a315b7fd3169fe298 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Aug 2022 20:58:38 +0200 Subject: [PATCH 770/903] Migrate osramlightify light to color_mode (#70915) --- .../components/osramlightify/light.py | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 6c5c7179006..9fc54d0352c 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -16,11 +16,10 @@ from homeassistant.components.light import ( ATTR_TRANSITION, EFFECT_RANDOM, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ColorMode, LightEntity, LightEntityFeature, + brightness_supported, ) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant @@ -206,21 +205,35 @@ class Luminary(LightEntity): """Get a unique ID (not implemented).""" raise NotImplementedError + def _get_supported_color_modes(self): + """Get supported color modes.""" + color_modes = set() + if "temp" in self._luminary.supported_features(): + color_modes.add(ColorMode.COLOR_TEMP) + + if "rgb" in self._luminary.supported_features(): + color_modes.add(ColorMode.HS) + + if not color_modes and "lum" in self._luminary.supported_features(): + color_modes.add(ColorMode.BRIGHTNESS) + + if not color_modes: + color_modes.add(ColorMode.ONOFF) + + return color_modes + def _get_supported_features(self): """Get list of supported features.""" features = 0 if "lum" in self._luminary.supported_features(): - features = features | SUPPORT_BRIGHTNESS | LightEntityFeature.TRANSITION + features = features | LightEntityFeature.TRANSITION if "temp" in self._luminary.supported_features(): - features = features | SUPPORT_COLOR_TEMP | LightEntityFeature.TRANSITION + features = features | LightEntityFeature.TRANSITION if "rgb" in self._luminary.supported_features(): features = ( - features - | SUPPORT_COLOR - | LightEntityFeature.TRANSITION - | LightEntityFeature.EFFECT + features | LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT ) return features @@ -350,31 +363,42 @@ class Luminary(LightEntity): def update_static_attributes(self): """Update static attributes of the luminary.""" self._unique_id = self._get_unique_id() + self._attr_supported_color_modes = self._get_supported_color_modes() self._supported_features = self._get_supported_features() self._effect_list = self._get_effect_list() - if self._supported_features & SUPPORT_COLOR_TEMP: + if ColorMode.COLOR_TEMP in self._attr_supported_color_modes: self._min_mireds = color_util.color_temperature_kelvin_to_mired( self._luminary.max_temp() or DEFAULT_KELVIN ) self._max_mireds = color_util.color_temperature_kelvin_to_mired( self._luminary.min_temp() or DEFAULT_KELVIN ) + if len(self._attr_supported_color_modes == 1): + # The light supports only a single color mode + self._attr_color_mode = list(self._attr_supported_color_modes)[0] def update_dynamic_attributes(self): """Update dynamic attributes of the luminary.""" self._is_on = self._luminary.on() self._available = self._luminary.reachable() and not self._luminary.deleted() - if self._supported_features & SUPPORT_BRIGHTNESS: + if brightness_supported(self._attr_supported_color_modes): self._brightness = int(self._luminary.lum() * 2.55) - if self._supported_features & SUPPORT_COLOR_TEMP: + if ColorMode.COLOR_TEMP in self._attr_supported_color_modes: self._color_temp = color_util.color_temperature_kelvin_to_mired( self._luminary.temp() or DEFAULT_KELVIN ) - if self._supported_features & SUPPORT_COLOR: + if ColorMode.HS in self._attr_supported_color_modes: self._rgb_color = self._luminary.rgb() + if len(self._attr_supported_color_modes > 1): + # The light supports hs + color temp, determine which one it is + if self._rgb_color == (0, 0, 0): + self._attr_color_mode = ColorMode.COLOR_TEMP + else: + self._attr_color_mode = ColorMode.HS + def update(self) -> None: """Synchronize state with bridge.""" changed = self.update_func() From 809aa6b30f3f0ce9235d0c54d0bfce718491d40c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 21:05:49 +0200 Subject: [PATCH 771/903] Adjust type hints in gitlab_ci (#77493) --- homeassistant/components/gitlab_ci/sensor.py | 74 ++++++-------------- 1 file changed, 20 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index 21e1221e6b8..ed0db5416c1 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -55,7 +55,7 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the GitLab sensor platform.""" - _name = config.get(CONF_NAME) + _name = config.get(CONF_NAME, DEFAULT_NAME) _interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) _url = config.get(CONF_URL) @@ -74,55 +74,18 @@ class GitLabSensor(SensorEntity): _attr_attribution = ATTRIBUTION - def __init__(self, gitlab_data, name): + def __init__(self, gitlab_data: GitLabData, name: str) -> None: """Initialize the GitLab sensor.""" - self._available = False - self._state = None - self._started_at = None - self._finished_at = None - self._duration = None - self._commit_id = None - self._commit_date = None - self._build_id = None - self._branch = None + self._attr_available = False self._gitlab_data = gitlab_data - self._name = name + self._attr_name = name @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_BUILD_STATUS: self._state, - ATTR_BUILD_STARTED: self._started_at, - ATTR_BUILD_FINISHED: self._finished_at, - ATTR_BUILD_DURATION: self._duration, - ATTR_BUILD_COMMIT_ID: self._commit_id, - ATTR_BUILD_COMMIT_DATE: self._commit_date, - ATTR_BUILD_ID: self._build_id, - ATTR_BUILD_BRANCH: self._branch, - } - - @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend.""" - if self._state == "success": + if self.native_value == "success": return ICON_HAPPY - if self._state == "failed": + if self.native_value == "failed": return ICON_SAD return ICON_OTHER @@ -130,15 +93,18 @@ class GitLabSensor(SensorEntity): """Collect updated data from GitLab API.""" self._gitlab_data.update() - self._state = self._gitlab_data.status - self._started_at = self._gitlab_data.started_at - self._finished_at = self._gitlab_data.finished_at - self._duration = self._gitlab_data.duration - self._commit_id = self._gitlab_data.commit_id - self._commit_date = self._gitlab_data.commit_date - self._build_id = self._gitlab_data.build_id - self._branch = self._gitlab_data.branch - self._available = self._gitlab_data.available + self._attr_native_value = self._gitlab_data.status + self._attr_extra_state_attributes = { + ATTR_BUILD_STATUS: self._gitlab_data.status, + ATTR_BUILD_STARTED: self._gitlab_data.started_at, + ATTR_BUILD_FINISHED: self._gitlab_data.finished_at, + ATTR_BUILD_DURATION: self._gitlab_data.duration, + ATTR_BUILD_COMMIT_ID: self._gitlab_data.commit_id, + ATTR_BUILD_COMMIT_DATE: self._gitlab_data.commit_date, + ATTR_BUILD_ID: self._gitlab_data.build_id, + ATTR_BUILD_BRANCH: self._gitlab_data.branch, + } + self._attr_available = self._gitlab_data.available class GitLabData: @@ -162,7 +128,7 @@ class GitLabData: self.build_id = None self.branch = None - def _update(self): + def _update(self) -> None: try: _projects = self._gitlab.projects.get(self._gitlab_id) _last_pipeline = _projects.pipelines.list(page=1)[0] From 67db3802535a57dd86f4568a06a60eae94388d46 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 21:06:31 +0200 Subject: [PATCH 772/903] Adjust type hints in greewave (#77492) --- homeassistant/components/greenwave/light.py | 27 +++++---------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index 1e991feee90..9f904018796 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -72,29 +72,14 @@ class GreenwaveLight(LightEntity): def __init__(self, light, host, token, gatewaydata): """Initialize a Greenwave Reality Light.""" self._did = int(light["did"]) - self._name = light["name"] + self._attr_name = light["name"] self._state = int(light["state"]) - self._brightness = greenwave.hass_brightness(light) + self._attr_brightness = greenwave.hass_brightness(light) self._host = host - self._online = greenwave.check_online(light) + self._attr_available = greenwave.check_online(light) self._token = token self._gatewaydata = gatewaydata - @property - def available(self): - """Return True if entity is available.""" - return self._online - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def brightness(self): - """Return the brightness of the light.""" - return self._brightness - @property def is_on(self): """Return true if light is on.""" @@ -116,9 +101,9 @@ class GreenwaveLight(LightEntity): bulbs = self._gatewaydata.greenwave self._state = int(bulbs[self._did]["state"]) - self._brightness = greenwave.hass_brightness(bulbs[self._did]) - self._online = greenwave.check_online(bulbs[self._did]) - self._name = bulbs[self._did]["name"] + self._attr_brightness = greenwave.hass_brightness(bulbs[self._did]) + self._attr_available = greenwave.check_online(bulbs[self._did]) + self._attr_name = bulbs[self._did]["name"] class GatewayData: From 4655ed995ef89ac0c37cbd62def1b4fa856739f2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Aug 2022 21:07:50 +0200 Subject: [PATCH 773/903] Fix resetting of attributes in EntityRegistry.async_get_or_create (#77516) * Fix resetting of attributes in EntityRegistry.async_get_or_create * Fix typing * Fix resetting config entry * Improve test * Update tests --- homeassistant/helpers/entity_registry.py | 91 +++++++++++-------- .../alarm_control_panel/test_device_action.py | 8 +- .../binary_sensor/test_device_condition.py | 5 +- .../binary_sensor/test_device_trigger.py | 5 +- .../components/config/test_entity_registry.py | 53 +++++------ tests/components/cover/test_device_action.py | 15 ++- .../components/cover/test_device_condition.py | 12 +-- tests/components/cover/test_device_trigger.py | 12 +-- .../sensor/test_device_condition.py | 5 +- .../components/sensor/test_device_trigger.py | 5 +- tests/helpers/test_entity_registry.py | 54 +++++++++-- 11 files changed, 151 insertions(+), 114 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index ff38a48da75..23e9cc5f752 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -12,7 +12,7 @@ from __future__ import annotations from collections import UserDict from collections.abc import Callable, Iterable, Mapping import logging -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast import attr import voluptuous as vol @@ -53,6 +53,8 @@ if TYPE_CHECKING: from .entity import EntityCategory +T = TypeVar("T") + PATH_REGISTRY = "entity_registry.yaml" DATA_REGISTRY = "entity_registry" EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" @@ -324,41 +326,43 @@ class EntityRegistry: disabled_by: RegistryEntryDisabler | None = None, hidden_by: RegistryEntryHider | None = None, # Data that we want entry to have - area_id: str | None = None, - capabilities: Mapping[str, Any] | None = None, - config_entry: ConfigEntry | None = None, - device_id: str | None = None, - entity_category: EntityCategory | None = None, - has_entity_name: bool | None = None, - original_device_class: str | None = None, - original_icon: str | None = None, - original_name: str | None = None, - supported_features: int | None = None, - unit_of_measurement: str | None = None, + area_id: str | None | UndefinedType = UNDEFINED, + capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, + config_entry: ConfigEntry | None | UndefinedType = UNDEFINED, + device_id: str | None | UndefinedType = UNDEFINED, + entity_category: EntityCategory | UndefinedType | None = UNDEFINED, + has_entity_name: bool | UndefinedType = UNDEFINED, + original_device_class: str | None | UndefinedType = UNDEFINED, + original_icon: str | None | UndefinedType = UNDEFINED, + original_name: str | None | UndefinedType = UNDEFINED, + supported_features: int | None | UndefinedType = UNDEFINED, + unit_of_measurement: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" - config_entry_id = None - if config_entry: + config_entry_id: str | None | UndefinedType = UNDEFINED + if not config_entry: + config_entry_id = None + elif config_entry is not UNDEFINED: config_entry_id = config_entry.entry_id + supported_features = supported_features or 0 + entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: return self.async_update_entity( entity_id, - area_id=area_id or UNDEFINED, - capabilities=capabilities or UNDEFINED, - config_entry_id=config_entry_id or UNDEFINED, - device_id=device_id or UNDEFINED, - entity_category=entity_category or UNDEFINED, - has_entity_name=has_entity_name - if has_entity_name is not None - else UNDEFINED, - original_device_class=original_device_class or UNDEFINED, - original_icon=original_icon or UNDEFINED, - original_name=original_name or UNDEFINED, - supported_features=supported_features or UNDEFINED, - unit_of_measurement=unit_of_measurement or UNDEFINED, + area_id=area_id, + capabilities=capabilities, + config_entry_id=config_entry_id, + device_id=device_id, + entity_category=entity_category, + has_entity_name=has_entity_name, + original_device_class=original_device_class, + original_icon=original_icon, + original_name=original_name, + supported_features=supported_features, + unit_of_measurement=unit_of_measurement, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -380,32 +384,41 @@ class EntityRegistry: if ( disabled_by is None and config_entry + and config_entry is not UNDEFINED and config_entry.pref_disable_new_entities ): disabled_by = RegistryEntryDisabler.INTEGRATION from .entity import EntityCategory # pylint: disable=import-outside-toplevel - if entity_category and not isinstance(entity_category, EntityCategory): + if ( + entity_category + and entity_category is not UNDEFINED + and not isinstance(entity_category, EntityCategory) + ): raise ValueError("entity_category must be a valid EntityCategory instance") + def none_if_undefined(value: T | UndefinedType) -> T | None: + """Return None if value is UNDEFINED, otherwise return value.""" + return None if value is UNDEFINED else value + entry = RegistryEntry( - area_id=area_id, - capabilities=capabilities, - config_entry_id=config_entry_id, - device_id=device_id, + area_id=none_if_undefined(area_id), + capabilities=none_if_undefined(capabilities), + config_entry_id=none_if_undefined(config_entry_id), + device_id=none_if_undefined(device_id), disabled_by=disabled_by, - entity_category=entity_category, + entity_category=none_if_undefined(entity_category), entity_id=entity_id, hidden_by=hidden_by, - has_entity_name=has_entity_name or False, - original_device_class=original_device_class, - original_icon=original_icon, - original_name=original_name, + has_entity_name=none_if_undefined(has_entity_name) or False, + original_device_class=none_if_undefined(original_device_class), + original_icon=none_if_undefined(original_icon), + original_name=none_if_undefined(original_name), platform=platform, - supported_features=supported_features or 0, + supported_features=none_if_undefined(supported_features) or 0, unique_id=unique_id, - unit_of_measurement=unit_of_measurement, + unit_of_measurement=none_if_undefined(unit_of_measurement), ) self.entities[entity_id] = entry _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index ae10a199f9e..b02fac96633 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -226,6 +226,8 @@ async def test_get_action_capabilities( """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -239,8 +241,6 @@ async def test_get_action_capabilities( platform.ENTITIES["no_arm_code"].unique_id, device_id=device_entry.id, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() expected_capabilities = { "arm_away": {"extra_fields": []}, @@ -270,6 +270,8 @@ async def test_get_action_capabilities_arm_code( """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -283,8 +285,6 @@ async def test_get_action_capabilities_arm_code( platform.ENTITIES["arm_code"].unique_id, device_id=device_entry.id, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() expected_capabilities = { "arm_away": { diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 11d925a8d98..2ef6fad7817 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -49,6 +49,8 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr """Test we get the expected conditions from a binary_sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -64,9 +66,6 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr device_id=device_entry.id, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - expected_conditions = [ { "condition": "device", diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 5eca2d36109..549318090a4 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -49,6 +49,8 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat """Test we get the expected triggers from a binary_sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -64,9 +66,6 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat device_id=device_entry.id, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - expected_triggers = [ { "platform": "device", diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index e472736ee6c..11a2adb5646 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -9,6 +9,7 @@ from homeassistant.helpers.entity_registry import ( RegistryEntry, RegistryEntryDisabler, RegistryEntryHider, + async_get as async_get_entity_registry, ) from tests.common import ( @@ -374,25 +375,15 @@ async def test_update_entity(hass, client): async def test_update_entity_require_restart(hass, client): """Test updating entity.""" + entity_id = "test_domain.test_platform_1234" config_entry = MockConfigEntry(domain="test_platform") config_entry.add_to_hass(hass) - mock_registry( - hass, - { - "test_domain.world": RegistryEntry( - config_entry_id=config_entry.entry_id, - entity_id="test_domain.world", - unique_id="1234", - # Using component.async_add_entities is equal to platform "domain" - platform="test_platform", - ) - }, - ) platform = MockEntityPlatform(hass) + platform.config_entry = config_entry entity = MockEntity(unique_id="1234") await platform.async_add_entities([entity]) - state = hass.states.get("test_domain.world") + state = hass.states.get(entity_id) assert state is not None # UPDATE DISABLED_BY TO NONE @@ -400,7 +391,7 @@ async def test_update_entity_require_restart(hass, client): { "id": 8, "type": "config/entity_registry/update", - "entity_id": "test_domain.world", + "entity_id": entity_id, "disabled_by": None, } ) @@ -416,7 +407,7 @@ async def test_update_entity_require_restart(hass, client): "device_id": None, "disabled_by": None, "entity_category": None, - "entity_id": "test_domain.world", + "entity_id": entity_id, "icon": None, "hidden_by": None, "has_entity_name": False, @@ -434,6 +425,7 @@ async def test_update_entity_require_restart(hass, client): async def test_enable_entity_disabled_device(hass, client, device_registry): """Test enabling entity of disabled device.""" + entity_id = "test_domain.test_platform_1234" config_entry = MockConfigEntry(domain="test_platform") config_entry.add_to_hass(hass) @@ -445,33 +437,30 @@ async def test_enable_entity_disabled_device(hass, client, device_registry): model="model", disabled_by=DeviceEntryDisabler.USER, ) + device_info = { + "connections": {("ethernet", "12:34:56:78:90:AB:CD:EF")}, + } - mock_registry( - hass, - { - "test_domain.world": RegistryEntry( - config_entry_id=config_entry.entry_id, - entity_id="test_domain.world", - unique_id="1234", - # Using component.async_add_entities is equal to platform "domain" - platform="test_platform", - device_id=device.id, - ) - }, - ) platform = MockEntityPlatform(hass) - entity = MockEntity(unique_id="1234") + platform.config_entry = config_entry + entity = MockEntity(unique_id="1234", device_info=device_info) await platform.async_add_entities([entity]) - state = hass.states.get("test_domain.world") - assert state is not None + state = hass.states.get(entity_id) + assert state is None + + entity_reg = async_get_entity_registry(hass) + entity_entry = entity_reg.async_get(entity_id) + assert entity_entry.config_entry_id == config_entry.entry_id + assert entity_entry.device_id == device.id + assert entity_entry.disabled_by == RegistryEntryDisabler.DEVICE # UPDATE DISABLED_BY TO NONE await client.send_json( { "id": 8, "type": "config/entity_registry/update", - "entity_id": "test_domain.world", + "entity_id": entity_id, "disabled_by": None, } ) diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index d4cf5901bbf..8bbe42e9537 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -181,6 +181,8 @@ async def test_get_action_capabilities( ), ) ent = platform.ENTITIES[0] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -192,9 +194,6 @@ async def test_get_action_capabilities( DOMAIN, "test", ent.unique_id, device_id=device_entry.id ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id ) @@ -215,6 +214,8 @@ async def test_get_action_capabilities_set_pos( platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() ent = platform.ENTITIES[1] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -226,9 +227,6 @@ async def test_get_action_capabilities_set_pos( DOMAIN, "test", ent.unique_id, device_id=device_entry.id ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - expected_capabilities = { "extra_fields": [ { @@ -264,6 +262,8 @@ async def test_get_action_capabilities_set_tilt_pos( platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() ent = platform.ENTITIES[3] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -275,9 +275,6 @@ async def test_get_action_capabilities_set_tilt_pos( DOMAIN, "test", ent.unique_id, device_id=device_entry.id ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - expected_capabilities = { "extra_fields": [ { diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 735996ba574..d387a492a9d 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -171,6 +171,8 @@ async def test_get_condition_capabilities( platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() ent = platform.ENTITIES[0] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -182,8 +184,6 @@ async def test_get_condition_capabilities( DOMAIN, "test", ent.unique_id, device_id=device_entry.id ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id ) @@ -202,6 +202,8 @@ async def test_get_condition_capabilities_set_pos( platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() ent = platform.ENTITIES[1] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -213,8 +215,6 @@ async def test_get_condition_capabilities_set_pos( DOMAIN, "test", ent.unique_id, device_id=device_entry.id ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - expected_capabilities = { "extra_fields": [ { @@ -256,6 +256,8 @@ async def test_get_condition_capabilities_set_tilt_pos( platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() ent = platform.ENTITIES[3] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -267,8 +269,6 @@ async def test_get_condition_capabilities_set_tilt_pos( DOMAIN, "test", ent.unique_id, device_id=device_entry.id ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - expected_capabilities = { "extra_fields": [ { diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index d92d444920c..1d75e996335 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -191,6 +191,8 @@ async def test_get_trigger_capabilities( platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() ent = platform.ENTITIES[0] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -202,8 +204,6 @@ async def test_get_trigger_capabilities( DOMAIN, "test", ent.unique_id, device_id=device_entry.id ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id ) @@ -226,6 +226,8 @@ async def test_get_trigger_capabilities_set_pos( platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() ent = platform.ENTITIES[1] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -237,8 +239,6 @@ async def test_get_trigger_capabilities_set_pos( DOMAIN, "test", ent.unique_id, device_id=device_entry.id ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - expected_capabilities = { "extra_fields": [ { @@ -288,6 +288,8 @@ async def test_get_trigger_capabilities_set_tilt_pos( platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() ent = platform.ENTITIES[3] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -299,8 +301,6 @@ async def test_get_trigger_capabilities_set_tilt_pos( DOMAIN, "test", ent.unique_id, device_id=device_entry.id ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - expected_capabilities = { "extra_fields": [ { diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index acba724c842..ab143e615d1 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -51,6 +51,8 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr """Test we get the expected conditions from a sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -66,9 +68,6 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr device_id=device_entry.id, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - expected_conditions = [ { "condition": "device", diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index eb46d7b458b..2592d51dcdf 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -55,6 +55,8 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat """Test we get the expected triggers from a sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -70,9 +72,6 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat device_id=device_entry.id, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - expected_triggers = [ { "platform": "device", diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index ba69a98d5a8..9c2592eace0 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -79,8 +79,8 @@ def test_get_or_create_updates_data(registry): device_id="mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, - hidden_by=er.RegistryEntryHider.INTEGRATION, has_entity_name=True, + hidden_by=er.RegistryEntryHider.INTEGRATION, original_device_class="mock-device-class", original_icon="initial-original_icon", original_name="initial-original_name", @@ -99,10 +99,10 @@ def test_get_or_create_updates_data(registry): device_id="mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, + has_entity_name=True, hidden_by=er.RegistryEntryHider.INTEGRATION, icon=None, id=orig_entry.id, - has_entity_name=True, name=None, original_device_class="mock-device-class", original_icon="initial-original_icon", @@ -122,9 +122,9 @@ def test_get_or_create_updates_data(registry): config_entry=new_config_entry, device_id="new-mock-dev-id", disabled_by=er.RegistryEntryDisabler.USER, - entity_category=None, - hidden_by=er.RegistryEntryHider.USER, + entity_category=EntityCategory.DIAGNOSTIC, has_entity_name=False, + hidden_by=er.RegistryEntryHider.USER, original_device_class="new-mock-device-class", original_icon="updated-original_icon", original_name="updated-original_name", @@ -142,11 +142,11 @@ def test_get_or_create_updates_data(registry): device_class=None, device_id="new-mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated - entity_category=EntityCategory.CONFIG, + entity_category=EntityCategory.DIAGNOSTIC, + has_entity_name=False, hidden_by=er.RegistryEntryHider.INTEGRATION, # Should not be updated icon=None, id=orig_entry.id, - has_entity_name=False, name=None, original_device_class="new-mock-device-class", original_icon="updated-original_icon", @@ -155,6 +155,48 @@ def test_get_or_create_updates_data(registry): unit_of_measurement="updated-unit_of_measurement", ) + new_entry = registry.async_get_or_create( + "light", + "hue", + "5678", + area_id=None, + capabilities=None, + config_entry=None, + device_id=None, + disabled_by=None, + entity_category=None, + has_entity_name=None, + hidden_by=None, + original_device_class=None, + original_icon=None, + original_name=None, + supported_features=None, + unit_of_measurement=None, + ) + + assert new_entry == er.RegistryEntry( + "light.hue_5678", + "5678", + "hue", + area_id=None, + capabilities=None, + config_entry_id=None, + device_class=None, + device_id=None, + disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated + entity_category=None, + has_entity_name=None, + hidden_by=er.RegistryEntryHider.INTEGRATION, # Should not be updated + icon=None, + id=orig_entry.id, + name=None, + original_device_class=None, + original_icon=None, + original_name=None, + supported_features=0, # supported_features is stored as an int + unit_of_measurement=None, + ) + def test_get_or_create_suggested_object_id_conflict_register(registry): """Test that we don't generate an entity id that is already registered.""" From 46affe5c824b9f0dae6d44370076c0ae4f7e9170 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 21:46:50 +0200 Subject: [PATCH 774/903] Adjust type hints in generic_thermostat (#77490) --- .../components/generic_thermostat/climate.py | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 9dd49dd851d..f095037d7f7 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -189,7 +189,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): unique_id, ): """Initialize the thermostat.""" - self._name = name + self._attr_name = name self.heater_entity_id = heater_entity_id self.sensor_entity_id = sensor_entity_id self.ac_mode = ac_mode @@ -202,9 +202,9 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self._temp_precision = precision self._temp_target_temperature_step = target_temperature_step if self.ac_mode: - self._hvac_list = [HVACMode.COOL, HVACMode.OFF] + self._attr_hvac_modes = [HVACMode.COOL, HVACMode.OFF] else: - self._hvac_list = [HVACMode.HEAT, HVACMode.OFF] + self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] self._active = False self._cur_temp = None self._temp_lock = asyncio.Lock() @@ -212,8 +212,8 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self._max_temp = max_temp self._attr_preset_mode = PRESET_NONE self._target_temp = target_temp - self._unit = unit - self._unique_id = unique_id + self._attr_temperature_unit = unit + self._attr_unique_id = unique_id self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE if len(presets): self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE @@ -222,7 +222,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self._attr_preset_modes = [PRESET_NONE] self._presets = presets - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" await super().async_added_to_hass() @@ -283,7 +283,10 @@ class GenericThermostat(ClimateEntity, RestoreEntity): ) else: self._target_temp = float(old_state.attributes[ATTR_TEMPERATURE]) - if old_state.attributes.get(ATTR_PRESET_MODE) in self._attr_preset_modes: + if ( + self.preset_modes + and old_state.attributes.get(ATTR_PRESET_MODE) in self.preset_modes + ): self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE) if not self._hvac_mode and old_state.state: self._hvac_mode = old_state.state @@ -303,16 +306,6 @@ class GenericThermostat(ClimateEntity, RestoreEntity): if not self._hvac_mode: self._hvac_mode = HVACMode.OFF - @property - def name(self): - """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.""" @@ -328,11 +321,6 @@ class GenericThermostat(ClimateEntity, RestoreEntity): # if a target_temperature_step is not defined, fallback to equal the precision return self.precision - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._unit - @property def current_temperature(self): """Return the sensor temperature.""" @@ -362,11 +350,6 @@ class GenericThermostat(ClimateEntity, RestoreEntity): """Return the temperature we try to reach.""" return self._target_temp - @property - def hvac_modes(self): - """List of available operation modes.""" - return self._hvac_list - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" if hvac_mode == HVACMode.HEAT: @@ -540,9 +523,9 @@ class GenericThermostat(ClimateEntity, RestoreEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode not in (self._attr_preset_modes or []): + if preset_mode not in (self.preset_modes or []): raise ValueError( - f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" + f"Got unsupported preset_mode {preset_mode}. Must be one of {self.preset_modes}" ) if preset_mode == self._attr_preset_mode: # I don't think we need to call async_write_ha_state if we didn't change the state From 7f883b7ff3f354eb7c3518fdbb12660c20273d18 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 21:49:28 +0200 Subject: [PATCH 775/903] Use attributes in mochad (#76032) --- homeassistant/components/mochad/light.py | 74 +++++++++-------------- homeassistant/components/mochad/switch.py | 43 ++++++------- 2 files changed, 47 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 3d06a09f479..b2dba04b205 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from pymochad import device +from pymochad import controller, device from pymochad.exceptions import MochadException import voluptuous as vol @@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import CONF_COMM_TYPE, DOMAIN, REQ_LOCK +from . import CONF_COMM_TYPE, DOMAIN, REQ_LOCK, MochadCtrl _LOGGER = logging.getLogger(__name__) CONF_BRIGHTNESS_LEVELS = "brightness_levels" @@ -49,66 +49,50 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up X10 dimmers over a mochad controller.""" - mochad_controller = hass.data[DOMAIN] - devs = config[CONF_DEVICES] + mochad_controller: MochadCtrl = hass.data[DOMAIN] + devs: list[dict[str, Any]] = config[CONF_DEVICES] add_entities([MochadLight(hass, mochad_controller.ctrl, dev) for dev in devs]) class MochadLight(LightEntity): """Representation of a X10 dimmer over Mochad.""" + _attr_assumed_state = True # X10 devices are normally 1-way _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def __init__(self, hass, ctrl, dev): + def __init__( + self, hass: HomeAssistant, ctrl: controller.PyMochad, dev: dict[str, Any] + ) -> None: """Initialize a Mochad Light Device.""" self._controller = ctrl - self._address = dev[CONF_ADDRESS] - self._name = dev.get(CONF_NAME, f"x10_light_dev_{self._address}") - self._comm_type = dev.get(CONF_COMM_TYPE, "pl") + self._address: str = dev[CONF_ADDRESS] + self._attr_name: str = dev.get(CONF_NAME, f"x10_light_dev_{self._address}") + self._comm_type: str = dev.get(CONF_COMM_TYPE, "pl") self.light = device.Device(ctrl, self._address, comm_type=self._comm_type) - self._brightness = 0 - self._state = self._get_device_status() - self._brightness_levels = dev.get(CONF_BRIGHTNESS_LEVELS) - 1 + self._attr_brightness = 0 + self._attr_is_on = self._get_device_status() + self._brightness_levels: int = dev[CONF_BRIGHTNESS_LEVELS] - 1 - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - def _get_device_status(self): + def _get_device_status(self) -> bool: """Get the status of the light from mochad.""" with REQ_LOCK: status = self.light.get_status().rstrip() return status == "on" - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def is_on(self): - """Return true if the light is on.""" - return self._state - - @property - def assumed_state(self): - """X10 devices are normally 1-way so we have to assume the state.""" - return True - - def _calculate_brightness_value(self, value): + def _calculate_brightness_value(self, value: int) -> int: return int(value * (float(self._brightness_levels) / 255.0)) - def _adjust_brightness(self, brightness): - if self._brightness > brightness: - bdelta = self._brightness - brightness + def _adjust_brightness(self, brightness: int) -> None: + assert self.brightness is not None + if self.brightness > brightness: + bdelta = self.brightness - brightness mochad_brightness = self._calculate_brightness_value(bdelta) self.light.send_cmd(f"dim {mochad_brightness}") self._controller.read_data() - elif self._brightness < brightness: - bdelta = brightness - self._brightness + elif self.brightness < brightness: + bdelta = brightness - self.brightness mochad_brightness = self._calculate_brightness_value(bdelta) self.light.send_cmd(f"bright {mochad_brightness}") self._controller.read_data() @@ -116,7 +100,7 @@ class MochadLight(LightEntity): def turn_on(self, **kwargs: Any) -> None: """Send the command to turn the light on.""" _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) - brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + brightness: int = kwargs.get(ATTR_BRIGHTNESS, 255) with REQ_LOCK: try: # Recycle socket on new command to recover mochad connection @@ -130,11 +114,11 @@ class MochadLight(LightEntity): self._controller.read_data() # There is no persistence for X10 modules so a fresh on command # will be full brightness - if self._brightness == 0: - self._brightness = 255 + if self.brightness == 0: + self._attr_brightness = 255 self._adjust_brightness(brightness) - self._brightness = brightness - self._state = True + self._attr_brightness = brightness + self._attr_is_on = True except (MochadException, OSError) as exc: _LOGGER.error("Error with mochad communication: %s", exc) @@ -150,7 +134,7 @@ class MochadLight(LightEntity): # There is no persistence for X10 modules so we need to prepare # to track a fresh on command will full brightness if self._brightness_levels == 31: - self._brightness = 0 - self._state = False + self._attr_brightness = 0 + self._attr_is_on = False except (MochadException, OSError) as exc: _LOGGER.error("Error with mochad communication: %s", exc) diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py index 7175955d81a..fe90d94e40a 100644 --- a/homeassistant/components/mochad/switch.py +++ b/homeassistant/components/mochad/switch.py @@ -2,8 +2,9 @@ from __future__ import annotations import logging +from typing import Any -from pymochad import device +from pymochad import controller, device from pymochad.exceptions import MochadException import voluptuous as vol @@ -14,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import CONF_COMM_TYPE, DOMAIN, REQ_LOCK +from . import CONF_COMM_TYPE, DOMAIN, REQ_LOCK, MochadCtrl _LOGGER = logging.getLogger(__name__) @@ -40,35 +41,32 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up X10 switches over a mochad controller.""" - mochad_controller = hass.data[DOMAIN] - devs = config[CONF_DEVICES] + mochad_controller: MochadCtrl = hass.data[DOMAIN] + devs: list[dict[str, str]] = config[CONF_DEVICES] add_entities([MochadSwitch(hass, mochad_controller.ctrl, dev) for dev in devs]) class MochadSwitch(SwitchEntity): """Representation of a X10 switch over Mochad.""" - def __init__(self, hass, ctrl, dev): + def __init__( + self, hass: HomeAssistant, ctrl: controller.PyMochad, dev: dict[str, str] + ) -> None: """Initialize a Mochad Switch Device.""" self._controller = ctrl - self._address = dev[CONF_ADDRESS] - self._name = dev.get(CONF_NAME, f"x10_switch_dev_{self._address}") - self._comm_type = dev.get(CONF_COMM_TYPE, "pl") + self._address: str = dev[CONF_ADDRESS] + self._attr_name: str = dev.get(CONF_NAME, f"x10_switch_dev_{self._address}") + self._comm_type: str = dev.get(CONF_COMM_TYPE, "pl") self.switch = device.Device(ctrl, self._address, comm_type=self._comm_type) # Init with false to avoid locking HA for long on CM19A (goes from rf # to pl via TM751, but not other way around) if self._comm_type == "pl": - self._state = self._get_device_status() + self._attr_is_on = self._get_device_status() else: - self._state = False + self._attr_is_on = False - @property - def name(self): - """Get the name of the switch.""" - return self._name - - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) @@ -80,11 +78,11 @@ class MochadSwitch(SwitchEntity): # No read data on CM19A which is rf only if self._comm_type == "pl": self._controller.read_data() - self._state = True + self._attr_is_on = True except (MochadException, OSError) as exc: _LOGGER.error("Error with mochad communication: %s", exc) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) @@ -96,17 +94,12 @@ class MochadSwitch(SwitchEntity): # No read data on CM19A which is rf only if self._comm_type == "pl": self._controller.read_data() - self._state = False + self._attr_is_on = False except (MochadException, OSError) as exc: _LOGGER.error("Error with mochad communication: %s", exc) - def _get_device_status(self): + def _get_device_status(self) -> bool: """Get the status of the switch from mochad.""" with REQ_LOCK: status = self.switch.get_status().rstrip() return status == "on" - - @property - def is_on(self): - """Return true if switch is on.""" - return self._state From 4b2e4c8276e7dea480ea32dfee87a629f65a84ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Aug 2022 21:51:21 +0200 Subject: [PATCH 776/903] Improve type hints in demo [3/3] (#77186) --- homeassistant/components/demo/__init__.py | 89 +++++++++++++---------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 8572ddfbbe2..cc80edbd484 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -1,4 +1,6 @@ """Set up the demo environment that mimics interaction with devices.""" +from __future__ import annotations + import asyncio import datetime from random import random @@ -6,7 +8,7 @@ from random import random from homeassistant import config_entries, setup from homeassistant.components import persistent_notification from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.models import StatisticMetaData +from homeassistant.components.recorder.models import StatisticData, StatisticMetaData from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, @@ -16,9 +18,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, SOUND_PRESSURE_DB, + Platform, ) import homeassistant.core as ha -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType @@ -27,36 +30,36 @@ import homeassistant.util.dt as dt_util DOMAIN = "demo" COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ - "air_quality", - "alarm_control_panel", - "binary_sensor", - "button", - "camera", - "climate", - "cover", - "fan", - "humidifier", - "light", - "lock", - "media_player", - "number", - "select", - "sensor", - "siren", - "switch", - "update", - "vacuum", - "water_heater", + Platform.AIR_QUALITY, + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.LOCK, + Platform.MEDIA_PLAYER, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, + Platform.UPDATE, + Platform.VACUUM, + Platform.WATER_HEATER, ] COMPONENTS_WITH_DEMO_PLATFORM = [ - "tts", - "stt", - "mailbox", - "notify", - "image_processing", - "calendar", - "device_tracker", + Platform.TTS, + Platform.STT, + Platform.MAILBOX, + Platform.NOTIFY, + Platform.IMAGE_PROCESSING, + Platform.CALENDAR, + Platform.DEVICE_TRACKER, ] @@ -173,7 +176,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: title="Example Notification", ) - async def demo_start_listener(_event): + async def demo_start_listener(_event: Event) -> None: """Finish set up.""" await finish_setup(hass, config) @@ -225,8 +228,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def _generate_mean_statistics(start, end, init_value, max_diff): - statistics = [] +def _generate_mean_statistics( + start: datetime.datetime, end: datetime.datetime, init_value: float, max_diff: float +) -> list[StatisticData]: + statistics: list[StatisticData] = [] mean = init_value now = start while now < end: @@ -244,10 +249,16 @@ def _generate_mean_statistics(start, end, init_value, max_diff): return statistics -async def _insert_sum_statistics(hass, metadata, start, end, max_diff): - statistics = [] +async def _insert_sum_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + start: datetime.datetime, + end: datetime.datetime, + max_diff: float, +): + statistics: list[StatisticData] = [] now = start - sum_ = 0 + sum_ = 0.0 statistic_id = metadata["statistic_id"] last_stats = await get_instance(hass).async_add_executor_job( @@ -349,10 +360,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def finish_setup(hass, config): +async def finish_setup(hass: HomeAssistant, config: ConfigType) -> None: """Finish set up once demo platforms are set up.""" - switches = None - lights = None + switches: list[str] | None = None + lights: list[str] | None = None while not switches and not lights: # Not all platforms might be loaded. @@ -361,6 +372,8 @@ async def finish_setup(hass, config): switches = sorted(hass.states.async_entity_ids("switch")) lights = sorted(hass.states.async_entity_ids("light")) + assert switches is not None + assert lights is not None # Set up scripts await setup.async_setup_component( hass, From 7c5a5f86ee41765d4c04a8da9e4775ab1462bfe9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Aug 2022 21:54:31 +0200 Subject: [PATCH 777/903] Allow setting to-time in schedule to 24:00 (#77558) --- homeassistant/components/schedule/__init__.py | 46 ++++++- tests/components/schedule/test_init.py | 127 +++++++++++++++++- 2 files changed, 162 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index 96d452469a5..c698993440a 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -2,10 +2,10 @@ from __future__ import annotations from collections.abc import Callable -from datetime import datetime, timedelta +from datetime import datetime, time, timedelta import itertools import logging -from typing import Literal +from typing import Any, Literal import voluptuous as vol @@ -81,6 +81,30 @@ def valid_schedule(schedule: list[dict[str, str]]) -> list[dict[str, str]]: return schedule +def deserialize_to_time(value: Any) -> Any: + """Convert 24:00 and 24:00:00 to time.max.""" + if not isinstance(value, str): + return cv.time(value) + + parts = value.split(":") + if len(parts) < 2: + return cv.time(value) + hour = int(parts[0]) + minute = int(parts[1]) + + if hour == 24 and minute == 0: + return time.max + + return cv.time(value) + + +def serialize_to_time(value: Any) -> Any: + """Convert time.max to 24:00:00.""" + if value == time.max: + return "24:00:00" + return vol.Coerce(str)(value) + + BASE_SCHEMA = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_ICON): cv.icon, @@ -88,12 +112,14 @@ BASE_SCHEMA = { TIME_RANGE_SCHEMA = { vol.Required(CONF_FROM): cv.time, - vol.Required(CONF_TO): cv.time, + vol.Required(CONF_TO): deserialize_to_time, } + +# Serialize time in validated config STORAGE_TIME_RANGE_SCHEMA = vol.Schema( { - vol.Required(CONF_FROM): vol.All(cv.time, vol.Coerce(str)), - vol.Required(CONF_TO): vol.All(cv.time, vol.Coerce(str)), + vol.Required(CONF_FROM): vol.Coerce(str), + vol.Required(CONF_TO): serialize_to_time, } ) @@ -111,11 +137,17 @@ STORAGE_SCHEDULE_SCHEMA = { } +# Validate YAML config CONFIG_SCHEMA = vol.Schema( {DOMAIN: cv.schema_with_slug_keys(vol.All(BASE_SCHEMA | SCHEDULE_SCHEMA))}, extra=vol.ALLOW_EXTRA, ) +# Validate storage config STORAGE_SCHEMA = vol.Schema( + {vol.Required(CONF_ID): cv.string} | BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA +) +# Validate + transform entity config +ENTITY_SCHEMA = vol.Schema( {vol.Required(CONF_ID): cv.string} | BASE_SCHEMA | SCHEDULE_SCHEMA ) @@ -219,7 +251,7 @@ class Schedule(Entity): def __init__(self, config: ConfigType, editable: bool = True) -> None: """Initialize a schedule.""" - self._config = STORAGE_SCHEMA(config) + self._config = ENTITY_SCHEMA(config) self._attr_capability_attributes = {ATTR_EDITABLE: editable} self._attr_icon = self._config.get(CONF_ICON) self._attr_name = self._config[CONF_NAME] @@ -234,7 +266,7 @@ class Schedule(Entity): async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" - self._config = STORAGE_SCHEMA(config) + self._config = ENTITY_SCHEMA(config) self._attr_icon = config.get(CONF_ICON) self._attr_name = config[CONF_NAME] self._clean_up_listener() diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index a1161800e9e..825bac5686c 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -69,7 +69,7 @@ def schedule_setup( {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}, ], CONF_SUNDAY: [ - {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}, + {CONF_FROM: "00:00:00", CONF_TO: "24:00:00"}, ], } ] @@ -225,6 +225,61 @@ async def test_events_one_day( assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T07:00:00-07:00" +@pytest.mark.parametrize( + "schedule", + ( + {CONF_FROM: "00:00:00", CONF_TO: "24:00"}, + {CONF_FROM: "00:00:00", CONF_TO: "24:00:00"}, + ), +) +async def test_to_midnight( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + caplog: pytest.LogCaptureFixture, + schedule: list[dict[str, str]], + freezer, +) -> None: + """Test time range allow to 24:00.""" + freezer.move_to("2022-08-30 13:20:00-07:00") + + assert await schedule_setup( + config={ + DOMAIN: { + "from_yaml": { + CONF_NAME: "from yaml", + CONF_ICON: "mdi:party-popper", + CONF_SUNDAY: schedule, + } + } + }, + items=[], + ) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T00:00:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert ( + state.attributes[ATTR_NEXT_EVENT].isoformat() + == "2022-09-04T23:59:59.999999-07:00" + ) + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T00:00:00-07:00" + + async def test_setup_no_config(hass: HomeAssistant, hass_admin_user: MockUser) -> None: """Test component setup with no config.""" count_start = len(hass.states.async_entity_ids()) @@ -310,6 +365,15 @@ async def test_ws_list( assert len(result) == 1 assert result["from_storage"][ATTR_NAME] == "from storage" + assert result["from_storage"][CONF_FRIDAY] == [ + {CONF_FROM: "17:00:00", CONF_TO: "23:59:59"} + ] + assert result["from_storage"][CONF_SATURDAY] == [ + {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"} + ] + assert result["from_storage"][CONF_SUNDAY] == [ + {CONF_FROM: "00:00:00", CONF_TO: "24:00:00"} + ] assert "from_yaml" not in result @@ -340,10 +404,21 @@ async def test_ws_delete( @pytest.mark.freeze_time("2022-08-10 20:10:00-07:00") +@pytest.mark.parametrize( + "to, next_event, saved_to", + ( + ("23:59:59", "2022-08-10T23:59:59-07:00", "23:59:59"), + ("24:00", "2022-08-10T23:59:59.999999-07:00", "24:00:00"), + ("24:00:00", "2022-08-10T23:59:59.999999-07:00", "24:00:00"), + ), +) async def test_update( hass: HomeAssistant, hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + to: str, + next_event: str, + saved_to: str, ) -> None: """Test updating the schedule.""" ent_reg = er.async_get(hass) @@ -369,7 +444,7 @@ async def test_update( CONF_ICON: "mdi:party-pooper", CONF_MONDAY: [], CONF_TUESDAY: [], - CONF_WEDNESDAY: [{CONF_FROM: "17:00:00", CONF_TO: "23:59:59"}], + CONF_WEDNESDAY: [{CONF_FROM: "17:00:00", CONF_TO: to}], CONF_THURSDAY: [], CONF_FRIDAY: [], CONF_SATURDAY: [], @@ -384,16 +459,41 @@ async def test_update( assert state.state == STATE_ON assert state.attributes[ATTR_FRIENDLY_NAME] == "Party pooper" assert state.attributes[ATTR_ICON] == "mdi:party-pooper" - assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-10T23:59:59-07:00" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == next_event + + await client.send_json({"id": 2, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert result["from_storage"][CONF_WEDNESDAY] == [ + {CONF_FROM: "17:00:00", CONF_TO: saved_to} + ] @pytest.mark.freeze_time("2022-08-11 8:52:00-07:00") +@pytest.mark.parametrize( + "to, next_event, saved_to", + ( + ("14:00:00", "2022-08-15T14:00:00-07:00", "14:00:00"), + ("24:00", "2022-08-15T23:59:59.999999-07:00", "24:00:00"), + ("24:00:00", "2022-08-15T23:59:59.999999-07:00", "24:00:00"), + ), +) async def test_ws_create( hass: HomeAssistant, hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + freezer, + to: str, + next_event: str, + saved_to: str, ) -> None: """Test create WS.""" + freezer.move_to("2022-08-11 8:52:00-07:00") + ent_reg = er.async_get(hass) assert await schedule_setup(items=[]) @@ -409,7 +509,7 @@ async def test_ws_create( "type": f"{DOMAIN}/create", "name": "Party mode", "icon": "mdi:party-popper", - "monday": [{"from": "12:00:00", "to": "14:00:00"}], + "monday": [{"from": "12:00:00", "to": to}], } ) resp = await client.receive_json() @@ -422,3 +522,22 @@ async def test_ws_create( assert state.attributes[ATTR_EDITABLE] is True assert state.attributes[ATTR_ICON] == "mdi:party-popper" assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-15T12:00:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get("schedule.party_mode") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == next_event + + await client.send_json({"id": 2, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert result["party_mode"][CONF_MONDAY] == [ + {CONF_FROM: "12:00:00", CONF_TO: saved_to} + ] From f43f440739c2e97d222c0005c34fcf7d010cc64a Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Tue, 30 Aug 2022 23:03:41 +0200 Subject: [PATCH 778/903] Add new sensors to BThome (#77561) --- homeassistant/components/bthome/manifest.json | 2 +- homeassistant/components/bthome/sensor.py | 34 ++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bthome/__init__.py | 8 +- tests/components/bthome/test_config_flow.py | 18 ++-- tests/components/bthome/test_sensor.py | 89 ++++++++++++++++++- 7 files changed, 134 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index d823d70dd39..bdb4b75bfa9 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -13,7 +13,7 @@ "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==0.4.0"], + "requirements": ["bthome-ble==0.5.2"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 5cc3317ea82..71601fa24c0 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, LIGHT_LUX, MASS_KILOGRAMS, + MASS_POUNDS, PERCENTAGE, POWER_WATT, PRESSURE_MBAR, @@ -132,13 +133,41 @@ SENSOR_DESCRIPTIONS = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - # Used for e.g. weight sensor + # Used for mass sensor with kg unit (None, Units.MASS_KILOGRAMS): SensorEntityDescription( - key=str(Units.MASS_KILOGRAMS), + key=f"{DeviceClass.MASS}_{Units.MASS_KILOGRAMS}", device_class=None, native_unit_of_measurement=MASS_KILOGRAMS, state_class=SensorStateClass.MEASUREMENT, ), + # Used for mass sensor with lb unit + (None, Units.MASS_POUNDS): SensorEntityDescription( + key=f"{DeviceClass.MASS}_{Units.MASS_POUNDS}", + device_class=None, + native_unit_of_measurement=MASS_POUNDS, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for moisture sensor + (None, Units.PERCENTAGE,): SensorEntityDescription( + key=f"{DeviceClass.MOISTURE}_{Units.PERCENTAGE}", + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for dew point sensor + (None, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for count sensor + (None, None): SensorEntityDescription( + key=f"{DeviceClass.COUNT}", + device_class=None, + native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -156,7 +185,6 @@ def sensor_update_to_bluetooth_data_update( (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() - if description.native_unit_of_measurement }, entity_data={ device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value diff --git a/requirements_all.txt b/requirements_all.txt index fca5683ce7a..b2fb4f2825c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -458,7 +458,7 @@ bsblan==0.5.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==0.4.0 +bthome-ble==0.5.2 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 777c97d1083..60c5bbead9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -359,7 +359,7 @@ brunt==1.2.0 bsblan==0.5.0 # homeassistant.components.bthome -bthome-ble==0.4.0 +bthome-ble==0.5.2 # homeassistant.components.buienradar buienradar==1.0.5 diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index 7cb6496b5c5..be59cd7e8cb 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -37,18 +37,18 @@ TEMP_HUMI_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( connectable=False, ) -PM_SERVICE_INFO = BluetoothServiceInfoBleak( - name="TEST DEVICE 8F80A5", +PRST_SERVICE_INFO = BluetoothServiceInfoBleak( + name="prst 8F80A5", address="54:48:E6:8F:80:A5", device=BLEDevice("54:48:E6:8F:80:A5", None), rssi=-63, manufacturer_data={}, service_data={ - "0000181c-0000-1000-8000-00805f9b34fb": b"\x03\r\x12\x0c\x03\x0e\x02\x1c" + "0000181c-0000-1000-8000-00805f9b34fb": b'\x02\x14\x00\n"\x02\xdd\n\x02\x03{\x12\x02\x0c\n\x0b' }, service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"], source="local", - advertisement=AdvertisementData(local_name="Not it"), + advertisement=AdvertisementData(local_name="prst"), time=0, connectable=False, ) diff --git a/tests/components/bthome/test_config_flow.py b/tests/components/bthome/test_config_flow.py index b1154ca9223..fd8f8dfaa35 100644 --- a/tests/components/bthome/test_config_flow.py +++ b/tests/components/bthome/test_config_flow.py @@ -11,7 +11,7 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( NOT_BTHOME_SERVICE_INFO, - PM_SERVICE_INFO, + PRST_SERVICE_INFO, TEMP_HUMI_ENCRYPTED_SERVICE_INFO, TEMP_HUMI_SERVICE_INFO, ) @@ -185,7 +185,7 @@ async def test_async_step_user_with_found_devices(hass): """Test setup from service info cache with devices found.""" with patch( "homeassistant.components.bthome.config_flow.async_discovered_service_info", - return_value=[PM_SERVICE_INFO], + return_value=[PRST_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -199,7 +199,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "54:48:E6:8F:80:A5"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "TEST DEVICE 80A5" + assert result2["title"] == "b-parasite 80A5" assert result2["data"] == {} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -384,7 +384,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=PM_SERVICE_INFO, + data=PRST_SERVICE_INFO, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -395,7 +395,7 @@ async def test_async_step_bluetooth_already_in_progress(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=PM_SERVICE_INFO, + data=PRST_SERVICE_INFO, ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" @@ -403,7 +403,7 @@ async def test_async_step_bluetooth_already_in_progress(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=PM_SERVICE_INFO, + data=PRST_SERVICE_INFO, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -414,14 +414,14 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=PM_SERVICE_INFO, + data=PRST_SERVICE_INFO, ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.bthome.config_flow.async_discovered_service_info", - return_value=[PM_SERVICE_INFO], + return_value=[PRST_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -435,7 +435,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "54:48:E6:8F:80:A5"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "TEST DEVICE 80A5" + assert result2["title"] == "b-parasite 80A5" assert result2["data"] == {} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 07ccfe2288c..f73d3bf379c 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -106,6 +106,73 @@ from tests.common import MockConfigEntry }, ], ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x03\x06\x5e\x1f", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_mass", + "friendly_name": "Test Device 18B2 Mass", + "unit_of_measurement": "kg", + "state_class": "measurement", + "expected_state": "80.3", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x03\x07\x3e\x1d", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_mass", + "friendly_name": "Test Device 18B2 Mass", + "unit_of_measurement": "lb", + "state_class": "measurement", + "expected_state": "74.86", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x23\x08\xCA\x06", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_dew_point", + "friendly_name": "Test Device 18B2 Dew Point", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "17.38", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x02\x09\x60", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_count", + "friendly_name": "Test Device 18B2 Count", + "state_class": "measurement", + "expected_state": "96", + }, + ], + ), ( "A4:C1:38:8D:18:B2", make_advertisement( @@ -215,6 +282,23 @@ from tests.common import MockConfigEntry }, ], ), + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x03\x14\x02\x0c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_moisture", + "friendly_name": "Test Device 18B2 Moisture", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "30.74", + }, + ], + ), ( "54:48:E6:8F:80:A5", make_encrypted_advertisement( @@ -283,9 +367,10 @@ async def test_sensors( sensor = hass.states.get(meas["sensor_entity"]) sensor_attr = sensor.attributes assert sensor.state == meas["expected_state"] - assert sensor_attr[ATTR_FRIENDLY_NAME] == meas["friendly_name"] - assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == meas["unit_of_measurement"] + if ATTR_UNIT_OF_MEASUREMENT in sensor_attr: + # Count sensor does not have a unit of measurement + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == meas["unit_of_measurement"] assert sensor_attr[ATTR_STATE_CLASS] == meas["state_class"] assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() From 8d94c8f74aea9a6a75dbc5ffbb8fb6b8ad4442d7 Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Tue, 30 Aug 2022 17:06:44 -0400 Subject: [PATCH 779/903] Add Melnor Bluetooth valve watering Integration (#70457) --- .coveragerc | 4 + CODEOWNERS | 2 + homeassistant/components/melnor/__init__.py | 74 +++++++++ .../components/melnor/config_flow.py | 116 ++++++++++++++ homeassistant/components/melnor/const.py | 8 + homeassistant/components/melnor/manifest.json | 16 ++ homeassistant/components/melnor/models.py | 74 +++++++++ homeassistant/components/melnor/strings.json | 13 ++ homeassistant/components/melnor/switch.py | 75 +++++++++ .../components/melnor/translations/en.json | 13 ++ homeassistant/generated/bluetooth.py | 7 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/melnor/__init__.py | 64 ++++++++ tests/components/melnor/test_config_flow.py | 147 ++++++++++++++++++ 16 files changed, 620 insertions(+) create mode 100644 homeassistant/components/melnor/__init__.py create mode 100644 homeassistant/components/melnor/config_flow.py create mode 100644 homeassistant/components/melnor/const.py create mode 100644 homeassistant/components/melnor/manifest.json create mode 100644 homeassistant/components/melnor/models.py create mode 100644 homeassistant/components/melnor/strings.json create mode 100644 homeassistant/components/melnor/switch.py create mode 100644 homeassistant/components/melnor/translations/en.json create mode 100644 tests/components/melnor/__init__.py create mode 100644 tests/components/melnor/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 770cfb978d9..d6a27259871 100644 --- a/.coveragerc +++ b/.coveragerc @@ -720,6 +720,10 @@ omit = homeassistant/components/melcloud/const.py homeassistant/components/melcloud/sensor.py homeassistant/components/melcloud/water_heater.py + homeassistant/components/melnor/__init__.py + homeassistant/components/melnor/const.py + homeassistant/components/melnor/models.py + homeassistant/components/melnor/switch.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py homeassistant/components/met_eireann/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 9a6578d8fd5..7fdfc5a73c2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -653,6 +653,8 @@ build.json @home-assistant/supervisor /tests/components/melcloud/ @vilppuvuorinen /homeassistant/components/melissa/ @kennedyshead /tests/components/melissa/ @kennedyshead +/homeassistant/components/melnor/ @vanstinator +/tests/components/melnor/ @vanstinator /homeassistant/components/met/ @danielhiversen @thimic /tests/components/met/ @danielhiversen @thimic /homeassistant/components/met_eireann/ @DylanGore diff --git a/homeassistant/components/melnor/__init__.py b/homeassistant/components/melnor/__init__.py new file mode 100644 index 00000000000..5fd697b2088 --- /dev/null +++ b/homeassistant/components/melnor/__init__.py @@ -0,0 +1,74 @@ +"""The melnor integration.""" + +from __future__ import annotations + +from melnor_bluetooth.device import Device + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .models import MelnorDataUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up melnor from a config entry.""" + + hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {}) + + ble_device = bluetooth.async_ble_device_from_address(hass, entry.data[CONF_ADDRESS]) + + if not ble_device: + raise ConfigEntryNotReady( + f"Couldn't find a nearby device for address: {entry.data[CONF_ADDRESS]}" + ) + + # Create the device and connect immediately so we can pull down + # required attributes before building out our entities + device = Device(ble_device) + await device.connect(retry_attempts=4) + + if not device.is_connected: + raise ConfigEntryNotReady(f"Failed to connect to: {device.mac}") + + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a ble callback.""" + device.update_ble_device(service_info.device) + + bluetooth.async_register_callback( + hass, + _async_update_ble, + BluetoothCallbackMatcher(address=device.mac), + bluetooth.BluetoothScanningMode.PASSIVE, + ) + + coordinator = MelnorDataUpdateCoordinator(hass, device) + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + device: Device = hass.data[DOMAIN][entry.entry_id].data + + await device.disconnect() + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/melnor/config_flow.py b/homeassistant/components/melnor/config_flow.py new file mode 100644 index 00000000000..7e9aed24e8a --- /dev/null +++ b/homeassistant/components/melnor/config_flow.py @@ -0,0 +1,116 @@ +"""Config flow for melnor.""" + +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.bluetooth import async_discovered_service_info +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN, MANUFACTURER_DATA_START, MANUFACTURER_ID + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for melnor.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_address: str + self._discovered_addresses: list[str] = [] + + def _create_entry(self, address: str) -> FlowResult: + """Create an entry for a discovered device.""" + + return self.async_create_entry( + title=address, + data={ + CONF_ADDRESS: address, + }, + ) + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle user-confirmation of discovered device.""" + + if user_input is not None: + return self._create_entry(self._discovered_address) + + return self.async_show_form( + step_id="bluetooth_confirm", + description_placeholders={"name": self._discovered_address}, + ) + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle a flow initialized by Bluetooth discovery.""" + + address = discovery_info.address + + await self.async_set_unique_id(address) + self._abort_if_unique_id_configured(updates={CONF_ADDRESS: address}) + + self._discovered_address = address + + self.context["title_placeholders"] = {"name": address} + return await self.async_step_bluetooth_confirm() + + async def async_step_pick_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the step to pick discovered device.""" + + if user_input is not None: + address = user_input[CONF_ADDRESS] + + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + + return self._create_entry(address) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info( + self.hass, connectable=True + ): + + if discovery_info.manufacturer_id == MANUFACTURER_ID and any( + manufacturer_data.startswith(MANUFACTURER_DATA_START) + for manufacturer_data in discovery_info.manufacturer_data.values() + ): + + address = discovery_info.address + if ( + address not in current_addresses + and address not in self._discovered_addresses + ): + self._discovered_addresses.append(address) + + addresses = { + address + for address in self._discovered_addresses + if address not in current_addresses + } + + # Check if there is at least one device + if not addresses: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="pick_device", + data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(addresses)}), + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + + return await self.async_step_pick_device() diff --git a/homeassistant/components/melnor/const.py b/homeassistant/components/melnor/const.py new file mode 100644 index 00000000000..cadf9c0a618 --- /dev/null +++ b/homeassistant/components/melnor/const.py @@ -0,0 +1,8 @@ +"""Constants for the melnor integration.""" + + +DOMAIN = "melnor" +DEFAULT_NAME = "Melnor Bluetooth" + +MANUFACTURER_ID = 13 +MANUFACTURER_DATA_START = bytearray([89]) diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json new file mode 100644 index 00000000000..37ac40cb3aa --- /dev/null +++ b/homeassistant/components/melnor/manifest.json @@ -0,0 +1,16 @@ +{ + "after_dependencies": ["bluetooth"], + "bluetooth": [ + { + "manufacturer_data_start": [89], + "manufacturer_id": 13 + } + ], + "codeowners": ["@vanstinator"], + "config_flow": true, + "domain": "melnor", + "documentation": "https://www.home-assistant.io/integrations/melnor", + "iot_class": "local_polling", + "name": "Melnor Bluetooth", + "requirements": ["melnor-bluetooth==0.0.13"] +} diff --git a/homeassistant/components/melnor/models.py b/homeassistant/components/melnor/models.py new file mode 100644 index 00000000000..4796bf601ff --- /dev/null +++ b/homeassistant/components/melnor/models.py @@ -0,0 +1,74 @@ +"""Melnor integration models.""" + +from datetime import timedelta +import logging + +from melnor_bluetooth.device import Device + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]): + """Melnor data update coordinator.""" + + _device: Device + + def __init__(self, hass: HomeAssistant, device: Device) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Melnor Bluetooth", + update_interval=timedelta(seconds=5), + ) + self._device = device + + async def _async_update_data(self): + """Update the device state.""" + + await self._device.fetch_state() + return self._device + + +class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): + """Base class for melnor entities.""" + + _device: Device + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + ) -> None: + """Initialize a melnor base entity.""" + super().__init__(coordinator) + + self._device = coordinator.data + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device.mac)}, + manufacturer="Melnor", + model=self._device.model, + name=self._device.name, + ) + self._attr_name = self._device.name + self._attr_unique_id = self._device.mac + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._device = self.coordinator.data + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._device.is_connected diff --git a/homeassistant/components/melnor/strings.json b/homeassistant/components/melnor/strings.json new file mode 100644 index 00000000000..42309c3bf72 --- /dev/null +++ b/homeassistant/components/melnor/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "There aren't any Melnor Bluetooth devices nearby." + }, + "step": { + "bluetooth_confirm": { + "description": "Do you want to add the Melnor Bluetooth valve `{name}` to Home Assistant?", + "title": "Discovered Melnor Bluetooth valve" + } + } + } +} diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py new file mode 100644 index 00000000000..c2d32c428d3 --- /dev/null +++ b/homeassistant/components/melnor/switch.py @@ -0,0 +1,75 @@ +"""Support for Melnor RainCloud sprinkler water timer.""" + +from __future__ import annotations + +from typing import Any, cast + +from melnor_bluetooth.device import Valve + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .models import MelnorBluetoothBaseEntity, MelnorDataUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_devices: AddEntitiesCallback, +) -> None: + """Set up the switch platform.""" + switches = [] + + coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + # This device may not have 4 valves total, but the library will only expose the right number of valves + for i in range(1, 5): + if coordinator.data[f"zone{i}"] is not None: + switches.append(MelnorSwitch(coordinator, i)) + + async_add_devices(switches, True) + + +class MelnorSwitch(MelnorBluetoothBaseEntity, SwitchEntity): + """A switch implementation for a melnor device.""" + + _valve_index: int + _attr_icon = "mdi:sprinkler" + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + valve_index: int, + ) -> None: + """Initialize a switch for a melnor device.""" + super().__init__(coordinator) + self._valve_index = valve_index + + self._attr_unique_id = ( + f"switch-{self._attr_unique_id}-zone{self._valve().id}-manual" + ) + + self._attr_name = f"{self._device.name} Zone {self._valve().id+1}" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._valve().is_watering + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + self._valve().is_watering = True + await self._device.push_state() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + self._valve().is_watering = False + await self._device.push_state() + self.async_write_ha_state() + + def _valve(self) -> Valve: + return cast(Valve, self._device[f"zone{self._valve_index}"]) diff --git a/homeassistant/components/melnor/translations/en.json b/homeassistant/components/melnor/translations/en.json new file mode 100644 index 00000000000..c179e46a070 --- /dev/null +++ b/homeassistant/components/melnor/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "There aren't any Melnor Bluetooth devices nearby." + }, + "step": { + "bluetooth_confirm": { + "description": "Do you want to add the Melnor Bluetooth valve `{name}` to Home Assistant?", + "title": "Discovered Melnor Bluetooth valve" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 320c4c296da..5de90d731bb 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -138,6 +138,13 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "led_ble", "local_name": "LEDBlue*" }, + { + "domain": "melnor", + "manufacturer_data_start": [ + 89 + ], + "manufacturer_id": 13 + }, { "domain": "moat", "local_name": "Moat_S*", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 133c02fd210..ec09c9a7756 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -218,6 +218,7 @@ FLOWS = { "mazda", "meater", "melcloud", + "melnor", "met", "met_eireann", "meteo_france", diff --git a/requirements_all.txt b/requirements_all.txt index b2fb4f2825c..bd940716ebc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1033,6 +1033,9 @@ mcstatus==6.0.0 # homeassistant.components.meater meater-python==0.0.8 +# homeassistant.components.melnor +melnor-bluetooth==0.0.13 + # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60c5bbead9a..8c43519e0a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -738,6 +738,9 @@ mcstatus==6.0.0 # homeassistant.components.meater meater-python==0.0.8 +# homeassistant.components.melnor +melnor-bluetooth==0.0.13 + # homeassistant.components.meteo_france meteofrance-api==1.0.2 diff --git a/tests/components/melnor/__init__.py b/tests/components/melnor/__init__.py new file mode 100644 index 00000000000..7af59d55a11 --- /dev/null +++ b/tests/components/melnor/__init__.py @@ -0,0 +1,64 @@ +"""Tests for the melnor integration.""" + +from __future__ import annotations + +from unittest.mock import patch + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak + +FAKE_ADDRESS_1 = "FAKE-ADDRESS-1" +FAKE_ADDRESS_2 = "FAKE-ADDRESS-2" + + +FAKE_SERVICE_INFO_1 = BluetoothServiceInfoBleak( + name="YM_TIMER%", + address=FAKE_ADDRESS_1, + rssi=-63, + manufacturer_data={ + 13: b"Y\x08\x02\x8f\x00\x00\x00\x00\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0*\x9b\xcf\xbc" + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(FAKE_ADDRESS_1, None), + advertisement=AdvertisementData(local_name=""), + time=0, + connectable=True, +) + +FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak( + name="YM_TIMER%", + address=FAKE_ADDRESS_2, + rssi=-63, + manufacturer_data={ + 13: b"Y\x08\x02\x8f\x00\x00\x00\x00\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0*\x9b\xcf\xbc" + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(FAKE_ADDRESS_2, None), + advertisement=AdvertisementData(local_name=""), + time=0, + connectable=True, +) + + +def patch_async_setup_entry(return_value=True): + """Patch async setup entry to return True.""" + return patch( + "homeassistant.components.melnor.async_setup_entry", + return_value=return_value, + ) + + +def patch_async_discovered_service_info( + return_value: list[BluetoothServiceInfoBleak] = [FAKE_SERVICE_INFO_1], +): + """Patch async_discovered_service_info a mocked device info.""" + return patch( + "homeassistant.components.melnor.config_flow.async_discovered_service_info", + return_value=return_value, + ) diff --git a/tests/components/melnor/test_config_flow.py b/tests/components/melnor/test_config_flow.py new file mode 100644 index 00000000000..3b550fba3f7 --- /dev/null +++ b/tests/components/melnor/test_config_flow.py @@ -0,0 +1,147 @@ +"""Test the melnor config flow.""" + +import pytest +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.melnor.const import DOMAIN +from homeassistant.const import CONF_ADDRESS, CONF_MAC +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + FAKE_ADDRESS_1, + FAKE_SERVICE_INFO_1, + FAKE_SERVICE_INFO_2, + patch_async_discovered_service_info, + patch_async_setup_entry, +) + + +async def test_user_step_no_devices(hass): + """Test we handle no devices found.""" + with patch_async_setup_entry() as mock_setup_entry, patch_async_discovered_service_info( + [] + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_user_step_discovered_devices(hass): + """Test we properly handle device picking.""" + + with patch_async_setup_entry() as mock_setup_entry, patch_async_discovered_service_info(): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "pick_device" + + with pytest.raises(vol.MultipleInvalid): + await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_ADDRESS: "wrong_address"} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_ADDRESS: FAKE_ADDRESS_1} + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == {CONF_ADDRESS: FAKE_ADDRESS_1} + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_with_existing_device(hass): + """Test we properly handle device picking.""" + + with patch_async_setup_entry() as mock_setup_entry, patch_async_discovered_service_info( + [FAKE_SERVICE_INFO_1, FAKE_SERVICE_INFO_2] + ): + + # Create the config flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_BLUETOOTH, + "step_id": "bluetooth_confirm", + "user_input": {CONF_MAC: FAKE_ADDRESS_1}, + }, + data=FAKE_SERVICE_INFO_1, + ) + + # And create an entry + await hass.config_entries.flow.async_configure(result["flow_id"], user_input={}) + + mock_setup_entry.reset_mock() + + # Now open the picker and validate the current address isn't valid + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + + assert result["type"] == FlowResultType.FORM + + with pytest.raises(vol.MultipleInvalid): + await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_ADDRESS: FAKE_ADDRESS_1} + ) + + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_bluetooth_discovered(hass): + """Test we short circuit to config entry creation.""" + + with patch_async_setup_entry() as mock_setup_entry: + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=FAKE_SERVICE_INFO_1, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + assert result["description_placeholders"] == {"name": FAKE_ADDRESS_1} + + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_bluetooth_confirm(hass): + """Test we short circuit to config entry creation.""" + + with patch_async_setup_entry() as mock_setup_entry: + + # Create the config flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_BLUETOOTH, + "step_id": "bluetooth_confirm", + "user_input": {CONF_MAC: FAKE_ADDRESS_1}, + }, + data=FAKE_SERVICE_INFO_1, + ) + + # Interact with it like a user would + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == FAKE_ADDRESS_1 + assert result2["data"] == {CONF_ADDRESS: FAKE_ADDRESS_1} + + assert len(mock_setup_entry.mock_calls) == 1 From 4b3355c1115b98c19206929c29294e0aabff2cb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Aug 2022 16:14:58 -0500 Subject: [PATCH 780/903] Bump flux_led to 0.28.31 to add support for Armacost devices (#77500) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 7ccd708f89b..4afd0cdb855 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.30"], + "requirements": ["flux_led==0.28.31"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index bd940716ebc..617f9615921 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -676,7 +676,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.30 +flux_led==0.28.31 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c43519e0a2..b909571ac52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -495,7 +495,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.30 +flux_led==0.28.31 # homeassistant.components.homekit # homeassistant.components.recorder From a3af8c07a9e3700663fc0922f1731f8466ff10e7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 30 Aug 2022 19:03:44 -0400 Subject: [PATCH 781/903] Fix SkyConnect unit tests broken by #77044 (#77570) --- .../homeassistant_sky_connect/test_init.py | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 74c1b9cb14f..05b883a9726 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -1,8 +1,11 @@ """Test the Home Assistant Sky Connect integration.""" -from unittest.mock import patch +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch import pytest +from homeassistant.components import zha from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -19,11 +22,31 @@ CONFIG_ENTRY_DATA = { } +@pytest.fixture +def mock_zha_config_flow_setup() -> Generator[None, None, None]: + """Mock the radio connection and probing of the ZHA config flow.""" + + def mock_probe(config: dict[str, Any]) -> None: + # The radio probing will return the correct baudrate + return {**config, "baudrate": 115200} + + mock_connect_app = MagicMock() + mock_connect_app.__aenter__.return_value.backups.backups = [] + + with patch( + "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe + ), patch( + "homeassistant.components.zha.config_flow.BaseZhaFlow._connect_zigpy_app", + return_value=mock_connect_app, + ): + yield + + @pytest.mark.parametrize( "onboarded, num_entries, num_flows", ((False, 1, 0), (True, 0, 1)) ) async def test_setup_entry( - hass: HomeAssistant, onboarded, num_entries, num_flows + mock_zha_config_flow_setup, hass: HomeAssistant, onboarded, num_entries, num_flows ) -> None: """Test setup of a config entry, including setup of zha.""" # Setup the config entry @@ -39,18 +62,28 @@ async def test_setup_entry( return_value=True, ) as mock_is_plugged_in, patch( "homeassistant.components.onboarding.async_is_onboarded", return_value=onboarded - ), patch( - "zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(mock_is_plugged_in.mock_calls) == 1 - assert len(hass.config_entries.async_entries("zha")) == num_entries + # Finish setting up ZHA + if num_entries > 0: + zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") + assert len(zha_flows) == 1 + assert zha_flows[0]["step_id"] == "choose_formation_strategy" + + await hass.config_entries.flow.async_configure( + zha_flows[0]["flow_id"], + user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows + assert len(hass.config_entries.async_entries("zha")) == num_entries -async def test_setup_zha(hass: HomeAssistant) -> None: +async def test_setup_zha(mock_zha_config_flow_setup, hass: HomeAssistant) -> None: """Test zha gets the right config.""" # Setup the config entry config_entry = MockConfigEntry( @@ -65,17 +98,30 @@ async def test_setup_zha(hass: HomeAssistant) -> None: return_value=True, ) as mock_is_plugged_in, patch( "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), patch( - "zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True ): - assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(mock_is_plugged_in.mock_calls) == 1 + zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") + assert len(zha_flows) == 1 + assert zha_flows[0]["step_id"] == "choose_formation_strategy" + + # Finish setting up ZHA + await hass.config_entries.flow.async_configure( + zha_flows[0]["flow_id"], + user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + config_entry = hass.config_entries.async_entries("zha")[0] assert config_entry.data == { - "device": {"baudrate": 115200, "flow_control": None, "path": "bla_device"}, - "radio_type": "znp", + "device": { + "baudrate": 115200, + "flow_control": "software", + "path": "bla_device", + }, + "radio_type": "ezsp", } assert config_entry.options == {} assert config_entry.title == "bla_description" From 1f08635d0abc8e52b5f150db1a36110a675a91fe Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 31 Aug 2022 00:31:51 +0000 Subject: [PATCH 782/903] [ci skip] Translation update --- .../components/bluetooth/translations/pl.json | 3 +- .../components/bthome/translations/pl.json | 22 +++ .../components/lametric/translations/ca.json | 2 + .../components/led_ble/translations/et.json | 23 ++++ .../components/led_ble/translations/no.json | 23 ++++ .../components/led_ble/translations/pl.json | 15 ++ .../led_ble/translations/pt-BR.json | 6 +- .../components/led_ble/translations/ru.json | 23 ++++ .../led_ble/translations/zh-Hant.json | 23 ++++ .../litterrobot/translations/pl.json | 9 +- .../litterrobot/translations/ru.json | 10 +- .../components/melnor/translations/pt-BR.json | 13 ++ .../nam/translations/sensor.ru.json | 11 ++ .../components/prusalink/translations/ca.json | 18 +++ .../components/prusalink/translations/de.json | 18 +++ .../components/prusalink/translations/en.json | 1 + .../components/prusalink/translations/es.json | 18 +++ .../components/prusalink/translations/et.json | 18 +++ .../components/prusalink/translations/no.json | 18 +++ .../components/prusalink/translations/pl.json | 18 +++ .../prusalink/translations/pt-BR.json | 18 +++ .../components/prusalink/translations/ru.json | 18 +++ .../prusalink/translations/sensor.ca.json | 11 ++ .../prusalink/translations/sensor.de.json | 11 ++ .../prusalink/translations/sensor.es.json | 11 ++ .../prusalink/translations/sensor.et.json | 11 ++ .../prusalink/translations/sensor.no.json | 11 ++ .../prusalink/translations/sensor.pl.json | 11 ++ .../prusalink/translations/sensor.pt-BR.json | 11 ++ .../prusalink/translations/sensor.ru.json | 11 ++ .../translations/sensor.zh-Hant.json | 11 ++ .../prusalink/translations/zh-Hant.json | 18 +++ .../components/skybell/translations/pl.json | 6 + .../thermobeacon/translations/pl.json | 21 +++ .../components/thermopro/translations/pl.json | 21 +++ .../unifiprotect/translations/ru.json | 4 + .../components/zha/translations/ca.json | 56 ++++++++ .../components/zha/translations/de.json | 129 +++++++++++++++++- .../components/zha/translations/en.json | 23 ++++ .../components/zha/translations/es.json | 128 ++++++++++++++++- .../components/zha/translations/pt-BR.json | 127 ++++++++++++++++- .../components/zha/translations/ru.json | 41 +++++- 42 files changed, 991 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/bthome/translations/pl.json create mode 100644 homeassistant/components/led_ble/translations/et.json create mode 100644 homeassistant/components/led_ble/translations/no.json create mode 100644 homeassistant/components/led_ble/translations/pl.json create mode 100644 homeassistant/components/led_ble/translations/ru.json create mode 100644 homeassistant/components/led_ble/translations/zh-Hant.json create mode 100644 homeassistant/components/melnor/translations/pt-BR.json create mode 100644 homeassistant/components/nam/translations/sensor.ru.json create mode 100644 homeassistant/components/prusalink/translations/ca.json create mode 100644 homeassistant/components/prusalink/translations/de.json create mode 100644 homeassistant/components/prusalink/translations/es.json create mode 100644 homeassistant/components/prusalink/translations/et.json create mode 100644 homeassistant/components/prusalink/translations/no.json create mode 100644 homeassistant/components/prusalink/translations/pl.json create mode 100644 homeassistant/components/prusalink/translations/pt-BR.json create mode 100644 homeassistant/components/prusalink/translations/ru.json create mode 100644 homeassistant/components/prusalink/translations/sensor.ca.json create mode 100644 homeassistant/components/prusalink/translations/sensor.de.json create mode 100644 homeassistant/components/prusalink/translations/sensor.es.json create mode 100644 homeassistant/components/prusalink/translations/sensor.et.json create mode 100644 homeassistant/components/prusalink/translations/sensor.no.json create mode 100644 homeassistant/components/prusalink/translations/sensor.pl.json create mode 100644 homeassistant/components/prusalink/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/prusalink/translations/sensor.ru.json create mode 100644 homeassistant/components/prusalink/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/prusalink/translations/zh-Hant.json create mode 100644 homeassistant/components/thermobeacon/translations/pl.json create mode 100644 homeassistant/components/thermopro/translations/pl.json diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index 0c7c8f828b5..9c52b5a136f 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -24,7 +24,8 @@ "step": { "init": { "data": { - "adapter": "Adapter Bluetooth u\u017cywany do skanowania" + "adapter": "Adapter Bluetooth u\u017cywany do skanowania", + "passive": "Skanowanie pasywne" } } } diff --git a/homeassistant/components/bthome/translations/pl.json b/homeassistant/components/bthome/translations/pl.json new file mode 100644 index 00000000000..814c4cd9757 --- /dev/null +++ b/homeassistant/components/bthome/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ca.json b/homeassistant/components/lametric/translations/ca.json index bdeed6a5759..4c8f002ce68 100644 --- a/homeassistant/components/lametric/translations/ca.json +++ b/homeassistant/components/lametric/translations/ca.json @@ -4,6 +4,8 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "invalid_discovery_info": "S'ha rebut informaci\u00f3 de descobriment no v\u00e0lida", + "missing_configuration": "La integraci\u00f3 LaMetric no est\u00e0 configurada. Consulta la documentaci\u00f3.", + "no_devices": "L'usuari autoritzat no t\u00e9 cap dispositiu LaMetric", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" }, "error": { diff --git a/homeassistant/components/led_ble/translations/et.json b/homeassistant/components/led_ble/translations/et.json new file mode 100644 index 00000000000..4de1315540e --- /dev/null +++ b/homeassistant/components/led_ble/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "no_unconfigured_devices": "H\u00e4\u00e4lestamata seadmeid ei leitud", + "not_supported": "Seadet ei toetata" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetoothi aadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/no.json b/homeassistant/components/led_ble/translations/no.json new file mode 100644 index 00000000000..4f21fe61bc7 --- /dev/null +++ b/homeassistant/components/led_ble/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "no_unconfigured_devices": "Fant ingen ukonfigurerte enheter.", + "not_supported": "Enheten st\u00f8ttes ikke" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/pl.json b/homeassistant/components/led_ble/translations/pl.json new file mode 100644 index 00000000000..b59a7e3bc02 --- /dev/null +++ b/homeassistant/components/led_ble/translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "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": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/pt-BR.json b/homeassistant/components/led_ble/translations/pt-BR.json index 6b58b4193a5..ef680fa0f81 100644 --- a/homeassistant/components/led_ble/translations/pt-BR.json +++ b/homeassistant/components/led_ble/translations/pt-BR.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado.", "not_supported": "Dispositivo n\u00e3o suportado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "flow_title": "{name}", diff --git a/homeassistant/components/led_ble/translations/ru.json b/homeassistant/components/led_ble/translations/ru.json new file mode 100644 index 00000000000..dc0d2db5a6e --- /dev/null +++ b/homeassistant/components/led_ble/translations/ru.json @@ -0,0 +1,23 @@ +{ + "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.", + "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_unconfigured_devices": "\u041d\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e.", + "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/zh-Hant.json b/homeassistant/components/led_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..ac129f22d4b --- /dev/null +++ b/homeassistant/components/led_ble/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\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", + "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "\u85cd\u7259\u4f4d\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/pl.json b/homeassistant/components/litterrobot/translations/pl.json index 41654933a6f..c0a83866936 100644 --- a/homeassistant/components/litterrobot/translations/pl.json +++ b/homeassistant/components/litterrobot/translations/pl.json @@ -1,7 +1,8 @@ { "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": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,6 +10,12 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/litterrobot/translations/ru.json b/homeassistant/components/litterrobot/translations/ru.json index c31f79d1d04..a336adcc787 100644 --- a/homeassistant/components/litterrobot/translations/ru.json +++ b/homeassistant/components/litterrobot/translations/ru.json @@ -1,7 +1,8 @@ { "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": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +10,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/melnor/translations/pt-BR.json b/homeassistant/components/melnor/translations/pt-BR.json new file mode 100644 index 00000000000..4e6c1b29a9c --- /dev/null +++ b/homeassistant/components/melnor/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "N\u00e3o h\u00e1 dispositivos Melnor Bluetooth nas proximidades." + }, + "step": { + "bluetooth_confirm": { + "description": "Deseja adicionar a v\u00e1lvula Melnor Bluetooth `{name}` ao Home Assistant?", + "title": "V\u00e1lvula Melnor Bluetooth descoberta" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.ru.json b/homeassistant/components/nam/translations/sensor.ru.json new file mode 100644 index 00000000000..7d360dbefa2 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.ru.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", + "low": "\u041d\u0438\u0437\u043a\u0438\u0439", + "medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439", + "very high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439", + "very low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/ca.json b/homeassistant/components/prusalink/translations/ca.json new file mode 100644 index 00000000000..aaabfa27677 --- /dev/null +++ b/homeassistant/components/prusalink/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "not_supported": "Nom\u00e9s s'admet l'API PrusaLink v2", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/de.json b/homeassistant/components/prusalink/translations/de.json new file mode 100644 index 00000000000..bf30cbc3f28 --- /dev/null +++ b/homeassistant/components/prusalink/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "not_supported": "Nur PrusaLink API v2 wird unterst\u00fctzt", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/en.json b/homeassistant/components/prusalink/translations/en.json index e9be6d8d96e..ad52d4082de 100644 --- a/homeassistant/components/prusalink/translations/en.json +++ b/homeassistant/components/prusalink/translations/en.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "not_supported": "Only PrusaLink API v2 is supported", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/prusalink/translations/es.json b/homeassistant/components/prusalink/translations/es.json new file mode 100644 index 00000000000..094840cf785 --- /dev/null +++ b/homeassistant/components/prusalink/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "not_supported": "Solo se admite la API v2 de PrusaLink", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/et.json b/homeassistant/components/prusalink/translations/et.json new file mode 100644 index 00000000000..250183a30cf --- /dev/null +++ b/homeassistant/components/prusalink/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "not_supported": "Toetatud on ainult PrusaLink API v2", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/no.json b/homeassistant/components/prusalink/translations/no.json new file mode 100644 index 00000000000..4a81bfb490f --- /dev/null +++ b/homeassistant/components/prusalink/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "not_supported": "Bare PrusaLink API v2 st\u00f8ttes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/pl.json b/homeassistant/components/prusalink/translations/pl.json new file mode 100644 index 00000000000..2cbbaeec07d --- /dev/null +++ b/homeassistant/components/prusalink/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "not_supported": "Obs\u0142ugiwane jest tylko PrusaLink API v2", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/pt-BR.json b/homeassistant/components/prusalink/translations/pt-BR.json new file mode 100644 index 00000000000..2959fa6fe99 --- /dev/null +++ b/homeassistant/components/prusalink/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "not_supported": "Somente a PrusaLink API v2 \u00e9 suportada", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "host": "Nome do host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/ru.json b/homeassistant/components/prusalink/translations/ru.json new file mode 100644 index 00000000000..7bde8101310 --- /dev/null +++ b/homeassistant/components/prusalink/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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "not_supported": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e PrusaLink API v2.", + "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", + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.ca.json b/homeassistant/components/prusalink/translations/sensor.ca.json new file mode 100644 index 00000000000..b262208a54e --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.ca.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "S'est\u00e0 cancel\u00b7lant", + "idle": "Inactiva", + "paused": "Pausada", + "pausing": "Aturant", + "printing": "Imprimint" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.de.json b/homeassistant/components/prusalink/translations/sensor.de.json new file mode 100644 index 00000000000..cc56a336447 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.de.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Abbrechen", + "idle": "Leerlauf", + "paused": "Angehalten", + "pausing": "Anhalten", + "printing": "Druckt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.es.json b/homeassistant/components/prusalink/translations/sensor.es.json new file mode 100644 index 00000000000..0ad06d967b2 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.es.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Cancelando", + "idle": "Inactivo", + "paused": "En pausa", + "pausing": "Pausando", + "printing": "Imprimiendo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.et.json b/homeassistant/components/prusalink/translations/sensor.et.json new file mode 100644 index 00000000000..8b8dd37e2f3 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.et.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Loobumine", + "idle": "Ootel", + "paused": "Peatatud", + "pausing": "Pausi tegemine", + "printing": "Tr\u00fckkimine" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.no.json b/homeassistant/components/prusalink/translations/sensor.no.json new file mode 100644 index 00000000000..3f353c53b88 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.no.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Avbryter", + "idle": "Inaktiv", + "paused": "Pauset", + "pausing": "Setter p\u00e5 pause", + "printing": "Printing" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.pl.json b/homeassistant/components/prusalink/translations/sensor.pl.json new file mode 100644 index 00000000000..a28232a63a1 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.pl.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Anulowanie", + "idle": "Bezczynny", + "paused": "Wstrzymany", + "pausing": "Wstrzymywanie", + "printing": "Drukowanie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.pt-BR.json b/homeassistant/components/prusalink/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..bbe7eb630d6 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.pt-BR.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Cancelando", + "idle": "Ocioso", + "paused": "Pausado", + "pausing": "Pausando", + "printing": "Imprimindo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.ru.json b/homeassistant/components/prusalink/translations/sensor.ru.json new file mode 100644 index 00000000000..9cf95a03c77 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.ru.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u041e\u0442\u043c\u0435\u043d\u0430", + "idle": "\u0411\u0435\u0437\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435", + "paused": "\u041f\u0440\u0438\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d", + "pausing": "\u041f\u0440\u0438\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430", + "printing": "\u041f\u0435\u0447\u0430\u0442\u044c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.zh-Hant.json b/homeassistant/components/prusalink/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..0d537a0edfe --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.zh-Hant.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u6b63\u5728\u53d6\u6d88", + "idle": "\u9592\u7f6e", + "paused": "\u5df2\u66ab\u505c", + "pausing": "\u66ab\u505c\u4e2d", + "printing": "\u5217\u5370\u4e2d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/zh-Hant.json b/homeassistant/components/prusalink/translations/zh-Hant.json new file mode 100644 index 00000000000..43d34d20540 --- /dev/null +++ b/homeassistant/components/prusalink/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "not_supported": "\u50c5\u652f\u63f4 PrusaLink API v2", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/pl.json b/homeassistant/components/skybell/translations/pl.json index 32e23d406ab..51aac2f85ac 100644 --- a/homeassistant/components/skybell/translations/pl.json +++ b/homeassistant/components/skybell/translations/pl.json @@ -10,6 +10,12 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "email": "Adres e-mail", diff --git a/homeassistant/components/thermobeacon/translations/pl.json b/homeassistant/components/thermobeacon/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/pl.json b/homeassistant/components/thermopro/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/thermopro/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index e81404f2a95..c1e5558c204 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u043e\u0431\u043e\u0439 \u0441\u043f\u0438\u0441\u043e\u043a MAC-\u0430\u0434\u0440\u0435\u0441\u043e\u0432, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438." + }, "step": { "init": { "data": { "all_updates": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438 (\u0412\u041d\u0418\u041c\u0410\u041d\u0418\u0415: \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043d\u0430 \u0426\u041f)", "disable_rtsp": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0442\u043e\u043a RTSP", + "ignored_devices": "\u0421\u043f\u0438\u0441\u043e\u043a MAC-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c, \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e", "max_media": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0434\u043b\u044f \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u043c\u0443\u043b\u044c\u0442\u0438\u043c\u0435\u0434\u0438\u0430 (\u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0439 \u043f\u0430\u043c\u044f\u0442\u0438)", "override_connection_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0443\u0437\u0435\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 6d387bc1306..792ea63c57d 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -10,12 +10,27 @@ }, "flow_title": "{name}", "step": { + "choose_serial_port": { + "title": "Selecciona un port s\u00e8rie" + }, "confirm": { "description": "Vols configurar {name}?" }, "confirm_hardware": { "description": "Vols configurar {name}?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Tipus de r\u00e0dio" + }, + "title": "Tipus de r\u00e0dio" + }, + "manual_port_config": { + "data": { + "baudrate": "velocitat del port" + }, + "title": "Configuraci\u00f3 del port s\u00e8rie" + }, "pick_radio": { "data": { "radio_type": "Tipus de r\u00e0dio" @@ -32,6 +47,11 @@ "description": "Introdueix la configuraci\u00f3 espec\u00edfica de port", "title": "Configuraci\u00f3" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Puja un fitxer" + } + }, "user": { "data": { "path": "Ruta del port s\u00e8rie al dispositiu" @@ -114,5 +134,41 @@ "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades" } + }, + "options": { + "abort": { + "not_zha_device": "Aquest no \u00e9s un dispositiu zha", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", + "usb_probe_failed": "No s'ha pogut provar el dispositiu USB" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{name}", + "step": { + "choose_serial_port": { + "title": "Selecciona un port s\u00e8rie" + }, + "init": { + "title": "Reconfiguraci\u00f3 de ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Tipus de r\u00e0dio" + }, + "title": "Tipus de r\u00e0dio" + }, + "manual_port_config": { + "data": { + "baudrate": "velocitat del port" + }, + "title": "Configuraci\u00f3 del port s\u00e8rie" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Puja un fitxer" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index c85b94a632d..60ea4fcc615 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -6,16 +6,64 @@ "usb_probe_failed": "Fehler beim Testen des USB-Ger\u00e4ts" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_backup_json": "Ung\u00fcltige Backup-JSON" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "W\u00e4hle ein automatisches Backup" + }, + "description": "Wiederherstellung der Netzwerkeinstellungen aus einer automatischen Sicherung", + "title": "Automatisches Backup wiederherstellen" + }, + "choose_formation_strategy": { + "description": "W\u00e4hle die Netzwerkeinstellungen f\u00fcr dein Funkger\u00e4t.", + "menu_options": { + "choose_automatic_backup": "Wiederherstellung eines automatischen Backups", + "form_new_network": "L\u00f6schen der Netzwerkeinstellungen und Aufbau eines neuen Netzwerks", + "reuse_settings": "Funknetzeinstellungen beibehalten", + "upload_manual_backup": "Manuelles Backup hochladen" + }, + "title": "Netzwerkbildung" + }, + "choose_serial_port": { + "data": { + "path": "Serieller Ger\u00e4tepfad" + }, + "description": "W\u00e4hle den seriellen Anschluss f\u00fcr dein Zigbee-Funkger\u00e4t", + "title": "W\u00e4hle einen seriellen Port" + }, "confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, "confirm_hardware": { "description": "M\u00f6chtest du {name} einrichten?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Funktyp" + }, + "description": "W\u00e4hle deinen Zigbee-Funktyp aus", + "title": "Funktyp" + }, + "manual_port_config": { + "data": { + "baudrate": "Port-Geschwindigkeit", + "flow_control": "Datenflusskontrolle", + "path": "Serieller Ger\u00e4tepfad" + }, + "description": "Eingabe der Einstellungen f\u00fcr die serielle Schnittstelle", + "title": "Einstellungen der seriellen Schnittstelle" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Dauerhaftes Ersetzen der IEEE-Funkadresse" + }, + "description": "Dein Backup hat eine andere IEEE-Adresse als dein Funkger\u00e4t. Damit dein Netzwerk ordnungsgem\u00e4\u00df funktioniert, sollte auch die IEEE-Adresse deines Funkger\u00e4ts ge\u00e4ndert werden.\n\nDies ist ein permanenter Vorgang.", + "title": "Funk-IEEE-Adresse \u00fcberschreiben" + }, "pick_radio": { "data": { "radio_type": "Funktyp" @@ -32,6 +80,13 @@ "description": "Gib die portspezifischen Einstellungen ein", "title": "Einstellungen" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Datei hochladen" + }, + "description": "Stelle deine Netzwerkeinstellungen aus einer hochgeladenen Backup-JSON-Datei wieder her. Du kannst eine von einer anderen ZHA-Installation unter **Netzwerkeinstellungen** herunterladen oder eine Zigbee2MQTT-Datei \"coordinator_backup.json\" verwenden.", + "title": "Manuelles Backup hochladen" + }, "user": { "data": { "path": "Serieller Ger\u00e4tepfad" @@ -114,5 +169,77 @@ "remote_button_short_release": "\"{subtype}\" Taste losgelassen", "remote_button_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt" } + }, + "options": { + "abort": { + "not_zha_device": "Dieses Ger\u00e4t ist kein ZHA-Ger\u00e4t", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "usb_probe_failed": "Fehler beim Testen des USB-Ger\u00e4ts" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_backup_json": "Ung\u00fcltige Backup-JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "W\u00e4hle ein automatisches Backup" + }, + "description": "Wiederherstellung der Netzwerkeinstellungen aus einer automatischen Sicherung", + "title": "Automatisches Backup wiederherstellen" + }, + "choose_formation_strategy": { + "description": "W\u00e4hle die Netzwerkeinstellungen f\u00fcr dein Funkger\u00e4t.", + "menu_options": { + "choose_automatic_backup": "Wiederherstellung eines automatischen Backups", + "form_new_network": "L\u00f6schen der Netzwerkeinstellungen und Aufbau eines neuen Netzwerks", + "reuse_settings": "Funknetzeinstellungen beibehalten", + "upload_manual_backup": "Manuelles Backup hochladen" + }, + "title": "Netzwerkbildung" + }, + "choose_serial_port": { + "data": { + "path": "Serieller Ger\u00e4tepfad" + }, + "description": "W\u00e4hle den seriellen Anschluss f\u00fcr dein Zigbee-Funkger\u00e4t", + "title": "W\u00e4hle einen seriellen Port" + }, + "init": { + "description": "ZHA wird gestoppt. M\u00f6chtest du fortfahren?", + "title": "ZHA rekonfigurieren" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Funktyp" + }, + "description": "W\u00e4hle deinen Zigbee-Funktyp aus", + "title": "Funktyp" + }, + "manual_port_config": { + "data": { + "baudrate": "Port-Geschwindigkeit", + "flow_control": "Datenflusskontrolle", + "path": "Serieller Ger\u00e4tepfad" + }, + "description": "Eingabe der Einstellungen f\u00fcr die serielle Schnittstelle", + "title": "Einstellungen der seriellen Schnittstelle" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Dauerhaftes Ersetzen der IEEE-Funkadresse" + }, + "description": "Dein Backup hat eine andere IEEE-Adresse als dein Funkger\u00e4t. Damit dein Netzwerk ordnungsgem\u00e4\u00df funktioniert, sollte auch die IEEE-Adresse deines Funkger\u00e4ts ge\u00e4ndert werden.\n\nDies ist ein permanenter Vorgang.", + "title": "Funk-IEEE-Adresse \u00fcberschreiben" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Datei hochladen" + }, + "description": "Stelle deine Netzwerkeinstellungen aus einer hochgeladenen Backup-JSON-Datei wieder her. Du kannst eine von einer anderen ZHA-Installation unter **Netzwerkeinstellungen** herunterladen oder eine Zigbee2MQTT-Datei \"coordinator_backup.json\" verwenden.", + "title": "Manuelles Backup hochladen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 27a4db9ef02..1ba19bc0f9e 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -64,12 +64,35 @@ "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", "title": "Overwrite Radio IEEE Address" }, + "pick_radio": { + "data": { + "radio_type": "Radio Type" + }, + "description": "Pick a type of your Zigbee radio", + "title": "Radio Type" + }, + "port_config": { + "data": { + "baudrate": "port speed", + "flow_control": "data flow control", + "path": "Serial device path" + }, + "description": "Enter port specific settings", + "title": "Settings" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Upload a file" }, "description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.", "title": "Upload a Manual Backup" + }, + "user": { + "data": { + "path": "Serial Device Path" + }, + "description": "Select serial port for Zigbee radio", + "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index a3e5f0b63ef..feb8b9efaed 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -6,16 +6,64 @@ "usb_probe_failed": "Error al sondear el dispositivo USB" }, "error": { - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "invalid_backup_json": "Copia de seguridad JSON no v\u00e1lida" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Elige una copia de seguridad autom\u00e1tica" + }, + "description": "Restaura la configuraci\u00f3n de tu red desde una copia de seguridad autom\u00e1tica", + "title": "Restaurar copia de seguridad autom\u00e1tica" + }, + "choose_formation_strategy": { + "description": "Elige la configuraci\u00f3n de red para tu radio.", + "menu_options": { + "choose_automatic_backup": "Restaurar una copia de seguridad autom\u00e1tica", + "form_new_network": "Borrar la configuraci\u00f3n de red y formar una nueva red", + "reuse_settings": "Mantener la configuraci\u00f3n de red de la radio", + "upload_manual_backup": "Subir una copia de seguridad manual" + }, + "title": "Formaci\u00f3n de la red" + }, + "choose_serial_port": { + "data": { + "path": "Ruta del Dispositivo Serie" + }, + "description": "Selecciona el puerto serie para tu radio Zigbee", + "title": "Selecciona un puerto serie" + }, "confirm": { "description": "\u00bfQuieres configurar {name} ?" }, "confirm_hardware": { "description": "\u00bfQuieres configurar {name}?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Tipo de Radio" + }, + "description": "Elige tu tipo de radio Zigbee", + "title": "Tipo de Radio" + }, + "manual_port_config": { + "data": { + "baudrate": "velocidad del puerto", + "flow_control": "control de flujo de datos", + "path": "Ruta del dispositivo serie" + }, + "description": "Introduce la configuraci\u00f3n del puerto serie", + "title": "Configuraci\u00f3n del puerto serie" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Reemplazar permanentemente la direcci\u00f3n IEEE de la radio" + }, + "description": "Tu copia de seguridad tiene una direcci\u00f3n IEEE diferente a la de tu radio. Para que tu red funcione correctamente, tambi\u00e9n debes cambiar la direcci\u00f3n IEEE de tu radio. \n\nEsta es una operaci\u00f3n permanente.", + "title": "Sobrescribir la direcci\u00f3n IEEE de la radio" + }, "pick_radio": { "data": { "radio_type": "Tipo de Radio" @@ -32,6 +80,13 @@ "description": "Introduce los ajustes espec\u00edficos del puerto", "title": "Configuraci\u00f3n" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Subir un archivo" + }, + "description": "Restaura la configuraci\u00f3n de tu red desde un archivo JSON de copia de seguridad subido. Puedes descargar uno de una instalaci\u00f3n diferente de ZHA desde **Configuraci\u00f3n de red**, o usar un archivo `coordinator_backup.json` de Zigbee2MQTT.", + "title": "Subir una copia de seguridad manual" + }, "user": { "data": { "path": "Ruta del Dispositivo Serie" @@ -114,5 +169,76 @@ "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado tres veces" } + }, + "options": { + "abort": { + "not_zha_device": "Este dispositivo no es un dispositivo zha", + "usb_probe_failed": "Error al sondear el dispositivo USB" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_backup_json": "Copia de seguridad JSON no v\u00e1lida" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Elige una copia de seguridad autom\u00e1tica" + }, + "description": "Restaura la configuraci\u00f3n de tu red desde una copia de seguridad autom\u00e1tica", + "title": "Restaurar copia de seguridad autom\u00e1tica" + }, + "choose_formation_strategy": { + "description": "Elige la configuraci\u00f3n de red para tu radio.", + "menu_options": { + "choose_automatic_backup": "Restaurar una copia de seguridad autom\u00e1tica", + "form_new_network": "Borrar la configuraci\u00f3n de red y formar una nueva red", + "reuse_settings": "Mantener la configuraci\u00f3n de red de la radio", + "upload_manual_backup": "Subir una copia de seguridad manual" + }, + "title": "Formaci\u00f3n de la red" + }, + "choose_serial_port": { + "data": { + "path": "Ruta del Dispositivo Serie" + }, + "description": "Selecciona el puerto serie para tu radio Zigbee", + "title": "Selecciona un puerto serie" + }, + "init": { + "description": "ZHA se detendr\u00e1. \u00bfDeseas continuar?", + "title": "Reconfigurar ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Tipo de Radio" + }, + "description": "Elige tu tipo de radio Zigbee", + "title": "Tipo de Radio" + }, + "manual_port_config": { + "data": { + "baudrate": "velocidad del puerto", + "flow_control": "control de flujo de datos", + "path": "Ruta del dispositivo serie" + }, + "description": "Introduce la configuraci\u00f3n del puerto serie", + "title": "Configuraci\u00f3n del puerto serie" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Reemplazar permanentemente la direcci\u00f3n IEEE de la radio" + }, + "description": "Tu copia de seguridad tiene una direcci\u00f3n IEEE diferente a la de tu radio. Para que tu red funcione correctamente, tambi\u00e9n debes cambiar la direcci\u00f3n IEEE de tu radio. \n\nEsta es una operaci\u00f3n permanente.", + "title": "Sobrescribir la direcci\u00f3n IEEE de la radio" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Subir un archivo" + }, + "description": "Restaura la configuraci\u00f3n de tu red desde un archivo JSON de copia de seguridad subido. Puedes descargar uno de una instalaci\u00f3n diferente de ZHA desde **Configuraci\u00f3n de red**, o usar un archivo `coordinator_backup.json` de Zigbee2MQTT.", + "title": "Subir una copia de seguridad manual" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 35936dbe60b..0d78cea3201 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -6,16 +6,64 @@ "usb_probe_failed": "Falha ao sondar o dispositivo usb" }, "error": { - "cannot_connect": "Falha ao conectar" + "cannot_connect": "Falha ao conectar", + "invalid_backup_json": "JSON de backup inv\u00e1lido" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Escolha um backup autom\u00e1tico" + }, + "description": "Restaure suas configura\u00e7\u00f5es de rede a partir de um backup autom\u00e1tico", + "title": "Restaurar Backup Autom\u00e1tico" + }, + "choose_formation_strategy": { + "description": "Escolha as configura\u00e7\u00f5es de rede para o seu r\u00e1dio.", + "menu_options": { + "choose_automatic_backup": "Restaurar um backup autom\u00e1tico", + "form_new_network": "Apague as configura\u00e7\u00f5es de rede e forme uma nova rede", + "reuse_settings": "Manter as configura\u00e7\u00f5es de rede de r\u00e1dio", + "upload_manual_backup": "Carregar um backup manualmente" + }, + "title": "Forma\u00e7\u00e3o de rede" + }, + "choose_serial_port": { + "data": { + "path": "Caminho do dispositivo serial" + }, + "description": "Selecione a porta serial para o seu r\u00e1dio Zigbee", + "title": "Selecione uma porta serial" + }, "confirm": { "description": "Voc\u00ea deseja configurar {name}?" }, "confirm_hardware": { "description": "Deseja configurar {name}?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Tipo de r\u00e1dio" + }, + "description": "Escolha seu tipo de r\u00e1dio Zigbee", + "title": "Tipo de r\u00e1dio" + }, + "manual_port_config": { + "data": { + "baudrate": "velocidade da porta", + "flow_control": "controle de fluxo de dados", + "path": "Caminho do dispositivo serial" + }, + "description": "Digite as configura\u00e7\u00f5es da porta serial", + "title": "Configura\u00e7\u00f5es da porta serial" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Substituir permanentemente o endere\u00e7o IEEE do r\u00e1dio" + }, + "description": "Seu backup tem um endere\u00e7o IEEE diferente do seu r\u00e1dio. Para que sua rede funcione corretamente, o endere\u00e7o IEEE do seu r\u00e1dio tamb\u00e9m deve ser alterado. \n\n Esta \u00e9 uma opera\u00e7\u00e3o permanente.", + "title": "Sobrescrever o endere\u00e7o IEEE do r\u00e1dio" + }, "pick_radio": { "data": { "radio_type": "Tipo de hub zigbee" @@ -32,6 +80,13 @@ "description": "Digite configura\u00e7\u00f5es espec\u00edficas da porta", "title": "Configura\u00e7\u00f5es" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Carregar um arquivo" + }, + "description": "Restaure suas configura\u00e7\u00f5es de rede de um arquivo JSON de backup carregado. Voc\u00ea pode baixar um de uma instala\u00e7\u00e3o ZHA diferente em **Network Settings**, ou usar um arquivo Zigbee2MQTT `coordinator_backup.json`.", + "title": "Carregar um backup manualmente" + }, "user": { "data": { "path": "Caminho do dispositivo serial" @@ -114,5 +169,75 @@ "remote_button_short_release": "Bot\u00e3o \" {subtype} \" liberado", "remote_button_triple_press": "Bot\u00e3o \" {subtype} \" clicado tr\u00eas vezes" } + }, + "options": { + "abort": { + "not_zha_device": "Este dispositivo n\u00e3o \u00e9 um dispositivo zha", + "usb_probe_failed": "Falha ao sondar o dispositivo usb" + }, + "error": { + "invalid_backup_json": "JSON de backup inv\u00e1lido" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Escolha um backup autom\u00e1tico" + }, + "description": "Restaure suas configura\u00e7\u00f5es de rede a partir de um backup autom\u00e1tico", + "title": "Restaurar Backup Autom\u00e1tico" + }, + "choose_formation_strategy": { + "description": "Escolha as configura\u00e7\u00f5es de rede para o seu r\u00e1dio.", + "menu_options": { + "choose_automatic_backup": "Restaurar um backup autom\u00e1tico", + "form_new_network": "Apague as configura\u00e7\u00f5es de rede e forme uma nova rede", + "reuse_settings": "Manter as configura\u00e7\u00f5es de rede de r\u00e1dio", + "upload_manual_backup": "Carregar um backup manualmente" + }, + "title": "Forma\u00e7\u00e3o de rede" + }, + "choose_serial_port": { + "data": { + "path": "Caminho do dispositivo serial" + }, + "description": "Selecione a porta serial para o seu r\u00e1dio Zigbee", + "title": "Selecione uma porta serial" + }, + "init": { + "description": "ZHA ser\u00e1 interrompido. Voc\u00ea deseja continuar?", + "title": "Reconfigurar ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Tipo de r\u00e1dio" + }, + "description": "Escolha seu tipo de r\u00e1dio Zigbee", + "title": "Tipo de r\u00e1dio" + }, + "manual_port_config": { + "data": { + "baudrate": "velocidade da porta", + "flow_control": "controle de fluxo de dados", + "path": "Caminho do dispositivo serial" + }, + "description": "Digite as configura\u00e7\u00f5es da porta serial", + "title": "Configura\u00e7\u00f5es da porta serial" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Substituir permanentemente o endere\u00e7o IEEE do r\u00e1dio" + }, + "description": "Seu backup tem um endere\u00e7o IEEE diferente do seu r\u00e1dio. Para que sua rede funcione corretamente, o endere\u00e7o IEEE do seu r\u00e1dio tamb\u00e9m deve ser alterado.\n\nEsta \u00e9 uma opera\u00e7\u00e3o permanente.", + "title": "Sobrescrever o endere\u00e7o IEEE do r\u00e1dio" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Carregar um arquivo" + }, + "description": "Restaure suas configura\u00e7\u00f5es de rede de um arquivo JSON de backup carregado. Voc\u00ea pode baixar um de uma instala\u00e7\u00e3o ZHA diferente em **Network Settings**, ou usar um arquivo Zigbee2MQTT `coordinator_backup.json`.", + "title": "Carregar um backup manualmente" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index ef705784be8..9204a618f2f 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -6,16 +6,55 @@ "usb_probe_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\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." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_backup_json": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 JSON \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0433\u043e \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0435 \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0438\u0437 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438", + "title": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0438\u0437 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438" + }, + "choose_formation_strategy": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e.", + "menu_options": { + "choose_automatic_backup": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0438\u0437 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438", + "form_new_network": "\u0421\u0442\u0435\u0440\u0435\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438 \u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0441\u0435\u0442\u044c", + "reuse_settings": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0440\u0430\u0434\u0438\u043e\u0441\u0435\u0442\u0438", + "upload_manual_backup": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u0443\u044e \u043a\u043e\u043f\u0438\u044e \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + }, + "title": "\u0424\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0438" + }, + "choose_serial_port": { + "data": { + "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Zigbee.", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442" + }, "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, "confirm_hardware": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Zigbee", + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "manual_port_config": { + "data": { + "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u043e\u0440\u0442\u0430", + "flow_control": "\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e\u0442\u043e\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0445", + "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + }, "pick_radio": { "data": { "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" From cdca08e68aed3f76345b295662e9ca6dd8f960c9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 31 Aug 2022 03:45:13 +0200 Subject: [PATCH 783/903] Add periodic system stats to hardware integration (#76873) --- homeassistant/components/hardware/__init__.py | 2 +- .../components/hardware/manifest.json | 3 +- .../components/hardware/websocket_api.py | 80 ++++++++++++++++++- requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../components/hardware/test_websocket_api.py | 71 ++++++++++++++++ 6 files changed, 156 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hardware/__init__.py b/homeassistant/components/hardware/__init__.py index b3f342d4e32..a1198534213 100644 --- a/homeassistant/components/hardware/__init__.py +++ b/homeassistant/components/hardware/__init__.py @@ -12,6 +12,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Hardware.""" hass.data[DOMAIN] = {} - websocket_api.async_setup(hass) + await websocket_api.async_setup(hass) return True diff --git a/homeassistant/components/hardware/manifest.json b/homeassistant/components/hardware/manifest.json index e7e156b6065..94571ce4528 100644 --- a/homeassistant/components/hardware/manifest.json +++ b/homeassistant/components/hardware/manifest.json @@ -3,5 +3,6 @@ "name": "Hardware", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/hardware", - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "requirements": ["psutil-home-assistant==0.0.1"] } diff --git a/homeassistant/components/hardware/websocket_api.py b/homeassistant/components/hardware/websocket_api.py index df3b8868053..5c4b14570a9 100644 --- a/homeassistant/components/hardware/websocket_api.py +++ b/homeassistant/components/hardware/websocket_api.py @@ -2,23 +2,41 @@ from __future__ import annotations import contextlib -from dataclasses import asdict +from dataclasses import asdict, dataclass +from datetime import datetime, timedelta +import psutil_home_assistant as ha_psutil import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.util.dt as dt_util from .const import DOMAIN from .hardware import async_process_hardware_platforms from .models import HardwareProtocol -@callback -def async_setup(hass: HomeAssistant) -> None: +@dataclass +class SystemStatus: + """System status.""" + + ha_psutil: ha_psutil + remove_periodic_timer: CALLBACK_TYPE | None + subscribers: set[tuple[websocket_api.ActiveConnection, int]] + + +async def async_setup(hass: HomeAssistant) -> None: """Set up the hardware websocket API.""" websocket_api.async_register_command(hass, ws_info) + websocket_api.async_register_command(hass, ws_subscribe_system_status) + hass.data[DOMAIN]["system_status"] = SystemStatus( + ha_psutil=await hass.async_add_executor_job(ha_psutil.PsutilWrapper), + remove_periodic_timer=None, + subscribers=set(), + ) @websocket_api.websocket_command( @@ -45,3 +63,57 @@ async def ws_info( hardware_info.extend([asdict(hw) for hw in platform.async_info(hass)]) connection.send_result(msg["id"], {"hardware": hardware_info}) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hardware/subscribe_system_status", + } +) +@websocket_api.async_response +async def ws_subscribe_system_status( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +): + """Subscribe to system status updates.""" + + system_status: SystemStatus = hass.data[DOMAIN]["system_status"] + + @callback + def async_update_status(now: datetime) -> None: + # Although cpu_percent and virtual_memory access files in the /proc vfs, those + # accesses do not block and we don't need to wrap the calls in an executor. + # https://elixir.bootlin.com/linux/v5.19.4/source/fs/proc/stat.c + # https://elixir.bootlin.com/linux/v5.19.4/source/fs/proc/meminfo.c#L32 + cpu_percentage = round( + system_status.ha_psutil.psutil.cpu_percent(interval=None) + ) + virtual_memory = system_status.ha_psutil.psutil.virtual_memory() + json_msg = { + "cpu_percent": cpu_percentage, + "memory_used_percent": virtual_memory.percent, + "memory_used_mb": round( + (virtual_memory.total - virtual_memory.available) / 1024**2, 1 + ), + "memory_free_mb": round(virtual_memory.available / 1024**2, 1), + "timestamp": dt_util.utcnow().isoformat(), + } + for connection, msg_id in system_status.subscribers: + connection.send_message(websocket_api.event_message(msg_id, json_msg)) + + if not system_status.subscribers: + system_status.remove_periodic_timer = async_track_time_interval( + hass, async_update_status, timedelta(seconds=5) + ) + + system_status.subscribers.add((connection, msg["id"])) + + @callback + def cancel_subscription() -> None: + system_status.subscribers.remove((connection, msg["id"])) + if not system_status.subscribers and system_status.remove_periodic_timer: + system_status.remove_periodic_timer() + system_status.remove_periodic_timer = None + + connection.subscriptions[msg["id"]] = cancel_subscription + + connection.send_message(websocket_api.result_message(msg["id"])) diff --git a/requirements_all.txt b/requirements_all.txt index 617f9615921..2f4680a361d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1309,6 +1309,9 @@ prometheus_client==0.7.1 # homeassistant.components.proxmoxve proxmoxer==1.3.1 +# homeassistant.components.hardware +psutil-home-assistant==0.0.1 + # homeassistant.components.systemmonitor psutil==5.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b909571ac52..93ef4590930 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -924,6 +924,9 @@ progettihwsw==0.1.1 # homeassistant.components.prometheus prometheus_client==0.7.1 +# homeassistant.components.hardware +psutil-home-assistant==0.0.1 + # homeassistant.components.androidtv pure-python-adb[async]==0.3.0.dev0 diff --git a/tests/components/hardware/test_websocket_api.py b/tests/components/hardware/test_websocket_api.py index 116879aa628..bc6fb5f11dd 100644 --- a/tests/components/hardware/test_websocket_api.py +++ b/tests/components/hardware/test_websocket_api.py @@ -1,7 +1,14 @@ """Test the hardware websocket API.""" +from collections import namedtuple +import datetime +from unittest.mock import patch + +import psutil_home_assistant as ha_psutil + from homeassistant.components.hardware.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util async def test_board_info(hass: HomeAssistant, hass_ws_client) -> None: @@ -16,3 +23,67 @@ async def test_board_info(hass: HomeAssistant, hass_ws_client) -> None: assert msg["id"] == 1 assert msg["success"] assert msg["result"] == {"hardware": []} + + +TEST_TIME_ADVANCE_INTERVAL = datetime.timedelta(seconds=5 + 1) + + +async def test_system_status_subscription(hass: HomeAssistant, hass_ws_client, freezer): + """Test websocket system status subscription.""" + + mock_psutil = None + orig_psutil_wrapper = ha_psutil.PsutilWrapper + + def create_mock_psutil(): + nonlocal mock_psutil + mock_psutil = orig_psutil_wrapper() + return mock_psutil + + with patch( + "homeassistant.components.hardware.websocket_api.ha_psutil.PsutilWrapper", + wraps=create_mock_psutil, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "hardware/subscribe_system_status"}) + response = await client.receive_json() + assert response["success"] + + VirtualMem = namedtuple("VirtualMemory", ["available", "percent", "total"]) + vmem = VirtualMem(10 * 1024**2, 50, 30 * 1024**2) + + with patch.object( + mock_psutil.psutil, + "cpu_percent", + return_value=123, + ), patch.object( + mock_psutil.psutil, + "virtual_memory", + return_value=vmem, + ): + freezer.tick(TEST_TIME_ADVANCE_INTERVAL) + await hass.async_block_till_done() + + response = await client.receive_json() + assert response["event"] == { + "cpu_percent": 123, + "memory_free_mb": 10.0, + "memory_used_mb": 20.0, + "memory_used_percent": 50, + "timestamp": dt_util.utcnow().isoformat(), + } + + # Unsubscribe + await client.send_json({"id": 8, "type": "unsubscribe_events", "subscription": 1}) + response = await client.receive_json() + assert response["success"] + + with patch.object(mock_psutil.psutil, "cpu_percent") as cpu_mock, patch.object( + mock_psutil.psutil, "virtual_memory" + ) as vmem_mock: + freezer.tick(TEST_TIME_ADVANCE_INTERVAL) + await hass.async_block_till_done() + cpu_mock.assert_not_called() + vmem_mock.assert_not_called() From edb8c5856669aeefd1d9cc56eca580675d2f72d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Aug 2022 21:49:31 -0400 Subject: [PATCH 784/903] Add sensorpro (BLE) integration (#77569) --- CODEOWNERS | 2 + .../components/sensorpro/__init__.py | 49 +++++ .../components/sensorpro/config_flow.py | 94 ++++++++ homeassistant/components/sensorpro/const.py | 3 + homeassistant/components/sensorpro/device.py | 31 +++ .../components/sensorpro/manifest.json | 22 ++ homeassistant/components/sensorpro/sensor.py | 140 ++++++++++++ .../components/sensorpro/strings.json | 22 ++ .../components/sensorpro/translations/en.json | 22 ++ homeassistant/generated/bluetooth.py | 22 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/sensorpro/__init__.py | 26 +++ tests/components/sensorpro/conftest.py | 8 + .../components/sensorpro/test_config_flow.py | 200 ++++++++++++++++++ tests/components/sensorpro/test_sensor.py | 50 +++++ 17 files changed, 698 insertions(+) create mode 100644 homeassistant/components/sensorpro/__init__.py create mode 100644 homeassistant/components/sensorpro/config_flow.py create mode 100644 homeassistant/components/sensorpro/const.py create mode 100644 homeassistant/components/sensorpro/device.py create mode 100644 homeassistant/components/sensorpro/manifest.json create mode 100644 homeassistant/components/sensorpro/sensor.py create mode 100644 homeassistant/components/sensorpro/strings.json create mode 100644 homeassistant/components/sensorpro/translations/en.json create mode 100644 tests/components/sensorpro/__init__.py create mode 100644 tests/components/sensorpro/conftest.py create mode 100644 tests/components/sensorpro/test_config_flow.py create mode 100644 tests/components/sensorpro/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 7fdfc5a73c2..575a79d7afa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -965,6 +965,8 @@ build.json @home-assistant/supervisor /tests/components/sensibo/ @andrey-git @gjohansson-ST /homeassistant/components/sensor/ @home-assistant/core /tests/components/sensor/ @home-assistant/core +/homeassistant/components/sensorpro/ @bdraco +/tests/components/sensorpro/ @bdraco /homeassistant/components/sensorpush/ @bdraco /tests/components/sensorpush/ @bdraco /homeassistant/components/sentry/ @dcramer @frenck diff --git a/homeassistant/components/sensorpro/__init__.py b/homeassistant/components/sensorpro/__init__.py new file mode 100644 index 00000000000..43c87ad32ee --- /dev/null +++ b/homeassistant/components/sensorpro/__init__.py @@ -0,0 +1,49 @@ +"""The SensorPro integration.""" +from __future__ import annotations + +import logging + +from sensorpro_ble import SensorProBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up SensorPro BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = SensorProBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/sensorpro/config_flow.py b/homeassistant/components/sensorpro/config_flow.py new file mode 100644 index 00000000000..182a35880ab --- /dev/null +++ b/homeassistant/components/sensorpro/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for sensorpro ble integration.""" +from __future__ import annotations + +from typing import Any + +from sensorpro_ble import SensorProBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class SensorProConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for sensorpro.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/sensorpro/const.py b/homeassistant/components/sensorpro/const.py new file mode 100644 index 00000000000..59d96cdcfc7 --- /dev/null +++ b/homeassistant/components/sensorpro/const.py @@ -0,0 +1,3 @@ +"""Constants for the SensorPro integration.""" + +DOMAIN = "sensorpro" diff --git a/homeassistant/components/sensorpro/device.py b/homeassistant/components/sensorpro/device.py new file mode 100644 index 00000000000..b5b44eef50f --- /dev/null +++ b/homeassistant/components/sensorpro/device.py @@ -0,0 +1,31 @@ +"""Support for SensorPro devices.""" +from __future__ import annotations + +from sensorpro_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensorpro device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/sensorpro/manifest.json b/homeassistant/components/sensorpro/manifest.json new file mode 100644 index 00000000000..6f2f8806262 --- /dev/null +++ b/homeassistant/components/sensorpro/manifest.json @@ -0,0 +1,22 @@ +{ + "domain": "sensorpro", + "name": "SensorPro", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sensorpro", + "bluetooth": [ + { + "manufacturer_id": 43605, + "manufacturer_data_start": [1, 1, 164, 193], + "connectable": false + }, + { + "manufacturer_id": 43605, + "manufacturer_data_start": [1, 5, 164, 193], + "connectable": false + } + ], + "requirements": ["sensorpro-ble==0.5.0"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/sensorpro/sensor.py b/homeassistant/components/sensorpro/sensor.py new file mode 100644 index 00000000000..8866ed44587 --- /dev/null +++ b/homeassistant/components/sensorpro/sensor.py @@ -0,0 +1,140 @@ +"""Support for SensorPro sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from sensorpro_ble import ( + SensorDeviceClass as SensorProSensorDeviceClass, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ELECTRIC_POTENTIAL_VOLT, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +SENSOR_DESCRIPTIONS = { + (SensorProSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{SensorProSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (SensorProSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{SensorProSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + SensorProSensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{SensorProSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + ( + SensorProSensorDeviceClass.TEMPERATURE, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{SensorProSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + SensorProSensorDeviceClass.VOLTAGE, + Units.ELECTRIC_POTENTIAL_VOLT, + ): SensorEntityDescription( + key=f"{SensorProSensorDeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_VOLT}", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SensorPro BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + SensorProBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class SensorProBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a SensorPro sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/sensorpro/strings.json b/homeassistant/components/sensorpro/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/sensorpro/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/sensorpro/translations/en.json b/homeassistant/components/sensorpro/translations/en.json new file mode 100644 index 00000000000..ebd9760c161 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Device not supported" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 5de90d731bb..54823a85e1b 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -165,6 +165,28 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_data_uuid": "0000fdcd-0000-1000-8000-00805f9b34fb", "connectable": False }, + { + "domain": "sensorpro", + "manufacturer_id": 43605, + "manufacturer_data_start": [ + 1, + 1, + 164, + 193 + ], + "connectable": False + }, + { + "domain": "sensorpro", + "manufacturer_id": 43605, + "manufacturer_data_start": [ + 1, + 5, + 164, + 193 + ], + "connectable": False + }, { "domain": "sensorpush", "local_name": "SensorPush*", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ec09c9a7756..127beed575e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -326,6 +326,7 @@ FLOWS = { "sense", "senseme", "sensibo", + "sensorpro", "sensorpush", "sentry", "senz", diff --git a/requirements_all.txt b/requirements_all.txt index 2f4680a361d..18af990ae7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2193,6 +2193,9 @@ sendgrid==6.8.2 # homeassistant.components.sense sense_energy==0.10.4 +# homeassistant.components.sensorpro +sensorpro-ble==0.5.0 + # homeassistant.components.sensorpush sensorpush-ble==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93ef4590930..66c80797fec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1496,6 +1496,9 @@ securetar==2022.2.0 # homeassistant.components.sense sense_energy==0.10.4 +# homeassistant.components.sensorpro +sensorpro-ble==0.5.0 + # homeassistant.components.sensorpush sensorpush-ble==1.5.2 diff --git a/tests/components/sensorpro/__init__.py b/tests/components/sensorpro/__init__.py new file mode 100644 index 00000000000..f92eb700093 --- /dev/null +++ b/tests/components/sensorpro/__init__.py @@ -0,0 +1,26 @@ +"""Tests for the SensorPro integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_SENSORPRO_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +SENSORPRO_SERVICE_INFO = BluetoothServiceInfo( + name="T201", + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={}, + manufacturer_data={ + 43605: b"\x01\x01\xa4\xc18.\xcan\x01\x07\n\x02\x13\x9dd\x00\x01\x01\x01\xa4\xc18.\xcan\x01\x07\n\x02\x13\x9dd\x00\x01" + }, + service_uuids=[], + source="local", +) diff --git a/tests/components/sensorpro/conftest.py b/tests/components/sensorpro/conftest.py new file mode 100644 index 00000000000..85c56845ad8 --- /dev/null +++ b/tests/components/sensorpro/conftest.py @@ -0,0 +1,8 @@ +"""SensorPro session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/sensorpro/test_config_flow.py b/tests/components/sensorpro/test_config_flow.py new file mode 100644 index 00000000000..9b5a0a6bab5 --- /dev/null +++ b/tests/components/sensorpro/test_config_flow.py @@ -0,0 +1,200 @@ +"""Test the SensorPro config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.sensorpro.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import NOT_SENSORPRO_SERVICE_INFO, SENSORPRO_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSORPRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.sensorpro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "T201 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_sensorpro(hass): + """Test discovery via bluetooth not sensorpro.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_SENSORPRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.sensorpro.config_flow.async_discovered_service_info", + return_value=[SENSORPRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.sensorpro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "T201 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.sensorpro.config_flow.async_discovered_service_info", + return_value=[SENSORPRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensorpro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensorpro.config_flow.async_discovered_service_info", + return_value=[SENSORPRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSORPRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSORPRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSORPRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSORPRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.sensorpro.config_flow.async_discovered_service_info", + return_value=[SENSORPRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.sensorpro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "T201 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/sensorpro/test_sensor.py b/tests/components/sensorpro/test_sensor.py new file mode 100644 index 00000000000..0d27d07995f --- /dev/null +++ b/tests/components/sensorpro/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the SensorPro sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.sensorpro.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import SENSORPRO_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + saved_callback(SENSORPRO_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 4 + + humid_sensor = hass.states.get("sensor.t201_eeff_humidity") + humid_sensor_attrs = humid_sensor.attributes + assert humid_sensor.state == "50.21" + assert humid_sensor_attrs[ATTR_FRIENDLY_NAME] == "T201 EEFF Humidity" + assert humid_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert humid_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 5297dc1d5f95e966a7833bfd92ce777da56d4ba8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Aug 2022 22:01:35 -0400 Subject: [PATCH 785/903] Bump govee-ble to add support for H5185 firmware variant (#77564) --- homeassistant/components/govee_ble/manifest.json | 7 ++++++- homeassistant/generated/bluetooth.py | 6 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index d3aaf6066d4..6f9e15463cd 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -41,9 +41,14 @@ "manufacturer_id": 10032, "service_uuid": "00008251-0000-1000-8000-00805f9b34fb", "connectable": false + }, + { + "manufacturer_id": 19506, + "service_uuid": "00001801-0000-1000-8000-00805f9b34fb", + "connectable": false } ], - "requirements": ["govee-ble==0.16.1"], + "requirements": ["govee-ble==0.17.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 54823a85e1b..8422ce64f95 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -86,6 +86,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb", "connectable": False }, + { + "domain": "govee_ble", + "manufacturer_id": 19506, + "service_uuid": "00001801-0000-1000-8000-00805f9b34fb", + "connectable": False + }, { "domain": "homekit_controller", "manufacturer_id": 76, diff --git a/requirements_all.txt b/requirements_all.txt index 18af990ae7f..ebaa328a071 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -766,7 +766,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.16.1 +govee-ble==0.17.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66c80797fec..cf539eb31fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.16.1 +govee-ble==0.17.0 # homeassistant.components.gree greeclimate==1.3.0 From 3ba1fbe69d24f7d86ca4a8127d23bccf0c8e4427 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 30 Aug 2022 20:01:49 -0600 Subject: [PATCH 786/903] Add pet weight sensor for Litter-Robot 4 (#77566) --- .../components/litterrobot/sensor.py | 115 +++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 386d0e04f3c..90bdfcbda73 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,13 +1,12 @@ """Support for Litter-Robot sensors.""" from __future__ import annotations -from collections.abc import Callable, Iterable +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -import itertools from typing import Any, Generic, Union, cast -from pylitterbot import FeederRobot, LitterRobot +from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot from homeassistant.components.sensor import ( SensorDeviceClass, @@ -15,7 +14,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import MASS_POUNDS, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -77,45 +76,56 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): return super().icon -LITTER_ROBOT_SENSORS = [ - RobotSensorEntityDescription[LitterRobot]( - name="Waste Drawer", - key="waste_drawer_level", - native_unit_of_measurement=PERCENTAGE, - icon_fn=lambda state: icon_for_gauge_level(state, 10), - ), - RobotSensorEntityDescription[LitterRobot]( - name="Sleep Mode Start Time", - key="sleep_mode_start_time", - device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, - ), - RobotSensorEntityDescription[LitterRobot]( - name="Sleep Mode End Time", - key="sleep_mode_end_time", - device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, - ), - RobotSensorEntityDescription[LitterRobot]( - name="Last Seen", - key="last_seen", - device_class=SensorDeviceClass.TIMESTAMP, - entity_category=EntityCategory.DIAGNOSTIC, - ), - RobotSensorEntityDescription[LitterRobot]( - name="Status Code", - key="status_code", - device_class="litterrobot__status_code", - entity_category=EntityCategory.DIAGNOSTIC, - ), -] - -FEEDER_ROBOT_SENSOR = RobotSensorEntityDescription[FeederRobot]( - name="Food Level", - key="food_level", - native_unit_of_measurement=PERCENTAGE, - icon_fn=lambda state: icon_for_gauge_level(state, 10), -) +ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { + LitterRobot: [ + RobotSensorEntityDescription[LitterRobot]( + name="Waste Drawer", + key="waste_drawer_level", + native_unit_of_measurement=PERCENTAGE, + icon_fn=lambda state: icon_for_gauge_level(state, 10), + ), + RobotSensorEntityDescription[LitterRobot]( + name="Sleep Mode Start Time", + key="sleep_mode_start_time", + device_class=SensorDeviceClass.TIMESTAMP, + should_report=lambda robot: robot.sleep_mode_enabled, + ), + RobotSensorEntityDescription[LitterRobot]( + name="Sleep Mode End Time", + key="sleep_mode_end_time", + device_class=SensorDeviceClass.TIMESTAMP, + should_report=lambda robot: robot.sleep_mode_enabled, + ), + RobotSensorEntityDescription[LitterRobot]( + name="Last Seen", + key="last_seen", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + ), + RobotSensorEntityDescription[LitterRobot]( + name="Status Code", + key="status_code", + device_class="litterrobot__status_code", + entity_category=EntityCategory.DIAGNOSTIC, + ), + ], + LitterRobot4: [ + RobotSensorEntityDescription[LitterRobot4]( + key="pet_weight", + name="Pet weight", + icon="mdi:scale", + native_unit_of_measurement=MASS_POUNDS, + ) + ], + FeederRobot: [ + RobotSensorEntityDescription[FeederRobot]( + name="Food level", + key="food_level", + native_unit_of_measurement=PERCENTAGE, + icon_fn=lambda state: icon_for_gauge_level(state, 10), + ) + ], +} async def async_setup_entry( @@ -125,17 +135,10 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot sensors using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities: Iterable[LitterRobotSensorEntity] = itertools.chain( - ( - LitterRobotSensorEntity(robot=robot, hub=hub, description=description) - for description in LITTER_ROBOT_SENSORS - for robot in hub.litter_robots() - ), - ( - LitterRobotSensorEntity( - robot=robot, hub=hub, description=FEEDER_ROBOT_SENSOR - ) - for robot in hub.feeder_robots() - ), + async_add_entities( + LitterRobotSensorEntity(robot=robot, hub=hub, description=description) + for robot in hub.account.robots + for robot_type, entity_descriptions in ROBOT_SENSOR_MAP.items() + if isinstance(robot, robot_type) + for description in entity_descriptions ) - async_add_entities(entities) From 0c35166a7bfb1e5e5ee9d25f38c2c217519c12a1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 30 Aug 2022 22:02:13 -0400 Subject: [PATCH 787/903] Simplify zwave_js update entity (#77572) --- homeassistant/components/zwave_js/update.py | 24 +++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index d179700c724..134c6cc6661 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -67,7 +67,6 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Initialize a Z-Wave device firmware update entity.""" self.driver = driver self.node = node - self.available_firmware_updates: list[FirmwareUpdateInfo] = [] self._latest_version_firmware: FirmwareUpdateInfo | None = None self._status_unsub: Callable[[], None] | None = None @@ -98,12 +97,16 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): if not self._status_unsub: self._status_unsub = self.node.once("wake up", self._update_on_wake_up) return - self.available_firmware_updates = ( + if available_firmware_updates := ( await self.driver.controller.async_get_available_firmware_updates( self.node, API_KEY_FIRMWARE_UPDATE_SERVICE ) - ) - self._async_process_available_updates(write_state) + ): + self._latest_version_firmware = max( + available_firmware_updates, + key=lambda x: AwesomeVersion(x.version), + ) + self._async_process_available_updates(write_state) @callback def _async_process_available_updates(self, write_state: bool = True) -> None: @@ -114,18 +117,11 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """ # If we have an available firmware update that is a higher version than what's # on the node, we should advertise it, otherwise we are on the latest version - if self.available_firmware_updates and AwesomeVersion( - ( - firmware := max( - self.available_firmware_updates, - key=lambda x: AwesomeVersion(x.version), - ) - ).version + if (firmware := self._latest_version_firmware) and AwesomeVersion( + firmware.version ) > AwesomeVersion(self.node.firmware_version): - self._latest_version_firmware = firmware self._attr_latest_version = firmware.version else: - self._latest_version_firmware = None self._attr_latest_version = self._attr_installed_version if write_state: self.async_write_ha_state() @@ -153,7 +149,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): raise HomeAssistantError(err) from err else: self._attr_installed_version = firmware.version - self.available_firmware_updates.remove(firmware) + self._latest_version_firmware = None self._async_process_available_updates() finally: self._attr_in_progress = False From 5b3f4ec471d2490d665522ef877dfc5bba14b036 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 30 Aug 2022 22:17:03 -0400 Subject: [PATCH 788/903] Fix failing unifiprotect unit tests (#77575) * Patch `final` pydantic fields during unit test * Use a fixed date with 31 days to ensure unit tests pass every month --- tests/components/unifiprotect/test_camera.py | 4 ++-- tests/components/unifiprotect/test_light.py | 4 ++-- tests/components/unifiprotect/test_lock.py | 4 ++-- .../unifiprotect/test_media_player.py | 20 ++++++++-------- .../unifiprotect/test_media_source.py | 2 +- tests/components/unifiprotect/test_number.py | 8 +++---- tests/components/unifiprotect/test_select.py | 24 +++++++++---------- .../components/unifiprotect/test_services.py | 10 ++++---- tests/components/unifiprotect/test_switch.py | 14 +++++------ 9 files changed, 45 insertions(+), 45 deletions(-) diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 455a69dd152..e7abbf7273a 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -492,7 +492,7 @@ async def test_camera_enable_motion( assert_entity_counts(hass, Platform.CAMERA, 2, 1) entity_id = "camera.test_camera_high" - camera.__fields__["set_motion_detection"] = Mock() + camera.__fields__["set_motion_detection"] = Mock(final=False) camera.set_motion_detection = AsyncMock() await hass.services.async_call( @@ -514,7 +514,7 @@ async def test_camera_disable_motion( assert_entity_counts(hass, Platform.CAMERA, 2, 1) entity_id = "camera.test_camera_high" - camera.__fields__["set_motion_detection"] = Mock() + camera.__fields__["set_motion_detection"] = Mock(final=False) camera.set_motion_detection = AsyncMock() await hass.services.async_call( diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index 401d222db8a..a1a3f9d071b 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -96,7 +96,7 @@ async def test_light_turn_on( assert_entity_counts(hass, Platform.LIGHT, 1, 1) entity_id = "light.test_light" - light.__fields__["set_light"] = Mock() + light.__fields__["set_light"] = Mock(final=False) light.set_light = AsyncMock() await hass.services.async_call( @@ -118,7 +118,7 @@ async def test_light_turn_off( assert_entity_counts(hass, Platform.LIGHT, 1, 1) entity_id = "light.test_light" - light.__fields__["set_light"] = Mock() + light.__fields__["set_light"] = Mock(final=False) light.set_light = AsyncMock() await hass.services.async_call( diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index dcbd7537100..2c58d5fff66 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -214,7 +214,7 @@ async def test_lock_do_lock( await init_entry(hass, ufp, [doorlock, unadopted_doorlock]) assert_entity_counts(hass, Platform.LOCK, 1, 1) - doorlock.__fields__["close_lock"] = Mock() + doorlock.__fields__["close_lock"] = Mock(final=False) doorlock.close_lock = AsyncMock() await hass.services.async_call( @@ -249,7 +249,7 @@ async def test_lock_do_unlock( ufp.ws_msg(mock_msg) await hass.async_block_till_done() - new_lock.__fields__["open_lock"] = Mock() + new_lock.__fields__["open_lock"] = Mock(final=False) new_lock.open_lock = AsyncMock() await hass.services.async_call( diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index c50409a7848..c78718e3c06 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -116,7 +116,7 @@ async def test_media_player_set_volume( await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) - doorbell.__fields__["set_speaker_volume"] = Mock() + doorbell.__fields__["set_speaker_volume"] = Mock(final=False) doorbell.set_speaker_volume = AsyncMock() await hass.services.async_call( @@ -173,9 +173,9 @@ async def test_media_player_play( await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) - doorbell.__fields__["stop_audio"] = Mock() - doorbell.__fields__["play_audio"] = Mock() - doorbell.__fields__["wait_until_audio_completes"] = Mock() + doorbell.__fields__["stop_audio"] = Mock(final=False) + doorbell.__fields__["play_audio"] = Mock(final=False) + doorbell.__fields__["wait_until_audio_completes"] = Mock(final=False) doorbell.stop_audio = AsyncMock() doorbell.play_audio = AsyncMock() doorbell.wait_until_audio_completes = AsyncMock() @@ -208,9 +208,9 @@ async def test_media_player_play_media_source( await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) - doorbell.__fields__["stop_audio"] = Mock() - doorbell.__fields__["play_audio"] = Mock() - doorbell.__fields__["wait_until_audio_completes"] = Mock() + doorbell.__fields__["stop_audio"] = Mock(final=False) + doorbell.__fields__["play_audio"] = Mock(final=False) + doorbell.__fields__["wait_until_audio_completes"] = Mock(final=False) doorbell.stop_audio = AsyncMock() doorbell.play_audio = AsyncMock() doorbell.wait_until_audio_completes = AsyncMock() @@ -247,7 +247,7 @@ async def test_media_player_play_invalid( await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) - doorbell.__fields__["play_audio"] = Mock() + doorbell.__fields__["play_audio"] = Mock(final=False) doorbell.play_audio = AsyncMock() with pytest.raises(HomeAssistantError): @@ -276,8 +276,8 @@ async def test_media_player_play_error( await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) - doorbell.__fields__["play_audio"] = Mock() - doorbell.__fields__["wait_until_audio_completes"] = Mock() + doorbell.__fields__["play_audio"] = Mock(final=False) + doorbell.__fields__["wait_until_audio_completes"] = Mock(final=False) doorbell.play_audio = AsyncMock(side_effect=StreamError) doorbell.wait_until_audio_completes = AsyncMock() diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index e1e7f3cacde..bb3bc8aa345 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -673,7 +673,7 @@ async def test_browse_media_browse_whole_month( ): """Test events for a specific day.""" - fixed_now = fixed_now.replace(month=11) + fixed_now = fixed_now.replace(month=10) last_month = fixed_now.replace(day=1) - timedelta(days=1) ufp.api.bootstrap._recording_start = last_month diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index b0d1b764999..5a5bf400169 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -153,7 +153,7 @@ async def test_number_light_sensitivity( description = LIGHT_NUMBERS[0] assert description.ufp_set_method is not None - light.__fields__["set_sensitivity"] = Mock() + light.__fields__["set_sensitivity"] = Mock(final=False) light.set_sensitivity = AsyncMock() _, entity_id = ids_from_device_description(Platform.NUMBER, light, description) @@ -175,7 +175,7 @@ async def test_number_light_duration( description = LIGHT_NUMBERS[1] - light.__fields__["set_duration"] = Mock() + light.__fields__["set_duration"] = Mock(final=False) light.set_duration = AsyncMock() _, entity_id = ids_from_device_description(Platform.NUMBER, light, description) @@ -201,7 +201,7 @@ async def test_number_camera_simple( assert description.ufp_set_method is not None - camera.__fields__[description.ufp_set_method] = Mock() + camera.__fields__[description.ufp_set_method] = Mock(final=False) setattr(camera, description.ufp_set_method, AsyncMock()) set_method = getattr(camera, description.ufp_set_method) @@ -224,7 +224,7 @@ async def test_number_lock_auto_close( description = DOORLOCK_NUMBERS[0] - doorlock.__fields__["set_auto_close_time"] = Mock() + doorlock.__fields__["set_auto_close_time"] = Mock(final=False) doorlock.set_auto_close_time = AsyncMock() _, entity_id = ids_from_device_description(Platform.NUMBER, doorlock, description) diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index 0cc8308f0f2..336a6f5af74 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -255,7 +255,7 @@ async def test_select_update_doorbell_settings( expected_length += 1 new_nvr = copy(ufp.api.bootstrap.nvr) - new_nvr.__fields__["update_all_messages"] = Mock() + new_nvr.__fields__["update_all_messages"] = Mock(final=False) new_nvr.update_all_messages = Mock() new_nvr.doorbell_settings.all_messages = [ @@ -325,7 +325,7 @@ async def test_select_set_option_light_motion( _, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[0]) - light.__fields__["set_light_settings"] = Mock() + light.__fields__["set_light_settings"] = Mock(final=False) light.set_light_settings = AsyncMock() await hass.services.async_call( @@ -350,7 +350,7 @@ async def test_select_set_option_light_camera( _, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[1]) - light.__fields__["set_paired_camera"] = Mock() + light.__fields__["set_paired_camera"] = Mock(final=False) light.set_paired_camera = AsyncMock() camera = list(light.api.bootstrap.cameras.values())[0] @@ -386,7 +386,7 @@ async def test_select_set_option_camera_recording( Platform.SELECT, doorbell, CAMERA_SELECTS[0] ) - doorbell.__fields__["set_recording_mode"] = Mock() + doorbell.__fields__["set_recording_mode"] = Mock(final=False) doorbell.set_recording_mode = AsyncMock() await hass.services.async_call( @@ -411,7 +411,7 @@ async def test_select_set_option_camera_ir( Platform.SELECT, doorbell, CAMERA_SELECTS[1] ) - doorbell.__fields__["set_ir_led_model"] = Mock() + doorbell.__fields__["set_ir_led_model"] = Mock(final=False) doorbell.set_ir_led_model = AsyncMock() await hass.services.async_call( @@ -436,7 +436,7 @@ async def test_select_set_option_camera_doorbell_custom( Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.__fields__["set_lcd_text"] = Mock(final=False) doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( @@ -463,7 +463,7 @@ async def test_select_set_option_camera_doorbell_unifi( Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.__fields__["set_lcd_text"] = Mock(final=False) doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( @@ -505,7 +505,7 @@ async def test_select_set_option_camera_doorbell_default( Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.__fields__["set_lcd_text"] = Mock(final=False) doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( @@ -534,7 +534,7 @@ async def test_select_set_option_viewer( Platform.SELECT, viewer, VIEWER_SELECTS[0] ) - viewer.__fields__["set_liveview"] = Mock() + viewer.__fields__["set_liveview"] = Mock(final=False) viewer.set_liveview = AsyncMock() liveview = list(viewer.api.bootstrap.liveviews.values())[0] @@ -561,7 +561,7 @@ async def test_select_service_doorbell_invalid( Platform.SELECT, doorbell, CAMERA_SELECTS[1] ) - doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.__fields__["set_lcd_text"] = Mock(final=False) doorbell.set_lcd_text = AsyncMock() with pytest.raises(HomeAssistantError): @@ -587,7 +587,7 @@ async def test_select_service_doorbell_success( Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.__fields__["set_lcd_text"] = Mock(final=False) doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( @@ -624,7 +624,7 @@ async def test_select_service_doorbell_with_reset( await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 4, 4) - doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.__fields__["set_lcd_text"] = Mock(final=False) doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 460ba488cb2..9da6b1107c3 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -49,7 +49,7 @@ async def test_global_service_bad_device(hass: HomeAssistant, ufp: MockUFPFixtur """Test global service, invalid device ID.""" nvr = ufp.api.bootstrap.nvr - nvr.__fields__["add_custom_doorbell_message"] = Mock() + nvr.__fields__["add_custom_doorbell_message"] = Mock(final=False) nvr.add_custom_doorbell_message = AsyncMock() with pytest.raises(HomeAssistantError): @@ -68,7 +68,7 @@ async def test_global_service_exception( """Test global service, unexpected error.""" nvr = ufp.api.bootstrap.nvr - nvr.__fields__["add_custom_doorbell_message"] = Mock() + nvr.__fields__["add_custom_doorbell_message"] = Mock(final=False) nvr.add_custom_doorbell_message = AsyncMock(side_effect=BadRequest) with pytest.raises(HomeAssistantError): @@ -87,7 +87,7 @@ async def test_add_doorbell_text( """Test add_doorbell_text service.""" nvr = ufp.api.bootstrap.nvr - nvr.__fields__["add_custom_doorbell_message"] = Mock() + nvr.__fields__["add_custom_doorbell_message"] = Mock(final=False) nvr.add_custom_doorbell_message = AsyncMock() await hass.services.async_call( @@ -105,7 +105,7 @@ async def test_remove_doorbell_text( """Test remove_doorbell_text service.""" nvr = ufp.api.bootstrap.nvr - nvr.__fields__["remove_custom_doorbell_message"] = Mock() + nvr.__fields__["remove_custom_doorbell_message"] = Mock(final=False) nvr.remove_custom_doorbell_message = AsyncMock() await hass.services.async_call( @@ -123,7 +123,7 @@ async def test_set_default_doorbell_text( """Test set_default_doorbell_text service.""" nvr = ufp.api.bootstrap.nvr - nvr.__fields__["set_default_doorbell_message"] = Mock() + nvr.__fields__["set_default_doorbell_message"] = Mock(final=False) nvr.set_default_doorbell_message = AsyncMock() await hass.services.async_call( diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 50f82736ee5..82bf90eefd4 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -78,7 +78,7 @@ async def test_switch_nvr(hass: HomeAssistant, ufp: MockUFPFixture): assert_entity_counts(hass, Platform.SWITCH, 2, 2) nvr = ufp.api.bootstrap.nvr - nvr.__fields__["set_insights"] = Mock() + nvr.__fields__["set_insights"] = Mock(final=False) nvr.set_insights = AsyncMock() entity_id = "switch.unifiprotect_insights_enabled" @@ -264,7 +264,7 @@ async def test_switch_light_status( description = LIGHT_SWITCHES[1] - light.__fields__["set_status_light"] = Mock() + light.__fields__["set_status_light"] = Mock(final=False) light.set_status_light = AsyncMock() _, entity_id = ids_from_device_description(Platform.SWITCH, light, description) @@ -292,7 +292,7 @@ async def test_switch_camera_ssh( description = CAMERA_SWITCHES[0] - doorbell.__fields__["set_ssh"] = Mock() + doorbell.__fields__["set_ssh"] = Mock(final=False) doorbell.set_ssh = AsyncMock() _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) @@ -325,7 +325,7 @@ async def test_switch_camera_simple( assert description.ufp_set_method is not None - doorbell.__fields__[description.ufp_set_method] = Mock() + doorbell.__fields__[description.ufp_set_method] = Mock(final=False) setattr(doorbell, description.ufp_set_method, AsyncMock()) set_method = getattr(doorbell, description.ufp_set_method) @@ -354,7 +354,7 @@ async def test_switch_camera_highfps( description = CAMERA_SWITCHES[3] - doorbell.__fields__["set_video_mode"] = Mock() + doorbell.__fields__["set_video_mode"] = Mock(final=False) doorbell.set_video_mode = AsyncMock() _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) @@ -385,7 +385,7 @@ async def test_switch_camera_privacy( description = PRIVACY_MODE_SWITCH - doorbell.__fields__["set_privacy"] = Mock() + doorbell.__fields__["set_privacy"] = Mock(final=False) doorbell.set_privacy = AsyncMock() _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) @@ -437,7 +437,7 @@ async def test_switch_camera_privacy_already_on( description = PRIVACY_MODE_SWITCH - doorbell.__fields__["set_privacy"] = Mock() + doorbell.__fields__["set_privacy"] = Mock(final=False) doorbell.set_privacy = AsyncMock() _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) From ff3d3088eee588b508dffe880b609aea11829ae8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 31 Aug 2022 05:33:05 +0200 Subject: [PATCH 789/903] Add Aqara FP1 support to deCONZ integration (#77568) --- homeassistant/components/deconz/button.py | 39 +++- homeassistant/components/deconz/const.py | 1 + .../components/deconz/deconz_event.py | 50 +++- .../components/deconz/device_trigger.py | 10 +- homeassistant/components/deconz/gateway.py | 4 +- homeassistant/components/deconz/select.py | 143 ++++++++++++ tests/components/deconz/test_button.py | 45 +++- tests/components/deconz/test_deconz_event.py | 103 ++++++++ tests/components/deconz/test_diagnostics.py | 1 + tests/components/deconz/test_gateway.py | 8 +- tests/components/deconz/test_select.py | 219 ++++++++++++++++++ 11 files changed, 610 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/deconz/select.py create mode 100644 tests/components/deconz/test_select.py diff --git a/homeassistant/components/deconz/button.py b/homeassistant/components/deconz/button.py index 552723d6f9c..b45d7955517 100644 --- a/homeassistant/components/deconz/button.py +++ b/homeassistant/components/deconz/button.py @@ -6,9 +6,11 @@ from dataclasses import dataclass from pydeconz.models.event import EventType from pydeconz.models.scene import Scene as PydeconzScene +from pydeconz.models.sensor.presence import Presence from homeassistant.components.button import ( DOMAIN, + ButtonDeviceClass, ButtonEntity, ButtonEntityDescription, ) @@ -17,7 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .deconz_device import DeconzSceneMixin +from .deconz_device import DeconzDevice, DeconzSceneMixin from .gateway import DeconzGateway, get_gateway_from_config_entry @@ -61,7 +63,7 @@ async def async_setup_entry( """Add scene button from deCONZ.""" scene = gateway.api.scenes[scene_id] async_add_entities( - DeconzButton(scene, gateway, description) + DeconzSceneButton(scene, gateway, description) for description in ENTITY_DESCRIPTIONS.get(PydeconzScene, []) ) @@ -70,8 +72,20 @@ async def async_setup_entry( gateway.api.scenes, ) + @callback + def async_add_presence_sensor(_: EventType, sensor_id: str) -> None: + """Add presence sensor reset button from deCONZ.""" + sensor = gateway.api.sensors.presence[sensor_id] + if sensor.presence_event is not None: + async_add_entities([DeconzPresenceResetButton(sensor, gateway)]) -class DeconzButton(DeconzSceneMixin, ButtonEntity): + gateway.register_platform_add_device_callback( + async_add_presence_sensor, + gateway.api.sensors.presence, + ) + + +class DeconzSceneButton(DeconzSceneMixin, ButtonEntity): """Representation of a deCONZ button entity.""" TYPE = DOMAIN @@ -99,3 +113,22 @@ class DeconzButton(DeconzSceneMixin, ButtonEntity): def get_device_identifier(self) -> str: """Return a unique identifier for this scene.""" return f"{super().get_device_identifier()}-{self.entity_description.key}" + + +class DeconzPresenceResetButton(DeconzDevice[Presence], ButtonEntity): + """Representation of a deCONZ presence reset button entity.""" + + _name_suffix = "Reset Presence" + unique_id_suffix = "reset_presence" + + _attr_entity_category = EntityCategory.CONFIG + _attr_device_class = ButtonDeviceClass.RESTART + + TYPE = DOMAIN + + async def async_press(self) -> None: + """Store reset presence state.""" + await self.gateway.api.sensors.presence.set_config( + id=self._device.resource_id, + reset_presence=True, + ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 60ae610bd5a..6070f83871f 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -35,6 +35,7 @@ PLATFORMS = [ Platform.LOCK, Platform.NUMBER, Platform.SCENE, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 6f7b6f9038a..35e1ba79948 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -9,6 +9,7 @@ from pydeconz.models.sensor.ancillary_control import ( AncillaryControl, AncillaryControlAction, ) +from pydeconz.models.sensor.presence import Presence, PresenceStatePresenceEvent from pydeconz.models.sensor.switch import Switch from homeassistant.const import ( @@ -28,6 +29,7 @@ from .gateway import DeconzGateway CONF_DECONZ_EVENT = "deconz_event" CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event" +CONF_DECONZ_PRESENCE_EVENT = "deconz_presence_event" SUPPORTED_DECONZ_ALARM_EVENTS = { AncillaryControlAction.EMERGENCY, @@ -35,6 +37,16 @@ SUPPORTED_DECONZ_ALARM_EVENTS = { AncillaryControlAction.INVALID_CODE, AncillaryControlAction.PANIC, } +SUPPORTED_DECONZ_PRESENCE_EVENTS = { + PresenceStatePresenceEvent.ENTER, + PresenceStatePresenceEvent.LEAVE, + PresenceStatePresenceEvent.ENTER_LEFT, + PresenceStatePresenceEvent.RIGHT_LEAVE, + PresenceStatePresenceEvent.ENTER_RIGHT, + PresenceStatePresenceEvent.LEFT_LEAVE, + PresenceStatePresenceEvent.APPROACHING, + PresenceStatePresenceEvent.ABSENTING, +} async def async_setup_events(gateway: DeconzGateway) -> None: @@ -43,7 +55,7 @@ async def async_setup_events(gateway: DeconzGateway) -> None: @callback def async_add_sensor(_: EventType, sensor_id: str) -> None: """Create DeconzEvent.""" - new_event: DeconzAlarmEvent | DeconzEvent + new_event: DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent sensor = gateway.api.sensors[sensor_id] if isinstance(sensor, Switch): @@ -52,6 +64,11 @@ async def async_setup_events(gateway: DeconzGateway) -> None: elif isinstance(sensor, AncillaryControl): new_event = DeconzAlarmEvent(sensor, gateway) + elif isinstance(sensor, Presence): + if sensor.presence_event is None: + return + new_event = DeconzPresenceEvent(sensor, gateway) + gateway.hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) @@ -63,6 +80,10 @@ async def async_setup_events(gateway: DeconzGateway) -> None: async_add_sensor, gateway.api.sensors.ancillary_control, ) + gateway.register_platform_add_device_callback( + async_add_sensor, + gateway.api.sensors.presence, + ) @callback @@ -83,7 +104,7 @@ class DeconzEventBase(DeconzBase): def __init__( self, - device: AncillaryControl | Switch, + device: AncillaryControl | Presence | Switch, gateway: DeconzGateway, ) -> None: """Register callback that will be used for signals.""" @@ -181,3 +202,28 @@ class DeconzAlarmEvent(DeconzEventBase): } self.gateway.hass.bus.async_fire(CONF_DECONZ_ALARM_EVENT, data) + + +class DeconzPresenceEvent(DeconzEventBase): + """Presence event.""" + + _device: Presence + + @callback + def async_update_callback(self) -> None: + """Fire the event if reason is new action is updated.""" + if ( + self.gateway.ignore_state_updates + or "presenceevent" not in self._device.changed_keys + or self._device.presence_event not in SUPPORTED_DECONZ_PRESENCE_EVENTS + ): + return + + data = { + CONF_ID: self.event_id, + CONF_UNIQUE_ID: self.serial, + CONF_DEVICE_ID: self.device_id, + CONF_EVENT: self._device.presence_event.value, + } + + self.gateway.hass.bus.async_fire(CONF_DECONZ_PRESENCE_EVENT, data) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 601fe95616b..e4d9a818a4e 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -22,7 +22,13 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN -from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE, DeconzAlarmEvent, DeconzEvent +from .deconz_event import ( + CONF_DECONZ_EVENT, + CONF_GESTURE, + DeconzAlarmEvent, + DeconzEvent, + DeconzPresenceEvent, +) from .gateway import DeconzGateway CONF_SUBTYPE = "subtype" @@ -622,7 +628,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( def _get_deconz_event_from_device( hass: HomeAssistant, device: dr.DeviceEntry, -) -> DeconzAlarmEvent | DeconzEvent: +) -> DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent: """Resolve deconz event from device.""" gateways: dict[str, DeconzGateway] = hass.data.get(DOMAIN, {}) for gateway in gateways.values(): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 6f29cef5190..1c381bc194a 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -41,7 +41,7 @@ from .const import ( from .errors import AuthenticationRequired, CannotConnect if TYPE_CHECKING: - from .deconz_event import DeconzAlarmEvent, DeconzEvent + from .deconz_event import DeconzAlarmEvent, DeconzEvent, DeconzPresenceEvent SENSORS = ( sensors.SensorResourceManager, @@ -93,7 +93,7 @@ class DeconzGateway: self.deconz_ids: dict[str, str] = {} self.entities: dict[str, set[str]] = {} - self.events: list[DeconzAlarmEvent | DeconzEvent] = [] + self.events: list[DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent] = [] self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set() self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set() self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() diff --git a/homeassistant/components/deconz/select.py b/homeassistant/components/deconz/select.py new file mode 100644 index 00000000000..8027a4aa822 --- /dev/null +++ b/homeassistant/components/deconz/select.py @@ -0,0 +1,143 @@ +"""Support for deCONZ select entities.""" + +from __future__ import annotations + +from pydeconz.models.event import EventType +from pydeconz.models.sensor.presence import ( + Presence, + PresenceConfigDeviceMode, + PresenceConfigSensitivity, + PresenceConfigTriggerDistance, +) + +from homeassistant.components.select import DOMAIN, SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry + +SENSITIVITY_TO_DECONZ = { + "High": PresenceConfigSensitivity.HIGH.value, + "Medium": PresenceConfigSensitivity.MEDIUM.value, + "Low": PresenceConfigSensitivity.LOW.value, +} +DECONZ_TO_SENSITIVITY = {value: key for key, value in SENSITIVITY_TO_DECONZ.items()} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the deCONZ button entity.""" + gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() + + @callback + def async_add_presence_sensor(_: EventType, sensor_id: str) -> None: + """Add presence select entity from deCONZ.""" + sensor = gateway.api.sensors.presence[sensor_id] + if sensor.presence_event is not None: + async_add_entities( + [ + DeconzPresenceDeviceModeSelect(sensor, gateway), + DeconzPresenceSensitivitySelect(sensor, gateway), + DeconzPresenceTriggerDistanceSelect(sensor, gateway), + ] + ) + + gateway.register_platform_add_device_callback( + async_add_presence_sensor, + gateway.api.sensors.presence, + ) + + +class DeconzPresenceDeviceModeSelect(DeconzDevice[Presence], SelectEntity): + """Representation of a deCONZ presence device mode entity.""" + + _name_suffix = "Device Mode" + unique_id_suffix = "device_mode" + _update_key = "devicemode" + + _attr_entity_category = EntityCategory.CONFIG + _attr_options = [ + PresenceConfigDeviceMode.LEFT_AND_RIGHT.value, + PresenceConfigDeviceMode.UNDIRECTED.value, + ] + + TYPE = DOMAIN + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + if self._device.device_mode is not None: + return self._device.device_mode.value + return None + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.gateway.api.sensors.presence.set_config( + id=self._device.resource_id, + device_mode=PresenceConfigDeviceMode(option), + ) + + +class DeconzPresenceSensitivitySelect(DeconzDevice[Presence], SelectEntity): + """Representation of a deCONZ presence sensitivity entity.""" + + _name_suffix = "Sensitivity" + unique_id_suffix = "sensitivity" + _update_key = "sensitivity" + + _attr_entity_category = EntityCategory.CONFIG + _attr_options = list(SENSITIVITY_TO_DECONZ) + + TYPE = DOMAIN + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + if self._device.sensitivity is not None: + return DECONZ_TO_SENSITIVITY[self._device.sensitivity] + return None + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.gateway.api.sensors.presence.set_config( + id=self._device.resource_id, + sensitivity=SENSITIVITY_TO_DECONZ[option], + ) + + +class DeconzPresenceTriggerDistanceSelect(DeconzDevice[Presence], SelectEntity): + """Representation of a deCONZ presence trigger distance entity.""" + + _name_suffix = "Trigger Distance" + unique_id_suffix = "trigger_distance" + _update_key = "triggerdistance" + + _attr_entity_category = EntityCategory.CONFIG + _attr_options = [ + PresenceConfigTriggerDistance.FAR.value, + PresenceConfigTriggerDistance.MEDIUM.value, + PresenceConfigTriggerDistance.NEAR.value, + ] + + TYPE = DOMAIN + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + if self._device.trigger_distance is not None: + return self._device.trigger_distance.value + return None + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.gateway.api.sensors.presence.set_config( + id=self._device.resource_id, + trigger_distance=PresenceConfigTriggerDistance(option), + ) diff --git a/tests/components/deconz/test_button.py b/tests/components/deconz/test_button.py index 804a93d5ea4..c3cdf8160c8 100644 --- a/tests/components/deconz/test_button.py +++ b/tests/components/deconz/test_button.py @@ -48,6 +48,49 @@ TEST_DATA = [ "friendly_name": "Light group Scene Store Current Scene", }, "request": "/groups/1/scenes/1/store", + "request_data": {}, + }, + ), + ( # Presence reset button + { + "sensors": { + "1": { + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", + } + } + }, + { + "entity_count": 5, + "device_count": 3, + "entity_id": "button.aqara_fp1_reset_presence", + "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-reset_presence", + "entity_category": EntityCategory.CONFIG, + "attributes": { + "device_class": "restart", + "friendly_name": "Aqara FP1 Reset Presence", + }, + "request": "/sensors/1/config", + "request_data": {"resetpresence": True}, }, ), ] @@ -92,7 +135,7 @@ async def test_button(hass, aioclient_mock, raw_data, expected): {ATTR_ENTITY_ID: expected["entity_id"]}, blocking=True, ) - assert aioclient_mock.mock_calls[1][2] == {} + assert aioclient_mock.mock_calls[1][2] == expected["request_data"] # Unload entry diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index c326892aef2..0d99d33e571 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -6,11 +6,13 @@ from pydeconz.models.sensor.ancillary_control import ( AncillaryControlAction, AncillaryControlPanel, ) +from pydeconz.models.sensor.presence import PresenceStatePresenceEvent from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.deconz_event import ( CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT, + CONF_DECONZ_PRESENCE_EVENT, ) from homeassistant.const import ( CONF_DEVICE_ID, @@ -412,6 +414,107 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 0 +async def test_deconz_presence_events(hass, aioclient_mock, mock_deconz_websocket): + """Test successful creation of deconz presence events.""" + data = { + "sensors": { + "1": { + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + device_registry = dr.async_get(hass) + + assert len(hass.states.async_all()) == 5 + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 3 + ) + + device = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, "xx:xx:xx:xx:xx:xx:xx:xx")} + ) + + captured_events = async_capture_events(hass, CONF_DECONZ_PRESENCE_EVENT) + + for presence_event in ( + PresenceStatePresenceEvent.ABSENTING, + PresenceStatePresenceEvent.APPROACHING, + PresenceStatePresenceEvent.ENTER, + PresenceStatePresenceEvent.ENTER_LEFT, + PresenceStatePresenceEvent.ENTER_RIGHT, + PresenceStatePresenceEvent.LEAVE, + PresenceStatePresenceEvent.LEFT_LEAVE, + PresenceStatePresenceEvent.RIGHT_LEAVE, + ): + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"presenceevent": presence_event}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(captured_events) == 1 + assert captured_events[0].data == { + CONF_ID: "aqara_fp1", + CONF_UNIQUE_ID: "xx:xx:xx:xx:xx:xx:xx:xx", + CONF_DEVICE_ID: device.id, + CONF_EVENT: presence_event.value, + } + captured_events.clear() + + # Unsupported presence event + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"presenceevent": PresenceStatePresenceEvent.NINE}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(captured_events) == 0 + + 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 + + async def test_deconz_events_bad_unique_id(hass, aioclient_mock): """Verify no devices are created if unique id is bad or missing.""" data = { diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index 459e0e910ab..45298ca090d 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -57,6 +57,7 @@ async def test_entry_diagnostics( str(Platform.LOCK): [], str(Platform.NUMBER): [], str(Platform.SCENE): [], + str(Platform.SELECT): [], str(Platform.SENSOR): [], str(Platform.SIREN): [], str(Platform.SWITCH): [], diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 9471752eb8d..c69c51d13a6 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -28,6 +28,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN from homeassistant.components.ssdp import ( @@ -169,9 +170,10 @@ async def test_gateway_setup(hass, aioclient_mock): assert forward_entry_setup.mock_calls[7][1] == (config_entry, LOCK_DOMAIN) assert forward_entry_setup.mock_calls[8][1] == (config_entry, NUMBER_DOMAIN) assert forward_entry_setup.mock_calls[9][1] == (config_entry, SCENE_DOMAIN) - assert forward_entry_setup.mock_calls[10][1] == (config_entry, SENSOR_DOMAIN) - assert forward_entry_setup.mock_calls[11][1] == (config_entry, SIREN_DOMAIN) - assert forward_entry_setup.mock_calls[12][1] == (config_entry, SWITCH_DOMAIN) + assert forward_entry_setup.mock_calls[10][1] == (config_entry, SELECT_DOMAIN) + assert forward_entry_setup.mock_calls[11][1] == (config_entry, SENSOR_DOMAIN) + assert forward_entry_setup.mock_calls[12][1] == (config_entry, SIREN_DOMAIN) + assert forward_entry_setup.mock_calls[13][1] == (config_entry, SWITCH_DOMAIN) device_registry = dr.async_get(hass) gateway_entry = device_registry.async_get_device( diff --git a/tests/components/deconz/test_select.py b/tests/components/deconz/test_select.py new file mode 100644 index 00000000000..c23f08794a9 --- /dev/null +++ b/tests/components/deconz/test_select.py @@ -0,0 +1,219 @@ +"""deCONZ select platform tests.""" + +from unittest.mock import patch + +from pydeconz.models.sensor.presence import ( + PresenceConfigDeviceMode, + PresenceConfigTriggerDistance, +) +import pytest + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory + +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) + + +async def test_no_select_entities(hass, aioclient_mock): + """Test that no sensors in deconz results in no sensor entities.""" + await setup_deconz_integration(hass, aioclient_mock) + assert len(hass.states.async_all()) == 0 + + +TEST_DATA = [ + ( # Presence Device Mode + { + "sensors": { + "1": { + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", + } + } + }, + { + "entity_count": 5, + "device_count": 3, + "entity_id": "select.aqara_fp1_device_mode", + "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-device_mode", + "entity_category": EntityCategory.CONFIG, + "attributes": { + "friendly_name": "Aqara FP1 Device Mode", + "options": ["leftright", "undirected"], + }, + "option": PresenceConfigDeviceMode.LEFT_AND_RIGHT.value, + "request": "/sensors/1/config", + "request_data": {"devicemode": "leftright"}, + }, + ), + ( # Presence Sensitivity + { + "sensors": { + "1": { + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", + } + } + }, + { + "entity_count": 5, + "device_count": 3, + "entity_id": "select.aqara_fp1_sensitivity", + "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-sensitivity", + "entity_category": EntityCategory.CONFIG, + "attributes": { + "friendly_name": "Aqara FP1 Sensitivity", + "options": ["High", "Medium", "Low"], + }, + "option": "Medium", + "request": "/sensors/1/config", + "request_data": {"sensitivity": 2}, + }, + ), + ( # Presence Trigger Distance + { + "sensors": { + "1": { + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", + } + } + }, + { + "entity_count": 5, + "device_count": 3, + "entity_id": "select.aqara_fp1_trigger_distance", + "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-trigger_distance", + "entity_category": EntityCategory.CONFIG, + "attributes": { + "friendly_name": "Aqara FP1 Trigger Distance", + "options": ["far", "medium", "near"], + }, + "option": PresenceConfigTriggerDistance.FAR.value, + "request": "/sensors/1/config", + "request_data": {"triggerdistance": "far"}, + }, + ), +] + + +@pytest.mark.parametrize("raw_data, expected", TEST_DATA) +async def test_select(hass, aioclient_mock, raw_data, expected): + """Test successful creation of button entities.""" + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + + with patch.dict(DECONZ_WEB_REQUEST, raw_data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == expected["entity_count"] + + # Verify state data + + button = hass.states.get(expected["entity_id"]) + assert button.attributes == expected["attributes"] + + # Verify entity registry data + + ent_reg_entry = ent_reg.async_get(expected["entity_id"]) + assert ent_reg_entry.entity_category is expected["entity_category"] + assert ent_reg_entry.unique_id == expected["unique_id"] + + # Verify device registry data + + assert ( + len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)) + == expected["device_count"] + ) + + # Verify selecting option + + mock_deconz_put_request(aioclient_mock, config_entry.data, expected["request"]) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: expected["entity_id"], + ATTR_OPTION: expected["option"], + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == expected["request_data"] + + # Unload entry + + await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE + + # Remove entry + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 From 4185a70882e3abdab159b595dc268cd8c4a9c261 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 31 Aug 2022 05:40:25 +0200 Subject: [PATCH 790/903] =?UTF-8?q?Allow=20data=20from=20un-connectable=20?= =?UTF-8?q?sources=20in=20fj=C3=A4r=C3=A5skupan=20(#77236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/fjaraskupan/__init__.py | 34 ++++++++++++++----- .../components/fjaraskupan/manifest.json | 3 +- homeassistant/generated/bluetooth.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 75086c631a1..4a6c40fbeae 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -22,6 +22,7 @@ from homeassistant.components.bluetooth import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -33,6 +34,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DISPATCH_DETECTION, DOMAIN + +class UnableToConnect(HomeAssistantError): + """Exception to indicate that we can not connect to device.""" + + PLATFORMS = [ Platform.BINARY_SENSOR, Platform.FAN, @@ -75,28 +81,39 @@ class Coordinator(DataUpdateCoordinator[State]): async def _async_update_data(self) -> State: """Handle an explicit update request.""" if self._refresh_was_scheduled: - if async_address_present(self.hass, self.device.address): + if async_address_present(self.hass, self.device.address, False): return self.device.state raise UpdateFailed( "No data received within schedule, and device is no longer present" ) - await self.device.update() + if ( + ble_device := async_ble_device_from_address( + self.hass, self.device.address, True + ) + ) is None: + raise UpdateFailed("No connectable path to device") + async with self.device.connect(ble_device) as device: + await device.update() return self.device.state def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None: """Handle a new announcement of data.""" - self.device.device = service_info.device self.device.detection_callback(service_info.device, service_info.advertisement) self.async_set_updated_data(self.device.state) @asynccontextmanager async def async_connect_and_update(self) -> AsyncIterator[Device]: """Provide an up to date device for use during connections.""" - if ble_device := async_ble_device_from_address(self.hass, self.device.address): - self.device.device = ble_device - async with self.device: - yield self.device + if ( + ble_device := async_ble_device_from_address( + self.hass, self.device.address, True + ) + ) is None: + raise UnableToConnect("No connectable path to device") + + async with self.device.connect(ble_device) as device: + yield device self.async_set_updated_data(self.device.state) @@ -126,7 +143,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: _LOGGER.debug("Detected: %s", service_info) - device = Device(service_info.device) + device = Device(service_info.device.address) device_info = DeviceInfo( connections={(dr.CONNECTION_BLUETOOTH, service_info.address)}, identifiers={(DOMAIN, service_info.address)}, @@ -149,6 +166,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: BluetoothCallbackMatcher( manufacturer_id=20296, manufacturer_data_start=[79, 68, 70, 74, 65, 82], + connectable=False, ), BluetoothScanningMode.ACTIVE, ) diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index bf7956d297d..7381fc36a08 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -3,13 +3,14 @@ "name": "Fj\u00e4r\u00e5skupan", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fjaraskupan", - "requirements": ["fjaraskupan==1.0.2"], + "requirements": ["fjaraskupan==2.0.0"], "codeowners": ["@elupus"], "iot_class": "local_polling", "loggers": ["bleak", "fjaraskupan"], "dependencies": ["bluetooth"], "bluetooth": [ { + "connectable": false, "manufacturer_id": 20296, "manufacturer_data_start": [79, 68, 70, 74, 65, 82] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 8422ce64f95..3718bbdc913 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -19,6 +19,7 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ }, { "domain": "fjaraskupan", + "connectable": False, "manufacturer_id": 20296, "manufacturer_data_start": [ 79, diff --git a/requirements_all.txt b/requirements_all.txt index ebaa328a071..48819ec8bf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -670,7 +670,7 @@ fivem-api==0.1.2 fixerio==1.0.0a0 # homeassistant.components.fjaraskupan -fjaraskupan==1.0.2 +fjaraskupan==2.0.0 # homeassistant.components.flipr flipr-api==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf539eb31fb..f3b460e8cf3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -489,7 +489,7 @@ file-read-backwards==2.0.0 fivem-api==0.1.2 # homeassistant.components.fjaraskupan -fjaraskupan==1.0.2 +fjaraskupan==2.0.0 # homeassistant.components.flipr flipr-api==1.4.2 From 3caa4963bc5f80e28e5828e4ce72a34a7f82d7aa Mon Sep 17 00:00:00 2001 From: On Freund Date: Wed, 31 Aug 2022 06:48:03 +0300 Subject: [PATCH 791/903] Use partition name as device name in Risco alarm control panels (#77526) --- homeassistant/components/risco/alarm_control_panel.py | 2 +- tests/components/risco/test_alarm_control_panel.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 4196ee0cf42..79da100d6e1 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -235,7 +235,7 @@ class RiscoLocalAlarm(RiscoAlarm): self._attr_unique_id = f"{system_id}_{partition_id}_local" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._attr_unique_id)}, - name=f"Risco {system_id} Partition {partition_id}", + name=partition.name, manufacturer="Risco", ) diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index ca0eb604eef..0014e712ab1 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -36,8 +36,8 @@ from .util import TEST_SITE_UUID FIRST_CLOUD_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0" SECOND_CLOUD_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_1" -FIRST_LOCAL_ENTITY_ID = "alarm_control_panel.risco_test_site_uuid_partition_0" -SECOND_LOCAL_ENTITY_ID = "alarm_control_panel.risco_test_site_uuid_partition_1" +FIRST_LOCAL_ENTITY_ID = "alarm_control_panel.name_0" +SECOND_LOCAL_ENTITY_ID = "alarm_control_panel.name_1" CODES_REQUIRED_OPTIONS = {"code_arm_required": True, "code_disarm_required": True} TEST_RISCO_TO_HA = { @@ -112,8 +112,12 @@ def two_part_local_alarm(): partition_mocks = {0: _partition_mock(), 1: _partition_mock()} with patch.object( partition_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), patch.object( + partition_mocks[0], "name", new_callable=PropertyMock(return_value="Name 0") ), patch.object( partition_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), patch.object( + partition_mocks[1], "name", new_callable=PropertyMock(return_value="Name 1") ), patch( "homeassistant.components.risco.RiscoLocal.zones", new_callable=PropertyMock(return_value={}), From e192c99d2f993c12b3605aafe94e4d05febb7373 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 30 Aug 2022 22:03:02 -0600 Subject: [PATCH 792/903] Add support for Feeder-Robot switches (#77503) --- .../components/litterrobot/switch.py | 128 ++++++++++-------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index de401576a78..2f54ede38b8 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -1,78 +1,91 @@ """Support for Litter-Robot switches.""" from __future__ import annotations -from typing import Any +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any, Generic, Union -from pylitterbot import LitterRobot +from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity +from .entity import LitterRobotConfigEntity, _RobotT from .hub import LitterRobotHub -class LitterRobotNightLightModeSwitch( - LitterRobotConfigEntity[LitterRobot], SwitchEntity -): - """Litter-Robot Night Light Mode Switch.""" +@dataclass +class RequiredKeysMixin(Generic[_RobotT]): + """A class that describes robot switch entity required keys.""" - @property - def is_on(self) -> bool | None: - """Return true if switch is on.""" - if self._refresh_callback is not None: - return self._assumed_state - return self.robot.night_light_mode_enabled - - @property - def icon(self) -> str: - """Return the icon.""" - return "mdi:lightbulb-on" if self.is_on else "mdi:lightbulb-off" - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the switch on.""" - await self.perform_action_and_assume_state(self.robot.set_night_light, True) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the switch off.""" - await self.perform_action_and_assume_state(self.robot.set_night_light, False) + icons: tuple[str, str] + set_fn: Callable[[_RobotT], Callable[[bool], Coroutine[Any, Any, bool]]] -class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity[LitterRobot], SwitchEntity): - """Litter-Robot Panel Lockout Switch.""" - - @property - def is_on(self) -> bool | None: - """Return true if switch is on.""" - if self._refresh_callback is not None: - return self._assumed_state - return self.robot.panel_lock_enabled - - @property - def icon(self) -> str: - """Return the icon.""" - return "mdi:lock" if self.is_on else "mdi:lock-open" - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the switch on.""" - await self.perform_action_and_assume_state(self.robot.set_panel_lockout, True) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the switch off.""" - await self.perform_action_and_assume_state(self.robot.set_panel_lockout, False) +@dataclass +class RobotSwitchEntityDescription(SwitchEntityDescription, RequiredKeysMixin[_RobotT]): + """A class that describes robot switch entities.""" -ROBOT_SWITCHES: list[ - tuple[type[LitterRobotNightLightModeSwitch | LitterRobotPanelLockoutSwitch], str] -] = [ - (LitterRobotNightLightModeSwitch, "Night Light Mode"), - (LitterRobotPanelLockoutSwitch, "Panel Lockout"), +ROBOT_SWITCHES = [ + RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( + key="night_light_mode_enabled", + name="Night Light Mode", + icons=("mdi:lightbulb-on", "mdi:lightbulb-off"), + set_fn=lambda robot: robot.set_night_light, + ), + RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( + key="panel_lock_enabled", + name="Panel Lockout", + icons=("mdi:lock", "mdi:lock-open"), + set_fn=lambda robot: robot.set_panel_lockout, + ), ] +class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): + """Litter-Robot switch entity.""" + + entity_description: RobotSwitchEntityDescription[_RobotT] + + def __init__( + self, + robot: _RobotT, + hub: LitterRobotHub, + description: RobotSwitchEntityDescription[_RobotT], + ) -> None: + """Initialize a Litter-Robot switch entity.""" + assert description.name + super().__init__(robot, description.name, hub) + self.entity_description = description + + @property + def is_on(self) -> bool | None: + """Return true if switch is on.""" + if self._refresh_callback is not None: + return self._assumed_state + return bool(getattr(self.robot, self.entity_description.key)) + + @property + def icon(self) -> str: + """Return the icon.""" + icon_on, icon_off = self.entity_description.icons + return icon_on if self.is_on else icon_off + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + set_fn = self.entity_description.set_fn + await self.perform_action_and_assume_state(set_fn(self.robot), True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + set_fn = self.entity_description.set_fn + await self.perform_action_and_assume_state(set_fn(self.robot), False) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -81,7 +94,8 @@ async def async_setup_entry( """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] async_add_entities( - switch_class(robot=robot, entity_type=switch_type, hub=hub) - for switch_class, switch_type in ROBOT_SWITCHES - for robot in hub.litter_robots() + RobotSwitchEntity(robot=robot, hub=hub, description=description) + for description in ROBOT_SWITCHES + for robot in hub.account.robots + if isinstance(robot, (LitterRobot, FeederRobot)) ) From 3df2ec1ed61d751826bec483272f7e0d7b328149 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Aug 2022 06:07:32 +0200 Subject: [PATCH 793/903] Implement reauth_confirm in icloud (#77530) --- homeassistant/components/icloud/account.py | 19 ++----------- .../components/icloud/config_flow.py | 28 +++++++++++-------- homeassistant/components/icloud/strings.json | 2 +- .../components/icloud/translations/en.json | 2 +- tests/components/icloud/test_config_flow.py | 8 +++--- 5 files changed, 26 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index d0e3b8059a4..fe542f6bf0f 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -15,7 +15,7 @@ from pyicloud.exceptions import ( from pyicloud.services.findmyiphone import AppleDevice from homeassistant.components.zone import async_active_zone -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -132,7 +132,7 @@ class IcloudAccount: self._config_entry.data[CONF_USERNAME], ) - self._require_reauth() + self._config_entry.async_start_reauth(self.hass) return try: @@ -164,7 +164,7 @@ class IcloudAccount: return if self.api.requires_2fa: - self._require_reauth() + self._config_entry.async_start_reauth(self.hass) return api_devices = {} @@ -230,19 +230,6 @@ 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 a4c29acff75..e82fb230eb1 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -1,6 +1,10 @@ """Config flow to configure the iCloud integration.""" +from __future__ import annotations + +from collections.abc import Mapping import logging import os +from typing import Any from pyicloud import PyiCloudService from pyicloud.exceptions import ( @@ -13,6 +17,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.storage import Store from .const import ( @@ -173,22 +178,23 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._validate_and_create_entry(user_input, "user") - async def async_step_reauth(self, user_input=None): - """Update password for a config entry that can't authenticate.""" + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Initialise re-authentication.""" # Store existing entry data so it can be used later and set unique ID # so existing config entry can be updated - if not self._existing_entry: - await self.async_set_unique_id(user_input.pop("unique_id")) - self._existing_entry = user_input.copy() - self._description_placeholders = {"username": user_input[CONF_USERNAME]} - user_input = None + await self.async_set_unique_id(self.context["unique_id"]) + self._existing_entry = {**entry_data} + self._description_placeholders = {"username": entry_data[CONF_USERNAME]} + return await self.async_step_reauth_confirm() + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Update password for a config entry that can't authenticate.""" if user_input is None: - return self._show_setup_form(step_id=config_entries.SOURCE_REAUTH) + return self._show_setup_form(step_id="reauth_confirm") - return await self._validate_and_create_entry( - user_input, config_entries.SOURCE_REAUTH - ) + return await self._validate_and_create_entry(user_input, "reauth_confirm") async def async_step_trusted_device(self, user_input=None, errors=None): """We need a trusted device.""" diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index 70ab11157d3..385dc74a0ab 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -10,7 +10,7 @@ "with_family": "With family" } }, - "reauth": { + "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "Your previously entered password for {username} is no longer working. Update your password to keep using this integration.", "data": { diff --git a/homeassistant/components/icloud/translations/en.json b/homeassistant/components/icloud/translations/en.json index 36e657011e3..65a8892c480 100644 --- a/homeassistant/components/icloud/translations/en.json +++ b/homeassistant/components/icloud/translations/en.json @@ -11,7 +11,7 @@ "validate_verification_code": "Failed to verify your verification code, try again" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Password" }, diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 3c854d468b8..598e44248fa 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -385,8 +385,8 @@ async def test_password_update(hass: HomeAssistant, service_authenticated: Magic result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_REAUTH}, - data={**MOCK_CONFIG, "unique_id": USERNAME}, + context={"source": SOURCE_REAUTH, "unique_id": config_entry.unique_id}, + data={**MOCK_CONFIG}, ) assert result["type"] == data_entry_flow.FlowResultType.FORM @@ -409,8 +409,8 @@ async def test_password_update_wrong_password(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_REAUTH}, - data={**MOCK_CONFIG, "unique_id": USERNAME}, + context={"source": SOURCE_REAUTH, "unique_id": config_entry.unique_id}, + data={**MOCK_CONFIG}, ) assert result["type"] == data_entry_flow.FlowResultType.FORM From 61ff52c93acd2f274d24469494b51356c88bb66f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 31 Aug 2022 08:12:25 +0200 Subject: [PATCH 794/903] Normalize deCONZ sensor unique IDs (#76357) * Normalize deCONZ sensor unique IDs * Handle battery sensors properly * Fix daylight sensor unique ID --- homeassistant/components/deconz/sensor.py | 91 ++++++++++++++--------- tests/components/deconz/test_sensor.py | 91 +++++++++++++++++------ 2 files changed, 127 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 055067cc36f..09d248756fc 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -9,7 +9,7 @@ from pydeconz.interfaces.sensors import SensorResources from pydeconz.models.event import EventType from pydeconz.models.sensor.air_quality import AirQuality from pydeconz.models.sensor.consumption import Consumption -from pydeconz.models.sensor.daylight import Daylight +from pydeconz.models.sensor.daylight import DAYLIGHT_STATUS, Daylight from pydeconz.models.sensor.generic_status import GenericStatus from pydeconz.models.sensor.humidity import Humidity from pydeconz.models.sensor.light_level import LightLevel @@ -41,21 +41,23 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.typing import StateType import homeassistant.util.dt as dt_util -from .const import ATTR_DARK, ATTR_ON +from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry PROVIDES_EXTRA_ATTRIBUTES = ( "battery", "consumption", - "status", + "daylight_status", "humidity", "light_level", "power", "pressure", + "status", "temperature", ) @@ -119,8 +121,8 @@ ENTITY_DESCRIPTIONS = { ], Daylight: [ DeconzSensorDescription( - key="status", - value_fn=lambda device: device.status + key="daylight_status", + value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status] if isinstance(device, Daylight) else None, update_key="status", @@ -232,6 +234,27 @@ COMMON_SENSOR_DESCRIPTIONS = [ ] +@callback +def async_update_unique_id( + hass: HomeAssistant, unique_id: str, description: DeconzSensorDescription +) -> None: + """Update unique ID to always have a suffix. + + Introduced with release 2022.9. + """ + ent_reg = er.async_get(hass) + + new_unique_id = f"{unique_id}-{description.key}" + if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): + return + + if description.suffix: + unique_id = f'{unique_id.split("-", 1)[0]}-{description.suffix.lower()}' + + if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -241,29 +264,46 @@ async def async_setup_entry( gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() + known_device_entities: dict[str, set[str]] = { + description.key: set() for description in COMMON_SENSOR_DESCRIPTIONS + } + @callback def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add sensor from deCONZ.""" sensor = gateway.api.sensors[sensor_id] entities: list[DeconzSensor] = [] - if sensor.battery is None and not sensor.type.startswith("CLIP"): - DeconzBatteryTracker(sensor_id, gateway, async_add_entities) - - known_entities = set(gateway.entities[DOMAIN]) - for description in ( ENTITY_DESCRIPTIONS.get(type(sensor), []) + COMMON_SENSOR_DESCRIPTIONS ): + no_sensor_data = False if ( not hasattr(sensor, description.key) or description.value_fn(sensor) is None ): + no_sensor_data = True + + if description in COMMON_SENSOR_DESCRIPTIONS: + if ( + sensor.type.startswith("CLIP") + or (no_sensor_data and description.key != "battery") + or ( + (unique_id := sensor.unique_id.rsplit("-", 1)[0]) + in known_device_entities[description.key] + ) + ): + continue + known_device_entities[description.key].add(unique_id) + if no_sensor_data and description.key == "battery": + DeconzBatteryTracker(sensor_id, gateway, async_add_entities) + continue + + if no_sensor_data: continue - entity = DeconzSensor(sensor, gateway, description) - if entity.unique_id not in known_entities: - entities.append(entity) + async_update_unique_id(hass, sensor.unique_id, description) + entities.append(DeconzSensor(sensor, gateway, description)) async_add_entities(entities) @@ -301,21 +341,7 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): @property def unique_id(self) -> str: """Return a unique identifier for this device.""" - if ( - self.entity_description.key == "battery" - and self._device.manufacturer == "Danfoss" - and self._device.model_id - in [ - "0x8030", - "0x8031", - "0x8034", - "0x8035", - ] - ): - return f"{super().unique_id}-battery" - if self.entity_description.suffix: - return f"{self.serial}-{self.entity_description.suffix.lower()}" - return super().unique_id + return f"{self._device.unique_id}-{self.entity_description.key}" @property def native_value(self) -> StateType | datetime: @@ -386,9 +412,6 @@ class DeconzBatteryTracker: """Update the device's state.""" if "battery" in self.sensor.changed_keys: self.unsubscribe() - known_entities = set(self.gateway.entities[DOMAIN]) - entity = DeconzSensor( - self.sensor, self.gateway, COMMON_SENSOR_DESCRIPTIONS[0] - ) - if entity.unique_id not in known_entities: - self.async_add_entities([entity]) + desc = COMMON_SENSOR_DESCRIPTIONS[0] + async_update_unique_id(self.gateway.hass, self.sensor.unique_id, desc) + self.async_add_entities([DeconzSensor(self.sensor, self.gateway, desc)]) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 5f11a4d7b0b..1078d888c0e 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -5,7 +5,10 @@ from unittest.mock import patch import pytest -from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR +from homeassistant.components.deconz.const import ( + CONF_ALLOW_CLIP_SENSOR, + DOMAIN as DECONZ_DOMAIN, +) from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -54,7 +57,8 @@ TEST_DATA = [ "entity_count": 2, "device_count": 3, "entity_id": "sensor.bosch_air_quality_sensor", - "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef", + "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef-air_quality", + "old_unique_id": "00:12:4b:00:14:4d:00:07-02-fdef", "state": "poor", "entity_category": None, "device_class": None, @@ -92,7 +96,8 @@ TEST_DATA = [ "entity_count": 2, "device_count": 3, "entity_id": "sensor.bosch_air_quality_sensor_ppb", - "unique_id": "00:12:4b:00:14:4d:00:07-ppb", + "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef-air_quality_ppb", + "old_unique_id": "00:12:4b:00:14:4d:00:07-ppb", "state": "809", "entity_category": None, "device_class": SensorDeviceClass.AQI, @@ -131,7 +136,8 @@ TEST_DATA = [ "entity_count": 1, "device_count": 3, "entity_id": "sensor.fyrtur_block_out_roller_blind_battery", - "unique_id": "00:0d:6f:ff:fe:01:23:45-battery", + "unique_id": "00:0d:6f:ff:fe:01:23:45-01-0001-battery", + "old_unique_id": "00:0d:6f:ff:fe:01:23:45-battery", "state": "100", "entity_category": EntityCategory.DIAGNOSTIC, "device_class": SensorDeviceClass.BATTERY, @@ -167,7 +173,8 @@ TEST_DATA = [ "entity_count": 1, "device_count": 3, "entity_id": "sensor.consumption_15", - "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702", + "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702-consumption", + "old_unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702", "state": "11.342", "entity_category": None, "device_class": SensorDeviceClass.ENERGY, @@ -203,13 +210,15 @@ TEST_DATA = [ }, "swversion": "1.0", "type": "Daylight", + "uniqueid": "01:23:4E:FF:FF:56:78:9A-01", }, { "enable_entity": True, "entity_count": 1, - "device_count": 2, + "device_count": 3, "entity_id": "sensor.daylight", - "unique_id": "", + "unique_id": "01:23:4E:FF:FF:56:78:9A-01-daylight_status", + "old-unique_id": "01:23:4E:FF:FF:56:78:9A-01", "state": "solar_noon", "entity_category": None, "device_class": None, @@ -246,7 +255,8 @@ TEST_DATA = [ "entity_count": 1, "device_count": 2, "entity_id": "sensor.fsm_state_motion_stair", - "unique_id": "fsm-state-1520195376277", + "unique_id": "fsm-state-1520195376277-status", + "old_unique_id": "fsm-state-1520195376277", "state": "0", "entity_category": None, "device_class": None, @@ -284,7 +294,8 @@ TEST_DATA = [ "entity_count": 2, "device_count": 3, "entity_id": "sensor.mi_temperature_1", - "unique_id": "00:15:8d:00:02:45:dc:53-01-0405", + "unique_id": "00:15:8d:00:02:45:dc:53-01-0405-humidity", + "old_unique_id": "00:15:8d:00:02:45:dc:53-01-0405", "state": "35.5", "entity_category": None, "device_class": SensorDeviceClass.HUMIDITY, @@ -333,7 +344,8 @@ TEST_DATA = [ "entity_count": 2, "device_count": 3, "entity_id": "sensor.motion_sensor_4", - "unique_id": "00:17:88:01:03:28:8c:9b-02-0400", + "unique_id": "00:17:88:01:03:28:8c:9b-02-0400-light_level", + "old_unique_id": "00:17:88:01:03:28:8c:9b-02-0400", "state": "5.0", "entity_category": None, "device_class": SensorDeviceClass.ILLUMINANCE, @@ -375,7 +387,8 @@ TEST_DATA = [ "entity_count": 1, "device_count": 3, "entity_id": "sensor.power_16", - "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04", + "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04-power", + "old_unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04", "state": "64", "entity_category": None, "device_class": SensorDeviceClass.POWER, @@ -417,7 +430,8 @@ TEST_DATA = [ "entity_count": 2, "device_count": 3, "entity_id": "sensor.mi_temperature_1", - "unique_id": "00:15:8d:00:02:45:dc:53-01-0403", + "unique_id": "00:15:8d:00:02:45:dc:53-01-0403-pressure", + "old_unique_id": "00:15:8d:00:02:45:dc:53-01-0403", "state": "1010", "entity_category": None, "device_class": SensorDeviceClass.PRESSURE, @@ -458,7 +472,8 @@ TEST_DATA = [ "entity_count": 2, "device_count": 3, "entity_id": "sensor.mi_temperature_1", - "unique_id": "00:15:8d:00:02:45:dc:53-01-0402", + "unique_id": "00:15:8d:00:02:45:dc:53-01-0402-temperature", + "old_unique_id": "00:15:8d:00:02:45:dc:53-01-0402", "state": "21.8", "entity_category": None, "device_class": SensorDeviceClass.TEMPERATURE, @@ -501,7 +516,8 @@ TEST_DATA = [ "entity_count": 2, "device_count": 3, "entity_id": "sensor.etrv_sejour", - "unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", + "unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a-last_set", + "old_unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", "state": "2020-11-19T08:07:08+00:00", "entity_category": None, "device_class": SensorDeviceClass.TIMESTAMP, @@ -515,7 +531,7 @@ TEST_DATA = [ "next_state": "2020-12-14T10:12:14+00:00", }, ), - ( # Secondary temperature sensor + ( # Internal temperature sensor { "config": { "battery": 100, @@ -542,7 +558,8 @@ TEST_DATA = [ "entity_count": 3, "device_count": 3, "entity_id": "sensor.alarm_10_temperature", - "unique_id": "00:15:8d:00:02:b5:d1:80-temperature", + "unique_id": "00:15:8d:00:02:b5:d1:80-01-0500-internal_temperature", + "old_unique_id": "00:15:8d:00:02:b5:d1:80-temperature", "state": "26.0", "entity_category": None, "device_class": SensorDeviceClass.TEMPERATURE, @@ -583,7 +600,8 @@ TEST_DATA = [ "entity_count": 1, "device_count": 3, "entity_id": "sensor.dimmer_switch_3_battery", - "unique_id": "00:17:88:01:02:0e:32:a3-battery", + "unique_id": "00:17:88:01:02:0e:32:a3-02-fc00-battery", + "old_unique_id": "00:17:88:01:02:0e:32:a3-battery", "state": "90", "entity_category": EntityCategory.DIAGNOSTIC, "device_class": SensorDeviceClass.BATTERY, @@ -611,6 +629,15 @@ async def test_sensors( ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) + # Create entity entry to migrate to new unique ID + if "old_unique_id" in expected: + ent_reg.async_get_or_create( + SENSOR_DOMAIN, + DECONZ_DOMAIN, + expected["old_unique_id"], + suggested_object_id=expected["entity_id"].replace("sensor.", ""), + ) + with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}): config_entry = await setup_deconz_integration( hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} @@ -848,7 +875,11 @@ async def test_air_quality_sensor_without_ppb(hass, aioclient_mock): async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): - """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" + """Test that a battery sensor can be created later on. + + Without an initial battery state a battery sensor + can be created once a value is reported. + """ data = { "sensors": { "1": { @@ -856,15 +887,33 @@ async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): "type": "ZHASwitch", "state": {"buttonevent": 1000}, "config": {}, - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "uniqueid": "00:00:00:00:00:00:00:00-00-0000", + }, + "2": { + "name": "Switch 2", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:00-00-0001", + }, } } with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 - assert not hass.states.get("sensor.switch_1_battery") + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "2", + "config": {"battery": 50}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 event_changed_sensor = { "t": "event", From 7c585bd380407cb325cb8a43832576e12604773f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Aug 2022 10:52:41 +0200 Subject: [PATCH 795/903] Fix sync context in icloud (#77582) Co-authored-by: Martin Hjelmare --- homeassistant/components/icloud/account.py | 8 +++- .../components/icloud/config_flow.py | 8 ++-- tests/components/icloud/conftest.py | 7 --- tests/components/icloud/const.py | 30 +++++++++++++ tests/components/icloud/test_config_flow.py | 32 ++++++------- tests/components/icloud/test_init.py | 45 +++++++++++++++++++ 6 files changed, 99 insertions(+), 31 deletions(-) create mode 100644 tests/components/icloud/const.py create mode 100644 tests/components/icloud/test_init.py diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index fe542f6bf0f..c51f6a3ac26 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -132,7 +132,7 @@ class IcloudAccount: self._config_entry.data[CONF_USERNAME], ) - self._config_entry.async_start_reauth(self.hass) + self._require_reauth() return try: @@ -164,7 +164,7 @@ class IcloudAccount: return if self.api.requires_2fa: - self._config_entry.async_start_reauth(self.hass) + self._require_reauth() return api_devices = {} @@ -230,6 +230,10 @@ class IcloudAccount: utcnow() + timedelta(minutes=self._fetch_interval), ) + def _require_reauth(self): + """Require the user to log in again.""" + self.hass.add_job(self._config_entry.async_start_reauth, self.hass) + 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 e82fb230eb1..6cdde2249c8 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -55,7 +55,7 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._trusted_device = None self._verification_code = None - self._existing_entry = None + self._existing_entry_data = None self._description_placeholders = None def _show_setup_form(self, user_input=None, errors=None, step_id="user"): @@ -99,8 +99,8 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # If an existing entry was found, meaning this is a password update attempt, # use those to get config values that aren't changing - if self._existing_entry: - extra_inputs = self._existing_entry + if self._existing_entry_data: + extra_inputs = self._existing_entry_data self._username = extra_inputs[CONF_USERNAME] self._with_family = extra_inputs.get(CONF_WITH_FAMILY, DEFAULT_WITH_FAMILY) @@ -183,7 +183,7 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Store existing entry data so it can be used later and set unique ID # so existing config entry can be updated await self.async_set_unique_id(self.context["unique_id"]) - self._existing_entry = {**entry_data} + self._existing_entry_data = {**entry_data} self._description_placeholders = {"username": entry_data[CONF_USERNAME]} return await self.async_step_reauth_confirm() diff --git a/tests/components/icloud/conftest.py b/tests/components/icloud/conftest.py index c8195471878..2437d05f575 100644 --- a/tests/components/icloud/conftest.py +++ b/tests/components/icloud/conftest.py @@ -4,13 +4,6 @@ from unittest.mock import patch import pytest -@pytest.fixture(name="icloud_bypass_setup", autouse=True) -def icloud_bypass_setup_fixture(): - """Mock component setup.""" - with patch("homeassistant.components.icloud.async_setup_entry", return_value=True): - yield - - @pytest.fixture(autouse=True) def icloud_not_create_dir(): """Mock component setup.""" diff --git a/tests/components/icloud/const.py b/tests/components/icloud/const.py new file mode 100644 index 00000000000..459f18e17cc --- /dev/null +++ b/tests/components/icloud/const.py @@ -0,0 +1,30 @@ +"""Constants for the iCloud tests.""" +from homeassistant.components.icloud.const import ( + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + CONF_WITH_FAMILY, + DEFAULT_GPS_ACCURACY_THRESHOLD, + DEFAULT_MAX_INTERVAL, + DEFAULT_WITH_FAMILY, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +USERNAME = "username@me.com" +USERNAME_2 = "second_username@icloud.com" +PASSWORD = "password" +PASSWORD_2 = "second_password" +WITH_FAMILY = True +MAX_INTERVAL = 15 +GPS_ACCURACY_THRESHOLD = 250 + +MOCK_CONFIG = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_WITH_FAMILY: DEFAULT_WITH_FAMILY, + CONF_MAX_INTERVAL: DEFAULT_MAX_INTERVAL, + CONF_GPS_ACCURACY_THRESHOLD: DEFAULT_GPS_ACCURACY_THRESHOLD, +} + +TRUSTED_DEVICES = [ + {"deviceType": "SMS", "areaCode": "", "phoneNumber": "*******58", "deviceId": "1"} +] diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 598e44248fa..ef866dd4aeb 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -22,27 +22,23 @@ from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from .const import ( + MOCK_CONFIG, + PASSWORD, + PASSWORD_2, + TRUSTED_DEVICES, + USERNAME, + WITH_FAMILY, +) + from tests.common import MockConfigEntry -USERNAME = "username@me.com" -USERNAME_2 = "second_username@icloud.com" -PASSWORD = "password" -PASSWORD_2 = "second_password" -WITH_FAMILY = True -MAX_INTERVAL = 15 -GPS_ACCURACY_THRESHOLD = 250 -MOCK_CONFIG = { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_WITH_FAMILY: DEFAULT_WITH_FAMILY, - CONF_MAX_INTERVAL: DEFAULT_MAX_INTERVAL, - CONF_GPS_ACCURACY_THRESHOLD: DEFAULT_GPS_ACCURACY_THRESHOLD, -} - -TRUSTED_DEVICES = [ - {"deviceType": "SMS", "areaCode": "", "phoneNumber": "*******58", "deviceId": "1"} -] +@pytest.fixture(name="icloud_bypass_setup", autouse=True) +def icloud_bypass_setup_fixture(): + """Mock component setup.""" + with patch("homeassistant.components.icloud.async_setup_entry", return_value=True): + yield @pytest.fixture(name="service") diff --git a/tests/components/icloud/test_init.py b/tests/components/icloud/test_init.py new file mode 100644 index 00000000000..60ab00a6262 --- /dev/null +++ b/tests/components/icloud/test_init.py @@ -0,0 +1,45 @@ +"""Tests for the iCloud config flow.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.components.icloud.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from .const import MOCK_CONFIG, USERNAME + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="service_2fa") +def mock_controller_2fa_service(): + """Mock a successful 2fa service.""" + with patch( + "homeassistant.components.icloud.account.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.mark.usefixtures("service_2fa") +async def test_setup_2fa(hass: HomeAssistant) -> None: + """Test that invalid login triggers reauth flow.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME + ) + config_entry.add_to_hass(hass) + + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert not hass.config_entries.flow.async_progress() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + in_progress_flows = hass.config_entries.flow.async_progress() + assert len(in_progress_flows) == 1 + assert in_progress_flows[0]["context"]["unique_id"] == config_entry.unique_id From 008ac8d10d3655a233db0096a3ba63ac72cf6d8b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 31 Aug 2022 11:30:45 +0200 Subject: [PATCH 796/903] Improve statistics metadata WS API (#77209) --- .../components/recorder/statistics.py | 39 +- homeassistant/components/sensor/recorder.py | 6 +- tests/components/demo/test_init.py | 6 +- tests/components/history/test_init.py | 31 +- tests/components/recorder/test_statistics.py | 6 +- .../components/recorder/test_websocket_api.py | 15 +- tests/components/sensor/test_recorder.py | 464 +++++++++++------- 7 files changed, 360 insertions(+), 207 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 1b0a4e64897..0a9e29747a9 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -826,27 +826,28 @@ def list_statistic_ids( a recorder platform for statistic_ids which will be added in the next statistics period. """ - units = hass.config.units result = {} + def _display_unit(hass: HomeAssistant, unit: str | None) -> str | None: + if unit is None: + return None + return _configured_unit(unit, hass.config.units) + # Query the database with session_scope(hass=hass) as session: metadata = get_metadata_with_session( hass, session, statistic_type=statistic_type, statistic_ids=statistic_ids ) - for _, meta in metadata.values(): - if (unit := meta["unit_of_measurement"]) is not None: - # Display unit according to user settings - unit = _configured_unit(unit, units) - meta["unit_of_measurement"] = unit - result = { meta["statistic_id"]: { "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], + "display_unit_of_measurement": _display_unit( + hass, meta["unit_of_measurement"] + ), "unit_of_measurement": meta["unit_of_measurement"], } for _, meta in metadata.values() @@ -860,14 +861,19 @@ def list_statistic_ids( hass, statistic_ids=statistic_ids, statistic_type=statistic_type ) - for statistic_id, info in platform_statistic_ids.items(): - if (unit := info["unit_of_measurement"]) is not None: - # Display unit according to user settings - unit = _configured_unit(unit, units) - platform_statistic_ids[statistic_id]["unit_of_measurement"] = unit - - for key, value in platform_statistic_ids.items(): - result.setdefault(key, value) + for key, meta in platform_statistic_ids.items(): + if key in result: + continue + result[key] = { + "has_mean": meta["has_mean"], + "has_sum": meta["has_sum"], + "name": meta["name"], + "source": meta["source"], + "display_unit_of_measurement": _display_unit( + hass, meta["unit_of_measurement"] + ), + "unit_of_measurement": meta["unit_of_measurement"], + } # Return a list of statistic_id + metadata return [ @@ -877,7 +883,8 @@ def list_statistic_ids( "has_sum": info["has_sum"], "name": info.get("name"), "source": info["source"], - "unit_of_measurement": info["unit_of_measurement"], + "display_unit_of_measurement": info["display_unit_of_measurement"], + "statistics_unit_of_measurement": info["unit_of_measurement"], } for _id, info in result.items() ] diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index ea7d129a9c3..8c70a2c3ffe 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -622,7 +622,7 @@ def list_statistic_ids( """Return all or filtered statistic_ids and meta data.""" entities = _get_sensor_states(hass) - result = {} + result: dict[str, StatisticMetaData] = {} for state in entities: state_class = state.attributes[ATTR_STATE_CLASS] @@ -647,7 +647,9 @@ def list_statistic_ids( result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, + "name": None, "source": RECORDER_DOMAIN, + "statistic_id": state.entity_id, "unit_of_measurement": native_unit, } continue @@ -659,7 +661,9 @@ def list_statistic_ids( result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, + "name": None, "source": RECORDER_DOMAIN, + "statistic_id": state.entity_id, "unit_of_measurement": statistics_unit, } diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 5a407ba25fc..1577b411c23 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -62,20 +62,22 @@ async def test_demo_statistics(hass, recorder_mock): list_statistic_ids, hass ) assert { + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": "Outdoor temperature", "source": "demo", "statistic_id": "demo:temperature_outdoor", - "unit_of_measurement": "°C", + "statistics_unit_of_measurement": "°C", } in statistic_ids assert { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": "Energy consumption 1", "source": "demo", "statistic_id": "demo:energy_consumption_kwh", - "unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "kWh", } in statistic_ids diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 5eb4894c72a..854ddb76191 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1118,18 +1118,24 @@ async def test_statistics_during_period_bad_end_time( @pytest.mark.parametrize( - "units, attributes, unit", + "units, attributes, display_unit, statistics_unit", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi"), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa"), + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "°C"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "°C"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi", "Pa"), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa", "Pa"), ], ) async def test_list_statistic_ids( - hass, hass_ws_client, recorder_mock, units, attributes, unit + hass, + hass_ws_client, + recorder_mock, + units, + attributes, + display_unit, + statistics_unit, ): """Test list_statistic_ids.""" now = dt_util.utcnow() @@ -1158,7 +1164,8 @@ async def test_list_statistic_ids( "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": unit, + "display_unit_of_measurement": display_unit, + "statistics_unit_of_measurement": statistics_unit, } ] @@ -1178,7 +1185,8 @@ async def test_list_statistic_ids( "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": unit, + "display_unit_of_measurement": display_unit, + "statistics_unit_of_measurement": statistics_unit, } ] @@ -1200,7 +1208,8 @@ async def test_list_statistic_ids( "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": unit, + "display_unit_of_measurement": display_unit, + "statistics_unit_of_measurement": statistics_unit, } ] diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 5bc8aa76a00..970a7feac61 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -524,12 +524,13 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "kWh", } ] metadata = get_metadata(hass, statistic_ids=(statistic_id,)) @@ -616,12 +617,13 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy renamed", "source": source, - "unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "kWh", } ] metadata = get_metadata(hass, statistic_ids=(statistic_id,)) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index b604cc53e6c..269cebcba9f 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -226,11 +226,12 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": "W", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": "W", + "statistics_unit_of_measurement": "W", } ] @@ -252,11 +253,12 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": new_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": new_unit, + "statistics_unit_of_measurement": new_unit, } ] @@ -526,11 +528,12 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": unit, + "statistics_unit_of_measurement": unit, } ] @@ -552,11 +555,12 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": unit, + "statistics_unit_of_measurement": unit, } ] @@ -646,12 +650,13 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "kWh", } ] metadata = get_metadata(hass, statistic_ids=(statistic_id,)) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 64fd1884ae6..e7421e6a616 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -76,24 +76,32 @@ def set_time_zone(): @pytest.mark.parametrize( - "device_class,unit,native_unit,mean,min,max", + "device_class,state_unit,display_unit,statistics_unit,mean,min,max", [ - (None, "%", "%", 13.050847, -10, 30), - ("battery", "%", "%", 13.050847, -10, 30), - ("battery", None, None, 13.050847, -10, 30), - ("humidity", "%", "%", 13.050847, -10, 30), - ("humidity", None, None, 13.050847, -10, 30), - ("pressure", "Pa", "Pa", 13.050847, -10, 30), - ("pressure", "hPa", "Pa", 1305.0847, -1000, 3000), - ("pressure", "mbar", "Pa", 1305.0847, -1000, 3000), - ("pressure", "inHg", "Pa", 44195.25, -33863.89, 101591.67), - ("pressure", "psi", "Pa", 89982.42, -68947.57, 206842.71), - ("temperature", "°C", "°C", 13.050847, -10, 30), - ("temperature", "°F", "°C", -10.52731, -23.33333, -1.111111), + (None, "%", "%", "%", 13.050847, -10, 30), + ("battery", "%", "%", "%", 13.050847, -10, 30), + ("battery", None, None, None, 13.050847, -10, 30), + ("humidity", "%", "%", "%", 13.050847, -10, 30), + ("humidity", None, None, None, 13.050847, -10, 30), + ("pressure", "Pa", "Pa", "Pa", 13.050847, -10, 30), + ("pressure", "hPa", "Pa", "Pa", 1305.0847, -1000, 3000), + ("pressure", "mbar", "Pa", "Pa", 1305.0847, -1000, 3000), + ("pressure", "inHg", "Pa", "Pa", 44195.25, -33863.89, 101591.67), + ("pressure", "psi", "Pa", "Pa", 89982.42, -68947.57, 206842.71), + ("temperature", "°C", "°C", "°C", 13.050847, -10, 30), + ("temperature", "°F", "°C", "°C", -10.52731, -23.33333, -1.111111), ], ) def test_compile_hourly_statistics( - hass_recorder, caplog, device_class, unit, native_unit, mean, min, max + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistics_unit, + mean, + min, + max, ): """Test compiling hourly statistics.""" zero = dt_util.utcnow() @@ -103,7 +111,7 @@ def test_compile_hourly_statistics( attributes = { "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) hist = history.get_significant_states(hass, zero, four) @@ -115,11 +123,12 @@ def test_compile_hourly_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -142,13 +151,13 @@ def test_compile_hourly_statistics( @pytest.mark.parametrize( - "device_class,unit,native_unit", + "device_class,state_unit,display_unit,statistics_unit", [ - (None, "%", "%"), + (None, "%", "%", "%"), ], ) def test_compile_hourly_statistics_purged_state_changes( - hass_recorder, caplog, device_class, unit, native_unit + hass_recorder, caplog, device_class, state_unit, display_unit, statistics_unit ): """Test compiling hourly statistics.""" zero = dt_util.utcnow() @@ -158,7 +167,7 @@ def test_compile_hourly_statistics_purged_state_changes( attributes = { "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) hist = history.get_significant_states(hass, zero, four) @@ -181,12 +190,13 @@ def test_compile_hourly_statistics_purged_state_changes( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "display_unit_of_measurement": display_unit, "statistic_id": "sensor.test1", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -250,27 +260,30 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": "°C", + "statistics_unit_of_measurement": "°C", }, { "statistic_id": "sensor.test6", + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": "°C", + "statistics_unit_of_measurement": "°C", }, { "statistic_id": "sensor.test7", + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": "°C", + "statistics_unit_of_measurement": "°C", }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -320,20 +333,20 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "units,device_class,unit,display_unit,factor", + "units,device_class,state_unit,display_unit,statistics_unit,factor", [ - (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", 1), - (IMPERIAL_SYSTEM, "energy", "Wh", "kWh", 1 / 1000), - (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", 1), - (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", 1), - (IMPERIAL_SYSTEM, "gas", "m³", "ft³", 35.314666711), - (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", 1), - (METRIC_SYSTEM, "energy", "kWh", "kWh", 1), - (METRIC_SYSTEM, "energy", "Wh", "kWh", 1 / 1000), - (METRIC_SYSTEM, "monetary", "EUR", "EUR", 1), - (METRIC_SYSTEM, "monetary", "SEK", "SEK", 1), - (METRIC_SYSTEM, "gas", "m³", "m³", 1), - (METRIC_SYSTEM, "gas", "ft³", "m³", 0.0283168466), + (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", "kWh", 1), + (IMPERIAL_SYSTEM, "energy", "Wh", "kWh", "kWh", 1 / 1000), + (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", "EUR", 1), + (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", "SEK", 1), + (IMPERIAL_SYSTEM, "gas", "m³", "ft³", "m³", 35.314666711), + (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "m³", 1), + (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", 1), + (METRIC_SYSTEM, "energy", "Wh", "kWh", "kWh", 1 / 1000), + (METRIC_SYSTEM, "monetary", "EUR", "EUR", "EUR", 1), + (METRIC_SYSTEM, "monetary", "SEK", "SEK", "SEK", 1), + (METRIC_SYSTEM, "gas", "m³", "m³", "m³", 1), + (METRIC_SYSTEM, "gas", "ft³", "m³", "m³", 0.0283168466), ], ) async def test_compile_hourly_sum_statistics_amount( @@ -344,8 +357,9 @@ async def test_compile_hourly_sum_statistics_amount( units, state_class, device_class, - unit, + state_unit, display_unit, + statistics_unit, factor, ): """Test compiling hourly statistics.""" @@ -361,7 +375,7 @@ async def test_compile_hourly_sum_statistics_amount( attributes = { "device_class": device_class, "state_class": state_class, - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, "last_reset": None, } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] @@ -385,11 +399,12 @@ async def test_compile_hourly_sum_statistics_amount( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": display_unit, + "statistics_unit_of_measurement": statistics_unit, } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -496,18 +511,25 @@ async def test_compile_hourly_sum_statistics_amount( @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "device_class,unit,native_unit,factor", + "device_class,state_unit,display_unit,statistics_unit,factor", [ - ("energy", "kWh", "kWh", 1), - ("energy", "Wh", "kWh", 1 / 1000), - ("monetary", "EUR", "EUR", 1), - ("monetary", "SEK", "SEK", 1), - ("gas", "m³", "m³", 1), - ("gas", "ft³", "m³", 0.0283168466), + ("energy", "kWh", "kWh", "kWh", 1), + ("energy", "Wh", "kWh", "kWh", 1 / 1000), + ("monetary", "EUR", "EUR", "EUR", 1), + ("monetary", "SEK", "SEK", "SEK", 1), + ("gas", "m³", "m³", "m³", 1), + ("gas", "ft³", "m³", "m³", 0.0283168466), ], ) def test_compile_hourly_sum_statistics_amount_reset_every_state_change( - hass_recorder, caplog, state_class, device_class, unit, native_unit, factor + hass_recorder, + caplog, + state_class, + device_class, + state_unit, + display_unit, + statistics_unit, + factor, ): """Test compiling hourly statistics.""" zero = dt_util.utcnow() @@ -517,7 +539,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( attributes = { "device_class": device_class, "state_class": state_class, - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, "last_reset": None, } seq = [10, 15, 15, 15, 20, 20, 20, 25] @@ -566,11 +588,12 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -607,13 +630,20 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "device_class,unit,native_unit,factor", + "device_class,state_unit,display_unit,statistics_unit,factor", [ - ("energy", "kWh", "kWh", 1), + ("energy", "kWh", "kWh", "kWh", 1), ], ) def test_compile_hourly_sum_statistics_amount_invalid_last_reset( - hass_recorder, caplog, state_class, device_class, unit, native_unit, factor + hass_recorder, + caplog, + state_class, + device_class, + state_unit, + display_unit, + statistics_unit, + factor, ): """Test compiling hourly statistics.""" zero = dt_util.utcnow() @@ -623,7 +653,7 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( attributes = { "device_class": device_class, "state_class": state_class, - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, "last_reset": None, } seq = [10, 15, 15, 15, 20, 20, 20, 25] @@ -657,11 +687,12 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -686,13 +717,20 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "device_class,unit,native_unit,factor", + "device_class,state_unit,display_unit,statistics_unit,factor", [ - ("energy", "kWh", "kWh", 1), + ("energy", "kWh", "kWh", "kWh", 1), ], ) def test_compile_hourly_sum_statistics_nan_inf_state( - hass_recorder, caplog, state_class, device_class, unit, native_unit, factor + hass_recorder, + caplog, + state_class, + device_class, + state_unit, + display_unit, + statistics_unit, + factor, ): """Test compiling hourly statistics with nan and inf states.""" zero = dt_util.utcnow() @@ -702,7 +740,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( attributes = { "device_class": device_class, "state_class": state_class, - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, "last_reset": None, } seq = [10, math.nan, 15, 15, 20, math.inf, 20, 10] @@ -732,11 +770,12 @@ def test_compile_hourly_sum_statistics_nan_inf_state( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -780,9 +819,9 @@ def test_compile_hourly_sum_statistics_nan_inf_state( ) @pytest.mark.parametrize("state_class", ["total_increasing"]) @pytest.mark.parametrize( - "device_class,unit,native_unit,factor", + "device_class,state_unit,display_unit,statistics_unit,factor", [ - ("energy", "kWh", "kWh", 1), + ("energy", "kWh", "kWh", "kWh", 1), ], ) def test_compile_hourly_sum_statistics_negative_state( @@ -793,8 +832,9 @@ def test_compile_hourly_sum_statistics_negative_state( warning_2, state_class, device_class, - unit, - native_unit, + state_unit, + display_unit, + statistics_unit, factor, ): """Test compiling hourly statistics with negative states.""" @@ -815,7 +855,7 @@ def test_compile_hourly_sum_statistics_negative_state( attributes = { "device_class": device_class, "state_class": state_class, - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } seq = [15, 16, 15, 16, 20, -20, 20, 10] @@ -843,11 +883,12 @@ def test_compile_hourly_sum_statistics_negative_state( statistic_ids = list_statistic_ids(hass) assert { "name": None, + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "source": "recorder", "statistic_id": entity_id, - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, } in statistic_ids stats = statistics_during_period(hass, zero, period="5minute") assert stats[entity_id] == [ @@ -875,18 +916,24 @@ def test_compile_hourly_sum_statistics_negative_state( @pytest.mark.parametrize( - "device_class,unit,native_unit,factor", + "device_class,state_unit,display_unit,statistics_unit,factor", [ - ("energy", "kWh", "kWh", 1), - ("energy", "Wh", "kWh", 1 / 1000), - ("monetary", "EUR", "EUR", 1), - ("monetary", "SEK", "SEK", 1), - ("gas", "m³", "m³", 1), - ("gas", "ft³", "m³", 0.0283168466), + ("energy", "kWh", "kWh", "kWh", 1), + ("energy", "Wh", "kWh", "kWh", 1 / 1000), + ("monetary", "EUR", "EUR", "EUR", 1), + ("monetary", "SEK", "SEK", "SEK", 1), + ("gas", "m³", "m³", "m³", 1), + ("gas", "ft³", "m³", "m³", 0.0283168466), ], ) def test_compile_hourly_sum_statistics_total_no_reset( - hass_recorder, caplog, device_class, unit, native_unit, factor + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistics_unit, + factor, ): """Test compiling hourly statistics.""" period0 = dt_util.utcnow() @@ -899,7 +946,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( attributes = { "device_class": device_class, "state_class": "total", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] @@ -922,11 +969,12 @@ def test_compile_hourly_sum_statistics_total_no_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -971,16 +1019,22 @@ def test_compile_hourly_sum_statistics_total_no_reset( @pytest.mark.parametrize( - "device_class,unit,native_unit,factor", + "device_class,state_unit,display_unit,statistics_unit,factor", [ - ("energy", "kWh", "kWh", 1), - ("energy", "Wh", "kWh", 1 / 1000), - ("gas", "m³", "m³", 1), - ("gas", "ft³", "m³", 0.0283168466), + ("energy", "kWh", "kWh", "kWh", 1), + ("energy", "Wh", "kWh", "kWh", 1 / 1000), + ("gas", "m³", "m³", "m³", 1), + ("gas", "ft³", "m³", "m³", 0.0283168466), ], ) def test_compile_hourly_sum_statistics_total_increasing( - hass_recorder, caplog, device_class, unit, native_unit, factor + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistics_unit, + factor, ): """Test compiling hourly statistics.""" period0 = dt_util.utcnow() @@ -993,7 +1047,7 @@ def test_compile_hourly_sum_statistics_total_increasing( attributes = { "device_class": device_class, "state_class": "total_increasing", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] @@ -1016,11 +1070,12 @@ def test_compile_hourly_sum_statistics_total_increasing( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -1068,11 +1123,17 @@ def test_compile_hourly_sum_statistics_total_increasing( @pytest.mark.parametrize( - "device_class,unit,native_unit,factor", - [("energy", "kWh", "kWh", 1)], + "device_class,state_unit,display_unit,statistics_unit,factor", + [("energy", "kWh", "kWh", "kWh", 1)], ) def test_compile_hourly_sum_statistics_total_increasing_small_dip( - hass_recorder, caplog, device_class, unit, native_unit, factor + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistics_unit, + factor, ): """Test small dips in sensor readings do not trigger a reset.""" period0 = dt_util.utcnow() @@ -1085,7 +1146,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( attributes = { "device_class": device_class, "state_class": "total_increasing", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } seq = [10, 15, 20, 19, 30, 40, 39, 60, 70] @@ -1121,11 +1182,12 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -1214,11 +1276,12 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "kWh", } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -1305,27 +1368,30 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "kWh", }, { "statistic_id": "sensor.test2", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "kWh", }, { "statistic_id": "sensor.test3", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "kWh", }, ] stats = statistics_during_period(hass, period0, period="5minute") @@ -1440,7 +1506,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): @pytest.mark.parametrize( - "device_class,unit,value", + "device_class,state_unit,value", [ ("battery", "%", 30), ("battery", None, 30), @@ -1456,7 +1522,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): ], ) def test_compile_hourly_statistics_unchanged( - hass_recorder, caplog, device_class, unit, value + hass_recorder, caplog, device_class, state_unit, value ): """Test compiling hourly statistics, with no changes during the hour.""" zero = dt_util.utcnow() @@ -1466,7 +1532,7 @@ def test_compile_hourly_statistics_unchanged( attributes = { "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) hist = history.get_significant_states(hass, zero, four) @@ -1527,7 +1593,7 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): @pytest.mark.parametrize( - "device_class,unit,value", + "device_class,state_unit,value", [ ("battery", "%", 30), ("battery", None, 30), @@ -1543,7 +1609,7 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): ], ) def test_compile_hourly_statistics_unavailable( - hass_recorder, caplog, device_class, unit, value + hass_recorder, caplog, device_class, state_unit, value ): """Test compiling hourly statistics, with the sensor being unavailable.""" zero = dt_util.utcnow() @@ -1553,7 +1619,7 @@ def test_compile_hourly_statistics_unavailable( attributes = { "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } four, states = record_states_partially_unavailable( hass, zero, "sensor.test1", attributes @@ -1600,35 +1666,42 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): @pytest.mark.parametrize( - "state_class,device_class,unit,native_unit,statistic_type", + "state_class,device_class,state_unit,display_unit,statistics_unit,statistic_type", [ - ("measurement", "battery", "%", "%", "mean"), - ("measurement", "battery", None, None, "mean"), - ("total", "energy", "Wh", "kWh", "sum"), - ("total", "energy", "kWh", "kWh", "sum"), - ("measurement", "energy", "Wh", "kWh", "mean"), - ("measurement", "energy", "kWh", "kWh", "mean"), - ("measurement", "humidity", "%", "%", "mean"), - ("measurement", "humidity", None, None, "mean"), - ("total", "monetary", "USD", "USD", "sum"), - ("total", "monetary", "None", "None", "sum"), - ("total", "gas", "m³", "m³", "sum"), - ("total", "gas", "ft³", "m³", "sum"), - ("measurement", "monetary", "USD", "USD", "mean"), - ("measurement", "monetary", "None", "None", "mean"), - ("measurement", "gas", "m³", "m³", "mean"), - ("measurement", "gas", "ft³", "m³", "mean"), - ("measurement", "pressure", "Pa", "Pa", "mean"), - ("measurement", "pressure", "hPa", "Pa", "mean"), - ("measurement", "pressure", "mbar", "Pa", "mean"), - ("measurement", "pressure", "inHg", "Pa", "mean"), - ("measurement", "pressure", "psi", "Pa", "mean"), - ("measurement", "temperature", "°C", "°C", "mean"), - ("measurement", "temperature", "°F", "°C", "mean"), + ("measurement", "battery", "%", "%", "%", "mean"), + ("measurement", "battery", None, None, None, "mean"), + ("total", "energy", "Wh", "kWh", "kWh", "sum"), + ("total", "energy", "kWh", "kWh", "kWh", "sum"), + ("measurement", "energy", "Wh", "kWh", "kWh", "mean"), + ("measurement", "energy", "kWh", "kWh", "kWh", "mean"), + ("measurement", "humidity", "%", "%", "%", "mean"), + ("measurement", "humidity", None, None, None, "mean"), + ("total", "monetary", "USD", "USD", "USD", "sum"), + ("total", "monetary", "None", "None", "None", "sum"), + ("total", "gas", "m³", "m³", "m³", "sum"), + ("total", "gas", "ft³", "m³", "m³", "sum"), + ("measurement", "monetary", "USD", "USD", "USD", "mean"), + ("measurement", "monetary", "None", "None", "None", "mean"), + ("measurement", "gas", "m³", "m³", "m³", "mean"), + ("measurement", "gas", "ft³", "m³", "m³", "mean"), + ("measurement", "pressure", "Pa", "Pa", "Pa", "mean"), + ("measurement", "pressure", "hPa", "Pa", "Pa", "mean"), + ("measurement", "pressure", "mbar", "Pa", "Pa", "mean"), + ("measurement", "pressure", "inHg", "Pa", "Pa", "mean"), + ("measurement", "pressure", "psi", "Pa", "Pa", "mean"), + ("measurement", "temperature", "°C", "°C", "°C", "mean"), + ("measurement", "temperature", "°F", "°C", "°C", "mean"), ], ) def test_list_statistic_ids( - hass_recorder, caplog, state_class, device_class, unit, native_unit, statistic_type + hass_recorder, + caplog, + state_class, + device_class, + state_unit, + display_unit, + statistics_unit, + statistic_type, ): """Test listing future statistic ids.""" hass = hass_recorder() @@ -1638,18 +1711,19 @@ def test_list_statistic_ids( "device_class": device_class, "last_reset": 0, "state_class": state_class, - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } hass.states.set("sensor.test1", 0, attributes=attributes) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, }, ] for stat_type in ["mean", "sum", "dogs"]: @@ -1658,11 +1732,12 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, }, ] else: @@ -1697,16 +1772,24 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): @pytest.mark.parametrize( - "device_class,unit,native_unit,mean,min,max", + "device_class,state_unit,display_unit,statistics_unit,mean,min,max", [ - (None, None, None, 13.050847, -10, 30), - (None, "%", "%", 13.050847, -10, 30), - ("battery", "%", "%", 13.050847, -10, 30), - ("battery", None, None, 13.050847, -10, 30), + (None, None, None, None, 13.050847, -10, 30), + (None, "%", "%", "%", 13.050847, -10, 30), + ("battery", "%", "%", "%", 13.050847, -10, 30), + ("battery", None, None, None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_1( - hass_recorder, caplog, device_class, unit, native_unit, mean, min, max + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistics_unit, + mean, + min, + max, ): """Test compiling hourly statistics where units change from one hour to the next.""" zero = dt_util.utcnow() @@ -1716,7 +1799,7 @@ def test_compile_hourly_statistics_changing_units_1( attributes = { "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) attributes["unit_of_measurement"] = "cats" @@ -1738,11 +1821,12 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -1766,17 +1850,18 @@ def test_compile_hourly_statistics_changing_units_1( wait_recording_done(hass) assert ( "The unit of sensor.test1 (cats) does not match the unit of already compiled " - f"statistics ({native_unit})" in caplog.text + f"statistics ({display_unit})" in caplog.text ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -1799,16 +1884,24 @@ def test_compile_hourly_statistics_changing_units_1( @pytest.mark.parametrize( - "device_class,unit,native_unit,mean,min,max", + "device_class,state_unit,display_unit,statistics_unit,mean,min,max", [ - (None, None, None, 13.050847, -10, 30), - (None, "%", "%", 13.050847, -10, 30), - ("battery", "%", "%", 13.050847, -10, 30), - ("battery", None, None, 13.050847, -10, 30), + (None, None, None, None, 13.050847, -10, 30), + (None, "%", "%", "%", 13.050847, -10, 30), + ("battery", "%", "%", "%", 13.050847, -10, 30), + ("battery", None, None, None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_2( - hass_recorder, caplog, device_class, unit, native_unit, mean, min, max + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistics_unit, + mean, + min, + max, ): """Test compiling hourly statistics where units change during an hour.""" zero = dt_util.utcnow() @@ -1818,7 +1911,7 @@ def test_compile_hourly_statistics_changing_units_2( attributes = { "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) attributes["unit_of_measurement"] = "cats" @@ -1837,11 +1930,12 @@ def test_compile_hourly_statistics_changing_units_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "cats", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": "cats", + "statistics_unit_of_measurement": "cats", }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -1851,16 +1945,24 @@ def test_compile_hourly_statistics_changing_units_2( @pytest.mark.parametrize( - "device_class,unit,native_unit,mean,min,max", + "device_class,state_unit,display_unit,statistics_unit,mean,min,max", [ - (None, None, None, 13.050847, -10, 30), - (None, "%", "%", 13.050847, -10, 30), - ("battery", "%", "%", 13.050847, -10, 30), - ("battery", None, None, 13.050847, -10, 30), + (None, None, None, None, 13.050847, -10, 30), + (None, "%", "%", "%", 13.050847, -10, 30), + ("battery", "%", "%", "%", 13.050847, -10, 30), + ("battery", None, None, None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_3( - hass_recorder, caplog, device_class, unit, native_unit, mean, min, max + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistics_unit, + mean, + min, + max, ): """Test compiling hourly statistics where units change from one hour to the next.""" zero = dt_util.utcnow() @@ -1870,7 +1972,7 @@ def test_compile_hourly_statistics_changing_units_3( attributes = { "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) four, _states = record_states( @@ -1892,11 +1994,12 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -1919,16 +2022,19 @@ def test_compile_hourly_statistics_changing_units_3( do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) assert "The unit of sensor.test1 is changing" in caplog.text - assert f"matches the unit of already compiled statistics ({unit})" in caplog.text + assert ( + f"matches the unit of already compiled statistics ({state_unit})" in caplog.text + ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": native_unit, + "statistics_unit_of_measurement": statistics_unit, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -1979,11 +2085,12 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": state_unit, + "statistics_unit_of_measurement": state_unit, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2027,11 +2134,12 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": state_unit, + "statistics_unit_of_measurement": state_unit, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2083,11 +2191,12 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": statistic_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": statistic_unit, + "statistics_unit_of_measurement": statistic_unit, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2131,11 +2240,12 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": statistic_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": statistic_unit, + "statistics_unit_of_measurement": statistic_unit, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2158,13 +2268,21 @@ def test_compile_hourly_statistics_changing_device_class_2( @pytest.mark.parametrize( - "device_class,unit,native_unit,mean,min,max", + "device_class,state_unit,display_unit,statistics_unit,mean,min,max", [ - (None, None, None, 13.050847, -10, 30), + (None, None, None, None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_statistics( - hass_recorder, caplog, device_class, unit, native_unit, mean, min, max + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistics_unit, + mean, + min, + max, ): """Test compiling hourly statistics where units change during an hour.""" period0 = dt_util.utcnow() @@ -2176,12 +2294,12 @@ def test_compile_hourly_statistics_changing_statistics( attributes_1 = { "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } attributes_2 = { "device_class": device_class, "state_class": "total_increasing", - "unit_of_measurement": unit, + "unit_of_measurement": state_unit, } four, states = record_states(hass, period0, "sensor.test1", attributes_1) do_adhoc_statistics(hass, start=period0) @@ -2190,11 +2308,12 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": None, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": None, + "statistics_unit_of_measurement": None, }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) @@ -2224,11 +2343,12 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": None, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": None, + "statistics_unit_of_measurement": None, }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) @@ -2416,35 +2536,39 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": "%", + "statistics_unit_of_measurement": "%", }, { "statistic_id": "sensor.test2", + "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": "%", + "statistics_unit_of_measurement": "%", }, { "statistic_id": "sensor.test3", + "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", - "unit_of_measurement": "%", + "statistics_unit_of_measurement": "%", }, { "statistic_id": "sensor.test4", + "display_unit_of_measurement": "EUR", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", - "unit_of_measurement": "EUR", + "statistics_unit_of_measurement": "EUR", }, ] From ee6ffb1be4c0f1eb7b7b7181c33e5b56dcf6a7d9 Mon Sep 17 00:00:00 2001 From: likeablob <46628917+likeablob@users.noreply.github.com> Date: Wed, 31 Aug 2022 19:43:50 +0900 Subject: [PATCH 797/903] Fix `feedreader` component to keep the last entry timestamp up to date (#77547) Fix feedreader to keep the last entry timestamp up to date - Use `updated` date in precedence over `published` date to update `last_entry_timestamp` in the case a feed entry has both updated date and published date. --- .../components/feedreader/__init__.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 0ee4d3c39f3..50404bb96dc 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -156,26 +156,27 @@ class FeedManager: def _update_and_fire_entry(self, entry: feedparser.FeedParserDict) -> None: """Update last_entry_timestamp and fire entry.""" - # Check if the entry has a published or updated date. - if "published_parsed" in entry and entry.published_parsed: - # We are lucky, `published_parsed` data available, let's make use of - # it to publish only new available entries since the last run - self._has_published_parsed = True - self._last_entry_timestamp = max( - entry.published_parsed, self._last_entry_timestamp - ) - elif "updated_parsed" in entry and entry.updated_parsed: + # Check if the entry has a updated or published date. + # Start from a updated date because generally `updated` > `published`. + if "updated_parsed" in entry and entry.updated_parsed: # We are lucky, `updated_parsed` data available, let's make use of # it to publish only new available entries since the last run self._has_updated_parsed = True self._last_entry_timestamp = max( entry.updated_parsed, self._last_entry_timestamp ) + elif "published_parsed" in entry and entry.published_parsed: + # We are lucky, `published_parsed` data available, let's make use of + # it to publish only new available entries since the last run + self._has_published_parsed = True + self._last_entry_timestamp = max( + entry.published_parsed, self._last_entry_timestamp + ) else: - self._has_published_parsed = False self._has_updated_parsed = False + self._has_published_parsed = False _LOGGER.debug( - "No published_parsed or updated_parsed info available for entry %s", + "No updated_parsed or published_parsed info available for entry %s", entry, ) entry.update({"feed_url": self._url}) From 105bb3e08264c753012f10fd35f8358c8683646d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 31 Aug 2022 12:51:39 +0200 Subject: [PATCH 798/903] Ecowitt integration (#77441) * Add ecowitt integration * add tests * use total * use total * test coverage * Update homeassistant/components/ecowitt/__init__.py Co-authored-by: Paulus Schoutsen * Update homeassistant/components/ecowitt/binary_sensor.py Co-authored-by: Paulus Schoutsen * Update homeassistant/components/ecowitt/entity.py Co-authored-by: Paulus Schoutsen * Update homeassistant/components/ecowitt/diagnostics.py Co-authored-by: Paulus Schoutsen * add to async_on_unload * remove attr_name / unload callback * support unload platforms * using replace * address mapping * update type * mark final * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Fix bracket * Fix another bracket * Address comment * Add strings * update tests * Update homeassistant/components/ecowitt/strings.json Co-authored-by: Martin Hjelmare * update text * Update homeassistant/components/ecowitt/strings.json Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare --- .coveragerc | 7 +- CODEOWNERS | 2 + homeassistant/components/ecowitt/__init__.py | 47 ++++ .../components/ecowitt/binary_sensor.py | 71 +++++ .../components/ecowitt/config_flow.py | 79 ++++++ homeassistant/components/ecowitt/const.py | 7 + .../components/ecowitt/diagnostics.py | 39 +++ homeassistant/components/ecowitt/entity.py | 46 ++++ .../components/ecowitt/manifest.json | 9 + homeassistant/components/ecowitt/sensor.py | 247 ++++++++++++++++++ homeassistant/components/ecowitt/strings.json | 17 ++ .../components/ecowitt/translations/en.json | 17 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ecowitt/__init__.py | 1 + tests/components/ecowitt/test_config_flow.py | 110 ++++++++ 17 files changed, 705 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/ecowitt/__init__.py create mode 100644 homeassistant/components/ecowitt/binary_sensor.py create mode 100644 homeassistant/components/ecowitt/config_flow.py create mode 100644 homeassistant/components/ecowitt/const.py create mode 100644 homeassistant/components/ecowitt/diagnostics.py create mode 100644 homeassistant/components/ecowitt/entity.py create mode 100644 homeassistant/components/ecowitt/manifest.json create mode 100644 homeassistant/components/ecowitt/sensor.py create mode 100644 homeassistant/components/ecowitt/strings.json create mode 100644 homeassistant/components/ecowitt/translations/en.json create mode 100644 tests/components/ecowitt/__init__.py create mode 100644 tests/components/ecowitt/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index d6a27259871..3ff0d49965c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -260,6 +260,11 @@ omit = homeassistant/components/econet/const.py homeassistant/components/econet/sensor.py homeassistant/components/econet/water_heater.py + homeassistant/components/ecowitt/__init__.py + homeassistant/components/ecowitt/binary_sensor.py + homeassistant/components/ecowitt/diagnostics.py + homeassistant/components/ecowitt/entity.py + homeassistant/components/ecowitt/sensor.py homeassistant/components/ecovacs/* homeassistant/components/edl21/* homeassistant/components/eddystone_temperature/sensor.py @@ -394,7 +399,7 @@ omit = homeassistant/components/flock/notify.py homeassistant/components/flume/__init__.py homeassistant/components/flume/coordinator.py - homeassistant/components/flume/entity.py + homeassistant/components/flume/entity.py homeassistant/components/flume/sensor.py homeassistant/components/flunearyou/__init__.py homeassistant/components/flunearyou/repairs.py diff --git a/CODEOWNERS b/CODEOWNERS index 575a79d7afa..b135a418566 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -276,6 +276,8 @@ build.json @home-assistant/supervisor /homeassistant/components/econet/ @vangorra @w1ll1am23 /tests/components/econet/ @vangorra @w1ll1am23 /homeassistant/components/ecovacs/ @OverloadUT +/homeassistant/components/ecowitt/ @pvizeli +/tests/components/ecowitt/ @pvizeli /homeassistant/components/edl21/ @mtdcr /homeassistant/components/efergy/ @tkdrob /tests/components/efergy/ @tkdrob diff --git a/homeassistant/components/ecowitt/__init__.py b/homeassistant/components/ecowitt/__init__.py new file mode 100644 index 00000000000..71d42643cfb --- /dev/null +++ b/homeassistant/components/ecowitt/__init__.py @@ -0,0 +1,47 @@ +"""The Ecowitt Weather Station Component.""" +from __future__ import annotations + +from aioecowitt import EcoWittListener + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant + +from .const import CONF_PATH, DOMAIN + +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Ecowitt component from UI.""" + hass.data.setdefault(DOMAIN, {}) + + ecowitt = hass.data[DOMAIN][entry.entry_id] = EcoWittListener( + port=entry.data[CONF_PORT], path=entry.data[CONF_PATH] + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + await ecowitt.start() + + # Close on shutdown + async def _stop_ecowitt(_: Event): + """Stop the Ecowitt listener.""" + await ecowitt.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_ecowitt) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + ecowitt = hass.data[DOMAIN][entry.entry_id] + await ecowitt.stop() + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/ecowitt/binary_sensor.py b/homeassistant/components/ecowitt/binary_sensor.py new file mode 100644 index 00000000000..e487009d74b --- /dev/null +++ b/homeassistant/components/ecowitt/binary_sensor.py @@ -0,0 +1,71 @@ +"""Support for Ecowitt Weather Stations.""" +import dataclasses +from typing import Final + +from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import EcowittEntity + +ECOWITT_BINARYSENSORS_MAPPING: Final = { + EcoWittSensorTypes.LEAK: BinarySensorEntityDescription( + key="LEAK", device_class=BinarySensorDeviceClass.MOISTURE + ), + EcoWittSensorTypes.BATTERY_BINARY: BinarySensorEntityDescription( + key="BATTERY", device_class=BinarySensorDeviceClass.BATTERY + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add sensors if new.""" + ecowitt: EcoWittListener = hass.data[DOMAIN][entry.entry_id] + + def _new_sensor(sensor: EcoWittSensor) -> None: + """Add new sensor.""" + if sensor.stype not in ECOWITT_BINARYSENSORS_MAPPING: + return + mapping = ECOWITT_BINARYSENSORS_MAPPING[sensor.stype] + + # Setup sensor description + description = dataclasses.replace( + mapping, + key=sensor.key, + name=sensor.name, + ) + + async_add_entities([EcowittBinarySensorEntity(sensor, description)]) + + ecowitt.new_sensor_cb.append(_new_sensor) + entry.async_on_unload(lambda: ecowitt.new_sensor_cb.remove(_new_sensor)) + + # Add all sensors that are already known + for sensor in ecowitt.sensors.values(): + _new_sensor(sensor) + + +class EcowittBinarySensorEntity(EcowittEntity, BinarySensorEntity): + """Representation of a Ecowitt BinarySensor.""" + + def __init__( + self, sensor: EcoWittSensor, description: BinarySensorEntityDescription + ) -> None: + """Initialize the sensor.""" + super().__init__(sensor) + self.entity_description = description + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.ecowitt.value > 0 diff --git a/homeassistant/components/ecowitt/config_flow.py b/homeassistant/components/ecowitt/config_flow.py new file mode 100644 index 00000000000..c3406652665 --- /dev/null +++ b/homeassistant/components/ecowitt/config_flow.py @@ -0,0 +1,79 @@ +"""Config flow for ecowitt.""" +from __future__ import annotations + +import logging +import secrets +from typing import Any + +from aioecowitt import EcoWittListener +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv + +from .const import CONF_PATH, DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PATH, default=f"/{secrets.token_urlsafe(16)}"): cv.string, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: + """Validate user input.""" + # Check if the port is in use + try: + listener = EcoWittListener(port=data[CONF_PORT]) + await listener.start() + await listener.stop() + except OSError: + raise InvalidPort from None + + return {"title": f"Ecowitt on port {data[CONF_PORT]}"} + + +class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for the Ecowitt.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + # Check if the port is in use by another config entry + self._async_abort_entries_match({CONF_PORT: user_input[CONF_PORT]}) + + try: + info = await validate_input(self.hass, user_input) + except InvalidPort: + errors["base"] = "invalid_port" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class InvalidPort(HomeAssistantError): + """Error to indicate there port is not usable.""" diff --git a/homeassistant/components/ecowitt/const.py b/homeassistant/components/ecowitt/const.py new file mode 100644 index 00000000000..a7011f24e0c --- /dev/null +++ b/homeassistant/components/ecowitt/const.py @@ -0,0 +1,7 @@ +"""Constants used by ecowitt component.""" + +DOMAIN = "ecowitt" + +DEFAULT_PORT = 49199 + +CONF_PATH = "path" diff --git a/homeassistant/components/ecowitt/diagnostics.py b/homeassistant/components/ecowitt/diagnostics.py new file mode 100644 index 00000000000..d02a5dadbcc --- /dev/null +++ b/homeassistant/components/ecowitt/diagnostics.py @@ -0,0 +1,39 @@ +"""Provides diagnostics for EcoWitt.""" +from __future__ import annotations + +from typing import Any + +from aioecowitt import EcoWittListener + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import DOMAIN + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + ecowitt: EcoWittListener = hass.data[DOMAIN][entry.entry_id] + station_id = next(item[1] for item in device.identifiers if item[0] == DOMAIN) + + station = ecowitt.stations[station_id] + + data = { + "device": { + "name": station.station, + "model": station.model, + "frequency": station.frequency, + "version": station.version, + }, + "raw": ecowitt.last_values[station_id], + "sensors": { + sensor.key: sensor.value + for sensor in station.sensors + if sensor.station.key == station_id + }, + } + + return data diff --git a/homeassistant/components/ecowitt/entity.py b/homeassistant/components/ecowitt/entity.py new file mode 100644 index 00000000000..ca5e14b6d7b --- /dev/null +++ b/homeassistant/components/ecowitt/entity.py @@ -0,0 +1,46 @@ +"""The Ecowitt Weather Station Entity.""" +from __future__ import annotations + +import time + +from aioecowitt import EcoWittSensor + +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN + + +class EcowittEntity(Entity): + """Base class for Ecowitt Weather Station.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__(self, sensor: EcoWittSensor) -> None: + """Construct the entity.""" + self.ecowitt: EcoWittSensor = sensor + + self._attr_unique_id = f"{sensor.station.key}-{sensor.key}" + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, sensor.station.key), + }, + name=sensor.station.station, + model=sensor.station.model, + sw_version=sensor.station.version, + ) + + async def async_added_to_hass(self): + """Install listener for updates later.""" + + def _update_state(): + """Update the state on callback.""" + self.async_write_ha_state() + + self.ecowitt.update_cb.append(_update_state) + self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state)) + + @property + def available(self) -> bool: + """Return whether the state is based on actual reading from device.""" + return (self.ecowitt.last_update_m + 5 * 60) > time.monotonic() diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json new file mode 100644 index 00000000000..cafd140828e --- /dev/null +++ b/homeassistant/components/ecowitt/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ecowitt", + "name": "Ecowitt Weather Station", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ecowitt", + "requirements": ["aioecowitt==2022.08.3"], + "codeowners": ["@pvizeli"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py new file mode 100644 index 00000000000..843dc700dc0 --- /dev/null +++ b/homeassistant/components/ecowitt/sensor.py @@ -0,0 +1,247 @@ +"""Support for Ecowitt Weather Stations.""" +import dataclasses +from typing import Final + +from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + AREA_SQUARE_METERS, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + DEGREE, + ELECTRIC_POTENTIAL_VOLT, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + LIGHT_LUX, + PERCENTAGE, + POWER_WATT, + PRECIPITATION_INCHES_PER_HOUR, + PRECIPITATION_MILLIMETERS_PER_HOUR, + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + UV_INDEX, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .entity import EcowittEntity + +_METRIC: Final = ( + EcoWittSensorTypes.TEMPERATURE_C, + EcoWittSensorTypes.RAIN_COUNT_MM, + EcoWittSensorTypes.RAIN_RATE_MM, + EcoWittSensorTypes.LIGHTNING_DISTANCE_KM, + EcoWittSensorTypes.SPEED_KPH, + EcoWittSensorTypes.PRESSURE_HPA, +) +_IMPERIAL: Final = ( + EcoWittSensorTypes.TEMPERATURE_F, + EcoWittSensorTypes.RAIN_COUNT_INCHES, + EcoWittSensorTypes.RAIN_RATE_INCHES, + EcoWittSensorTypes.LIGHTNING_DISTANCE_MILES, + EcoWittSensorTypes.SPEED_MPH, + EcoWittSensorTypes.PRESSURE_INHG, +) + + +ECOWITT_SENSORS_MAPPING: Final = { + EcoWittSensorTypes.HUMIDITY: SensorEntityDescription( + key="HUMIDITY", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.DEGREE: SensorEntityDescription( + key="DEGREE", native_unit_of_measurement=DEGREE + ), + EcoWittSensorTypes.WATT_METERS_SQUARED: SensorEntityDescription( + key="WATT_METERS_SQUARED", + native_unit_of_measurement=f"{POWER_WATT}/{AREA_SQUARE_METERS}", + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.UV_INDEX: SensorEntityDescription( + key="UV_INDEX", + native_unit_of_measurement=UV_INDEX, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.PM25: SensorEntityDescription( + key="PM25", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.PM10: SensorEntityDescription( + key="PM10", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.BATTERY_PERCENTAGE: SensorEntityDescription( + key="BATTERY_PERCENTAGE", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.BATTERY_VOLTAGE: SensorEntityDescription( + key="BATTERY_VOLTAGE", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.CO2_PPM: SensorEntityDescription( + key="CO2_PPM", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.LUX: SensorEntityDescription( + key="LUX", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.TIMESTAMP: SensorEntityDescription( + key="TIMESTAMP", device_class=SensorDeviceClass.TIMESTAMP + ), + EcoWittSensorTypes.VOLTAGE: SensorEntityDescription( + key="VOLTAGE", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.LIGHTNING_COUNT: SensorEntityDescription( + key="LIGHTNING_COUNT", + native_unit_of_measurement="strikes", + state_class=SensorStateClass.TOTAL, + ), + EcoWittSensorTypes.TEMPERATURE_C: SensorEntityDescription( + key="TEMPERATURE_C", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.TEMPERATURE_F: SensorEntityDescription( + key="TEMPERATURE_F", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.RAIN_COUNT_MM: SensorEntityDescription( + key="RAIN_COUNT_MM", + native_unit_of_measurement=LENGTH_MILLIMETERS, + state_class=SensorStateClass.TOTAL, + ), + EcoWittSensorTypes.RAIN_COUNT_INCHES: SensorEntityDescription( + key="RAIN_COUNT_INCHES", + native_unit_of_measurement=LENGTH_INCHES, + state_class=SensorStateClass.TOTAL, + ), + EcoWittSensorTypes.RAIN_RATE_MM: SensorEntityDescription( + key="RAIN_RATE_MM", + native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.RAIN_RATE_INCHES: SensorEntityDescription( + key="RAIN_RATE_INCHES", + native_unit_of_measurement=PRECIPITATION_INCHES_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.LIGHTNING_DISTANCE_KM: SensorEntityDescription( + key="LIGHTNING_DISTANCE_KM", + native_unit_of_measurement=LENGTH_KILOMETERS, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.LIGHTNING_DISTANCE_MILES: SensorEntityDescription( + key="LIGHTNING_DISTANCE_MILES", + native_unit_of_measurement=LENGTH_MILES, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.SPEED_KPH: SensorEntityDescription( + key="SPEED_KPH", + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.SPEED_MPH: SensorEntityDescription( + key="SPEED_MPH", + native_unit_of_measurement=SPEED_MILES_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.PRESSURE_HPA: SensorEntityDescription( + key="PRESSURE_HPA", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + ), + EcoWittSensorTypes.PRESSURE_INHG: SensorEntityDescription( + key="PRESSURE_INHG", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_INHG, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add sensors if new.""" + ecowitt: EcoWittListener = hass.data[DOMAIN][entry.entry_id] + + def _new_sensor(sensor: EcoWittSensor) -> None: + """Add new sensor.""" + if sensor.stype not in ECOWITT_SENSORS_MAPPING: + return + + # Ignore metrics that are not supported by the user's locale + if sensor.stype in _METRIC and not hass.config.units.is_metric: + return + if sensor.stype in _IMPERIAL and hass.config.units.is_metric: + return + mapping = ECOWITT_SENSORS_MAPPING[sensor.stype] + + # Setup sensor description + description = dataclasses.replace( + mapping, + key=sensor.key, + name=sensor.name, + ) + + async_add_entities([EcowittSensorEntity(sensor, description)]) + + ecowitt.new_sensor_cb.append(_new_sensor) + entry.async_on_unload(lambda: ecowitt.new_sensor_cb.remove(_new_sensor)) + + # Add all sensors that are already known + for sensor in ecowitt.sensors.values(): + _new_sensor(sensor) + + +class EcowittSensorEntity(EcowittEntity, SensorEntity): + """Representation of a Ecowitt Sensor.""" + + def __init__( + self, sensor: EcoWittSensor, description: SensorEntityDescription + ) -> None: + """Initialize the sensor.""" + super().__init__(sensor) + self.entity_description = description + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.ecowitt.value diff --git a/homeassistant/components/ecowitt/strings.json b/homeassistant/components/ecowitt/strings.json new file mode 100644 index 00000000000..a4e12e69d57 --- /dev/null +++ b/homeassistant/components/ecowitt/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_port": "Port is already used.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "description": "The following steps must be performed to set up this integration.\n\nUse the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\nPick your station -> Menu Others -> DIY Upload Servers.\nHit next and select 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath have to match, you can copy with secure token /.\nSave configuration. The Ecowitt should then start attempting to send data to your server.", + "data": { + "port": "Listening port", + "path": "Path with Security token" + } + } + } + } +} diff --git a/homeassistant/components/ecowitt/translations/en.json b/homeassistant/components/ecowitt/translations/en.json new file mode 100644 index 00000000000..041fa9f697b --- /dev/null +++ b/homeassistant/components/ecowitt/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_port": "Port is already used.", + "unknown": "Unknown error." + }, + "step": { + "user": { + "description": "The following steps must be performed to set up this integration.\n\nUse the Ecowitt App (on your phone) or your Ecowitt WebUI over the station IP address.\nPick your station -> Menu Others -> DIY Upload Servers.\nHit next and select 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath have to match, you can copy with secure token /.\nSave configuration. The Ecowitt should then start attempting to send data to your server.", + "data": { + "port": "Listening port", + "path": "Path with Security token" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 127beed575e..c5437e14562 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -90,6 +90,7 @@ FLOWS = { "eafm", "ecobee", "econet", + "ecowitt", "efergy", "eight_sleep", "elgato", diff --git a/requirements_all.txt b/requirements_all.txt index 48819ec8bf2..7c299ce0893 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -146,6 +146,9 @@ aioeafm==0.1.2 # homeassistant.components.rainforest_eagle aioeagle==1.1.0 +# homeassistant.components.ecowitt +aioecowitt==2022.08.3 + # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3b460e8cf3..6a74fba107c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,6 +133,9 @@ aioeafm==0.1.2 # homeassistant.components.rainforest_eagle aioeagle==1.1.0 +# homeassistant.components.ecowitt +aioecowitt==2022.08.3 + # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/tests/components/ecowitt/__init__.py b/tests/components/ecowitt/__init__.py new file mode 100644 index 00000000000..58e4a991df1 --- /dev/null +++ b/tests/components/ecowitt/__init__.py @@ -0,0 +1 @@ +"""Ecowitt tests.""" diff --git a/tests/components/ecowitt/test_config_flow.py b/tests/components/ecowitt/test_config_flow.py new file mode 100644 index 00000000000..6ddd475121b --- /dev/null +++ b/tests/components/ecowitt/test_config_flow.py @@ -0,0 +1,110 @@ +"""Test the Ecowitt Weather Station config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.ecowitt.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_create_entry(hass: HomeAssistant) -> None: + """Test we can create a config entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.ecowitt.config_flow.EcoWittListener.start" + ), patch( + "homeassistant.components.ecowitt.config_flow.EcoWittListener.stop" + ), patch( + "homeassistant.components.ecowitt.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "port": 49911, + "path": "/ecowitt-station", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Ecowitt on port 49911" + assert result2["data"] == { + "port": 49911, + "path": "/ecowitt-station", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_port(hass: HomeAssistant) -> None: + """Test we handle invalid port.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ecowitt.config_flow.EcoWittListener.start", + side_effect=OSError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "port": 49911, + "path": "/ecowitt-station", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_port"} + + +async def test_already_configured_port(hass: HomeAssistant) -> None: + """Test already configured port.""" + MockConfigEntry(domain=DOMAIN, data={"port": 49911}).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ecowitt.config_flow.EcoWittListener.start", + side_effect=OSError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "port": 49911, + "path": "/ecowitt-station", + }, + ) + + assert result2["type"] == FlowResultType.ABORT + + +async def test_unknown_error(hass: HomeAssistant) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ecowitt.config_flow.EcoWittListener.start", + side_effect=Exception(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "port": 49911, + "path": "/ecowitt-station", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} From b303c8e0402b7c647532fe946f6bd5bbbf4edfdc Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Wed, 31 Aug 2022 13:52:52 +0300 Subject: [PATCH 799/903] Refactor version key in `glances` (#77541) * update version key * Fix merge mistake Co-authored-by: Erik Montnemery --- homeassistant/components/glances/config_flow.py | 10 +--------- tests/components/glances/test_config_flow.py | 16 ---------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index 568586f177b..a56fa795491 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -36,7 +36,7 @@ DATA_SCHEMA = vol.Schema( vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, vol.Required(CONF_PORT, default=DEFAULT_PORT): int, - vol.Required(CONF_VERSION, default=DEFAULT_VERSION): int, + vol.Required(CONF_VERSION, default=DEFAULT_VERSION): vol.In(SUPPORTED_VERSIONS), vol.Optional(CONF_SSL, default=False): bool, vol.Optional(CONF_VERIFY_SSL, default=False): bool, } @@ -45,8 +45,6 @@ DATA_SCHEMA = vol.Schema( async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - if data[CONF_VERSION] not in SUPPORTED_VERSIONS: - raise WrongVersion try: api = get_api(hass, data) await api.get_data("all") @@ -81,8 +79,6 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) except CannotConnect: errors["base"] = "cannot_connect" - except WrongVersion: - errors[CONF_VERSION] = "wrong_version" return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors @@ -117,7 +113,3 @@ class GlancesOptionsFlowHandler(config_entries.OptionsFlow): class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" - - -class WrongVersion(exceptions.HomeAssistantError): - """Error to indicate the selected version is wrong.""" diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 7b2dee6429e..40e40b45e11 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -75,22 +75,6 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "cannot_connect"} -async def test_form_wrong_version(hass: HomeAssistant) -> None: - """Test to check if wrong version is entered.""" - - user_input = DEMO_USER_INPUT.copy() - user_input.update(version=1) - result = await hass.config_entries.flow.async_init( - glances.DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=user_input - ) - - assert result["type"] == "form" - assert result["errors"] == {"version": "wrong_version"} - - async def test_form_already_configured(hass: HomeAssistant) -> None: """Test host is already configured.""" entry = MockConfigEntry( From e5eddba22365e8b9481f71da219c8c56faf013cb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 31 Aug 2022 06:54:59 -0400 Subject: [PATCH 800/903] Litterrobot - Do not load a platform if there is no device supporting it (#77497) * Do not load button platform if no Litter Robot 3 * uno mas * uno mas * Do not load Vacuum if not needed * Use dict to map platforms for each model * uno mas --- .../components/litterrobot/__init__.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 5aa186d0171..d302989fc01 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,6 +1,8 @@ """The Litter-Robot integration.""" from __future__ import annotations +from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4 + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -16,6 +18,34 @@ PLATFORMS = [ Platform.VACUUM, ] +PLATFORMS_BY_TYPE = { + LitterRobot: ( + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + Platform.VACUUM, + ), + LitterRobot3: ( + Platform.BUTTON, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + Platform.VACUUM, + ), + LitterRobot4: ( + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + Platform.VACUUM, + ), + FeederRobot: ( + Platform.BUTTON, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + ), +} + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" @@ -23,8 +53,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) await hub.login(load_robots=True) - if any(hub.litter_robots()): - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + platforms: set[str] = set() + for robot in hub.account.robots: + platforms.update(PLATFORMS_BY_TYPE[type(robot)]) + if platforms: + await hass.config_entries.async_forward_entry_setups(entry, platforms) return True From f98e86d3a61d8bcc0867b7d29dd8bc2a1e06204a Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 31 Aug 2022 12:00:42 +0100 Subject: [PATCH 801/903] Bump pyipma to 3.0.2 (#76332) * upgrade to pyipma 3.0.0 * bump to support python3.9 * remove deprecated async_setup_platform * full coverage * add migrate --- homeassistant/components/ipma/config_flow.py | 7 + homeassistant/components/ipma/manifest.json | 2 +- homeassistant/components/ipma/weather.py | 133 ++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ipma/test_config_flow.py | 53 +++++- tests/components/ipma/test_weather.py | 177 +++++++++++-------- 7 files changed, 205 insertions(+), 171 deletions(-) diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index 81ab8f98014..17fde104125 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -18,6 +18,13 @@ class IpmaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Init IpmaFlowHandler.""" self._errors = {} + async def async_step_import(self, config): + """Import a configuration from config.yaml.""" + + self._async_abort_entries_match(config) + config[CONF_MODE] = "daily" + return await self.async_step_user(user_input=config) + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" self._errors = {} diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 902a03b6c83..a391b24e3b4 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==2.0.5"], + "requirements": ["pyipma==3.0.2"], "codeowners": ["@dgomes", "@abmantis"], "iot_class": "cloud_polling", "loggers": ["geopy", "pyipma"] diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 7a3a28b8bd0..d20e5cb2f21 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -6,10 +6,12 @@ import logging import async_timeout from pyipma.api import IPMA_API +from pyipma.forecast import Forecast from pyipma.location import Location import voluptuous as vol from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, ATTR_CONDITION_FOG, @@ -48,9 +50,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.sun import is_up from homeassistant.util import Throttle -from homeassistant.util.dt import now, parse_datetime _LOGGER = logging.getLogger(__name__) @@ -73,6 +74,7 @@ CONDITION_CLASSES = { ATTR_CONDITION_WINDY: [], ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], + ATTR_CONDITION_CLEAR_NIGHT: [-1], } FORECAST_MODE = ["hourly", "daily"] @@ -87,31 +89,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the ipma platform. - - Deprecated. - """ - _LOGGER.warning("Loading IPMA via platform config is deprecated") - - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - - if None in (latitude, longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return - - api = await async_get_api(hass) - location = await async_get_location(hass, api, latitude, longitude) - - async_add_entities([IPMAWeather(location, api, config)], True) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -180,21 +157,24 @@ class IPMAWeather(WeatherEntity): _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_attribution = ATTRIBUTION + def __init__(self, location: Location, api: IPMA_API, config): """Initialise the platform with a data instance and station name.""" self._api = api self._location_name = config.get(CONF_NAME, location.name) self._mode = config.get(CONF_MODE) + self._period = 1 if config.get(CONF_MODE) == "hourly" else 24 self._location = location self._observation = None - self._forecast = None + self._forecast: list[Forecast] = [] @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Update Condition and Forecast.""" async with async_timeout.timeout(10): new_observation = await self._location.observation(self._api) - new_forecast = await self._location.forecast(self._api) + new_forecast = await self._location.forecast(self._api, self._period) if new_observation: self._observation = new_observation @@ -207,8 +187,9 @@ class IPMAWeather(WeatherEntity): _LOGGER.warning("Could not update weather forecast") _LOGGER.debug( - "Updated location %s, observation %s", + "Updated location %s based on %s, current observation %s", self._location.name, + self._location.station, self._observation, ) @@ -217,30 +198,28 @@ class IPMAWeather(WeatherEntity): """Return a unique id.""" return f"{self._location.station_latitude}, {self._location.station_longitude}, {self._mode}" - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - @property def name(self): """Return the name of the station.""" return self._location_name + def _condition_conversion(self, identifier, forecast_dt): + """Convert from IPMA weather_type id to HA.""" + if identifier == 1 and not is_up(self.hass, forecast_dt): + identifier = -identifier + + return next( + (k for k, v in CONDITION_CLASSES.items() if identifier in v), + None, + ) + @property def condition(self): """Return the current condition.""" if not self._forecast: return - return next( - ( - k - for k, v in CONDITION_CLASSES.items() - if self._forecast[0].weather_type in v - ), - None, - ) + return self._condition_conversion(self._forecast[0].weather_type.id, None) @property def native_temperature(self): @@ -288,57 +267,17 @@ class IPMAWeather(WeatherEntity): if not self._forecast: return [] - if self._mode == "hourly": - forecast_filtered = [ - x - for x in self._forecast - if x.forecasted_hours == 1 - and parse_datetime(x.forecast_date) - > (now().utcnow() - timedelta(hours=1)) - ] - - fcdata_out = [ - { - ATTR_FORECAST_TIME: data_in.forecast_date, - ATTR_FORECAST_CONDITION: next( - ( - k - for k, v in CONDITION_CLASSES.items() - if int(data_in.weather_type) in v - ), - None, - ), - ATTR_FORECAST_NATIVE_TEMP: float(data_in.feels_like_temperature), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( - int(float(data_in.precipitation_probability)) - if int(float(data_in.precipitation_probability)) >= 0 - else None - ), - ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, - ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, - } - for data_in in forecast_filtered - ] - else: - forecast_filtered = [f for f in self._forecast if f.forecasted_hours == 24] - fcdata_out = [ - { - ATTR_FORECAST_TIME: data_in.forecast_date, - ATTR_FORECAST_CONDITION: next( - ( - k - for k, v in CONDITION_CLASSES.items() - if int(data_in.weather_type) in v - ), - None, - ), - ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.min_temperature, - ATTR_FORECAST_NATIVE_TEMP: data_in.max_temperature, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: data_in.precipitation_probability, - ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, - ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, - } - for data_in in forecast_filtered - ] - - return fcdata_out + return [ + { + ATTR_FORECAST_TIME: data_in.forecast_date, + ATTR_FORECAST_CONDITION: self._condition_conversion( + data_in.weather_type.id, data_in.forecast_date + ), + ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.min_temperature, + ATTR_FORECAST_NATIVE_TEMP: data_in.max_temperature, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: data_in.precipitation_probability, + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, + } + for data_in in self._forecast + ] diff --git a/requirements_all.txt b/requirements_all.txt index 7c299ce0893..f203f1bcca5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1602,7 +1602,7 @@ pyinsteon==1.2.0 pyintesishome==1.8.0 # homeassistant.components.ipma -pyipma==2.0.5 +pyipma==3.0.2 # homeassistant.components.ipp pyipp==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a74fba107c..f9efb70b5b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1118,7 +1118,7 @@ pyicloud==1.0.0 pyinsteon==1.2.0 # homeassistant.components.ipma -pyipma==2.0.5 +pyipma==3.0.2 # homeassistant.components.ipp pyipp==0.11.0 diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 8c96b9a01d8..c8d53f95a4a 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -2,8 +2,10 @@ from unittest.mock import Mock, patch +from homeassistant import config_entries from homeassistant.components.ipma import DOMAIN, config_flow -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -11,6 +13,13 @@ from .test_weather import MockLocation from tests.common import MockConfigEntry, mock_registry +ENTRY_CONFIG = { + CONF_NAME: "Home Town", + CONF_LATITUDE: "1", + CONF_LONGITUDE: "2", + CONF_MODE: "hourly", +} + async def test_show_config_form(): """Test show configuration form.""" @@ -172,3 +181,45 @@ async def test_config_entry_migration(hass): weather_home2 = ent_reg.async_get("weather.hometown_2") assert weather_home2.unique_id == "0, 0, hourly" + + +async def test_import_flow_success(hass): + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.ipma.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Home Town" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_already_exist(hass): + """Test import of yaml already exist.""" + + MockConfigEntry( + domain=DOMAIN, + data=ENTRY_CONFIG, + ).add_to_hass(hass) + + with patch( + "homeassistant.components.ipma.async_setup_entry", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "already_configured" diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index e6469043474..942b9654895 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,8 +1,10 @@ """The tests for the IPMA weather component.""" from collections import namedtuple +from datetime import datetime, timezone from unittest.mock import patch -from homeassistant.components import weather +from freezegun import freeze_time + from homeassistant.components.weather import ( ATTR_FORECAST, ATTR_FORECAST_CONDITION, @@ -19,8 +21,7 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, ) -from homeassistant.setup import async_setup_component -from homeassistant.util.dt import now +from homeassistant.const import STATE_UNKNOWN from tests.common import MockConfigEntry @@ -31,6 +32,13 @@ TEST_CONFIG = { "mode": "daily", } +TEST_CONFIG_HOURLY = { + "name": "HomeTown", + "latitude": "40.00", + "longitude": "-8.00", + "mode": "hourly", +} + class MockLocation: """Mock Location from pyipma.""" @@ -52,7 +60,7 @@ class MockLocation: return Observation(0.0, 71.0, 1000.0, 0.0, 18.0, "NW", 3.94) - async def forecast(self, api): + async def forecast(self, api, period): """Mock Forecast.""" Forecast = namedtuple( "Forecast", @@ -72,42 +80,67 @@ class MockLocation: ], ) - return [ - Forecast( - None, - "2020-01-15T00:00:00", - 24, - None, - 16.2, - 10.6, - "100.0", - 13.4, - "2020-01-15T07:51:00", - 9, - "S", - "10", - ), - Forecast( - "7.7", - now().utcnow().strftime("%Y-%m-%dT%H:%M:%S"), - 1, - "86.9", - None, - None, - "80.0", - 10.6, - "2020-01-15T07:51:00", - 10, - "S", - "32.7", - ), - ] + WeatherType = namedtuple("WeatherType", ["id", "en", "pt"]) + + if period == 24: + return [ + Forecast( + None, + datetime(2020, 1, 16, 0, 0, 0), + 24, + None, + 16.2, + 10.6, + "100.0", + 13.4, + "2020-01-15T07:51:00", + WeatherType(9, "Rain/showers", "Chuva/aguaceiros"), + "S", + "10", + ), + ] + if period == 1: + return [ + Forecast( + "7.7", + datetime(2020, 1, 15, 1, 0, 0, tzinfo=timezone.utc), + 1, + "86.9", + 12.0, + None, + 80.0, + 10.6, + "2020-01-15T02:51:00", + WeatherType(10, "Light rain", "Chuva fraca ou chuvisco"), + "S", + "32.7", + ), + Forecast( + "5.7", + datetime(2020, 1, 15, 2, 0, 0, tzinfo=timezone.utc), + 1, + "86.9", + 12.0, + None, + 80.0, + 10.6, + "2020-01-15T02:51:00", + WeatherType(1, "Clear sky", "C\u00e9u limpo"), + "S", + "32.7", + ), + ] @property def name(self): """Mock location.""" return "HomeTown" + @property + def station(self): + """Mock station.""" + return "HomeTown Station" + @property def station_latitude(self): """Mock latitude.""" @@ -129,35 +162,22 @@ class MockLocation: return 0 -async def test_setup_configuration(hass): - """Test for successfully setting up the IPMA platform.""" - with patch( - "homeassistant.components.ipma.weather.async_get_location", - return_value=MockLocation(), - ): - assert await async_setup_component( - hass, - weather.DOMAIN, - {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}}, - ) - await hass.async_block_till_done() +class MockBadLocation(MockLocation): + """Mock Location with unresponsive api.""" - state = hass.states.get("weather.hometown") - assert state.state == "rainy" + async def observation(self, api): + """Mock Observation.""" + return None - data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == 18.0 - assert data.get(ATTR_WEATHER_HUMIDITY) == 71 - assert data.get(ATTR_WEATHER_PRESSURE) == 1000.0 - assert data.get(ATTR_WEATHER_WIND_SPEED) == 3.94 - assert data.get(ATTR_WEATHER_WIND_BEARING) == "NW" - assert state.attributes.get("friendly_name") == "HomeTown" + async def forecast(self, api, period): + """Mock Forecast.""" + return [] async def test_setup_config_flow(hass): """Test for successfully setting up the IPMA platform.""" with patch( - "homeassistant.components.ipma.weather.async_get_location", + "pyipma.location.Location.get", return_value=MockLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) @@ -179,21 +199,18 @@ async def test_setup_config_flow(hass): async def test_daily_forecast(hass): """Test for successfully getting daily forecast.""" with patch( - "homeassistant.components.ipma.weather.async_get_location", + "pyipma.location.Location.get", return_value=MockLocation(), ): - assert await async_setup_component( - hass, - weather.DOMAIN, - {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "daily"}}, - ) + entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) + await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) await hass.async_block_till_done() state = hass.states.get("weather.hometown") assert state.state == "rainy" forecast = state.attributes.get(ATTR_FORECAST)[0] - assert forecast.get(ATTR_FORECAST_TIME) == "2020-01-15T00:00:00" + assert forecast.get(ATTR_FORECAST_TIME) == datetime(2020, 1, 16, 0, 0, 0) assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" assert forecast.get(ATTR_FORECAST_TEMP) == 16.2 assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6 @@ -202,17 +219,15 @@ async def test_daily_forecast(hass): assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" +@freeze_time("2020-01-14 23:00:00") async def test_hourly_forecast(hass): """Test for successfully getting daily forecast.""" with patch( - "homeassistant.components.ipma.weather.async_get_location", + "pyipma.location.Location.get", return_value=MockLocation(), ): - assert await async_setup_component( - hass, - weather.DOMAIN, - {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}}, - ) + entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG_HOURLY) + await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) await hass.async_block_till_done() state = hass.states.get("weather.hometown") @@ -220,7 +235,29 @@ async def test_hourly_forecast(hass): forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" - assert forecast.get(ATTR_FORECAST_TEMP) == 7.7 + assert forecast.get(ATTR_FORECAST_TEMP) == 12.0 assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 80.0 assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 32.7 assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" + + +async def test_failed_get_observation_forecast(hass): + """Test for successfully setting up the IPMA platform.""" + with patch( + "pyipma.location.Location.get", + return_value=MockBadLocation(), + ): + entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) + await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + await hass.async_block_till_done() + + state = hass.states.get("weather.hometown") + assert state.state == STATE_UNKNOWN + + data = state.attributes + assert data.get(ATTR_WEATHER_TEMPERATURE) is None + assert data.get(ATTR_WEATHER_HUMIDITY) is None + assert data.get(ATTR_WEATHER_PRESSURE) is None + assert data.get(ATTR_WEATHER_WIND_SPEED) is None + assert data.get(ATTR_WEATHER_WIND_BEARING) is None + assert state.attributes.get("friendly_name") == "HomeTown" From cfa838b27aa08822bb1d46fcac50b72237a33505 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 31 Aug 2022 13:19:47 +0200 Subject: [PATCH 802/903] Small refactoring of BMW lock entity (#77451) * Refactor entity_description * Fix default attrs not always shown * Simplify further Co-authored-by: @emontnemery Co-authored-by: rikroe --- .../components/bmw_connected_drive/lock.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index c9198437e2f..ffc6cf6d8b7 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -7,7 +7,7 @@ from typing import Any from bimmer_connected.vehicle import MyBMWVehicle from bimmer_connected.vehicle.doors_windows import LockState -from homeassistant.components.lock import LockEntity, LockEntityDescription +from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,7 +36,6 @@ async def async_setup_entry( BMWLock( coordinator, vehicle, - LockEntityDescription(key="lock", device_class="lock", name="Lock"), ) ) async_add_entities(entities) @@ -45,17 +44,17 @@ async def async_setup_entry( class BMWLock(BMWBaseEntity, LockEntity): """Representation of a MyBMW vehicle lock.""" + _attr_name = "Lock" + def __init__( self, coordinator: BMWDataUpdateCoordinator, vehicle: MyBMWVehicle, - description: LockEntityDescription, ) -> None: """Initialize the lock.""" super().__init__(coordinator, vehicle) - self.entity_description = description - self._attr_unique_id = f"{vehicle.vin}-{description.key}" + self._attr_unique_id = f"{vehicle.vin}-lock" self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes async def async_lock(self, **kwargs: Any) -> None: @@ -84,17 +83,17 @@ class BMWLock(BMWBaseEntity, LockEntity): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" _LOGGER.debug("Updating lock data of %s", self.vehicle.name) + # Set default attributes + self._attr_extra_state_attributes = self._attrs + # Only update the HA state machine if the vehicle reliably reports its lock state if self.door_lock_state_available: self._attr_is_locked = self.vehicle.doors_and_windows.door_lock_state in { LockState.LOCKED, LockState.SECURED, } - self._attr_extra_state_attributes = dict( - self._attrs, - **{ - "door_lock_state": self.vehicle.doors_and_windows.door_lock_state.value, - }, - ) + self._attr_extra_state_attributes[ + "door_lock_state" + ] = self.vehicle.doors_and_windows.door_lock_state.value super()._handle_coordinator_update() From 5bc2f37bf8f2d6e21ae03d69756916fd68dd6f73 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Wed, 31 Aug 2022 05:23:51 -0600 Subject: [PATCH 803/903] Add support for Feeder-Robot select (#77512) * Add support for Feeder-Robot select * Use lambda to get current selected option * Use generics and required keys mixin * Code improvements * Even more generics * Fix missing type hint * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/litterrobot/select.py | 93 +++++++++++++++---- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index 2889499f1c4..a18cd3b46b5 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -1,18 +1,61 @@ """Support for Litter-Robot selects.""" from __future__ import annotations -from pylitterbot import LitterRobot +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +import itertools +from typing import Any, Generic, TypeVar -from homeassistant.components.select import SelectEntity +from pylitterbot import FeederRobot, LitterRobot + +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity +from .entity import LitterRobotConfigEntity, _RobotT from .hub import LitterRobotHub -TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES = "Clean Cycle Wait Time Minutes" +_CastTypeT = TypeVar("_CastTypeT", int, float) + + +@dataclass +class RequiredKeysMixin(Generic[_RobotT, _CastTypeT]): + """A class that describes robot select entity required keys.""" + + current_fn: Callable[[_RobotT], _CastTypeT] + options_fn: Callable[[_RobotT], list[_CastTypeT]] + select_fn: Callable[ + [_RobotT, str], + tuple[Callable[[_CastTypeT], Coroutine[Any, Any, bool]], _CastTypeT], + ] + + +@dataclass +class RobotSelectEntityDescription( + SelectEntityDescription, RequiredKeysMixin[_RobotT, _CastTypeT] +): + """A class that describes robot select entities.""" + + +LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( + key="clean_cycle_wait_time_minutes", + name="Clean Cycle Wait Time Minutes", + icon="mdi:timer-outline", + current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, + options_fn=lambda robot: robot.VALID_WAIT_TIMES, + select_fn=lambda robot, option: (robot.set_wait_time, int(option)), +) +FEEDER_ROBOT_SELECT = RobotSelectEntityDescription[FeederRobot, float]( + key="meal_insert_size", + name="Meal insert size", + icon="mdi:scale", + unit_of_measurement="cups", + current_fn=lambda robot: robot.meal_insert_size, + options_fn=lambda robot: robot.VALID_MEAL_INSERT_SIZES, + select_fn=lambda robot, option: (robot.set_meal_insert_size, float(option)), +) async def async_setup_entry( @@ -22,30 +65,46 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot selects using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - LitterRobotSelect( - robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub + itertools.chain( + ( + LitterRobotSelect(robot=robot, hub=hub, description=LITTER_ROBOT_SELECT) + for robot in hub.litter_robots() + ), + ( + LitterRobotSelect(robot=robot, hub=hub, description=FEEDER_ROBOT_SELECT) + for robot in hub.feeder_robots() + ), ) - for robot in hub.litter_robots() ) -class LitterRobotSelect(LitterRobotConfigEntity[LitterRobot], SelectEntity): +class LitterRobotSelect( + LitterRobotConfigEntity[_RobotT], SelectEntity, Generic[_RobotT, _CastTypeT] +): """Litter-Robot Select.""" - _attr_icon = "mdi:timer-outline" + entity_description: RobotSelectEntityDescription[_RobotT, _CastTypeT] + + def __init__( + self, + robot: _RobotT, + hub: LitterRobotHub, + description: RobotSelectEntityDescription[_RobotT, _CastTypeT], + ) -> None: + """Initialize a Litter-Robot select entity.""" + assert description.name + super().__init__(robot, description.name, hub) + self.entity_description = description + options = self.entity_description.options_fn(self.robot) + self._attr_options = list(map(str, options)) @property def current_option(self) -> str | None: """Return the selected entity option to represent the entity state.""" - return str(self.robot.clean_cycle_wait_time_minutes) - - @property - def options(self) -> list[str]: - """Return a set of selectable options.""" - return [str(minute) for minute in self.robot.VALID_WAIT_TIMES] + return str(self.entity_description.current_fn(self.robot)) async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.perform_action_and_refresh(self.robot.set_wait_time, int(option)) + action, adjusted_option = self.entity_description.select_fn(self.robot, option) + await self.perform_action_and_refresh(action, adjusted_option) From d18097580e6c710d435485cbe1b1e73cc500441a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 31 Aug 2022 14:23:57 +0200 Subject: [PATCH 804/903] Bump hatasmota to 0.6.0 (#77560) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 4268c4198b2..d6ba4cf90cc 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.5.1"], + "requirements": ["hatasmota==0.6.0"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index f203f1bcca5..7486e46e1dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -821,7 +821,7 @@ hass-nabucasa==0.55.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.5.1 +hatasmota==0.6.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9efb70b5b0..4544ad2a65e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,7 +607,7 @@ hangups==0.4.18 hass-nabucasa==0.55.0 # homeassistant.components.tasmota -hatasmota==0.5.1 +hatasmota==0.6.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 0b89d91831a..16bf71a08e5 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -182,7 +182,7 @@ async def test_attributes_rgb(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.attributes.get("effect_list") == [ - "None", + "Solid", "Wake up", "Cycle up", "Cycle down", @@ -217,7 +217,7 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.attributes.get("effect_list") == [ - "None", + "Solid", "Wake up", "Cycle up", "Cycle down", @@ -252,7 +252,7 @@ async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.attributes.get("effect_list") == [ - "None", + "Solid", "Wake up", "Cycle up", "Cycle down", @@ -288,7 +288,7 @@ async def test_attributes_rgbww_reduced(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.attributes.get("effect_list") == [ - "None", + "Solid", "Wake up", "Cycle up", "Cycle down", From 875651f32dc22eeb2a44015da33a0be1126ad563 Mon Sep 17 00:00:00 2001 From: MosheTzvi <109089618+MosheTzvi@users.noreply.github.com> Date: Wed, 31 Aug 2022 15:47:23 +0300 Subject: [PATCH 805/903] Add Chatzot Hayom to Jewish calendar (#76378) --- homeassistant/components/jewish_calendar/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 085dbcd8c98..d9e1d55afbf 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -88,6 +88,11 @@ TIME_SENSORS = ( name='Latest time for Tefilla MG"A', icon="mdi:calendar-clock", ), + SensorEntityDescription( + key="midday", + name="Chatzot Hayom", + icon="mdi:calendar-clock", + ), SensorEntityDescription( key="big_mincha", name="Mincha Gedola", From d0375959fdd556837547c2c1a74d8600b70a0fe5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 31 Aug 2022 15:19:50 +0200 Subject: [PATCH 806/903] Add additional test to schedule (#77601) --- homeassistant/components/schedule/__init__.py | 2 +- tests/components/schedule/test_init.py | 86 ++++++++++++++++++- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index c698993440a..6ad3bcff58e 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -303,7 +303,7 @@ class Schedule(Entity): # Find next event in the schedule, loop over each day (starting with # the current day) until the next event has been found. next_event = None - for day in range(8): # 8 because we need to search same weekday next week + for day in range(8): # 8 because we need to search today's weekday next week day_schedule = self._config.get( WEEKDAY_TO_CONF[(now.weekday() + day) % 7], [] ) diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index 825bac5686c..5d5de581349 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -34,11 +34,11 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, HomeAssistant +from homeassistant.core import EVENT_STATE_CHANGED, Context, HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import MockUser, async_fire_time_changed +from tests.common import MockUser, async_capture_events, async_fire_time_changed @pytest.fixture @@ -225,6 +225,88 @@ async def test_events_one_day( assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T07:00:00-07:00" +@pytest.mark.parametrize( + "sun_schedule, mon_schedule", + ( + ( + {CONF_FROM: "23:00:00", CONF_TO: "24:00:00"}, + {CONF_FROM: "00:00:00", CONF_TO: "01:00:00"}, + ), + ), +) +async def test_adjacent( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + caplog: pytest.LogCaptureFixture, + sun_schedule: dict[str, str], + mon_schedule: dict[str, str], + freezer, +) -> None: + """Test adjacent events don't toggle on->off->on.""" + freezer.move_to("2022-08-30 13:20:00-07:00") + + assert await schedule_setup( + config={ + DOMAIN: { + "from_yaml": { + CONF_NAME: "from yaml", + CONF_ICON: "mdi:party-popper", + CONF_SUNDAY: sun_schedule, + CONF_MONDAY: mon_schedule, + } + } + }, + items=[], + ) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T23:00:00-07:00" + + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert ( + state.attributes[ATTR_NEXT_EVENT].isoformat() + == "2022-09-04T23:59:59.999999-07:00" + ) + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-05T00:00:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-05T01:00:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T23:00:00-07:00" + + await hass.async_block_till_done() + assert len(state_changes) == 4 + for event in state_changes: + assert event.data["new_state"].state == STATE_ON + + @pytest.mark.parametrize( "schedule", ( From 0aae41642590eaa04d2bf68efdf48f2886f8c77d Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 31 Aug 2022 16:58:07 +0300 Subject: [PATCH 807/903] Log command list in Bravia TV Remote (#77329) --- homeassistant/components/braviatv/coordinator.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index b5d91263b34..2744911007d 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -255,4 +255,14 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): """Send command to device.""" for _ in range(repeats): for cmd in command: - await self.client.send_command(cmd) + response = await self.client.send_command(cmd) + if not response: + commands = await self.client.get_command_list() + commands_keys = ", ".join(commands.keys()) + # Logging an error instead of raising a ValueError + # https://github.com/home-assistant/core/pull/77329#discussion_r955768245 + _LOGGER.error( + "Unsupported command: %s, list of available commands: %s", + cmd, + commands_keys, + ) From 7d5c00b85112ca655e6d0b0905f5c6e156f055a5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 31 Aug 2022 15:58:18 +0200 Subject: [PATCH 808/903] Fix comment in login_flow (#77600) --- homeassistant/components/auth/login_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index df076a1b4c8..b907598fe5a 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -240,7 +240,7 @@ class LoginFlowBaseView(HomeAssistantView): class LoginFlowIndexView(LoginFlowBaseView): - """View to create a config flow.""" + """View to create a login flow.""" url = "/auth/login_flow" name = "api:auth:login_flow" From 4b243705497c180e83c84c901c690d7a86d7323a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 31 Aug 2022 11:21:37 -0400 Subject: [PATCH 809/903] ZHA Yellow config flow fixes (#77603) --- homeassistant/components/zha/api.py | 3 +- homeassistant/components/zha/config_flow.py | 28 ++------ homeassistant/components/zha/core/const.py | 4 ++ .../homeassistant_yellow/conftest.py | 21 +++++- .../homeassistant_yellow/test_init.py | 26 ++++++- tests/components/zha/test_api.py | 7 +- tests/components/zha/test_config_flow.py | 70 +++++-------------- 7 files changed, 75 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 40996be3248..1095bae5ac8 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -50,6 +50,7 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_GATEWAY, DOMAIN, + EZSP_OVERWRITE_EUI64, GROUP_ID, GROUP_IDS, GROUP_NAME, @@ -1140,7 +1141,7 @@ async def websocket_restore_network_backup( if msg["ezsp_force_write_eui64"]: backup.network_info.stack_specific.setdefault("ezsp", {})[ - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" + EZSP_OVERWRITE_EUI64 ] = True # This can take 30-40s diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 5684e784a6a..9fc17c25f5b 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -35,6 +35,7 @@ from .core.const import ( DATA_ZHA_CONFIG, DEFAULT_DATABASE_NAME, DOMAIN, + EZSP_OVERWRITE_EUI64, RadioType, ) @@ -91,9 +92,7 @@ def _allow_overwrite_ezsp_ieee( ) -> zigpy.backups.NetworkBackup: """Return a new backup with the flag to allow overwriting the EZSP EUI64.""" new_stack_specific = copy.deepcopy(backup.network_info.stack_specific) - new_stack_specific.setdefault("ezsp", {})[ - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" - ] = True + new_stack_specific.setdefault("ezsp", {})[EZSP_OVERWRITE_EUI64] = True return backup.replace( network_info=backup.network_info.replace(stack_specific=new_stack_specific) @@ -108,9 +107,7 @@ def _prevent_overwrite_ezsp_ieee( return backup new_stack_specific = copy.deepcopy(backup.network_info.stack_specific) - new_stack_specific.setdefault("ezsp", {}).pop( - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it", None - ) + new_stack_specific.setdefault("ezsp", {}).pop(EZSP_OVERWRITE_EUI64, None) return backup.replace( network_info=backup.network_info.replace(stack_specific=new_stack_specific) @@ -664,10 +661,12 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN """Handle hardware flow.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") + if not data: return self.async_abort(reason="invalid_hardware_data") if data.get("radio_type") != "efr32": return self.async_abort(reason="invalid_hardware_data") + self._radio_type = RadioType.ezsp schema = { @@ -689,23 +688,10 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN return self.async_abort(reason="invalid_hardware_data") self._title = data.get("name", data["port"]["path"]) - self._device_path = device_settings.pop(CONF_DEVICE_PATH) + self._device_path = device_settings[CONF_DEVICE_PATH] self._device_settings = device_settings - self._set_confirm_only() - return await self.async_step_confirm_hardware() - - async def async_step_confirm_hardware( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Confirm a hardware discovery.""" - if user_input is not None or not onboarding.async_is_onboarded(self.hass): - return await self._async_create_radio_entity() - - return self.async_show_form( - step_id="confirm_hardware", - description_placeholders={CONF_NAME: self._title}, - ) + return await self.async_step_choose_formation_strategy() class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index fa8b7148c77..a1c5a55bd76 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -412,3 +412,7 @@ class Strobe(t.enum8): STARTUP_FAILURE_DELAY_S = 3 STARTUP_RETRIES = 3 + +EZSP_OVERWRITE_EUI64 = ( + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" +) diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index 8700e361dc8..52759ba6d89 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -1,13 +1,28 @@ """Test fixtures for the Home Assistant Yellow integration.""" -from unittest.mock import patch +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch import pytest @pytest.fixture(autouse=True) -def mock_zha(): - """Mock the zha integration.""" +def mock_zha_config_flow_setup() -> Generator[None, None, None]: + """Mock the radio connection and probing of the ZHA config flow.""" + + def mock_probe(config: dict[str, Any]) -> None: + # The radio probing will return the correct baudrate + return {**config, "baudrate": 115200} + + mock_connect_app = MagicMock() + mock_connect_app.__aenter__.return_value.backups.backups = [] + with patch( + "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe + ), patch( + "homeassistant.components.zha.config_flow.BaseZhaFlow._connect_zigpy_app", + return_value=mock_connect_app, + ), patch( "homeassistant.components.zha.async_setup_entry", return_value=True, ): diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index f534c7cd587..bc36ae3cec2 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from homeassistant.components import zha from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -37,8 +38,20 @@ async def test_setup_entry( await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 - assert len(hass.config_entries.async_entries("zha")) == num_entries + # Finish setting up ZHA + if num_entries > 0: + zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") + assert len(zha_flows) == 1 + assert zha_flows[0]["step_id"] == "choose_formation_strategy" + + await hass.config_entries.flow.async_configure( + zha_flows[0]["flow_id"], + user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows + assert len(hass.config_entries.async_entries("zha")) == num_entries async def test_setup_zha(hass: HomeAssistant) -> None: @@ -63,6 +76,17 @@ async def test_setup_zha(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 + # Finish setting up ZHA + zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") + assert len(zha_flows) == 1 + assert zha_flows[0]["step_id"] == "choose_formation_strategy" + + await hass.config_entries.flow.async_configure( + zha_flows[0]["flow_id"], + user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + config_entry = hass.config_entries.async_entries("zha")[0] assert config_entry.data == { "device": { diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index b25bffebec7..e4daf7f365e 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -34,6 +34,7 @@ from homeassistant.components.zha.core.const import ( CLUSTER_TYPE_IN, DATA_ZHA, DATA_ZHA_GATEWAY, + EZSP_OVERWRITE_EUI64, GROUP_ID, GROUP_IDS, GROUP_NAME, @@ -709,11 +710,7 @@ async def test_restore_network_backup_force_write_eui64(app_controller, zha_clie p.assert_called_once_with( backup.replace( network_info=backup.network_info.replace( - stack_specific={ - "ezsp": { - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it": True - } - } + stack_specific={"ezsp": {EZSP_OVERWRITE_EUI64: True}} ) ) ) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 12f5434abd4..8a6496dbc5f 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -21,6 +21,7 @@ from homeassistant.components.zha.core.const import ( CONF_FLOWCONTROL, CONF_RADIO_TYPE, DOMAIN, + EZSP_OVERWRITE_EUI64, RadioType, ) from homeassistant.config_entries import ( @@ -857,8 +858,9 @@ async def test_migration_ti_cc_to_znp(old_type, new_type, hass, config_entry): assert config_entry.data[CONF_RADIO_TYPE] == new_type +@pytest.mark.parametrize("onboarded", [True, False]) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -async def test_hardware_not_onboarded(hass): +async def test_hardware(onboarded, hass): """Test hardware flow.""" data = { "name": "Yellow", @@ -870,52 +872,23 @@ async def test_hardware_not_onboarded(hass): }, } with patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False + "homeassistant.components.onboarding.async_is_onboarded", return_value=onboarded ): - result = await hass.config_entries.flow.async_init( + result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "hardware"}, data=data ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Yellow" - assert result["data"] == { - CONF_DEVICE: { - CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: "hardware", - CONF_DEVICE_PATH: "/dev/ttyAMA1", - }, - CONF_RADIO_TYPE: "ezsp", - } + assert result1["type"] == FlowResultType.MENU + assert result1["step_id"] == "choose_formation_strategy" - -@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -async def test_hardware_onboarded(hass): - """Test hardware flow.""" - data = { - "radio_type": "efr32", - "port": { - "path": "/dev/ttyAMA1", - "baudrate": 115200, - "flow_control": "hardware", - }, - } - with patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=True - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "hardware"}, data=data - ) - - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm_hardware" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, ) + await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "/dev/ttyAMA1" - assert result["data"] == { + assert result2["title"] == "Yellow" + assert result2["data"] == { CONF_DEVICE: { CONF_BAUDRATE: 115200, CONF_FLOWCONTROL: "hardware", @@ -968,25 +941,18 @@ def test_allow_overwrite_ezsp_ieee(): new_backup = config_flow._allow_overwrite_ezsp_ieee(backup) assert backup != new_backup - assert ( - new_backup.network_info.stack_specific["ezsp"][ - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" - ] - is True - ) + assert new_backup.network_info.stack_specific["ezsp"][EZSP_OVERWRITE_EUI64] is True def test_prevent_overwrite_ezsp_ieee(): """Test modifying the backup to prevent bellows from overriding the IEEE address.""" backup = zigpy.backups.NetworkBackup() - backup.network_info.stack_specific["ezsp"] = { - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it": True - } + backup.network_info.stack_specific["ezsp"] = {EZSP_OVERWRITE_EUI64: True} new_backup = config_flow._prevent_overwrite_ezsp_ieee(backup) assert backup != new_backup assert not new_backup.network_info.stack_specific.get("ezsp", {}).get( - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" + EZSP_OVERWRITE_EUI64 ) @@ -1356,9 +1322,7 @@ async def test_ezsp_restore_without_settings_change_ieee( mock_app.state.network_info.network_key.tx_counter += 10000 # Include the overwrite option, just in case someone uploads a backup with it - backup.network_info.metadata["ezsp"] = { - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it": True - } + backup.network_info.metadata["ezsp"] = {EZSP_OVERWRITE_EUI64: True} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], From 708e61482373dc9cb406cf39b203c12cafaef70a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 Aug 2022 12:41:04 -0400 Subject: [PATCH 810/903] Migrate Ecowitt to webhooks (#77610) --- homeassistant/components/ecowitt/__init__.py | 33 +++---- .../components/ecowitt/config_flow.py | 67 +++++---------- homeassistant/components/ecowitt/const.py | 4 - .../components/ecowitt/manifest.json | 2 +- homeassistant/components/ecowitt/strings.json | 13 +-- .../components/ecowitt/translations/en.json | 23 ++--- tests/components/ecowitt/test_config_flow.py | 86 ++----------------- 7 files changed, 58 insertions(+), 170 deletions(-) diff --git a/homeassistant/components/ecowitt/__init__.py b/homeassistant/components/ecowitt/__init__.py index 71d42643cfb..ebd861c1377 100644 --- a/homeassistant/components/ecowitt/__init__.py +++ b/homeassistant/components/ecowitt/__init__.py @@ -2,32 +2,38 @@ from __future__ import annotations from aioecowitt import EcoWittListener +from aiohttp import web +from homeassistant.components import webhook from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import Event, HomeAssistant +from homeassistant.const import CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback -from .const import CONF_PATH, DOMAIN +from .const import DOMAIN PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Ecowitt component from UI.""" - hass.data.setdefault(DOMAIN, {}) - - ecowitt = hass.data[DOMAIN][entry.entry_id] = EcoWittListener( - port=entry.data[CONF_PORT], path=entry.data[CONF_PATH] - ) + ecowitt = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = EcoWittListener() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - await ecowitt.start() + async def handle_webhook( + hass: HomeAssistant, webhook_id: str, request: web.Request + ) -> web.Response: + """Handle webhook callback.""" + return await ecowitt.handler(request) - # Close on shutdown - async def _stop_ecowitt(_: Event): + webhook.async_register( + hass, DOMAIN, entry.title, entry.data[CONF_WEBHOOK_ID], handle_webhook + ) + + @callback + def _stop_ecowitt(_: Event): """Stop the Ecowitt listener.""" - await ecowitt.stop() + webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_ecowitt) @@ -38,9 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - ecowitt = hass.data[DOMAIN][entry.entry_id] - await ecowitt.stop() - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/ecowitt/config_flow.py b/homeassistant/components/ecowitt/config_flow.py index c3406652665..2b6744790bf 100644 --- a/homeassistant/components/ecowitt/config_flow.py +++ b/homeassistant/components/ecowitt/config_flow.py @@ -1,77 +1,50 @@ """Config flow for ecowitt.""" from __future__ import annotations -import logging import secrets from typing import Any -from aioecowitt import EcoWittListener -import voluptuous as vol +from yarl import URL from homeassistant import config_entries -from homeassistant.const import CONF_PORT -from homeassistant.core import HomeAssistant +from homeassistant.components import webhook +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.network import get_url -from .const import CONF_PATH, DEFAULT_PORT, DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PATH, default=f"/{secrets.token_urlsafe(16)}"): cv.string, - } -) - - -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: - """Validate user input.""" - # Check if the port is in use - try: - listener = EcoWittListener(port=data[CONF_PORT]) - await listener.start() - await listener.stop() - except OSError: - raise InvalidPort from None - - return {"title": f"Ecowitt on port {data[CONF_PORT]}"} +from .const import DOMAIN class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for the Ecowitt.""" VERSION = 1 + _webhook_id: str async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" if user_input is None: + self._webhook_id = secrets.token_hex(16) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA + step_id="user", ) - errors = {} + base_url = URL(get_url(self.hass)) + assert base_url.host - # Check if the port is in use by another config entry - self._async_abort_entries_match({CONF_PORT: user_input[CONF_PORT]}) - - try: - info = await validate_input(self.hass, user_input) - except InvalidPort: - errors["base"] = "invalid_port" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - return self.async_create_entry(title=info["title"], data=user_input) - - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + return self.async_create_entry( + title="Ecowitt", + data={ + CONF_WEBHOOK_ID: self._webhook_id, + }, + description_placeholders={ + "path": webhook.async_generate_path(self._webhook_id), + "server": base_url.host, + "port": str(base_url.port), + }, ) diff --git a/homeassistant/components/ecowitt/const.py b/homeassistant/components/ecowitt/const.py index a7011f24e0c..3c8dcca8723 100644 --- a/homeassistant/components/ecowitt/const.py +++ b/homeassistant/components/ecowitt/const.py @@ -1,7 +1,3 @@ """Constants used by ecowitt component.""" DOMAIN = "ecowitt" - -DEFAULT_PORT = 49199 - -CONF_PATH = "path" diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index cafd140828e..348df17b0cd 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -1,6 +1,6 @@ { "domain": "ecowitt", - "name": "Ecowitt Weather Station", + "name": "Ecowitt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", "requirements": ["aioecowitt==2022.08.3"], diff --git a/homeassistant/components/ecowitt/strings.json b/homeassistant/components/ecowitt/strings.json index a4e12e69d57..cca51c1129e 100644 --- a/homeassistant/components/ecowitt/strings.json +++ b/homeassistant/components/ecowitt/strings.json @@ -1,17 +1,12 @@ { "config": { - "error": { - "invalid_port": "Port is already used.", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, "step": { "user": { - "description": "The following steps must be performed to set up this integration.\n\nUse the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\nPick your station -> Menu Others -> DIY Upload Servers.\nHit next and select 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath have to match, you can copy with secure token /.\nSave configuration. The Ecowitt should then start attempting to send data to your server.", - "data": { - "port": "Listening port", - "path": "Path with Security token" - } + "description": "Are you sure you want to set up Ecowitt?" } + }, + "create_entry": { + "default": "To finish setting up the integration, use the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\n\nPick your station -> Menu Others -> DIY Upload Servers. Hit next and select 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nClick on 'Save'." } } } diff --git a/homeassistant/components/ecowitt/translations/en.json b/homeassistant/components/ecowitt/translations/en.json index 041fa9f697b..b8ce69c10b8 100644 --- a/homeassistant/components/ecowitt/translations/en.json +++ b/homeassistant/components/ecowitt/translations/en.json @@ -1,17 +1,12 @@ { "config": { - "error": { - "invalid_port": "Port is already used.", - "unknown": "Unknown error." - }, - "step": { - "user": { - "description": "The following steps must be performed to set up this integration.\n\nUse the Ecowitt App (on your phone) or your Ecowitt WebUI over the station IP address.\nPick your station -> Menu Others -> DIY Upload Servers.\nHit next and select 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath have to match, you can copy with secure token /.\nSave configuration. The Ecowitt should then start attempting to send data to your server.", - "data": { - "port": "Listening port", - "path": "Path with Security token" - } - } - } + "create_entry": { + "default": "To finish setting up the integration, use the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\n\nPick your station -> Menu Others -> DIY Upload Servers. Hit next and select 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nClick on 'Save'." + }, + "step": { + "user": { + "description": "Are you sure you want to set up Ecowitt?" + } + } } -} +} \ No newline at end of file diff --git a/tests/components/ecowitt/test_config_flow.py b/tests/components/ecowitt/test_config_flow.py index 6ddd475121b..c09fb951b11 100644 --- a/tests/components/ecowitt/test_config_flow.py +++ b/tests/components/ecowitt/test_config_flow.py @@ -5,12 +5,13 @@ from homeassistant import config_entries from homeassistant.components.ecowitt.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType - -from tests.common import MockConfigEntry +from homeassistant.setup import async_setup_component async def test_create_entry(hass: HomeAssistant) -> None: """Test we can create a config entry.""" + await async_setup_component(hass, "http", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -18,93 +19,18 @@ async def test_create_entry(hass: HomeAssistant) -> None: assert result["errors"] is None with patch( - "homeassistant.components.ecowitt.config_flow.EcoWittListener.start" - ), patch( - "homeassistant.components.ecowitt.config_flow.EcoWittListener.stop" - ), patch( "homeassistant.components.ecowitt.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "port": 49911, - "path": "/ecowitt-station", - }, + {}, ) await hass.async_block_till_done() assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Ecowitt on port 49911" + assert result2["title"] == "Ecowitt" assert result2["data"] == { - "port": 49911, - "path": "/ecowitt-station", + "webhook_id": result2["description_placeholders"]["path"].split("/")[-1], } assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_invalid_port(hass: HomeAssistant) -> None: - """Test we handle invalid port.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ecowitt.config_flow.EcoWittListener.start", - side_effect=OSError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "port": 49911, - "path": "/ecowitt-station", - }, - ) - - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_port"} - - -async def test_already_configured_port(hass: HomeAssistant) -> None: - """Test already configured port.""" - MockConfigEntry(domain=DOMAIN, data={"port": 49911}).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ecowitt.config_flow.EcoWittListener.start", - side_effect=OSError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "port": 49911, - "path": "/ecowitt-station", - }, - ) - - assert result2["type"] == FlowResultType.ABORT - - -async def test_unknown_error(hass: HomeAssistant) -> None: - """Test we handle unknown error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ecowitt.config_flow.EcoWittListener.start", - side_effect=Exception(), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "port": 49911, - "path": "/ecowitt-station", - }, - ) - - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} From 67ccb6f25a3be1ccba049af7c47e369d0271b34b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Aug 2022 16:41:22 +0000 Subject: [PATCH 811/903] Fix yet another Govee H5181 variant (#77611) --- homeassistant/components/govee_ble/manifest.json | 7 ++++++- homeassistant/generated/bluetooth.py | 6 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 6f9e15463cd..e24e3bfea14 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -32,6 +32,11 @@ "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 63585, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", @@ -48,7 +53,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.17.0"], + "requirements": ["govee-ble==0.17.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 3718bbdc913..14156ada20c 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -75,6 +75,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", "connectable": False }, + { + "domain": "govee_ble", + "manufacturer_id": 63585, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": False + }, { "domain": "govee_ble", "manufacturer_id": 14474, diff --git a/requirements_all.txt b/requirements_all.txt index 7486e46e1dc..51fa0d5c6d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -769,7 +769,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.0 +govee-ble==0.17.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4544ad2a65e..69123fb6c28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -570,7 +570,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.0 +govee-ble==0.17.1 # homeassistant.components.gree greeclimate==1.3.0 From f8fc90bc07ccf6b0b493669226a8c471687fc514 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 31 Aug 2022 12:41:41 -0400 Subject: [PATCH 812/903] Add ZHA config flow single instance checks for zeroconf and hardware (#77612) --- homeassistant/components/zha/config_flow.py | 64 +++++----- tests/components/zha/test_config_flow.py | 134 +++++++++++++++----- 2 files changed, 136 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 9fc17c25f5b..ce2080e4a13 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -551,6 +551,36 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN return await self.async_step_choose_serial_port(user_input) + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm a discovery.""" + self._set_confirm_only() + + # Don't permit discovery if ZHA is already set up + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + # Without confirmation, discovery can automatically progress into parts of the + # config flow logic that interacts with hardware! + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + # Probe the radio type if we don't have one yet + if self._radio_type is None and not await self._detect_radio_type(): + # This path probably will not happen now that we have + # more precise USB matching unless there is a problem + # with the device + return self.async_abort(reason="usb_probe_failed") + + if self._device_settings is None: + return await self.async_step_manual_port_config() + + return await self.async_step_choose_formation_strategy() + + return self.async_show_form( + step_id="confirm", + description_placeholders={CONF_NAME: self._title}, + ) + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle usb discovery.""" vid = discovery_info.vid @@ -570,9 +600,6 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN }, } ) - # Check if already configured - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") # If they already have a discovery for deconz we ignore the usb discovery as # they probably want to use it there instead @@ -591,32 +618,14 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN vid, pid, ) - self._set_confirm_only() self.context["title_placeholders"] = {CONF_NAME: self._title} return await self.async_step_confirm() - async def async_step_confirm( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Confirm a discovery.""" - if user_input is not None or not onboarding.async_is_onboarded(self.hass): - if not await self._detect_radio_type(): - # This path probably will not happen now that we have - # more precise USB matching unless there is a problem - # with the device - return self.async_abort(reason="usb_probe_failed") - - return await self.async_step_choose_formation_strategy() - - return self.async_show_form( - step_id="confirm", - description_placeholders={CONF_NAME: self._title}, - ) - async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" + # Hostname is format: livingroom.local. local_name = discovery_info.hostname[:-1] radio_type = discovery_info.properties.get("radio_type") or local_name @@ -638,10 +647,6 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN } ) - # Check if already configured - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - self.context["title_placeholders"] = {CONF_NAME: node_name} self._title = device_path self._device_path = device_path @@ -653,15 +658,12 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN else: self._radio_type = RadioType.znp - return await self.async_step_manual_port_config() + return await self.async_step_confirm() async def async_step_hardware( self, data: dict[str, Any] | None = None ) -> FlowResult: """Handle hardware flow.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if not data: return self.async_abort(reason="invalid_hardware_data") if data.get("radio_type") != "efr32": @@ -691,7 +693,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN self._device_path = device_settings[CONF_DEVICE_PATH] self._device_settings = device_settings - return await self.async_step_choose_formation_strategy() + return await self.async_step_confirm() class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 8a6496dbc5f..d65732a6ab8 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -107,22 +107,31 @@ async def test_zeroconf_discovery_znp(hass): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) + assert flow["step_id"] == "confirm" + + # Confirm discovery result1 = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={} ) + assert result1["step_id"] == "manual_port_config" - assert result1["type"] == FlowResultType.MENU - assert result1["step_id"] == "choose_formation_strategy" - + # Confirm port settings result2 = await hass.config_entries.flow.async_configure( - result1["flow_id"], + result1["flow_id"], user_input={} + ) + + assert result2["type"] == FlowResultType.MENU + assert result2["step_id"] == "choose_formation_strategy" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "socket://192.168.1.200:6638" - assert result2["data"] == { + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "socket://192.168.1.200:6638" + assert result3["data"] == { CONF_DEVICE: { CONF_BAUDRATE: 115200, CONF_FLOWCONTROL: None, @@ -148,22 +157,31 @@ async def test_zigate_via_zeroconf(setup_entry_mock, hass): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) + assert flow["step_id"] == "confirm" + + # Confirm discovery result1 = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={} ) + assert result1["step_id"] == "manual_port_config" - assert result1["type"] == FlowResultType.MENU - assert result1["step_id"] == "choose_formation_strategy" - + # Confirm port settings result2 = await hass.config_entries.flow.async_configure( - result1["flow_id"], + result1["flow_id"], user_input={} + ) + + assert result2["type"] == FlowResultType.MENU + assert result2["step_id"] == "choose_formation_strategy" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "socket://192.168.1.200:1234" - assert result2["data"] == { + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "socket://192.168.1.200:1234" + assert result3["data"] == { CONF_DEVICE: { CONF_DEVICE_PATH: "socket://192.168.1.200:1234", }, @@ -187,22 +205,31 @@ async def test_efr32_via_zeroconf(hass): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) + assert flow["step_id"] == "confirm" + + # Confirm discovery result1 = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={} ) + assert result1["step_id"] == "manual_port_config" - assert result1["type"] == FlowResultType.MENU - assert result1["step_id"] == "choose_formation_strategy" - + # Confirm port settings result2 = await hass.config_entries.flow.async_configure( - result1["flow_id"], + result1["flow_id"], user_input={} + ) + + assert result2["type"] == FlowResultType.MENU + assert result2["step_id"] == "choose_formation_strategy" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "socket://192.168.1.200:6638" - assert result2["data"] == { + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "socket://192.168.1.200:6638" + assert result3["data"] == { CONF_DEVICE: { CONF_DEVICE_PATH: "socket://192.168.1.200:6638", CONF_BAUDRATE: 115200, @@ -282,6 +309,37 @@ async def test_discovery_via_zeroconf_ip_change_ignored(hass): } +async def test_discovery_confirm_final_abort_if_entries(hass): + """Test discovery aborts if ZHA was set up after the confirmation dialog is shown.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.1.200", + addresses=["192.168.1.200"], + hostname="tube._tube_zb_gw._tcp.local.", + name="tube", + port=6053, + properties={"name": "tube_123456"}, + type="mock_type", + ) + flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info + ) + assert flow["step_id"] == "confirm" + + # ZHA was somehow set up while we were in the config flow + with patch( + "homeassistant.config_entries.ConfigFlow._async_current_entries", + return_value=[MagicMock()], + ): + # Confirm discovery + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) + + # Config will fail + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + + @patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) async def test_discovery_via_usb(hass): """Test usb flow -- radio detected.""" @@ -293,15 +351,16 @@ async def test_discovery_via_usb(hass): description="zigbee radio", manufacturer="test", ) - result = await hass.config_entries.flow.async_init( + result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm" + + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result1["flow_id"], user_input={} ) await hass.async_block_till_done() @@ -878,17 +937,30 @@ async def test_hardware(onboarded, hass): DOMAIN, context={"source": "hardware"}, data=data ) - assert result1["type"] == FlowResultType.MENU - assert result1["step_id"] == "choose_formation_strategy" + if onboarded: + # Confirm discovery + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "confirm" - result2 = await hass.config_entries.flow.async_configure( - result1["flow_id"], + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={}, + ) + else: + # No need to confirm + result2 = result1 + + assert result2["type"] == FlowResultType.MENU + assert result2["step_id"] == "choose_formation_strategy" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, ) await hass.async_block_till_done() - assert result2["title"] == "Yellow" - assert result2["data"] == { + assert result3["title"] == "Yellow" + assert result3["data"] == { CONF_DEVICE: { CONF_BAUDRATE: 115200, CONF_FLOWCONTROL: "hardware", From 4e21d56d7b3eb88b486d9f4dadfd5e75ee12b03f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 31 Aug 2022 11:44:31 -0500 Subject: [PATCH 813/903] Bump plexapi to 4.13.0 (#77597) --- 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 1875b0e05dc..171648042c1 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.12.1", + "plexapi==4.13.0", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index 51fa0d5c6d8..a0799f8f448 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1274,7 +1274,7 @@ pillow==9.2.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.12.1 +plexapi==4.13.0 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69123fb6c28..85584f721a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ pilight==0.1.1 pillow==9.2.0 # homeassistant.components.plex -plexapi==4.12.1 +plexapi==4.13.0 # homeassistant.components.plex plexauth==0.0.6 From 1bee9923dcc2e71b4d0690020930dfba147f895e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 Aug 2022 13:28:58 -0400 Subject: [PATCH 814/903] Bump frontend to 20220831.0 (#77615) --- 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 f1e6f31fd4b..1bf8962d615 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==20220816.0"], + "requirements": ["home-assistant-frontend==20220831.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1da07040c9b..3f40589bf64 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220816.0 +home-assistant-frontend==20220831.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index a0799f8f448..ffbff8840a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -848,7 +848,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220816.0 +home-assistant-frontend==20220831.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85584f721a2..bc79492c7a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220816.0 +home-assistant-frontend==20220831.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 083e902dc0801fd9c87a86c9f4c1c8c4cbde9125 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 31 Aug 2022 19:53:14 +0200 Subject: [PATCH 815/903] Ignore unknown states in universal media player (#77388) Ignore unknown states --- homeassistant/components/universal/media_player.py | 13 ++++++++----- tests/components/universal/test_media_player.py | 6 ++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 099d786b901..ee2954aac28 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -109,6 +109,9 @@ STATES_ORDER = [ STATE_BUFFERING, STATE_PLAYING, ] +STATES_ORDER_LOOKUP = {state: idx for idx, state in enumerate(STATES_ORDER)} +STATES_ORDER_IDLE = STATES_ORDER_LOOKUP[STATE_IDLE] + ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) CMD_SCHEMA = cv.schema_with_slug_keys(cv.SERVICE_SCHEMA) @@ -626,12 +629,12 @@ class UniversalMediaPlayer(MediaPlayerEntity): """Update state in HA.""" self._child_state = None for child_name in self._children: - if (child_state := self.hass.states.get(child_name)) and STATES_ORDER.index( - child_state.state - ) >= STATES_ORDER.index(STATE_IDLE): + if (child_state := self.hass.states.get(child_name)) and ( + child_state_order := STATES_ORDER_LOOKUP.get(child_state.state, 0) + ) >= STATES_ORDER_IDLE: if self._child_state: - if STATES_ORDER.index(child_state.state) > STATES_ORDER.index( - self._child_state.state + if child_state_order > STATES_ORDER_LOOKUP.get( + self._child_state.state, 0 ): self._child_state = child_state else: diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index c50c3e97713..b589fc09a6a 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -434,6 +434,12 @@ async def test_active_child_state(hass, mock_states): await ump.async_update() assert mock_states.mock_mp_2.entity_id == ump._child_state.entity_id + mock_states.mock_mp_1._state = "invalid_state" + mock_states.mock_mp_1.async_schedule_update_ha_state() + await hass.async_block_till_done() + await ump.async_update() + assert mock_states.mock_mp_2.entity_id == ump._child_state.entity_id + async def test_name(hass): """Test name property.""" From 7115e6304431292e0291d485b1a71710e7c2f3cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 Aug 2022 14:11:29 -0400 Subject: [PATCH 816/903] Bumped version to 2022.9.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 750b014e0da..6c530231139 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index d68cac82923..9d5b168c559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0.dev0" +version = "2022.9.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8b8db998df0e341b8bc4ff6261fe8fd2fe1f45b8 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Wed, 31 Aug 2022 21:16:00 +0200 Subject: [PATCH 817/903] Catch unknown user exception in Overkiz integration (#76693) --- homeassistant/components/overkiz/config_flow.py | 3 +++ homeassistant/components/overkiz/strings.json | 3 ++- homeassistant/components/overkiz/translations/en.json | 3 ++- tests/components/overkiz/test_config_flow.py | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 2808c309938..d3ab9722fca 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -12,6 +12,7 @@ from pyoverkiz.exceptions import ( MaintenanceException, TooManyAttemptsBannedException, TooManyRequestsException, + UnknownUserException, ) from pyoverkiz.models import obfuscate_id import voluptuous as vol @@ -83,6 +84,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "server_in_maintenance" except TooManyAttemptsBannedException: errors["base"] = "too_many_attempts" + except UnknownUserException: + errors["base"] = "unknown_user" except Exception as exception: # pylint: disable=broad-except errors["base"] = "unknown" LOGGER.exception(exception) diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 9c64311a73e..ecc0329eb2a 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -18,7 +18,8 @@ "server_in_maintenance": "Server is down for maintenance", "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "unknown_user": "Unknown user. Somfy Protect accounts are not supported by this integration." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json index 9e24a9d3cb3..9c8ad538695 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Server is down for maintenance", "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unknown_user": "Unknown user. Somfy Protect accounts are not supported by this integration." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 0542f4dc9fc..940da7b39c2 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -9,6 +9,7 @@ from pyoverkiz.exceptions import ( MaintenanceException, TooManyAttemptsBannedException, TooManyRequestsException, + UnknownUserException, ) import pytest @@ -88,6 +89,7 @@ async def test_form(hass: HomeAssistant) -> None: (ClientError, "cannot_connect"), (MaintenanceException, "server_in_maintenance"), (TooManyAttemptsBannedException, "too_many_attempts"), + (UnknownUserException, "unknown_user"), (Exception, "unknown"), ], ) From 37acd3e3f234ec30f53bb311bef26eb3d5538d2c Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 1 Sep 2022 05:42:23 +0300 Subject: [PATCH 818/903] Suppress 404 in Bravia TV (#77288) --- homeassistant/components/braviatv/coordinator.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 2744911007d..bdacddcdb2f 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -7,7 +7,7 @@ from functools import wraps import logging from typing import Any, Final, TypeVar -from pybravia import BraviaTV, BraviaTVError +from pybravia import BraviaTV, BraviaTVError, BraviaTVNotFound from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player.const import ( @@ -79,6 +79,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.connected = False # Assume that the TV is in Play mode self.playing = True + self.skipped_updates = 0 super().__init__( hass, @@ -113,6 +114,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): power_status = await self.client.get_power_status() self.is_on = power_status == "active" + self.skipped_updates = 0 if self.is_on is False: return @@ -121,6 +123,13 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): await self.async_update_sources() await self.async_update_volume() await self.async_update_playing() + except BraviaTVNotFound as err: + if self.skipped_updates < 10: + self.connected = False + self.skipped_updates += 1 + _LOGGER.debug("Update skipped, Bravia API service is reloading") + return + raise UpdateFailed("Error communicating with device") from err except BraviaTVError as err: self.is_on = False self.connected = False From 7d90f6ccea04fc5326d43f74abf29496df0e2268 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 31 Aug 2022 16:35:58 -0400 Subject: [PATCH 819/903] Bump version of pyunifiprotect to 4.2.0 (#77618) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index a01706337e0..5958da8f00e 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.1.9", "unifi-discovery==1.1.6"], + "requirements": ["pyunifiprotect==4.2.0", "unifi-discovery==1.1.6"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index ffbff8840a0..fcb36668f7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2037,7 +2037,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.9 +pyunifiprotect==4.2.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc79492c7a2..26882369a6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1400,7 +1400,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.9 +pyunifiprotect==4.2.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From de35e84543d1c2446a3dd4fad3939db2eedbbd37 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 1 Sep 2022 01:18:34 +0200 Subject: [PATCH 820/903] Update xknx to 1.0.2 (#77627) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index bb5599939db..c0aa6c3941c 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==1.0.1"], + "requirements": ["xknx==1.0.2"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index fcb36668f7f..24ec7ecc560 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2522,7 +2522,7 @@ xboxapi==2.0.1 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==1.0.1 +xknx==1.0.2 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26882369a6f..002df1e17a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1729,7 +1729,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==1.0.1 +xknx==1.0.2 # homeassistant.components.bluesound # homeassistant.components.fritz From 129f7176342eee5ea25872423536181caae65d39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Sep 2022 02:43:18 +0000 Subject: [PATCH 821/903] Bump bleak to 0.16.0 (#77629) Co-authored-by: Justin Vanderhooft --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 5cd83a2d51a..981b7854e36 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["usb"], "quality_scale": "internal", "requirements": [ - "bleak==0.15.1", + "bleak==0.16.0", "bluetooth-adapters==0.3.2", "bluetooth-auto-recovery==0.3.0" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f40589bf64..a456e9ec965 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 -bleak==0.15.1 +bleak==0.16.0 bluetooth-adapters==0.3.2 bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index 24ec7ecc560..1e77606ec48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,7 @@ bimmer_connected==0.10.2 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.15.1 +bleak==0.16.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 002df1e17a2..a013c67cfc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ bellows==0.33.1 bimmer_connected==0.10.2 # homeassistant.components.bluetooth -bleak==0.15.1 +bleak==0.16.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 From a080256f88847f609fbd60693becf9c51d41c30b Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Wed, 31 Aug 2022 20:23:45 -0400 Subject: [PATCH 822/903] Bump melnor-bluetooth to 0.0.15 (#77631) --- homeassistant/components/melnor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json index 37ac40cb3aa..a59758f705b 100644 --- a/homeassistant/components/melnor/manifest.json +++ b/homeassistant/components/melnor/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/melnor", "iot_class": "local_polling", "name": "Melnor Bluetooth", - "requirements": ["melnor-bluetooth==0.0.13"] + "requirements": ["melnor-bluetooth==0.0.15"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e77606ec48..50306cf623b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1037,7 +1037,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.13 +melnor-bluetooth==0.0.15 # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a013c67cfc2..a0356065e94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -742,7 +742,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.13 +melnor-bluetooth==0.0.15 # homeassistant.components.meteo_france meteofrance-api==1.0.2 From 5f4411113a5b9196ea6dbb71c60fb95cb82357a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 Aug 2022 22:57:12 -0400 Subject: [PATCH 823/903] Bumped version to 2022.9.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6c530231139..8d5242123e5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 9d5b168c559..a2e1c4063a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b0" +version = "2022.9.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From aa57594d21dbc15b0e714c27f9d9b3d9a57cab2f Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 1 Sep 2022 03:40:13 +0200 Subject: [PATCH 824/903] Required config_flow values for here_travel_time (#75026) --- .../here_travel_time/config_flow.py | 46 ++++++++++++++----- .../components/here_travel_time/const.py | 2 + 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index e8a05796b66..b4756c82922 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -11,6 +11,8 @@ from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, CONF_ENTITY_NAMESPACE, + CONF_LATITUDE, + CONF_LONGITUDE, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, @@ -30,6 +32,8 @@ from .const import ( CONF_ARRIVAL_TIME, CONF_DEPARTURE, CONF_DEPARTURE_TIME, + CONF_DESTINATION, + CONF_ORIGIN, CONF_ROUTE_MODE, CONF_TRAFFIC_MODE, DEFAULT_NAME, @@ -187,13 +191,25 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Configure origin by using gps coordinates.""" if user_input is not None: - self._config[CONF_ORIGIN_LATITUDE] = user_input["origin"]["latitude"] - self._config[CONF_ORIGIN_LONGITUDE] = user_input["origin"]["longitude"] + self._config[CONF_ORIGIN_LATITUDE] = user_input[CONF_ORIGIN][CONF_LATITUDE] + self._config[CONF_ORIGIN_LONGITUDE] = user_input[CONF_ORIGIN][ + CONF_LONGITUDE + ] return self.async_show_menu( step_id="destination_menu", menu_options=["destination_coordinates", "destination_entity"], ) - schema = vol.Schema({"origin": selector({LocationSelector.selector_type: {}})}) + schema = vol.Schema( + { + vol.Required( + CONF_ORIGIN, + default={ + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, + ): LocationSelector() + } + ) return self.async_show_form(step_id="origin_coordinates", data_schema=schema) async def async_step_origin_entity( @@ -206,9 +222,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="destination_menu", menu_options=["destination_coordinates", "destination_entity"], ) - schema = vol.Schema( - {CONF_ORIGIN_ENTITY_ID: selector({EntitySelector.selector_type: {}})} - ) + schema = vol.Schema({vol.Required(CONF_ORIGIN_ENTITY_ID): EntitySelector()}) return self.async_show_form(step_id="origin_entity", data_schema=schema) async def async_step_destination_coordinates( @@ -217,11 +231,11 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Configure destination by using gps coordinates.""" if user_input is not None: - self._config[CONF_DESTINATION_LATITUDE] = user_input["destination"][ - "latitude" + self._config[CONF_DESTINATION_LATITUDE] = user_input[CONF_DESTINATION][ + CONF_LATITUDE ] - self._config[CONF_DESTINATION_LONGITUDE] = user_input["destination"][ - "longitude" + self._config[CONF_DESTINATION_LONGITUDE] = user_input[CONF_DESTINATION][ + CONF_LONGITUDE ] return self.async_create_entry( title=self._config[CONF_NAME], @@ -229,7 +243,15 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options=default_options(self.hass), ) schema = vol.Schema( - {"destination": selector({LocationSelector.selector_type: {}})} + { + vol.Required( + CONF_DESTINATION, + default={ + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, + ): LocationSelector() + } ) return self.async_show_form( step_id="destination_coordinates", data_schema=schema @@ -250,7 +272,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options=default_options(self.hass), ) schema = vol.Schema( - {CONF_DESTINATION_ENTITY_ID: selector({EntitySelector.selector_type: {}})} + {vol.Required(CONF_DESTINATION_ENTITY_ID): EntitySelector()} ) return self.async_show_form(step_id="destination_entity", data_schema=schema) diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index b3768b2d69d..4e9b8beaf12 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -9,9 +9,11 @@ DOMAIN = "here_travel_time" DEFAULT_SCAN_INTERVAL = 300 +CONF_DESTINATION = "destination" CONF_DESTINATION_LATITUDE = "destination_latitude" CONF_DESTINATION_LONGITUDE = "destination_longitude" CONF_DESTINATION_ENTITY_ID = "destination_entity_id" +CONF_ORIGIN = "origin" CONF_ORIGIN_LATITUDE = "origin_latitude" CONF_ORIGIN_LONGITUDE = "origin_longitude" CONF_ORIGIN_ENTITY_ID = "origin_entity_id" From b3830d0f17f3e2dc92007b0837190bb00e95c55f Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 1 Sep 2022 16:46:43 +0800 Subject: [PATCH 825/903] Fix basic browse_media support in forked-daapd (#77595) --- .../components/forked_daapd/const.py | 11 ++++++++ .../components/forked_daapd/media_player.py | 25 ++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index d711ae1b35b..f0d915ce3e5 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -1,6 +1,17 @@ """Const for forked-daapd.""" from homeassistant.components.media_player import MediaPlayerEntityFeature +CAN_PLAY_TYPE = { + "audio/mp4", + "audio/aac", + "audio/mpeg", + "audio/flac", + "audio/ogg", + "audio/x-ms-wma", + "audio/aiff", + "audio/wav", +} + CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port" CONF_MAX_PLAYLISTS = "max_playlists" diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 6c1a772fb4d..953461c1019 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -1,4 +1,6 @@ """This library brings support for forked_daapd to Home Assistant.""" +from __future__ import annotations + import asyncio from collections import defaultdict import logging @@ -8,7 +10,7 @@ from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI from homeassistant.components import media_source -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) @@ -35,6 +37,7 @@ from homeassistant.util.dt import utcnow from .const import ( CALLBACK_TIMEOUT, + CAN_PLAY_TYPE, CONF_LIBRESPOT_JAVA_PORT, CONF_MAX_PLAYLISTS, CONF_TTS_PAUSE_TIME, @@ -769,6 +772,18 @@ class ForkedDaapdMaster(MediaPlayerEntity): )() _LOGGER.warning("No pipe control available for %s", pipe_name) + async def async_browse_media( + self, + media_content_type: str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE, + ) + class ForkedDaapdUpdater: """Manage updates for the forked-daapd device.""" @@ -885,11 +900,3 @@ class ForkedDaapdUpdater: self._api, outputs_to_add, ) - - async def async_browse_media(self, media_content_type=None, media_content_id=None): - """Implement the websocket media browsing helper.""" - return await media_source.async_browse_media( - self.hass, - media_content_id, - content_filter=lambda item: item.media_content_type.startswith("audio/"), - ) From d6b2f0ff7637bfc9c1e423be56670395e02fc31a Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 1 Sep 2022 12:02:46 -0600 Subject: [PATCH 826/903] Code quality improvements for litterrobot integration (#77605) --- .../components/litterrobot/button.py | 52 +++++++++---------- .../components/litterrobot/entity.py | 50 ++++++++++++------ .../components/litterrobot/select.py | 20 ++++--- .../components/litterrobot/sensor.py | 32 +++++------- .../components/litterrobot/switch.py | 25 ++++----- .../components/litterrobot/vacuum.py | 19 +++---- tests/components/litterrobot/test_vacuum.py | 19 +++++++ 7 files changed, 123 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 74c659fd474..81d9c65927e 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -1,21 +1,25 @@ """Support for Litter-Robot button.""" from __future__ import annotations -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import Callable, Coroutine from dataclasses import dataclass import itertools from typing import Any, Generic from pylitterbot import FeederRobot, LitterRobot3 -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.components.button import ( + DOMAIN as PLATFORM, + ButtonEntity, + ButtonEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -26,21 +30,24 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities: Iterable[LitterRobotButtonEntity] = itertools.chain( - ( - LitterRobotButtonEntity( - robot=robot, hub=hub, description=LITTER_ROBOT_BUTTON - ) - for robot in hub.litter_robots() - if isinstance(robot, LitterRobot3) - ), - ( - LitterRobotButtonEntity( - robot=robot, hub=hub, description=FEEDER_ROBOT_BUTTON - ) - for robot in hub.feeder_robots() - ), + entities: list[LitterRobotButtonEntity] = list( + itertools.chain( + ( + LitterRobotButtonEntity( + robot=robot, hub=hub, description=LITTER_ROBOT_BUTTON + ) + for robot in hub.litter_robots() + if isinstance(robot, LitterRobot3) + ), + ( + LitterRobotButtonEntity( + robot=robot, hub=hub, description=FEEDER_ROBOT_BUTTON + ) + for robot in hub.feeder_robots() + ), + ) ) + async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) @@ -76,17 +83,6 @@ class LitterRobotButtonEntity(LitterRobotEntity[_RobotT], ButtonEntity): entity_description: RobotButtonEntityDescription[_RobotT] - def __init__( - self, - robot: _RobotT, - hub: LitterRobotHub, - description: RobotButtonEntityDescription[_RobotT], - ) -> None: - """Initialize a Litter-Robot button entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description - async def async_press(self) -> None: """Press the button.""" await self.entity_description.press_fn(self.robot) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 8471e007ce9..9716793f70e 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,7 +1,7 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Iterable from datetime import time import logging from typing import Any, Generic, TypeVar @@ -10,8 +10,9 @@ from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException from typing_extensions import ParamSpec -from homeassistant.core import CALLBACK_TYPE, callback -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, EntityCategory, EntityDescription +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -36,18 +37,18 @@ class LitterRobotEntity( _attr_has_entity_name = True - def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription + ) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.robot = robot - self.entity_type = entity_type self.hub = hub - self._attr_name = entity_type.capitalize() - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self.robot.serial}-{self.entity_type}" + self.entity_description = description + self._attr_unique_id = f"{self.robot.serial}-{description.key}" + # The following can be removed in 2022.12 after adjusting names in entities appropriately + if description.name is not None: + self._attr_name = description.name.capitalize() @property def device_info(self) -> DeviceInfo: @@ -65,9 +66,11 @@ class LitterRobotEntity( class LitterRobotControlEntity(LitterRobotEntity[_RobotT]): """A Litter-Robot entity that can control the unit.""" - def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription + ) -> None: """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, entity_type=entity_type, hub=hub) + super().__init__(robot=robot, hub=hub, description=description) self._refresh_callback: CALLBACK_TYPE | None = None async def perform_action_and_refresh( @@ -134,9 +137,11 @@ class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]): _attr_entity_category = EntityCategory.CONFIG - def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription + ) -> None: """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, entity_type=entity_type, hub=hub) + super().__init__(robot=robot, hub=hub, description=description) self._assumed_state: bool | None = None async def perform_action_and_assume_state( @@ -146,3 +151,18 @@ class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]): if await self.perform_action_and_refresh(action, assumed_state): self._assumed_state = assumed_state self.async_write_ha_state() + + +def async_update_unique_id( + hass: HomeAssistant, domain: str, entities: Iterable[LitterRobotEntity[_RobotT]] +) -> None: + """Update unique ID to be based on entity description key instead of name. + + Introduced with release 2022.9. + """ + ent_reg = er.async_get(hass) + for entity in entities: + old_unique_id = f"{entity.robot.serial}-{entity.entity_description.name}" + if entity_id := ent_reg.async_get_entity_id(domain, DOMAIN, old_unique_id): + new_unique_id = f"{entity.robot.serial}-{entity.entity_description.key}" + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index a18cd3b46b5..9ec784db8f2 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -8,13 +8,18 @@ from typing import Any, Generic, TypeVar from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.components.select import ( + DOMAIN as PLATFORM, + SelectEntity, + SelectEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TIME_MINUTES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT +from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub _CastTypeT = TypeVar("_CastTypeT", int, float) @@ -40,9 +45,10 @@ class RobotSelectEntityDescription( LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( - key="clean_cycle_wait_time_minutes", + key="cycle_delay", name="Clean Cycle Wait Time Minutes", icon="mdi:timer-outline", + unit_of_measurement=TIME_MINUTES, current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, options_fn=lambda robot: robot.VALID_WAIT_TIMES, select_fn=lambda robot, option: (robot.set_wait_time, int(option)), @@ -65,7 +71,7 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot selects using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( + entities: list[LitterRobotSelect] = list( itertools.chain( ( LitterRobotSelect(robot=robot, hub=hub, description=LITTER_ROBOT_SELECT) @@ -77,6 +83,8 @@ async def async_setup_entry( ), ) ) + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) class LitterRobotSelect( @@ -93,9 +101,7 @@ class LitterRobotSelect( description: RobotSelectEntityDescription[_RobotT, _CastTypeT], ) -> None: """Initialize a Litter-Robot select entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description + super().__init__(robot, hub, description) options = self.entity_description.options_fn(self.robot) self._attr_options = list(map(str, options)) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 90bdfcbda73..c904335d23f 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -9,6 +9,7 @@ from typing import Any, Generic, Union, cast from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot from homeassistant.components.sensor import ( + DOMAIN as PLATFORM, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -20,7 +21,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -48,17 +49,6 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): entity_description: RobotSensorEntityDescription[_RobotT] - def __init__( - self, - robot: _RobotT, - hub: LitterRobotHub, - description: RobotSensorEntityDescription[_RobotT], - ) -> None: - """Initialize a Litter-Robot sensor entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description - @property def native_value(self) -> float | datetime | str | None: """Return the state.""" @@ -79,32 +69,32 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { LitterRobot: [ RobotSensorEntityDescription[LitterRobot]( - name="Waste Drawer", key="waste_drawer_level", + name="Waste Drawer", native_unit_of_measurement=PERCENTAGE, icon_fn=lambda state: icon_for_gauge_level(state, 10), ), RobotSensorEntityDescription[LitterRobot]( - name="Sleep Mode Start Time", key="sleep_mode_start_time", + name="Sleep Mode Start Time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( - name="Sleep Mode End Time", key="sleep_mode_end_time", + name="Sleep Mode End Time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( - name="Last Seen", key="last_seen", + name="Last Seen", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, ), RobotSensorEntityDescription[LitterRobot]( - name="Status Code", key="status_code", + name="Status Code", device_class="litterrobot__status_code", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -119,8 +109,8 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { ], FeederRobot: [ RobotSensorEntityDescription[FeederRobot]( - name="Food level", key="food_level", + name="Food level", native_unit_of_measurement=PERCENTAGE, icon_fn=lambda state: icon_for_gauge_level(state, 10), ) @@ -135,10 +125,12 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot sensors using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ LitterRobotSensorEntity(robot=robot, hub=hub, description=description) for robot in hub.account.robots for robot_type, entity_descriptions in ROBOT_SENSOR_MAP.items() if isinstance(robot, robot_type) for description in entity_descriptions - ) + ] + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 2f54ede38b8..779ee699b41 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -7,13 +7,17 @@ from typing import Any, Generic, Union from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.components.switch import ( + DOMAIN as PLATFORM, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT +from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -51,17 +55,6 @@ class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): entity_description: RobotSwitchEntityDescription[_RobotT] - def __init__( - self, - robot: _RobotT, - hub: LitterRobotHub, - description: RobotSwitchEntityDescription[_RobotT], - ) -> None: - """Initialize a Litter-Robot switch entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description - @property def is_on(self) -> bool | None: """Return true if switch is on.""" @@ -93,9 +86,11 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ RobotSwitchEntity(robot=robot, hub=hub, description=description) for description in ROBOT_SWITCHES for robot in hub.account.robots if isinstance(robot, (LitterRobot, FeederRobot)) - ) + ] + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 9a4b825045f..27cd3e6758a 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,7 +1,6 @@ """Support for Litter-Robot "Vacuum".""" from __future__ import annotations -import logging from typing import Any from pylitterbot import LitterRobot @@ -9,11 +8,13 @@ from pylitterbot.enums import LitterBoxStatus import voluptuous as vol from homeassistant.components.vacuum import ( + DOMAIN as PLATFORM, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, STATE_PAUSED, StateVacuumEntity, + StateVacuumEntityDescription, VacuumEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -23,13 +24,9 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotControlEntity +from .entity import LitterRobotControlEntity, async_update_unique_id from .hub import LitterRobotHub -_LOGGER = logging.getLogger(__name__) - -TYPE_LITTER_BOX = "Litter Box" - SERVICE_SET_SLEEP_MODE = "set_sleep_mode" LITTER_BOX_STATUS_STATE_MAP = { @@ -44,6 +41,8 @@ LITTER_BOX_STATUS_STATE_MAP = { LitterBoxStatus.OFF: STATE_OFF, } +LITTER_BOX_ENTITY = StateVacuumEntityDescription("litter_box", name="Litter Box") + async def async_setup_entry( hass: HomeAssistant, @@ -53,10 +52,12 @@ async def async_setup_entry( """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) + entities = [ + LitterRobotCleaner(robot=robot, hub=hub, description=LITTER_BOX_ENTITY) for robot in hub.litter_robots() - ) + ] + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 02667bb8310..eb9a4c8c60b 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -21,6 +21,7 @@ from homeassistant.components.vacuum import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er from homeassistant.util.dt import utcnow from .common import VACUUM_ENTITY_ID @@ -28,6 +29,9 @@ from .conftest import setup_integration from tests.common import async_fire_time_changed +VACUUM_UNIQUE_ID_OLD = "LR3C012345-Litter Box" +VACUUM_UNIQUE_ID_NEW = "LR3C012345-litter_box" + COMPONENT_SERVICE_DOMAIN = { SERVICE_SET_SLEEP_MODE: DOMAIN, } @@ -35,6 +39,18 @@ COMPONENT_SERVICE_DOMAIN = { async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: """Tests the vacuum entity was set up.""" + ent_reg = er.async_get(hass) + + # Create entity entry to migrate to new unique ID + ent_reg.async_get_or_create( + PLATFORM_DOMAIN, + DOMAIN, + VACUUM_UNIQUE_ID_OLD, + suggested_object_id=VACUUM_ENTITY_ID.replace(PLATFORM_DOMAIN, ""), + ) + ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_OLD + await setup_integration(hass, mock_account, PLATFORM_DOMAIN) assert hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) @@ -43,6 +59,9 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: assert vacuum.state == STATE_DOCKED assert vacuum.attributes["is_sleeping"] is False + ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_NEW + async def test_vacuum_status_when_sleeping( hass: HomeAssistant, mock_account_with_sleeping_robot: MagicMock From 8c697b188106309e949b215e82003b44d54f4dd2 Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 1 Sep 2022 21:02:09 +0300 Subject: [PATCH 827/903] Increase sleep in Risco setup (#77619) --- homeassistant/components/risco/__init__.py | 4 ++++ homeassistant/components/risco/config_flow.py | 5 ----- homeassistant/components/risco/const.py | 2 -- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/risco/test_alarm_control_panel.py | 3 +++ tests/components/risco/test_config_flow.py | 6 ------ 8 files changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index e95b3016139..179ddd5cad6 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -154,6 +154,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: + if is_local(entry): + local_data: LocalData = hass.data[DOMAIN][entry.entry_id] + await local_data.system.disconnect() + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 1befe626347..5e1cdb75b5a 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Risco integration.""" from __future__ import annotations -import asyncio from collections.abc import Mapping import logging @@ -32,7 +31,6 @@ from .const import ( DEFAULT_OPTIONS, DOMAIN, RISCO_STATES, - SLEEP_INTERVAL, TYPE_LOCAL, ) @@ -150,9 +148,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(info["title"]) self._abort_if_unique_id_configured() - # Risco can hang if we don't wait before creating a new connection - await asyncio.sleep(SLEEP_INTERVAL) - return self.async_create_entry( title=info["title"], data={**user_input, **{CONF_TYPE: TYPE_LOCAL}} ) diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index f4ac170d3c7..9f0e71701c6 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -46,5 +46,3 @@ DEFAULT_OPTIONS = { CONF_RISCO_STATES_TO_HA: DEFAULT_RISCO_STATES_TO_HA, CONF_HA_STATES_TO_RISCO: DEFAULT_HA_STATES_TO_RISCO, } - -SLEEP_INTERVAL = 1 diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 38035e22c62..9703b5775bc 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.3"], + "requirements": ["pyrisco==0.5.4"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 50306cf623b..502851c9b83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1814,7 +1814,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.3 +pyrisco==0.5.4 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0356065e94..5efad4639d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1270,7 +1270,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.3 +pyrisco==0.5.4 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 0014e712ab1..1625e78ece6 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -479,6 +479,9 @@ async def test_local_setup(hass, two_part_local_alarm, setup_risco_local): device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1_local")}) assert device is not None assert device.manufacturer == "Risco" + with patch("homeassistant.components.risco.RiscoLocal.disconnect") as mock_close: + await hass.config_entries.async_unload(setup_risco_local.entry_id) + mock_close.assert_awaited_once() async def _check_local_state( diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index a39a724d7b9..396aad8015d 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -72,9 +72,6 @@ async def test_cloud_form(hass): ), patch( "homeassistant.components.risco.config_flow.RiscoCloud.close" ) as mock_close, patch( - "homeassistant.components.risco.config_flow.SLEEP_INTERVAL", - 0, - ), patch( "homeassistant.components.risco.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -168,9 +165,6 @@ async def test_local_form(hass): ), patch( "homeassistant.components.risco.config_flow.RiscoLocal.disconnect" ) as mock_close, patch( - "homeassistant.components.risco.config_flow.SLEEP_INTERVAL", - 0, - ), patch( "homeassistant.components.risco.async_setup_entry", return_value=True, ) as mock_setup_entry: From 68a01562ecfa335ed872ff5d45bc803bb0d04eb1 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Thu, 1 Sep 2022 14:52:06 +0200 Subject: [PATCH 828/903] Add and remove Snapcast client/group callbacks properly (#77624) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/snapcast/media_player.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 703cb41a38f..0e6524c8504 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -125,10 +125,17 @@ class SnapcastGroupDevice(MediaPlayerEntity): def __init__(self, group, uid_part): """Initialize the Snapcast group device.""" - group.set_callback(self.schedule_update_ha_state) self._group = group self._uid = f"{GROUP_PREFIX}{uid_part}_{self._group.identifier}" + async def async_added_to_hass(self) -> None: + """Subscribe to group events.""" + self._group.set_callback(self.schedule_update_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect group object when removed.""" + self._group.set_callback(None) + @property def state(self): """Return the state of the player.""" @@ -213,10 +220,17 @@ class SnapcastClientDevice(MediaPlayerEntity): def __init__(self, client, uid_part): """Initialize the Snapcast client device.""" - client.set_callback(self.schedule_update_ha_state) self._client = client self._uid = f"{CLIENT_PREFIX}{uid_part}_{self._client.identifier}" + async def async_added_to_hass(self) -> None: + """Subscribe to client events.""" + self._client.set_callback(self.schedule_update_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect client object when removed.""" + self._client.set_callback(None) + @property def unique_id(self): """ From 073ca240f136970611db41e6e4c7013d7c008825 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 1 Sep 2022 10:19:21 +0200 Subject: [PATCH 829/903] Required option_flow values for here_travel_time (#77651) --- .../here_travel_time/config_flow.py | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index b4756c82922..d42e6d6bf3e 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -24,7 +24,6 @@ from homeassistant.helpers.selector import ( EntitySelector, LocationSelector, TimeSelector, - selector, ) from .const import ( @@ -361,28 +360,30 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): menu_options=["departure_time", "no_time"], ) - options = { - vol.Optional( - CONF_TRAFFIC_MODE, - default=self.config_entry.options.get( - CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED - ), - ): vol.In(TRAFFIC_MODES), - vol.Optional( - CONF_ROUTE_MODE, - default=self.config_entry.options.get( - CONF_ROUTE_MODE, ROUTE_MODE_FASTEST - ), - ): vol.In(ROUTE_MODES), - vol.Optional( - CONF_UNIT_SYSTEM, - default=self.config_entry.options.get( - CONF_UNIT_SYSTEM, self.hass.config.units.name - ), - ): vol.In(UNITS), - } + schema = vol.Schema( + { + vol.Optional( + CONF_TRAFFIC_MODE, + default=self.config_entry.options.get( + CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED + ), + ): vol.In(TRAFFIC_MODES), + vol.Optional( + CONF_ROUTE_MODE, + default=self.config_entry.options.get( + CONF_ROUTE_MODE, ROUTE_MODE_FASTEST + ), + ): vol.In(ROUTE_MODES), + vol.Optional( + CONF_UNIT_SYSTEM, + default=self.config_entry.options.get( + CONF_UNIT_SYSTEM, self.hass.config.units.name + ), + ): vol.In(UNITS), + } + ) - return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + return self.async_show_form(step_id="init", data_schema=schema) async def async_step_no_time( self, user_input: dict[str, Any] | None = None @@ -398,12 +399,12 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): self._config[CONF_ARRIVAL_TIME] = user_input[CONF_ARRIVAL_TIME] return self.async_create_entry(title="", data=self._config) - options = {"arrival_time": selector({TimeSelector.selector_type: {}})} - - return self.async_show_form( - step_id="arrival_time", data_schema=vol.Schema(options) + schema = vol.Schema( + {vol.Required(CONF_ARRIVAL_TIME, default="00:00:00"): TimeSelector()} ) + return self.async_show_form(step_id="arrival_time", data_schema=schema) + async def async_step_departure_time( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -412,8 +413,8 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): self._config[CONF_DEPARTURE_TIME] = user_input[CONF_DEPARTURE_TIME] return self.async_create_entry(title="", data=self._config) - options = {"departure_time": selector({TimeSelector.selector_type: {}})} - - return self.async_show_form( - step_id="departure_time", data_schema=vol.Schema(options) + schema = vol.Schema( + {vol.Required(CONF_DEPARTURE_TIME, default="00:00:00"): TimeSelector()} ) + + return self.async_show_form(step_id="departure_time", data_schema=schema) From 37e425db30bd807100cd7d4cb267dcca0477bb9d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Sep 2022 17:45:19 +0200 Subject: [PATCH 830/903] Clean up user overridden device class in entity registry (#77662) --- homeassistant/helpers/entity_registry.py | 14 ++++- tests/helpers/test_entity_registry.py | 72 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 23e9cc5f752..d495d196440 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -30,6 +30,7 @@ from homeassistant.const import ( MAX_LENGTH_STATE_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) from homeassistant.core import ( Event, @@ -62,7 +63,7 @@ SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 7 +STORAGE_VERSION_MINOR = 8 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -970,10 +971,19 @@ async def _async_migrate( entity["hidden_by"] = None if old_major_version == 1 and old_minor_version < 7: - # Version 1.6 adds has_entity_name + # Version 1.7 adds has_entity_name for entity in data["entities"]: entity["has_entity_name"] = False + if old_major_version == 1 and old_minor_version < 8: + # Cleanup after frontend bug which incorrectly updated device_class + # Fixed by frontend PR #13551 + for entity in data["entities"]: + domain = split_entity_id(entity["entity_id"])[0] + if domain in [Platform.BINARY_SENSOR, Platform.COVER]: + continue + entity["device_class"] = None + if old_major_version > 1: raise NotImplementedError return data diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 9c2592eace0..e4c371a0198 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -528,6 +528,78 @@ async def test_migration_1_1(hass, hass_storage): assert entry.original_device_class == "best_class" +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_7(hass, hass_storage): + """Test migration from version 1.7. + + This tests cleanup after frontend bug which incorrectly updated device_class + """ + entity_dict = { + "area_id": None, + "capabilities": {}, + "config_entry_id": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": "12345", + "name": None, + "options": None, + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "supported_features": 0, + "unique_id": "very_unique", + "unit_of_measurement": None, + } + + hass_storage[er.STORAGE_KEY] = { + "version": 1, + "minor_version": 7, + "data": { + "entities": [ + { + **entity_dict, + "device_class": "original_class_by_integration", + "entity_id": "test.entity", + "original_device_class": "new_class_by_integration", + }, + { + **entity_dict, + "device_class": "class_by_user", + "entity_id": "binary_sensor.entity", + "original_device_class": "class_by_integration", + }, + { + **entity_dict, + "device_class": "class_by_user", + "entity_id": "cover.entity", + "original_device_class": "class_by_integration", + }, + ] + }, + } + + await er.async_load(hass) + registry = er.async_get(hass) + + entry = registry.async_get_or_create("test", "super_platform", "very_unique") + assert entry.device_class is None + assert entry.original_device_class == "new_class_by_integration" + + entry = registry.async_get_or_create( + "binary_sensor", "super_platform", "very_unique" + ) + assert entry.device_class == "class_by_user" + assert entry.original_device_class == "class_by_integration" + + entry = registry.async_get_or_create("cover", "super_platform", "very_unique") + assert entry.device_class == "class_by_user" + assert entry.original_device_class == "class_by_integration" + + @pytest.mark.parametrize("load_registries", [False]) async def test_loading_invalid_entity_id(hass, hass_storage): """Test we skip entities with invalid entity IDs.""" From 377791d6e7401b512b9110d1c2e1e4e1eccdce4a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Sep 2022 17:51:27 +0200 Subject: [PATCH 831/903] Include entity registry id in entity registry WS API (#77668) --- homeassistant/components/config/entity_registry.py | 1 + tests/components/config/test_entity_registry.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 6d022aa2d14..cbfd092bc0c 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -237,6 +237,7 @@ def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: "entity_id": entry.entity_id, "hidden_by": entry.hidden_by, "icon": entry.icon, + "id": entry.id, "name": entry.name, "original_name": entry.original_name, "platform": entry.platform, diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 11a2adb5646..30153195eec 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1,4 +1,6 @@ """Test entity_registry API.""" +from unittest.mock import ANY + import pytest from homeassistant.components.config import entity_registry @@ -67,6 +69,7 @@ async def test_list_entities(hass, client): "has_entity_name": False, "hidden_by": None, "icon": None, + "id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", @@ -81,6 +84,7 @@ async def test_list_entities(hass, client): "has_entity_name": False, "hidden_by": None, "icon": None, + "id": ANY, "name": None, "original_name": None, "platform": "test_platform", @@ -117,6 +121,7 @@ async def test_list_entities(hass, client): "has_entity_name": False, "hidden_by": None, "icon": None, + "id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", @@ -159,6 +164,7 @@ async def test_get_entity(hass, client): "entity_id": "test_domain.name", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": "Hello World", "options": {}, @@ -189,6 +195,7 @@ async def test_get_entity(hass, client): "entity_id": "test_domain.no_name", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": None, "options": {}, @@ -252,6 +259,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "id": ANY, "has_entity_name": False, "name": "after update", "options": {}, @@ -324,6 +332,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "id": ANY, "has_entity_name": False, "name": "after update", "options": {}, @@ -361,6 +370,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "id": ANY, "has_entity_name": False, "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, @@ -409,6 +419,7 @@ async def test_update_entity_require_restart(hass, client): "entity_category": None, "entity_id": entity_id, "icon": None, + "id": ANY, "hidden_by": None, "has_entity_name": False, "name": None, @@ -515,6 +526,7 @@ async def test_update_entity_no_changes(hass, client): "entity_id": "test_domain.world", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": "name of entity", "options": {}, @@ -601,6 +613,7 @@ async def test_update_entity_id(hass, client): "entity_id": "test_domain.planet", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": None, "options": {}, From ee0e12ac460bab29d557968f700835a0ffb4ef18 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 1 Sep 2022 17:57:49 +0100 Subject: [PATCH 832/903] Fix async_all_discovered_devices(False) to return connectable and unconnectable devices (#77670) --- homeassistant/components/bluetooth/manager.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index d2b59469bd9..d274939c610 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -221,10 +221,14 @@ class BluetoothManager: @hass_callback def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]: """Return all of discovered devices from all the scanners including duplicates.""" - return itertools.chain.from_iterable( - scanner.discovered_devices - for scanner in self._get_scanners_by_type(connectable) + yield from itertools.chain.from_iterable( + scanner.discovered_devices for scanner in self._get_scanners_by_type(True) ) + if not connectable: + yield from itertools.chain.from_iterable( + scanner.discovered_devices + for scanner in self._get_scanners_by_type(False) + ) @hass_callback def async_discovered_devices(self, connectable: bool) -> list[BLEDevice]: From 0cdbb295bc617d5f333fd846ada4dcbc0077f2ce Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 2 Sep 2022 00:08:14 +0200 Subject: [PATCH 833/903] bump pynetgear to 0.10.8 (#77672) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 69a21e5aace..92b3065147c 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.7"], + "requirements": ["pynetgear==0.10.8"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 502851c9b83..a7b8fcc8e0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.7 +pynetgear==0.10.8 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5efad4639d7..a733b8debea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1199,7 +1199,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.7 +pynetgear==0.10.8 # homeassistant.components.nina pynina==0.1.8 From c9d4924deadf715a093ebb1e19c1082381715ce6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Sep 2022 19:00:14 +0000 Subject: [PATCH 834/903] Bump pySwitchbot to 0.18.22 (#77673) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index cda3f958f5c..9fb73a62dd6 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.21"], + "requirements": ["PySwitchbot==0.18.22"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a7b8fcc8e0e..2b17b73d29c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.21 +PySwitchbot==0.18.22 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a733b8debea..b99703f4f07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.21 +PySwitchbot==0.18.22 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From dc2c0a159f8c78d380cdba552d9b06edd517487b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Sep 2022 18:10:20 +0000 Subject: [PATCH 835/903] Ensure unique id is set for esphome when setup via user flow (#77677) --- homeassistant/components/esphome/__init__.py | 4 +++ .../components/esphome/config_flow.py | 2 ++ tests/components/esphome/test_config_flow.py | 30 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2a885ed90ec..07b6d3071f6 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -333,6 +333,10 @@ async def async_setup_entry( # noqa: C901 if entry_data.device_info is not None and entry_data.device_info.name: cli.expected_name = entry_data.device_info.name reconnect_logic.name = entry_data.device_info.name + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=entry_data.device_info.name + ) await reconnect_logic.start() entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 9fd12634e43..ea64fb7fb7f 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -331,6 +331,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): await cli.disconnect(force=True) self._name = self._device_info.name + await self.async_set_unique_id(self._name, raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) return None diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 43e2f916082..a4d1f416868 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -81,6 +81,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): CONF_NOISE_PSK: "", } assert result["title"] == "test" + assert result["result"].unique_id == "test" assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 @@ -91,6 +92,35 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): assert mock_client.noise_psk is None +async def test_user_connection_updates_host(hass, mock_client, mock_zeroconf): + """Test setup up the same name updates the host.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="test", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data=None, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == "127.0.0.1" + + async def test_user_resolve_error(hass, mock_client, mock_zeroconf): """Test user step with IP resolve error.""" From 329c6920650e0d257d4d57c520e6f3d8213f92bd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Sep 2022 15:00:50 -0400 Subject: [PATCH 836/903] Pin Pandas 1.4.3 (#77679) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a456e9ec965..84edd18206c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -126,3 +126,6 @@ pubnub!=6.4.0 # Package's __init__.pyi stub has invalid syntax and breaks mypy # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 + +# Pandas 1.4.4 has issues with wheels om armhf + Py3.10 +pandas==1.4.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3709d4cff08..d0eb830f088 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -139,6 +139,9 @@ pubnub!=6.4.0 # Package's __init__.pyi stub has invalid syntax and breaks mypy # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 + +# Pandas 1.4.4 has issues with wheels om armhf + Py3.10 +pandas==1.4.3 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From f4273a098da8df7b0d024ff18d7b54d92b96f4f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Sep 2022 01:22:12 +0000 Subject: [PATCH 837/903] Bump bluetooth-adapters to 0.3.3 (#77683) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 981b7854e36..9bc4c50a1e4 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.2", + "bluetooth-adapters==0.3.3", "bluetooth-auto-recovery==0.3.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 84edd18206c..56a6e5efd05 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.2 +bluetooth-adapters==0.3.3 bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2b17b73d29c..5a69abaf5d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.2 +bluetooth-adapters==0.3.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b99703f4f07..85d67928246 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.2 +bluetooth-adapters==0.3.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 From 1f9c5ff3699f47b77ebf3e2395b6a1a189c7ccb2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Sep 2022 21:24:30 -0400 Subject: [PATCH 838/903] Bump frontend to 20220901.0 (#77689) --- 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 1bf8962d615..ebaa83f8d46 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==20220831.0"], + "requirements": ["home-assistant-frontend==20220901.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 56a6e5efd05..1fe755a9321 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220831.0 +home-assistant-frontend==20220901.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5a69abaf5d0..46a997631f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -848,7 +848,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220831.0 +home-assistant-frontend==20220901.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85d67928246..a46bf30baae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220831.0 +home-assistant-frontend==20220901.0 # homeassistant.components.home_connect homeconnect==0.7.2 From a10a16ab21b26cb17276dfff537eb50b2deec66c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Sep 2022 21:25:12 -0400 Subject: [PATCH 839/903] Bumped version to 2022.9.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8d5242123e5..b3ca5fd9fe3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index a2e1c4063a9..993fcd99839 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b1" +version = "2022.9.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 36c1b9a4191197420374d0dd034f769bffe1a3ad Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 1 Sep 2022 04:49:36 -0400 Subject: [PATCH 840/903] Fix timezone edge cases for Unifi Protect media source (#77636) * Fixes timezone edge cases for Unifi Protect media source * linting --- .../components/unifiprotect/media_source.py | 19 ++- .../unifiprotect/test_media_source.py | 133 ++++++++++++++++-- 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 58b14ab9b3b..4910c18cf5f 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -101,12 +101,12 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource: @callback -def _get_start_end(hass: HomeAssistant, start: datetime) -> tuple[datetime, datetime]: +def _get_month_start_end(start: datetime) -> tuple[datetime, datetime]: start = dt_util.as_local(start) end = dt_util.now() - start = start.replace(day=1, hour=1, minute=0, second=0, microsecond=0) - end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + start = start.replace(day=1, hour=0, minute=0, second=1, microsecond=0) + end = end.replace(day=1, hour=0, minute=0, second=2, microsecond=0) return start, end @@ -571,9 +571,16 @@ class ProtectMediaSource(MediaSource): if not build_children: return source - month = start.month + if data.api.bootstrap.recording_start is not None: + recording_start = data.api.bootstrap.recording_start.date() + start = max(recording_start, start) + + recording_end = dt_util.now().date() + end = start.replace(month=start.month + 1) - timedelta(days=1) + end = min(recording_end, end) + children = [self._build_days(data, camera_id, event_type, start, is_all=True)] - while start.month == month: + while start <= end: children.append( self._build_days(data, camera_id, event_type, start, is_all=False) ) @@ -702,7 +709,7 @@ class ProtectMediaSource(MediaSource): self._build_recent(data, camera_id, event_type, 30), ] - start, end = _get_start_end(self.hass, data.api.bootstrap.recording_start) + start, end = _get_month_start_end(data.api.bootstrap.recording_start) while end > start: children.append(self._build_month(data, camera_id, event_type, end.date())) end = (end - timedelta(days=1)).replace(day=1) diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index bb3bc8aa345..74a007e0ba0 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -4,7 +4,9 @@ from datetime import datetime, timedelta from ipaddress import IPv4Address from unittest.mock import AsyncMock, Mock, patch +from freezegun import freeze_time import pytest +import pytz from pyunifiprotect.data import ( Bootstrap, Camera, @@ -28,6 +30,7 @@ from homeassistant.components.unifiprotect.media_source import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .conftest import MockUFPFixture from .utils import init_entry @@ -430,13 +433,52 @@ async def test_browse_media_event_type( assert browse.children[3].identifier == "test_id:browse:all:smart" +ONE_MONTH_SIMPLE = ( + datetime( + year=2022, + month=9, + day=1, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 1, +) +TWO_MONTH_SIMPLE = ( + datetime( + year=2022, + month=8, + day=31, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 2, +) + + +@pytest.mark.parametrize( + "start,months", + [ONE_MONTH_SIMPLE, TWO_MONTH_SIMPLE], +) +@freeze_time("2022-09-15 03:00:00-07:00") async def test_browse_media_time( - hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + start: datetime, + months: int, ): """Test browsing time selector level media.""" - last_month = fixed_now.replace(day=1) - timedelta(days=1) - ufp.api.bootstrap._recording_start = last_month + end = datetime.fromisoformat("2022-09-15 03:00:00-07:00") + end_local = dt_util.as_local(end) + + ufp.api.bootstrap._recording_start = dt_util.as_utc(start) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) await init_entry(hass, ufp, [doorbell], regenerate_ids=False) @@ -449,17 +491,89 @@ async def test_browse_media_time( assert browse.title == f"UnifiProtect > {doorbell.name} > All Events" assert browse.identifier == base_id - assert len(browse.children) == 4 + assert len(browse.children) == 3 + months assert browse.children[0].title == "Last 24 Hours" assert browse.children[0].identifier == f"{base_id}:recent:1" assert browse.children[1].title == "Last 7 Days" assert browse.children[1].identifier == f"{base_id}:recent:7" assert browse.children[2].title == "Last 30 Days" assert browse.children[2].identifier == f"{base_id}:recent:30" - assert browse.children[3].title == f"{fixed_now.strftime('%B %Y')}" + assert browse.children[3].title == f"{end_local.strftime('%B %Y')}" assert ( browse.children[3].identifier - == f"{base_id}:range:{fixed_now.year}:{fixed_now.month}" + == f"{base_id}:range:{end_local.year}:{end_local.month}" + ) + + +ONE_MONTH_TIMEZONE = ( + datetime( + year=2022, + month=8, + day=1, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 1, +) +TWO_MONTH_TIMEZONE = ( + datetime( + year=2022, + month=7, + day=31, + hour=21, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 2, +) + + +@pytest.mark.parametrize( + "start,months", + [ONE_MONTH_TIMEZONE, TWO_MONTH_TIMEZONE], +) +@freeze_time("2022-08-31 21:00:00-07:00") +async def test_browse_media_time_timezone( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + start: datetime, + months: int, +): + """Test browsing time selector level media.""" + + end = datetime.fromisoformat("2022-08-31 21:00:00-07:00") + end_local = dt_util.as_local(end) + + ufp.api.bootstrap._recording_start = dt_util.as_utc(start) + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + base_id = f"test_id:browse:{doorbell.id}:all" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == f"UnifiProtect > {doorbell.name} > All Events" + assert browse.identifier == base_id + assert len(browse.children) == 3 + months + assert browse.children[0].title == "Last 24 Hours" + assert browse.children[0].identifier == f"{base_id}:recent:1" + assert browse.children[1].title == "Last 7 Days" + assert browse.children[1].identifier == f"{base_id}:recent:7" + assert browse.children[2].title == "Last 30 Days" + assert browse.children[2].identifier == f"{base_id}:recent:30" + assert browse.children[3].title == f"{end_local.strftime('%B %Y')}" + assert ( + browse.children[3].identifier + == f"{base_id}:range:{end_local.year}:{end_local.month}" ) @@ -599,13 +713,14 @@ async def test_browse_media_eventthumb( assert browse.media_class == MEDIA_CLASS_IMAGE +@freeze_time("2022-09-15 03:00:00-07:00") async def test_browse_media_day( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime ): """Test browsing day selector level media.""" - last_month = fixed_now.replace(day=1) - timedelta(days=1) - ufp.api.bootstrap._recording_start = last_month + start = datetime.fromisoformat("2022-09-03 03:00:00-07:00") + ufp.api.bootstrap._recording_start = dt_util.as_utc(start) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) await init_entry(hass, ufp, [doorbell], regenerate_ids=False) @@ -623,7 +738,7 @@ async def test_browse_media_day( == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')}" ) assert browse.identifier == base_id - assert len(browse.children) in (29, 30, 31, 32) + assert len(browse.children) == 14 assert browse.children[0].title == "Whole Month" assert browse.children[0].identifier == f"{base_id}:all" From 9652c0c3267c578aa9aca00ac50304e372043f37 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 2 Sep 2022 14:18:10 -0600 Subject: [PATCH 841/903] Adjust litterrobot platform loading/unloading (#77682) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/litterrobot/__init__.py | 57 +++++++------------ tests/components/litterrobot/conftest.py | 4 +- tests/components/litterrobot/test_vacuum.py | 9 ++- 3 files changed, 30 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index d302989fc01..742e9dcb9c7 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,7 +1,7 @@ """The Litter-Robot integration.""" from __future__ import annotations -from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4 +from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, Robot from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -10,65 +10,48 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .hub import LitterRobotHub -PLATFORMS = [ - Platform.BUTTON, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, -] - PLATFORMS_BY_TYPE = { - LitterRobot: ( - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ), - LitterRobot3: ( - Platform.BUTTON, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ), - LitterRobot4: ( - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ), - FeederRobot: ( - Platform.BUTTON, + Robot: ( Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ), + LitterRobot: (Platform.VACUUM,), + LitterRobot3: (Platform.BUTTON,), + FeederRobot: (Platform.BUTTON,), } +def get_platforms_for_robots(robots: list[Robot]) -> set[Platform]: + """Get platforms for robots.""" + return { + platform + for robot in robots + for robot_type, platforms in PLATFORMS_BY_TYPE.items() + if isinstance(robot, robot_type) + for platform in platforms + } + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) await hub.login(load_robots=True) - platforms: set[str] = set() - for robot in hub.account.robots: - platforms.update(PLATFORMS_BY_TYPE[type(robot)]) - if platforms: + if platforms := get_platforms_for_robots(hub.account.robots): await hass.config_entries.async_forward_entry_setups(entry, platforms) - return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] await hub.account.disconnect() + platforms = get_platforms_for_robots(hub.account.robots) + unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index d5d29e12988..e5d5e730b61 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -99,8 +99,8 @@ async def setup_integration( with patch( "homeassistant.components.litterrobot.hub.Account", return_value=mock_account ), patch( - "homeassistant.components.litterrobot.PLATFORMS", - [platform_domain] if platform_domain else [], + "homeassistant.components.litterrobot.PLATFORMS_BY_TYPE", + {Robot: (platform_domain,)} if platform_domain else {}, ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index eb9a4c8c60b..08aa8b2399b 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -52,6 +52,7 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_OLD await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + assert len(ent_reg.entities) == 1 assert hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) vacuum = hass.states.get(VACUUM_ENTITY_ID) @@ -78,10 +79,16 @@ async def test_no_robots( hass: HomeAssistant, mock_account_with_no_robots: MagicMock ) -> None: """Tests the vacuum entity was set up.""" - await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) + entry = await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) assert not hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) + ent_reg = er.async_get(hass) + assert len(ent_reg.entities) == 0 + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + async def test_vacuum_with_error( hass: HomeAssistant, mock_account_with_error: MagicMock From 6fff63332524eb75b6693950fab5e07b6b0fc6c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Sep 2022 20:17:35 +0000 Subject: [PATCH 842/903] Bump bluetooth-adapters to 3.3.4 (#77705) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9bc4c50a1e4..b98312040f0 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.3", + "bluetooth-adapters==0.3.4", "bluetooth-auto-recovery==0.3.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1fe755a9321..8cfb71c5db0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.3 +bluetooth-adapters==0.3.4 bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 46a997631f2..265d7b277e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.3 +bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a46bf30baae..63a5a9bb744 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.3 +bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 From 1d2439a6e54e8a5004de3908d905b8bbb7073a3d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 2 Sep 2022 10:50:05 -0400 Subject: [PATCH 843/903] Change zwave_js firmware update service API key (#77719) * Change zwave_js firmware update service API key * Update const.py --- homeassistant/components/zwave_js/const.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index cd10109bb3d..ddd4917e596 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -123,4 +123,6 @@ ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" # This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for # your own (https://github.com/zwave-js/firmware-updates/). -API_KEY_FIRMWARE_UPDATE_SERVICE = "b48e74337db217f44e1e003abb1e9144007d260a17e2b2422e0a45d0eaf6f4ad86f2a9943f17fee6dde343941f238a64" +API_KEY_FIRMWARE_UPDATE_SERVICE = ( + "55eea74f055bef2ad893348112df6a38980600aaf82d2b02011297fc7ba495f830ca2b70" +) From d6a99da461fa5b9b0b2defc9afbfcf52b96a1a66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Sep 2022 20:53:33 -0400 Subject: [PATCH 844/903] Bump frontend to 20220902.0 (#77734) --- 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 ebaa83f8d46..8459d08eab7 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==20220901.0"], + "requirements": ["home-assistant-frontend==20220902.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8cfb71c5db0..2e41ae13458 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220901.0 +home-assistant-frontend==20220902.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 265d7b277e2..51b7a506ef3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -848,7 +848,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220901.0 +home-assistant-frontend==20220902.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63a5a9bb744..1b613121245 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220901.0 +home-assistant-frontend==20220902.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 041eaf27a96fbc8206b9c13d2864452cf9ff5145 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Sep 2022 20:54:37 -0400 Subject: [PATCH 845/903] Bumped version to 2022.9.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b3ca5fd9fe3..502dd3510e2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 993fcd99839..3d608e722a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b2" +version = "2022.9.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bc04755d05871fe3052324bcbbbcd7e043db4ffc Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 2 Sep 2022 23:50:52 +0200 Subject: [PATCH 846/903] Register xiaomi_miio unload callbacks later in setup (#76714) --- homeassistant/components/xiaomi_miio/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 9f0be1da528..8719319aec8 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -387,8 +387,6 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> if gateway_id.endswith("-gateway"): hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) - entry.async_on_unload(entry.add_update_listener(update_listener)) - # Connect to gateway gateway = ConnectXiaomiGateway(hass, entry) try: @@ -444,6 +442,8 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> await hass.config_entries.async_forward_entry_setups(entry, GATEWAY_PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Xiaomi Miio device component from a config entry.""" @@ -453,10 +453,10 @@ async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> b if not platforms: return False - entry.async_on_unload(entry.add_update_listener(update_listener)) - await hass.config_entries.async_forward_entry_setups(entry, platforms) + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True From cd4c31bc79aa98c1e363a1220125b439e2c5c8e8 Mon Sep 17 00:00:00 2001 From: Simon Hansen <67142049+DurgNomis-drol@users.noreply.github.com> Date: Sat, 3 Sep 2022 10:32:03 +0200 Subject: [PATCH 847/903] Convert platform in iss integration (#77218) * Hopefully fix everthing and be happy * ... * update coverage file * Fix tests --- .coveragerc | 4 +-- homeassistant/components/iss/__init__.py | 14 +++------ homeassistant/components/iss/config_flow.py | 7 ++--- .../iss/{binary_sensor.py => sensor.py} | 31 ++++++------------- tests/components/iss/test_config_flow.py | 17 ---------- 5 files changed, 17 insertions(+), 56 deletions(-) rename homeassistant/components/iss/{binary_sensor.py => sensor.py} (67%) diff --git a/.coveragerc b/.coveragerc index 3ff0d49965c..99d98d36e6b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -587,7 +587,7 @@ omit = homeassistant/components/iqvia/sensor.py homeassistant/components/irish_rail_transport/sensor.py homeassistant/components/iss/__init__.py - homeassistant/components/iss/binary_sensor.py + homeassistant/components/iss/sensor.py homeassistant/components/isy994/__init__.py homeassistant/components/isy994/binary_sensor.py homeassistant/components/isy994/climate.py @@ -1216,7 +1216,7 @@ omit = homeassistant/components/switchbot/const.py homeassistant/components/switchbot/entity.py homeassistant/components/switchbot/cover.py - homeassistant/components/switchbot/light.py + homeassistant/components/switchbot/light.py homeassistant/components/switchbot/sensor.py homeassistant/components/switchbot/coordinator.py homeassistant/components/switchmate/switch.py diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py index d6065fd4f78..640e9d5d1da 100644 --- a/homeassistant/components/iss/__init__.py +++ b/homeassistant/components/iss/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import timedelta import logging import pyiss @@ -18,7 +18,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [Platform.SENSOR] @dataclass @@ -27,31 +27,25 @@ class IssData: number_of_people_in_space: int current_location: dict[str, str] - is_above: bool - next_rise: datetime -def update(iss: pyiss.ISS, latitude: float, longitude: float) -> IssData: +def update(iss: pyiss.ISS) -> IssData: """Retrieve data from the pyiss API.""" return IssData( number_of_people_in_space=iss.number_of_people_in_space(), current_location=iss.current_location(), - is_above=iss.is_ISS_above(latitude, longitude), - next_rise=iss.next_rise(latitude, longitude), ) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up this integration using UI.""" hass.data.setdefault(DOMAIN, {}) - latitude = hass.config.latitude - longitude = hass.config.longitude iss = pyiss.ISS() async def async_update() -> IssData: try: - return await hass.async_add_executor_job(update, iss, latitude, longitude) + return await hass.async_add_executor_job(update, iss) except (HTTPError, requests.exceptions.ConnectionError) as ex: raise UpdateFailed("Unable to retrieve data") from ex diff --git a/homeassistant/components/iss/config_flow.py b/homeassistant/components/iss/config_flow.py index b43949daadc..ebfd445f62c 100644 --- a/homeassistant/components/iss/config_flow.py +++ b/homeassistant/components/iss/config_flow.py @@ -7,9 +7,10 @@ from homeassistant.const import CONF_NAME, CONF_SHOW_ON_MAP from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .binary_sensor import DEFAULT_NAME from .const import DOMAIN +DEFAULT_NAME = "ISS" + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for iss component.""" @@ -30,10 +31,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - # Check if location have been defined. - if not self.hass.config.latitude and not self.hass.config.longitude: - return self.async_abort(reason="latitude_longitude_not_defined") - if user_input is not None: return self.async_create_entry( title=user_input.get(CONF_NAME, DEFAULT_NAME), diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/sensor.py similarity index 67% rename from homeassistant/components/iss/binary_sensor.py rename to homeassistant/components/iss/sensor.py index 77cb86fc45a..fac23dfd9fa 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/sensor.py @@ -1,10 +1,10 @@ -"""Support for iss binary sensor.""" +"""Support for iss sensor.""" from __future__ import annotations import logging from typing import Any -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant @@ -19,12 +19,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -ATTR_ISS_NEXT_RISE = "next_rise" -ATTR_ISS_NUMBER_PEOPLE_SPACE = "number_of_people_in_space" - -DEFAULT_NAME = "ISS" -DEFAULT_DEVICE_CLASS = "visible" - async def async_setup_entry( hass: HomeAssistant, @@ -37,15 +31,11 @@ async def async_setup_entry( name = entry.title show_on_map = entry.options.get(CONF_SHOW_ON_MAP, False) - async_add_entities([IssBinarySensor(coordinator, name, show_on_map)]) + async_add_entities([IssSensor(coordinator, name, show_on_map)]) -class IssBinarySensor( - CoordinatorEntity[DataUpdateCoordinator[IssData]], BinarySensorEntity -): - """Implementation of the ISS binary sensor.""" - - _attr_device_class = DEFAULT_DEVICE_CLASS +class IssSensor(CoordinatorEntity[DataUpdateCoordinator[IssData]], SensorEntity): + """Implementation of the ISS sensor.""" def __init__( self, coordinator: DataUpdateCoordinator[IssData], name: str, show: bool @@ -57,17 +47,14 @@ class IssBinarySensor( self._show_on_map = show @property - def is_on(self) -> bool: - """Return true if the binary sensor is on.""" - return self.coordinator.data.is_above is True + def native_value(self) -> int: + """Return number of people in space.""" + return self.coordinator.data.number_of_people_in_space @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - attrs = { - ATTR_ISS_NUMBER_PEOPLE_SPACE: self.coordinator.data.number_of_people_in_space, - ATTR_ISS_NEXT_RISE: self.coordinator.data.next_rise, - } + attrs = {} if self._show_on_map: attrs[ATTR_LONGITUDE] = self.coordinator.data.current_location.get( "longitude" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index eabca610ddf..a806bea3056 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -3,7 +3,6 @@ from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.iss.const import DOMAIN -from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant @@ -48,22 +47,6 @@ async def test_integration_already_exists(hass: HomeAssistant): assert result.get("reason") == "single_instance_allowed" -async def test_abort_no_home(hass: HomeAssistant): - """Test we don't create an entry if no coordinates are set.""" - - await async_process_ha_core_config( - hass, - {"latitude": 0.0, "longitude": 0.0}, - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={} - ) - - assert result.get("type") == data_entry_flow.FlowResultType.ABORT - assert result.get("reason") == "latitude_longitude_not_defined" - - async def test_options(hass: HomeAssistant): """Test options flow.""" From 0e930fd626c4ce41e531c1c0732f89bdbc781b26 Mon Sep 17 00:00:00 2001 From: Pete Date: Sat, 3 Sep 2022 22:55:34 +0200 Subject: [PATCH 848/903] Fix setting and reading percentage for MIOT based fans (#77626) --- homeassistant/components/xiaomi_miio/const.py | 9 +++++ homeassistant/components/xiaomi_miio/fan.py | 36 ++++++++++++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c0711a02a36..11922956c25 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -112,6 +112,15 @@ MODELS_FAN_MIOT = [ MODEL_FAN_ZA5, ] +# number of speed levels each fan has +SPEEDS_FAN_MIOT = { + MODEL_FAN_1C: 3, + MODEL_FAN_P10: 4, + MODEL_FAN_P11: 4, + MODEL_FAN_P9: 4, + MODEL_FAN_ZA5: 4, +} + MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3C, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 39988976564..901211d1d2d 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -85,6 +85,7 @@ from .const import ( MODELS_PURIFIER_MIOT, SERVICE_RESET_FILTER, SERVICE_SET_EXTRA_FEATURES, + SPEEDS_FAN_MIOT, ) from .device import XiaomiCoordinatedMiioEntity @@ -234,9 +235,13 @@ async def async_setup_entry( elif model in MODELS_FAN_MIIO: entity = XiaomiFan(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_ZA5: - entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator) + speed_count = SPEEDS_FAN_MIOT[model] + entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator, speed_count) elif model in MODELS_FAN_MIOT: - entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator) + speed_count = SPEEDS_FAN_MIOT[model] + entity = XiaomiFanMiot( + device, config_entry, unique_id, coordinator, speed_count + ) else: return @@ -1044,6 +1049,11 @@ class XiaomiFanP5(XiaomiGenericFan): class XiaomiFanMiot(XiaomiGenericFan): """Representation of a Xiaomi Fan Miot.""" + def __init__(self, device, entry, unique_id, coordinator, speed_count): + """Initialize MIOT fan with speed count.""" + super().__init__(device, entry, unique_id, coordinator) + self._speed_count = speed_count + @property def operation_mode_class(self): """Hold operation mode class.""" @@ -1061,7 +1071,9 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: - self._percentage = self.coordinator.data.speed + self._percentage = ranged_value_to_percentage( + (1, self._speed_count), self.coordinator.data.speed + ) else: self._percentage = 0 @@ -1087,16 +1099,22 @@ class XiaomiFanMiot(XiaomiGenericFan): await self.async_turn_off() return - await self._try_command( - "Setting fan speed percentage of the miio device failed.", - self._device.set_speed, - percentage, + speed = math.ceil( + percentage_to_ranged_value((1, self._speed_count), percentage) ) - self._percentage = percentage + # if the fan is not on, we have to turn it on first if not self.is_on: await self.async_turn_on() - else: + + result = await self._try_command( + "Setting fan speed percentage of the miio device failed.", + self._device.set_speed, + speed, + ) + + if result: + self._percentage = ranged_value_to_percentage((1, self._speed_count), speed) self.async_write_ha_state() From b215514c901ae5f34167cc5333a3574dd9dbbfc8 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:19:52 +0200 Subject: [PATCH 849/903] Fix upgrade api disabling during setup of Synology DSM (#77753) --- homeassistant/components/synology_dsm/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 12bad2954dd..82f2c214804 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -102,6 +102,7 @@ class SynoApi: self.dsm.upgrade.update() except SynologyDSMAPIErrorException as ex: self._with_upgrade = False + self.dsm.reset(SynoCoreUpgrade.API_KEY) LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex) self._fetch_device_configuration() From 9733887b6aa88beb45b0e5109af07f18455f17ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 09:45:52 -0400 Subject: [PATCH 850/903] Add BlueMaestro integration (#77758) * Add BlueMaestro integration * tests * dc --- CODEOWNERS | 2 + .../components/bluemaestro/__init__.py | 49 +++++ .../components/bluemaestro/config_flow.py | 94 ++++++++ homeassistant/components/bluemaestro/const.py | 3 + .../components/bluemaestro/device.py | 31 +++ .../components/bluemaestro/manifest.json | 16 ++ .../components/bluemaestro/sensor.py | 149 +++++++++++++ .../components/bluemaestro/strings.json | 22 ++ .../bluemaestro/translations/en.json | 22 ++ homeassistant/generated/bluetooth.py | 5 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/bluemaestro/__init__.py | 26 +++ tests/components/bluemaestro/conftest.py | 8 + .../bluemaestro/test_config_flow.py | 200 ++++++++++++++++++ tests/components/bluemaestro/test_sensor.py | 50 +++++ 17 files changed, 684 insertions(+) create mode 100644 homeassistant/components/bluemaestro/__init__.py create mode 100644 homeassistant/components/bluemaestro/config_flow.py create mode 100644 homeassistant/components/bluemaestro/const.py create mode 100644 homeassistant/components/bluemaestro/device.py create mode 100644 homeassistant/components/bluemaestro/manifest.json create mode 100644 homeassistant/components/bluemaestro/sensor.py create mode 100644 homeassistant/components/bluemaestro/strings.json create mode 100644 homeassistant/components/bluemaestro/translations/en.json create mode 100644 tests/components/bluemaestro/__init__.py create mode 100644 tests/components/bluemaestro/conftest.py create mode 100644 tests/components/bluemaestro/test_config_flow.py create mode 100644 tests/components/bluemaestro/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index b135a418566..55eda64cbe8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -137,6 +137,8 @@ build.json @home-assistant/supervisor /tests/components/blebox/ @bbx-a @riokuu /homeassistant/components/blink/ @fronzbot /tests/components/blink/ @fronzbot +/homeassistant/components/bluemaestro/ @bdraco +/tests/components/bluemaestro/ @bdraco /homeassistant/components/blueprint/ @home-assistant/core /tests/components/blueprint/ @home-assistant/core /homeassistant/components/bluesound/ @thrawnarn diff --git a/homeassistant/components/bluemaestro/__init__.py b/homeassistant/components/bluemaestro/__init__.py new file mode 100644 index 00000000000..45eebedcfb2 --- /dev/null +++ b/homeassistant/components/bluemaestro/__init__.py @@ -0,0 +1,49 @@ +"""The BlueMaestro integration.""" +from __future__ import annotations + +import logging + +from bluemaestro_ble import BlueMaestroBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up BlueMaestro BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = BlueMaestroBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/bluemaestro/config_flow.py b/homeassistant/components/bluemaestro/config_flow.py new file mode 100644 index 00000000000..ccb548fa42b --- /dev/null +++ b/homeassistant/components/bluemaestro/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for bluemaestro ble integration.""" +from __future__ import annotations + +from typing import Any + +from bluemaestro_ble import BlueMaestroBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class BlueMaestroConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for bluemaestro.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/bluemaestro/const.py b/homeassistant/components/bluemaestro/const.py new file mode 100644 index 00000000000..757f1b7c810 --- /dev/null +++ b/homeassistant/components/bluemaestro/const.py @@ -0,0 +1,3 @@ +"""Constants for the BlueMaestro integration.""" + +DOMAIN = "bluemaestro" diff --git a/homeassistant/components/bluemaestro/device.py b/homeassistant/components/bluemaestro/device.py new file mode 100644 index 00000000000..3d6e4546882 --- /dev/null +++ b/homeassistant/components/bluemaestro/device.py @@ -0,0 +1,31 @@ +"""Support for BlueMaestro devices.""" +from __future__ import annotations + +from bluemaestro_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a bluemaestro device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json new file mode 100644 index 00000000000..0ff9cdd0794 --- /dev/null +++ b/homeassistant/components/bluemaestro/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "bluemaestro", + "name": "BlueMaestro", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/bluemaestro", + "bluetooth": [ + { + "manufacturer_id": 307, + "connectable": false + } + ], + "requirements": ["bluemaestro-ble==0.2.0"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py new file mode 100644 index 00000000000..8afdef48d51 --- /dev/null +++ b/homeassistant/components/bluemaestro/sensor.py @@ -0,0 +1,149 @@ +"""Support for BlueMaestro sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from bluemaestro_ble import ( + SensorDeviceClass as BlueMaestroSensorDeviceClass, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +SENSOR_DESCRIPTIONS = { + (BlueMaestroSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (BlueMaestroSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + BlueMaestroSensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + ( + BlueMaestroSensorDeviceClass.TEMPERATURE, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + BlueMaestroSensorDeviceClass.DEW_POINT, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + BlueMaestroSensorDeviceClass.PRESSURE, + Units.PRESSURE_MBAR, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the BlueMaestro BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + BlueMaestroBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class BlueMaestroBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a BlueMaestro sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/bluemaestro/strings.json b/homeassistant/components/bluemaestro/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/bluemaestro/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/bluemaestro/translations/en.json b/homeassistant/components/bluemaestro/translations/en.json new file mode 100644 index 00000000000..ebd9760c161 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": "Device not supported" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 14156ada20c..c217cf790b8 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,11 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ + { + "domain": "bluemaestro", + "manufacturer_id": 307, + "connectable": False + }, { "domain": "bthome", "connectable": False, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c5437e14562..19be9cfdb89 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -48,6 +48,7 @@ FLOWS = { "balboa", "blebox", "blink", + "bluemaestro", "bluetooth", "bmw_connected_drive", "bond", diff --git a/requirements_all.txt b/requirements_all.txt index 51b7a506ef3..368d2770cf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,6 +422,9 @@ blinkstick==1.2.0 # homeassistant.components.bitcoin blockchain==1.4.4 +# homeassistant.components.bluemaestro +bluemaestro-ble==0.2.0 + # homeassistant.components.decora # homeassistant.components.zengge # bluepy==1.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b613121245..d13df506fe7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,6 +337,9 @@ blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 +# homeassistant.components.bluemaestro +bluemaestro-ble==0.2.0 + # homeassistant.components.bluetooth bluetooth-adapters==0.3.4 diff --git a/tests/components/bluemaestro/__init__.py b/tests/components/bluemaestro/__init__.py new file mode 100644 index 00000000000..bd9b86e040f --- /dev/null +++ b/tests/components/bluemaestro/__init__.py @@ -0,0 +1,26 @@ +"""Tests for the BlueMaestro integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_BLUEMAESTRO_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +BLUEMAESTRO_SERVICE_INFO = BluetoothServiceInfo( + name="FA17B62C", + manufacturer_data={ + 307: b"\x17d\x0e\x10\x00\x02\x00\xf2\x01\xf2\x00\x83\x01\x00\x01\r\x02\xab\x00\xf2\x01\xf2\x01\r\x02\xab\x00\xf2\x01\xf2\x00\xff\x02N\x00\x00\x00\x00\x00" + }, + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={}, + service_uuids=[], + source="local", +) diff --git a/tests/components/bluemaestro/conftest.py b/tests/components/bluemaestro/conftest.py new file mode 100644 index 00000000000..e40cf1e30f4 --- /dev/null +++ b/tests/components/bluemaestro/conftest.py @@ -0,0 +1,8 @@ +"""BlueMaestro session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/bluemaestro/test_config_flow.py b/tests/components/bluemaestro/test_config_flow.py new file mode 100644 index 00000000000..116380a0df0 --- /dev/null +++ b/tests/components/bluemaestro/test_config_flow.py @@ -0,0 +1,200 @@ +"""Test the BlueMaestro config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.bluemaestro.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import BLUEMAESTRO_SERVICE_INFO, NOT_BLUEMAESTRO_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.bluemaestro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Tempo Disc THD EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_bluemaestro(hass): + """Test discovery via bluetooth not bluemaestro.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.bluemaestro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Tempo Disc THD EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluemaestro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.bluemaestro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Tempo Disc THD EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/bluemaestro/test_sensor.py b/tests/components/bluemaestro/test_sensor.py new file mode 100644 index 00000000000..2f964e65481 --- /dev/null +++ b/tests/components/bluemaestro/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the BlueMaestro sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluemaestro.const import DOMAIN +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import BLUEMAESTRO_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + saved_callback(BLUEMAESTRO_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 4 + + humid_sensor = hass.states.get("sensor.tempo_disc_thd_eeff_temperature") + humid_sensor_attrs = humid_sensor.attributes + assert humid_sensor.state == "24.2" + assert humid_sensor_attrs[ATTR_FRIENDLY_NAME] == "Tempo Disc THD EEFF Temperature" + assert humid_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert humid_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 32a9fba58ebb51b8356cf990d7098f13b20a4d63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 05:13:34 -0400 Subject: [PATCH 851/903] Increase default august timeout (#77762) Fixes ``` 2022-08-28 20:32:46.223 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/helpers/debounce.py", line 82, in async_call await task File "/Users/bdraco/home-assistant/homeassistant/components/august/activity.py", line 49, in _async_update_house_id await self._async_update_house_id(house_id) File "/Users/bdraco/home-assistant/homeassistant/components/august/activity.py", line 137, in _async_update_house_id activities = await self._api.async_get_house_activities( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/yalexs/api_async.py", line 96, in async_get_house_activities response = await self._async_dict_to_api( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/yalexs/api_async.py", line 294, in _async_dict_to_api response = await self._aiohttp_session.request(method, url, **api_dict) File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/aiohttp/client.py", line 466, in _request with timer: File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/aiohttp/helpers.py", line 721, in __exit__ raise asyncio.TimeoutError from None asyncio.exceptions.TimeoutError ``` --- homeassistant/components/august/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index 9a724d4a87b..5b936e9f159 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -4,7 +4,7 @@ from datetime import timedelta from homeassistant.const import Platform -DEFAULT_TIMEOUT = 15 +DEFAULT_TIMEOUT = 25 CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file" CONF_LOGIN_METHOD = "login_method" From 3856178dc08d461cc37720c2fa108dac59571af4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 3 Sep 2022 16:53:21 -0400 Subject: [PATCH 852/903] Handle dead nodes in zwave_js update entity (#77763) --- homeassistant/components/zwave_js/update.py | 17 ++- tests/components/zwave_js/test_update.py | 140 +++++++++----------- 2 files changed, 77 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 134c6cc6661..1f04c3acc47 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -86,17 +86,24 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_installed_version = self._attr_latest_version = node.firmware_version - def _update_on_wake_up(self, _: dict[str, Any]) -> None: + def _update_on_status_change(self, _: dict[str, Any]) -> None: """Update the entity when node is awake.""" self._status_unsub = None self.hass.async_create_task(self.async_update(True)) async def async_update(self, write_state: bool = False) -> None: """Update the entity.""" - if self.node.status == NodeStatus.ASLEEP: - if not self._status_unsub: - self._status_unsub = self.node.once("wake up", self._update_on_wake_up) - return + for status, event_name in ( + (NodeStatus.ASLEEP, "wake up"), + (NodeStatus.DEAD, "alive"), + ): + if self.node.status == status: + if not self._status_unsub: + self._status_unsub = self.node.once( + event_name, self._update_on_status_change + ) + return + if available_firmware_updates := ( await self.driver.controller.async_get_available_firmware_updates( self.node, API_KEY_FIRMWARE_UPDATE_SERVICE diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 852dcba5954..c9ec8fa68c6 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -24,6 +24,31 @@ from homeassistant.util import datetime as dt_util from tests.common import async_fire_time_changed UPDATE_ENTITY = "update.z_wave_thermostat_firmware" +FIRMWARE_UPDATES = { + "updates": [ + { + "version": "10.11.1", + "changelog": "blah 1", + "files": [ + {"target": 0, "url": "https://example1.com", "integrity": "sha1"} + ], + }, + { + "version": "11.2.4", + "changelog": "blah 2", + "files": [ + {"target": 0, "url": "https://example2.com", "integrity": "sha2"} + ], + }, + { + "version": "11.1.5", + "changelog": "blah 3", + "files": [ + {"target": 0, "url": "https://example3.com", "integrity": "sha3"} + ], + }, + ] +} async def test_update_entity_success( @@ -60,31 +85,7 @@ async def test_update_entity_success( result = await ws_client.receive_json() assert result["result"] is None - client.async_send_command.return_value = { - "updates": [ - { - "version": "10.11.1", - "changelog": "blah 1", - "files": [ - {"target": 0, "url": "https://example1.com", "integrity": "sha1"} - ], - }, - { - "version": "11.2.4", - "changelog": "blah 2", - "files": [ - {"target": 0, "url": "https://example2.com", "integrity": "sha2"} - ], - }, - { - "version": "11.1.5", - "changelog": "blah 3", - "files": [ - {"target": 0, "url": "https://example3.com", "integrity": "sha3"} - ], - }, - ] - } + client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=2)) await hass.async_block_till_done() @@ -171,31 +172,7 @@ async def test_update_entity_failure( hass_ws_client, ): """Test update entity failed install.""" - client.async_send_command.return_value = { - "updates": [ - { - "version": "10.11.1", - "changelog": "blah 1", - "files": [ - {"target": 0, "url": "https://example1.com", "integrity": "sha1"} - ], - }, - { - "version": "11.2.4", - "changelog": "blah 2", - "files": [ - {"target": 0, "url": "https://example2.com", "integrity": "sha2"} - ], - }, - { - "version": "11.1.5", - "changelog": "blah 3", - "files": [ - {"target": 0, "url": "https://example3.com", "integrity": "sha3"} - ], - }, - ] - } + client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) await hass.async_block_till_done() @@ -228,31 +205,7 @@ async def test_update_entity_sleep( multisensor_6.receive_event(event) client.async_send_command.reset_mock() - client.async_send_command.return_value = { - "updates": [ - { - "version": "10.11.1", - "changelog": "blah 1", - "files": [ - {"target": 0, "url": "https://example1.com", "integrity": "sha1"} - ], - }, - { - "version": "11.2.4", - "changelog": "blah 2", - "files": [ - {"target": 0, "url": "https://example2.com", "integrity": "sha2"} - ], - }, - { - "version": "11.1.5", - "changelog": "blah 3", - "files": [ - {"target": 0, "url": "https://example3.com", "integrity": "sha3"} - ], - }, - ] - } + client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) await hass.async_block_till_done() @@ -273,3 +226,40 @@ async def test_update_entity_sleep( args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "controller.get_available_firmware_updates" assert args["nodeId"] == multisensor_6.node_id + + +async def test_update_entity_dead( + hass, + client, + multisensor_6, + integration, +): + """Test update occurs when device is dead after it becomes alive.""" + event = Event( + "dead", + data={"source": "node", "event": "dead", "nodeId": multisensor_6.node_id}, + ) + multisensor_6.receive_event(event) + client.async_send_command.reset_mock() + + client.async_send_command.return_value = FIRMWARE_UPDATES + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + # Because node is asleep we shouldn't attempt to check for firmware updates + assert len(client.async_send_command.call_args_list) == 0 + + event = Event( + "alive", + data={"source": "node", "event": "alive", "nodeId": multisensor_6.node_id}, + ) + multisensor_6.receive_event(event) + await hass.async_block_till_done() + + # Now that the node is up we can check for updates + assert len(client.async_send_command.call_args_list) > 0 + + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == multisensor_6.node_id From 5f4013164c1790b2270fc6b69bffd805012577a4 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 4 Sep 2022 07:51:48 -0700 Subject: [PATCH 853/903] Update smarttub to 0.0.33 (#77766) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 1d7500b9185..e2f72642a91 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.32"], + "requirements": ["python-smarttub==0.0.33"], "quality_scale": "platinum", "iot_class": "cloud_polling", "loggers": ["smarttub"] diff --git a/requirements_all.txt b/requirements_all.txt index 368d2770cf8..7cc189ff7d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.32 +python-smarttub==0.0.33 # homeassistant.components.songpal python-songpal==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d13df506fe7..0a59ca8e9df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1368,7 +1368,7 @@ python-nest==4.2.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.32 +python-smarttub==0.0.33 # homeassistant.components.songpal python-songpal==0.15 From 9387449abf98e05f6bd75be6717c660bf457efb7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 4 Sep 2022 18:57:50 +0200 Subject: [PATCH 854/903] Replace archived sucks by py-sucks and bump to 0.9.8 for Ecovacs integration (#77768) --- CODEOWNERS | 2 +- homeassistant/components/ecovacs/manifest.json | 4 ++-- requirements_all.txt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 55eda64cbe8..4609e6330d6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -277,7 +277,7 @@ build.json @home-assistant/supervisor /tests/components/ecobee/ @marthoc /homeassistant/components/econet/ @vangorra @w1ll1am23 /tests/components/econet/ @vangorra @w1ll1am23 -/homeassistant/components/ecovacs/ @OverloadUT +/homeassistant/components/ecovacs/ @OverloadUT @mib1185 /homeassistant/components/ecowitt/ @pvizeli /tests/components/ecowitt/ @pvizeli /homeassistant/components/edl21/ @mtdcr diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 1712cea1578..3ac277217b8 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -2,8 +2,8 @@ "domain": "ecovacs", "name": "Ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs", - "requirements": ["sucks==0.9.4"], - "codeowners": ["@OverloadUT"], + "requirements": ["py-sucks==0.9.8"], + "codeowners": ["@OverloadUT", "@mib1185"], "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7cc189ff7d1..619035ae83b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1351,6 +1351,9 @@ py-nightscout==1.2.2 # homeassistant.components.schluter py-schluter==0.1.7 +# homeassistant.components.ecovacs +py-sucks==0.9.8 + # homeassistant.components.synology_dsm py-synologydsm-api==1.0.8 @@ -2308,9 +2311,6 @@ stringcase==1.2.0 # homeassistant.components.subaru subarulink==0.5.0 -# homeassistant.components.ecovacs -sucks==0.9.4 - # homeassistant.components.solarlog sunwatcher==0.2.1 From ea0b40669251cb4fef42e951673dd8ca16bd2ed4 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 2 Sep 2022 08:07:21 +1000 Subject: [PATCH 855/903] Add binary sensor platform to LIFX integration (#77535) Co-authored-by: J. Nick Koston --- homeassistant/components/lifx/__init__.py | 2 +- .../components/lifx/binary_sensor.py | 70 ++++++++++++++++++ homeassistant/components/lifx/const.py | 11 ++- homeassistant/components/lifx/coordinator.py | 35 ++++++--- homeassistant/components/lifx/light.py | 8 +- tests/components/lifx/__init__.py | 26 ++++++- tests/components/lifx/test_binary_sensor.py | 74 +++++++++++++++++++ 7 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/lifx/binary_sensor.py create mode 100644 tests/components/lifx/test_binary_sensor.py diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 6af30b91d28..5c91efa1d02 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -57,7 +57,7 @@ CONFIG_SCHEMA = vol.All( ) -PLATFORMS = [Platform.BUTTON, Platform.LIGHT] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT] DISCOVERY_INTERVAL = timedelta(minutes=15) MIGRATION_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/lifx/binary_sensor.py b/homeassistant/components/lifx/binary_sensor.py new file mode 100644 index 00000000000..4a368a2f97f --- /dev/null +++ b/homeassistant/components/lifx/binary_sensor.py @@ -0,0 +1,70 @@ +"""Binary sensor entities for LIFX integration.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, HEV_CYCLE_STATE +from .coordinator import LIFXUpdateCoordinator +from .entity import LIFXEntity +from .util import lifx_features + +HEV_CYCLE_STATE_SENSOR = BinarySensorEntityDescription( + key=HEV_CYCLE_STATE, + name="Clean Cycle", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.RUNNING, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up LIFX from a config entry.""" + coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if lifx_features(coordinator.device)["hev"]: + async_add_entities( + [ + LIFXBinarySensorEntity( + coordinator=coordinator, description=HEV_CYCLE_STATE_SENSOR + ) + ] + ) + + +class LIFXBinarySensorEntity(LIFXEntity, BinarySensorEntity): + """LIFX sensor entity base class.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: LIFXUpdateCoordinator, + description: BinarySensorEntityDescription, + ) -> None: + """Initialise the sensor.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_name = description.name + self._attr_unique_id = f"{coordinator.serial_number}_{description.key}" + self._async_update_attrs() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Handle coordinator updates.""" + self._attr_is_on = self.coordinator.async_get_hev_cycle_state() diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index f6ec653c994..74960d59bd1 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -29,6 +29,15 @@ IDENTIFY_WAVEFORM = { IDENTIFY = "identify" RESTART = "restart" +ATTR_DURATION = "duration" +ATTR_INDICATION = "indication" +ATTR_INFRARED = "infrared" +ATTR_POWER = "power" +ATTR_REMAINING = "remaining" +ATTR_ZONES = "zones" + +HEV_CYCLE_STATE = "hev_cycle_state" + DATA_LIFX_MANAGER = "lifx_manager" -_LOGGER = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 1f3f49368ca..d01fb266c6f 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( _LOGGER, + ATTR_REMAINING, IDENTIFY_WAVEFORM, MESSAGE_RETRIES, MESSAGE_TIMEOUT, @@ -101,26 +102,25 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.device.get_hostfirmware() if self.device.product is None: self.device.get_version() - try: - response = await async_execute_lifx(self.device.get_color) - except asyncio.TimeoutError as ex: - raise UpdateFailed( - f"Failed to fetch state from device: {self.device.ip_addr}" - ) from ex + response = await async_execute_lifx(self.device.get_color) + if self.device.product is None: raise UpdateFailed( f"Failed to fetch get version from device: {self.device.ip_addr}" ) + # device.mac_addr is not the mac_address, its the serial number if self.device.mac_addr == TARGET_ANY: self.device.mac_addr = response.target_addr + if lifx_features(self.device)["multizone"]: - try: - await self.async_update_color_zones() - except asyncio.TimeoutError as ex: - raise UpdateFailed( - f"Failed to fetch zones from device: {self.device.ip_addr}" - ) from ex + await self.async_update_color_zones() + + if lifx_features(self.device)["hev"]: + if self.device.hev_cycle_configuration is None: + self.device.get_hev_configuration() + + await self.async_get_hev_cycle() async def async_update_color_zones(self) -> None: """Get updated color information for each zone.""" @@ -138,6 +138,17 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if zone == top - 1: zone -= 1 + def async_get_hev_cycle_state(self) -> bool | None: + """Return the current HEV cycle state.""" + if self.device.hev_cycle is None: + return None + return bool(self.device.hev_cycle.get(ATTR_REMAINING, 0) > 0) + + async def async_get_hev_cycle(self) -> None: + """Update the HEV cycle status from a LIFX Clean bulb.""" + if lifx_features(self.device)["hev"]: + await async_execute_lifx(self.device.get_hev_cycle) + async def async_set_waveform_optional( self, value: dict[str, Any], rapid: bool = False ) -> None: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 67bb3e91748..fe17dd95788 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.color as color_util -from .const import DATA_LIFX_MANAGER, DOMAIN +from .const import ATTR_INFRARED, ATTR_POWER, ATTR_ZONES, DATA_LIFX_MANAGER, DOMAIN from .coordinator import LIFXUpdateCoordinator from .entity import LIFXEntity from .manager import ( @@ -39,14 +39,8 @@ from .manager import ( ) from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk -SERVICE_LIFX_SET_STATE = "set_state" - COLOR_ZONE_POPULATE_DELAY = 0.3 -ATTR_INFRARED = "infrared" -ATTR_ZONES = "zones" -ATTR_POWER = "power" - SERVICE_LIFX_SET_STATE = "set_state" LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 8259314e77c..9e137c8532a 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -22,10 +22,13 @@ DEFAULT_ENTRY_TITLE = LABEL class MockMessage: """Mock a lifx message.""" - def __init__(self): + def __init__(self, **kwargs): """Init message.""" self.target_addr = SERIAL self.count = 9 + for k, v in kwargs.items(): + if k != "callb": + setattr(self, k, v) class MockFailingLifxCommand: @@ -50,15 +53,20 @@ class MockFailingLifxCommand: class MockLifxCommand: """Mock a lifx command.""" + def __name__(self): + """Return name.""" + return "mock_lifx_command" + def __init__(self, bulb, **kwargs): """Init command.""" self.bulb = bulb self.calls = [] + self.msg_kwargs = kwargs def __call__(self, *args, **kwargs): """Call command.""" if callb := kwargs.get("callb"): - callb(self.bulb, MockMessage()) + callb(self.bulb, MockMessage(**self.msg_kwargs)) self.calls.append([args, kwargs]) def reset_mock(self): @@ -108,6 +116,20 @@ def _mocked_brightness_bulb() -> Light: return bulb +def _mocked_clean_bulb() -> Light: + bulb = _mocked_bulb() + bulb.get_hev_cycle = MockLifxCommand( + bulb, duration=7200, remaining=0, last_power=False + ) + bulb.hev_cycle = { + "duration": 7200, + "remaining": 30, + "last_power": False, + } + bulb.product = 90 + return bulb + + def _mocked_light_strip() -> Light: bulb = _mocked_bulb() bulb.product = 31 # LIFX Z diff --git a/tests/components/lifx/test_binary_sensor.py b/tests/components/lifx/test_binary_sensor.py new file mode 100644 index 00000000000..bb0b210704a --- /dev/null +++ b/tests/components/lifx/test_binary_sensor.py @@ -0,0 +1,74 @@ +"""Test the lifx binary sensor platwform.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import lifx +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_HOST, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + SERIAL, + _mocked_clean_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_hev_cycle_state(hass: HomeAssistant) -> None: + """Test HEV cycle state binary sensor.""" + config_entry = MockConfigEntry( + domain=lifx.DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_clean_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "binary_sensor.my_bulb_clean_cycle" + entity_registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.RUNNING + + entry = entity_registry.async_get(entity_id) + assert state + assert entry.unique_id == f"{SERIAL}_hev_cycle_state" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + bulb.hev_cycle = {"duration": 7200, "remaining": 0, "last_power": False} + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.hev_cycle = None + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNKNOWN From f60ae406617a52a17df5929fdfa781a2f415ac9d Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 2 Sep 2022 22:13:03 +1000 Subject: [PATCH 856/903] Rename the binary sensor to better reflect its purpose (#77711) --- homeassistant/components/lifx/binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/binary_sensor.py b/homeassistant/components/lifx/binary_sensor.py index 4a368a2f97f..273ef035757 100644 --- a/homeassistant/components/lifx/binary_sensor.py +++ b/homeassistant/components/lifx/binary_sensor.py @@ -33,15 +33,15 @@ async def async_setup_entry( if lifx_features(coordinator.device)["hev"]: async_add_entities( [ - LIFXBinarySensorEntity( + LIFXHevCycleBinarySensorEntity( coordinator=coordinator, description=HEV_CYCLE_STATE_SENSOR ) ] ) -class LIFXBinarySensorEntity(LIFXEntity, BinarySensorEntity): - """LIFX sensor entity base class.""" +class LIFXHevCycleBinarySensorEntity(LIFXEntity, BinarySensorEntity): + """LIFX HEV cycle state binary sensor.""" _attr_has_entity_name = True From f9b95cc4a41a3fc0aaf9fd0d68c1660227231cc2 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 5 Sep 2022 01:51:57 +1000 Subject: [PATCH 857/903] Fix lifx service call interference (#77770) * Fix #77735 by restoring the wait to let state settle Signed-off-by: Avi Miller * Skip the asyncio.sleep during testing Signed-off-by: Avi Miller * Patch out asyncio.sleep for lifx tests Signed-off-by: Avi Miller * Patch out a constant instead of overriding asyncio.sleep directly Signed-off-by: Avi Miller Signed-off-by: Avi Miller --- homeassistant/components/lifx/coordinator.py | 3 ++- homeassistant/components/lifx/light.py | 7 +++++-- tests/components/lifx/conftest.py | 1 - tests/components/lifx/test_button.py | 11 +++++++++++ tests/components/lifx/test_light.py | 8 +++++++- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index d01fb266c6f..37e753c27a3 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -25,6 +25,7 @@ from .const import ( from .util import async_execute_lifx, get_real_mac_addr, lifx_features REQUEST_REFRESH_DELAY = 0.35 +LIFX_IDENTIFY_DELAY = 3.0 class LIFXUpdateCoordinator(DataUpdateCoordinator): @@ -92,7 +93,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): # Turn the bulb on first, flash for 3 seconds, then turn off await self.async_set_power(state=True, duration=1) await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM) - await asyncio.sleep(3) + await asyncio.sleep(LIFX_IDENTIFY_DELAY) await self.async_set_power(state=False, duration=1) async def _async_update_data(self) -> None: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index fe17dd95788..36d3b480f74 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -39,7 +39,7 @@ from .manager import ( ) from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk -COLOR_ZONE_POPULATE_DELAY = 0.3 +LIFX_STATE_SETTLE_DELAY = 0.3 SERVICE_LIFX_SET_STATE = "set_state" @@ -231,6 +231,9 @@ class LIFXLight(LIFXEntity, LightEntity): if power_off: await self.set_power(False, duration=fade) + # Avoid state ping-pong by holding off updates as the state settles + await asyncio.sleep(LIFX_STATE_SETTLE_DELAY) + # Update when the transition starts and ends await self.update_during_transition(fade) @@ -338,7 +341,7 @@ class LIFXStrip(LIFXColor): # Zone brightness is not reported when powered off if not self.is_on and hsbk[HSBK_BRIGHTNESS] is None: await self.set_power(True) - await asyncio.sleep(COLOR_ZONE_POPULATE_DELAY) + await asyncio.sleep(LIFX_STATE_SETTLE_DELAY) await self.update_color_zones() await self.set_power(False) diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py index 326c4f75413..a243132dc65 100644 --- a/tests/components/lifx/conftest.py +++ b/tests/components/lifx/conftest.py @@ -1,5 +1,4 @@ """Tests for the lifx integration.""" - from unittest.mock import AsyncMock, MagicMock, patch import pytest diff --git a/tests/components/lifx/test_button.py b/tests/components/lifx/test_button.py index abc91128e25..b166aa05d66 100644 --- a/tests/components/lifx/test_button.py +++ b/tests/components/lifx/test_button.py @@ -1,4 +1,8 @@ """Tests for button platform.""" +from unittest.mock import patch + +import pytest + from homeassistant.components import lifx from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.lifx.const import DOMAIN @@ -21,6 +25,13 @@ from . import ( from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def mock_lifx_coordinator_sleep(): + """Mock out lifx coordinator sleeps.""" + with patch("homeassistant.components.lifx.coordinator.LIFX_IDENTIFY_DELAY", 0): + yield + + async def test_button_restart(hass: HomeAssistant) -> None: """Test that a bulb can be restarted.""" config_entry = MockConfigEntry( diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 5b641e850f2..6229e130a40 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -50,6 +50,13 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.fixture(autouse=True) +def patch_lifx_state_settle_delay(): + """Set asyncio.sleep for state settles to zero.""" + with patch("homeassistant.components.lifx.light.LIFX_STATE_SETTLE_DELAY", 0): + yield + + async def test_light_unique_id(hass: HomeAssistant) -> None: """Test a light unique id.""" already_migrated_config_entry = MockConfigEntry( @@ -98,7 +105,6 @@ async def test_light_unique_id_new_firmware(hass: HomeAssistant) -> None: assert device.identifiers == {(DOMAIN, SERIAL)} -@patch("homeassistant.components.lifx.light.COLOR_ZONE_POPULATE_DELAY", 0) async def test_light_strip(hass: HomeAssistant) -> None: """Test a light strip.""" already_migrated_config_entry = MockConfigEntry( From da83ceca5b0f6a611d7e97d8da506ed3b30a1364 Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Sun, 4 Sep 2022 09:56:10 -0400 Subject: [PATCH 858/903] Tweak unique id formatting for Melnor Bluetooth switches (#77773) --- homeassistant/components/melnor/switch.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index c2d32c428d3..7a615a8582d 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -48,10 +48,7 @@ class MelnorSwitch(MelnorBluetoothBaseEntity, SwitchEntity): super().__init__(coordinator) self._valve_index = valve_index - self._attr_unique_id = ( - f"switch-{self._attr_unique_id}-zone{self._valve().id}-manual" - ) - + self._attr_unique_id = f"{self._attr_unique_id}-zone{self._valve().id}-manual" self._attr_name = f"{self._device.name} Zone {self._valve().id+1}" @property From 52abf0851b489e8f457cf976e1555b9ce3e610aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 12:00:19 -0400 Subject: [PATCH 859/903] Bump flux_led to 0.28.32 (#77787) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 4afd0cdb855..632ef04e456 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.31"], + "requirements": ["flux_led==0.28.32"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 619035ae83b..a7ba397b92c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -682,7 +682,7 @@ fjaraskupan==2.0.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.31 +flux_led==0.28.32 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a59ca8e9df..1074f3eff1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -501,7 +501,7 @@ fjaraskupan==2.0.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.31 +flux_led==0.28.32 # homeassistant.components.homekit # homeassistant.components.recorder From 9f06baa778dce68de2640f1db00c8d4b49686922 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 12:54:40 -0400 Subject: [PATCH 860/903] Bump led-ble to 0.6.0 (#77788) * Bump ble-led to 0.6.0 Fixes reading the white channel on same devices Changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v0.5.4...v0.6.0 * Bump flux_led to 0.28.32 Changelog: https://github.com/Danielhiversen/flux_led/compare/0.28.31...0.28.32 Fixes white channel support for some more older protocols * keep them in sync * Update homeassistant/components/led_ble/manifest.json --- homeassistant/components/led_ble/manifest.json | 6 ++++-- homeassistant/generated/bluetooth.py | 8 ++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 376fadcb3be..a0f5e3481d5 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -3,7 +3,7 @@ "name": "LED BLE", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ble_ble", - "requirements": ["led-ble==0.5.4"], + "requirements": ["led-ble==0.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ @@ -11,7 +11,9 @@ { "local_name": "BLE-LED*" }, { "local_name": "LEDBLE*" }, { "local_name": "Triones*" }, - { "local_name": "LEDBlue*" } + { "local_name": "LEDBlue*" }, + { "local_name": "Dream~*" }, + { "local_name": "QHM-*" } ], "iot_class": "local_polling" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index c217cf790b8..83bfd3ab5eb 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -156,6 +156,14 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "led_ble", "local_name": "LEDBlue*" }, + { + "domain": "led_ble", + "local_name": "Dream~*" + }, + { + "domain": "led_ble", + "local_name": "QHM-*" + }, { "domain": "melnor", "manufacturer_data_start": [ diff --git a/requirements_all.txt b/requirements_all.txt index a7ba397b92c..6b91183a0ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.5.4 +led-ble==0.6.0 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1074f3eff1c..92b944acdc6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.5.4 +led-ble==0.6.0 # homeassistant.components.foscam libpyfoscam==1.0 From c8156d5de6b19dd0461fdd9d7a21e31a49fae4d7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Sep 2022 18:06:04 +0200 Subject: [PATCH 861/903] Bump pysensibo to 1.0.19 (#77790) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 5ef8ff6fa4e..a2a7cbe3bd0 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.18"], + "requirements": ["pysensibo==1.0.19"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 6b91183a0ad..609b032cfe3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1838,7 +1838,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.18 +pysensibo==1.0.19 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92b944acdc6..d12689484d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1285,7 +1285,7 @@ pyruckus==0.16 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.18 +pysensibo==1.0.19 # homeassistant.components.serial # homeassistant.components.zha From 0d042d496de231cfef69ba662c623ac784378496 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 Sep 2022 13:00:37 -0400 Subject: [PATCH 862/903] Bumped version to 2022.9.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 502dd3510e2..7ae1fccf7f1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 3d608e722a0..fc5260f41df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b3" +version = "2022.9.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2fa517b81bc43816acac5d3e0a80743c6a85418a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Sep 2022 14:12:37 -0400 Subject: [PATCH 863/903] Make Sonos typing more complete (#68072) --- homeassistant/components/sonos/__init__.py | 38 ++++++---- .../components/sonos/binary_sensor.py | 2 +- homeassistant/components/sonos/diagnostics.py | 15 ++-- .../components/sonos/household_coordinator.py | 3 +- homeassistant/components/sonos/media.py | 5 +- .../components/sonos/media_browser.py | 59 ++++++++------ .../components/sonos/media_player.py | 76 +++++++++---------- homeassistant/components/sonos/number.py | 11 ++- homeassistant/components/sonos/sensor.py | 2 +- homeassistant/components/sonos/speaker.py | 60 ++++++++------- homeassistant/components/sonos/statistics.py | 2 +- homeassistant/components/sonos/switch.py | 37 +++++---- mypy.ini | 36 --------- script/hassfest/mypy_config.py | 15 +--- 14 files changed, 168 insertions(+), 193 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index d94b49e52f2..f0dd8e668fa 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -8,6 +8,7 @@ import datetime from functools import partial import logging import socket +from typing import TYPE_CHECKING, Any, Optional, cast from urllib.parse import urlparse from soco import events_asyncio @@ -21,7 +22,7 @@ from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import async_track_time_interval, call_later @@ -93,7 +94,7 @@ class SonosData: self.favorites: dict[str, SonosFavorites] = {} self.alarms: dict[str, SonosAlarms] = {} self.topology_condition = asyncio.Condition() - self.hosts_heartbeat = None + self.hosts_heartbeat: CALLBACK_TYPE | None = None self.discovery_known: set[str] = set() self.boot_counts: dict[str, int] = {} self.mdns_names: dict[str, str] = {} @@ -168,10 +169,10 @@ class SonosDiscoveryManager: self.data = data self.hosts = set(hosts) self.discovery_lock = asyncio.Lock() - self._known_invisible = set() + self._known_invisible: set[SoCo] = set() self._manual_config_required = bool(hosts) - async def async_shutdown(self): + async def async_shutdown(self) -> None: """Stop all running tasks.""" await self._async_stop_event_listener() self._stop_manual_heartbeat() @@ -236,6 +237,8 @@ class SonosDiscoveryManager: (SonosAlarms, self.data.alarms), (SonosFavorites, self.data.favorites), ): + if TYPE_CHECKING: + coord_dict = cast(dict[str, Any], coord_dict) if soco.household_id not in coord_dict: new_coordinator = coordinator(self.hass, soco.household_id) new_coordinator.setup(soco) @@ -298,7 +301,7 @@ class SonosDiscoveryManager: ) async def _async_handle_discovery_message( - self, uid: str, discovered_ip: str, boot_seqnum: int + self, uid: str, discovered_ip: str, boot_seqnum: int | None ) -> None: """Handle discovered player creation and activity.""" async with self.discovery_lock: @@ -338,22 +341,27 @@ class SonosDiscoveryManager: async_dispatcher_send(self.hass, f"{SONOS_VANISHED}-{uid}", reason) return - discovered_ip = urlparse(info.ssdp_location).hostname - boot_seqnum = info.ssdp_headers.get("X-RINCON-BOOTSEQ") self.async_discovered_player( "SSDP", info, - discovered_ip, + cast(str, urlparse(info.ssdp_location).hostname), uid, - boot_seqnum, - info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME), + info.ssdp_headers.get("X-RINCON-BOOTSEQ"), + cast(str, info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME)), None, ) @callback def async_discovered_player( - self, source, info, discovered_ip, uid, boot_seqnum, model, mdns_name - ): + self, + source: str, + info: ssdp.SsdpServiceInfo, + discovered_ip: str, + uid: str, + boot_seqnum: str | int | None, + model: str, + mdns_name: str | None, + ) -> None: """Handle discovery via ssdp or zeroconf.""" if self._manual_config_required: _LOGGER.warning( @@ -376,10 +384,12 @@ class SonosDiscoveryManager: _LOGGER.debug("New %s discovery uid=%s: %s", source, uid, info) self.data.discovery_known.add(uid) asyncio.create_task( - self._async_handle_discovery_message(uid, discovered_ip, boot_seqnum) + self._async_handle_discovery_message( + uid, discovered_ip, cast(Optional[int], boot_seqnum) + ) ) - async def setup_platforms_and_discovery(self): + async def setup_platforms_and_discovery(self) -> None: """Set up platforms and discovery.""" await self.hass.config_entries.async_forward_entry_setups(self.entry, PLATFORMS) self.entry.async_on_unload( diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index e890c1c64a8..3f736f83922 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -109,6 +109,6 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity): self.speaker.mic_enabled = self.soco.mic_enabled @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return the state of the binary sensor.""" return self.speaker.mic_enabled diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index 463884e1ea8..fda96b86215 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -47,11 +47,11 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - payload = {"current_timestamp": time.monotonic()} + payload: dict[str, Any] = {"current_timestamp": time.monotonic()} for section in ("discovered", "discovery_known"): payload[section] = {} - data = getattr(hass.data[DATA_SONOS], section) + data: set[Any] | dict[str, Any] = getattr(hass.data[DATA_SONOS], section) if isinstance(data, set): payload[section] = data continue @@ -60,7 +60,6 @@ async def async_get_config_entry_diagnostics( payload[section][key] = await async_generate_speaker_info(hass, value) else: payload[section][key] = value - return payload @@ -85,12 +84,12 @@ async def async_generate_media_info( hass: HomeAssistant, speaker: SonosSpeaker ) -> dict[str, Any]: """Generate a diagnostic payload for current media metadata.""" - payload = {} + payload: dict[str, Any] = {} for attrib in MEDIA_DIAGNOSTIC_ATTRIBUTES: payload[attrib] = getattr(speaker.media, attrib) - def poll_current_track_info(): + def poll_current_track_info() -> dict[str, Any] | str: try: return speaker.soco.avTransport.GetPositionInfo( [("InstanceID", 0), ("Channel", "Master")], @@ -110,9 +109,11 @@ async def async_generate_speaker_info( hass: HomeAssistant, speaker: SonosSpeaker ) -> dict[str, Any]: """Generate the diagnostic payload for a specific speaker.""" - payload = {} + payload: dict[str, Any] = {} - def get_contents(item): + def get_contents( + item: int | float | str | dict[str, Any] + ) -> int | float | str | dict[str, Any]: if isinstance(item, (int, float, str)): return item if isinstance(item, dict): diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py index 51d7e9cec8c..29b9a005552 100644 --- a/homeassistant/components/sonos/household_coordinator.py +++ b/homeassistant/components/sonos/household_coordinator.py @@ -20,13 +20,14 @@ _LOGGER = logging.getLogger(__name__) class SonosHouseholdCoordinator: """Base class for Sonos household-level storage.""" + cache_update_lock: asyncio.Lock + def __init__(self, hass: HomeAssistant, household_id: str) -> None: """Initialize the data.""" self.hass = hass self.household_id = household_id self.async_poll: Callable[[], Coroutine[None, None, None]] | None = None self.last_processed_event_id: int | None = None - self.cache_update_lock: asyncio.Lock | None = None def setup(self, soco: SoCo) -> None: """Set up the SonosAlarm instance.""" diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index 9608356ba64..24233b1316f 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -2,7 +2,6 @@ from __future__ import annotations import datetime -import logging from typing import Any from soco.core import ( @@ -43,8 +42,6 @@ UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} DURATION_SECONDS = "duration_in_s" POSITION_SECONDS = "position_in_s" -_LOGGER = logging.getLogger(__name__) - def _timespan_secs(timespan: str | None) -> None | float: """Parse a time-span into number of seconds.""" @@ -106,7 +103,7 @@ class SonosMedia: @soco_error() def poll_track_info(self) -> dict[str, Any]: """Poll the speaker for current track info, add converted position values, and return.""" - track_info = self.soco.get_current_track_info() + track_info: dict[str, Any] = self.soco.get_current_track_info() track_info[DURATION_SECONDS] = _timespan_secs(track_info.get("duration")) track_info[POSITION_SECONDS] = _timespan_secs(track_info.get("position")) return track_info diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index b2d881e8bf2..95ff08cb87b 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -5,8 +5,13 @@ from collections.abc import Callable from contextlib import suppress from functools import partial import logging +from typing import cast from urllib.parse import quote_plus, unquote +from soco.data_structures import DidlFavorite, DidlObject +from soco.ms_data_structures import MusicServiceItem +from soco.music_library import MusicLibrary + from homeassistant.components import media_source, plex, spotify from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( @@ -50,12 +55,12 @@ def get_thumbnail_url_full( ) -> str | None: """Get thumbnail URL.""" if is_internal: - item = get_media( # type: ignore[no-untyped-call] + item = get_media( media.library, media_content_id, media_content_type, ) - return getattr(item, "album_art_uri", None) # type: ignore[no-any-return] + return getattr(item, "album_art_uri", None) return get_browse_image_url( media_content_type, @@ -64,19 +69,19 @@ def get_thumbnail_url_full( ) -def media_source_filter(item: BrowseMedia): +def media_source_filter(item: BrowseMedia) -> bool: """Filter media sources.""" return item.media_content_type.startswith("audio/") async def async_browse_media( - hass, + hass: HomeAssistant, speaker: SonosSpeaker, media: SonosMedia, get_browse_image_url: GetBrowseImageUrlType, media_content_id: str | None, media_content_type: str | None, -): +) -> BrowseMedia: """Browse media.""" if media_content_id is None: @@ -86,6 +91,7 @@ async def async_browse_media( media, get_browse_image_url, ) + assert media_content_type is not None if media_source.is_media_source_id(media_content_id): return await media_source.async_browse_media( @@ -150,7 +156,9 @@ async def async_browse_media( return response -def build_item_response(media_library, payload, get_thumbnail_url=None): +def build_item_response( + media_library: MusicLibrary, payload: dict[str, str], get_thumbnail_url=None +) -> BrowseMedia | None: """Create response payload for the provided media query.""" if payload["search_type"] == MEDIA_TYPE_ALBUM and payload["idstring"].startswith( ("A:GENRE", "A:COMPOSER") @@ -166,7 +174,7 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): "Unknown media type received when building item response: %s", payload["search_type"], ) - return + return None media = media_library.browse_by_idstring( search_type, @@ -176,7 +184,7 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): ) if media is None: - return + return None thumbnail = None title = None @@ -222,7 +230,7 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): ) -def item_payload(item, get_thumbnail_url=None): +def item_payload(item: DidlObject, get_thumbnail_url=None) -> BrowseMedia: """ Create response payload for a single media item. @@ -256,9 +264,9 @@ async def root_payload( speaker: SonosSpeaker, media: SonosMedia, get_browse_image_url: GetBrowseImageUrlType, -): +) -> BrowseMedia: """Return root payload for Sonos.""" - children = [] + children: list[BrowseMedia] = [] if speaker.favorites: children.append( @@ -303,14 +311,15 @@ async def root_payload( if "spotify" in hass.config.components: result = await spotify.async_browse_media(hass, None, None) - children.extend(result.children) + if result.children: + children.extend(result.children) try: item = await media_source.async_browse_media( hass, None, content_filter=media_source_filter ) # If domain is None, it's overview of available sources - if item.domain is None: + if item.domain is None and item.children is not None: children.extend(item.children) else: children.append(item) @@ -338,7 +347,7 @@ async def root_payload( ) -def library_payload(media_library, get_thumbnail_url=None): +def library_payload(media_library: MusicLibrary, get_thumbnail_url=None) -> BrowseMedia: """ Create response payload to describe contents of a specific library. @@ -360,7 +369,7 @@ def library_payload(media_library, get_thumbnail_url=None): ) -def favorites_payload(favorites): +def favorites_payload(favorites: list[DidlFavorite]) -> BrowseMedia: """ Create response payload to describe contents of a specific library. @@ -398,7 +407,9 @@ def favorites_payload(favorites): ) -def favorites_folder_payload(favorites, media_content_id): +def favorites_folder_payload( + favorites: list[DidlFavorite], media_content_id: str +) -> BrowseMedia: """Create response payload to describe all items of a type of favorite. Used by async_browse_media. @@ -432,7 +443,7 @@ def favorites_folder_payload(favorites, media_content_id): ) -def get_media_type(item): +def get_media_type(item: DidlObject) -> str: """Extract media type of item.""" if item.item_class == "object.item.audioItem.musicTrack": return SONOS_TRACKS @@ -450,7 +461,7 @@ def get_media_type(item): return SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0], item.item_class) -def can_play(item): +def can_play(item: DidlObject) -> bool: """ Test if playable. @@ -459,7 +470,7 @@ def can_play(item): return SONOS_TO_MEDIA_TYPES.get(item) in PLAYABLE_MEDIA_TYPES -def can_expand(item): +def can_expand(item: DidlObject) -> bool: """ Test if expandable. @@ -474,14 +485,16 @@ def can_expand(item): return SONOS_TYPES_MAPPING.get(item.item_id) in EXPANDABLE_MEDIA_TYPES -def get_content_id(item): +def get_content_id(item: DidlObject) -> str: """Extract content id or uri.""" if item.item_class == "object.item.audioItem.musicTrack": - return item.get_uri() - return item.item_id + return cast(str, item.get_uri()) + return cast(str, item.item_id) -def get_media(media_library, item_id, search_type): +def get_media( + media_library: MusicLibrary, item_id: str, search_type: str +) -> MusicServiceItem: """Fetch media/album.""" search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 1f57cafbf09..14e0693f55a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -130,11 +130,11 @@ async def async_setup_entry( if service_call.service == SERVICE_SNAPSHOT: await SonosSpeaker.snapshot_multi( - hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] + hass, speakers, service_call.data[ATTR_WITH_GROUP] ) elif service_call.service == SERVICE_RESTORE: await SonosSpeaker.restore_multi( - hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] + hass, speakers, service_call.data[ATTR_WITH_GROUP] ) config_entry.async_on_unload( @@ -153,7 +153,7 @@ async def async_setup_entry( SONOS_DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_SET_TIMER, { vol.Required(ATTR_SLEEP_TIME): vol.All( @@ -163,9 +163,9 @@ async def async_setup_entry( "set_sleep_timer", ) - platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") # type: ignore + platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_UPDATE_ALARM, { vol.Required(ATTR_ALARM_ID): cv.positive_int, @@ -177,13 +177,13 @@ async def async_setup_entry( "set_alarm", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_PLAY_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, "play_queue", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_REMOVE_FROM_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, "remove_from_queue", @@ -239,8 +239,8 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return if the media_player is available.""" return ( self.speaker.available - and self.speaker.sonos_group_entities - and self.media.playback_status + and bool(self.speaker.sonos_group_entities) + and self.media.playback_status is not None ) @property @@ -257,7 +257,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return a hash of self.""" return hash(self.unique_id) - @property # type: ignore[misc] + @property def state(self) -> str: """Return the state of the entity.""" if self.media.playback_status in ( @@ -300,13 +300,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return true if volume is muted.""" return self.speaker.muted - @property # type: ignore[misc] - def shuffle(self) -> str | None: + @property + def shuffle(self) -> bool | None: """Shuffling state.""" - shuffle: str = PLAY_MODES[self.media.play_mode][0] - return shuffle + return PLAY_MODES[self.media.play_mode][0] - @property # type: ignore[misc] + @property def repeat(self) -> str | None: """Return current repeat mode.""" sonos_repeat = PLAY_MODES[self.media.play_mode][1] @@ -317,32 +316,32 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return the SonosMedia object from the coordinator speaker.""" return self.coordinator.media - @property # type: ignore[misc] + @property def media_content_id(self) -> str | None: """Content id of current playing media.""" return self.media.uri - @property # type: ignore[misc] - def media_duration(self) -> float | None: + @property + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" - return self.media.duration + return int(self.media.duration) if self.media.duration else None - @property # type: ignore[misc] - def media_position(self) -> float | None: + @property + def media_position(self) -> int | None: """Position of current playing media in seconds.""" - return self.media.position + return int(self.media.position) if self.media.position else None - @property # type: ignore[misc] + @property def media_position_updated_at(self) -> datetime.datetime | None: """When was the position of the current playing media valid.""" return self.media.position_updated_at - @property # type: ignore[misc] + @property def media_image_url(self) -> str | None: """Image url of current playing media.""" return self.media.image_url or None - @property # type: ignore[misc] + @property def media_channel(self) -> str | None: """Channel currently playing.""" return self.media.channel or None @@ -352,22 +351,22 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Title of playlist currently playing.""" return self.media.playlist_name - @property # type: ignore[misc] + @property def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" return self.media.artist or None - @property # type: ignore[misc] + @property def media_album_name(self) -> str | None: """Album name of current playing media, music track only.""" return self.media.album_name or None - @property # type: ignore[misc] + @property def media_title(self) -> str | None: """Title of current playing media.""" return self.media.title or None - @property # type: ignore[misc] + @property def source(self) -> str | None: """Name of the current input source.""" return self.media.source_name or None @@ -383,12 +382,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.soco.volume -= VOLUME_INCREMENT @soco_error() - def set_volume_level(self, volume: str) -> None: + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.soco.volume = str(int(volume * 100)) @soco_error(UPNP_ERRORS_TO_IGNORE) - def set_shuffle(self, shuffle: str) -> None: + def set_shuffle(self, shuffle: bool) -> None: """Enable/Disable shuffle mode.""" sonos_shuffle = shuffle sonos_repeat = PLAY_MODES[self.media.play_mode][1] @@ -486,7 +485,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.coordinator.soco.previous() @soco_error(UPNP_ERRORS_TO_IGNORE) - def media_seek(self, position: str) -> None: + def media_seek(self, position: float) -> None: """Send seek command.""" self.coordinator.soco.seek(str(datetime.timedelta(seconds=int(position)))) @@ -606,7 +605,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): - item = media_browser.get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] + item = media_browser.get_media(self.media.library, media_id, media_type) soco.play_uri(item.get_uri()) return try: @@ -619,7 +618,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.add_to_queue(playlist) soco.play_from_queue(0) elif media_type in PLAYABLE_MEDIA_TYPES: - item = media_browser.get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] + item = media_browser.get_media(self.media.library, media_id, media_type) if not item: _LOGGER.error('Could not find "%s" in the library', media_id) @@ -649,7 +648,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): include_linked_zones: bool | None = None, ) -> None: """Set the alarm clock on the player.""" - alarm = None + alarm: alarms.Alarm | None = None for one_alarm in alarms.get_alarms(self.coordinator.soco): if one_alarm.alarm_id == str(alarm_id): alarm = one_alarm @@ -710,8 +709,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): MEDIA_TYPES_TO_SONOS[media_content_type], ) if image_url := getattr(item, "album_art_uri", None): - result = await self._async_fetch_image(image_url) # type: ignore[no-untyped-call] - return result # type: ignore + return await self._async_fetch_image(image_url) return (None, None) @@ -728,7 +726,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_content_type, ) - async def async_join_players(self, group_members): + async def async_join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" speakers = [] for entity_id in group_members: @@ -739,7 +737,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): await SonosSpeaker.join_multi(self.hass, self.speaker, speakers) - async def async_unjoin_player(self): + async def async_unjoin_player(self) -> None: """Remove this player from any group. Coalesces all calls within UNJOIN_SERVICE_TIMEOUT to allow use of SonosSpeaker.unjoin_multi() diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index ccbcbc3c339..7a6edb0d293 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry @@ -24,6 +25,8 @@ LEVEL_TYPES = { "music_surround_level": (-15, 15), } +SocoFeatures = list[tuple[str, tuple[int, int]]] + _LOGGER = logging.getLogger(__name__) @@ -34,8 +37,8 @@ async def async_setup_entry( ) -> None: """Set up the Sonos number platform from a config entry.""" - def available_soco_attributes(speaker: SonosSpeaker) -> list[str]: - features = [] + def available_soco_attributes(speaker: SonosSpeaker) -> SocoFeatures: + features: SocoFeatures = [] for level_type, valid_range in LEVEL_TYPES.items(): if (state := getattr(speaker.soco, level_type, None)) is not None: setattr(speaker, level_type, state) @@ -67,7 +70,7 @@ class SonosLevelEntity(SonosEntity, NumberEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, speaker: SonosSpeaker, level_type: str, valid_range: tuple[int] + self, speaker: SonosSpeaker, level_type: str, valid_range: tuple[int, int] ) -> None: """Initialize the level entity.""" super().__init__(speaker) @@ -94,4 +97,4 @@ class SonosLevelEntity(SonosEntity, NumberEntity): @property def native_value(self) -> float: """Return the current value.""" - return getattr(self.speaker, self.level_type) + return cast(float, getattr(self.speaker, self.level_type)) diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 8477e523a40..d1705fb030d 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -100,7 +100,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): @property def available(self) -> bool: """Return whether this device is available.""" - return self.speaker.available and self.speaker.power_source + return self.speaker.available and self.speaker.power_source is not None class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 0c5bec06dfb..516a431295a 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -8,7 +8,7 @@ import datetime from functools import partial import logging import time -from typing import Any +from typing import Any, cast import async_timeout import defusedxml.ElementTree as ET @@ -97,17 +97,17 @@ class SonosSpeaker: self.media = SonosMedia(hass, soco) self._plex_plugin: PlexPlugin | None = None self._share_link_plugin: ShareLinkPlugin | None = None - self.available = True + self.available: bool = True # Device information - self.hardware_version = speaker_info["hardware_version"] - self.software_version = speaker_info["software_version"] - self.mac_address = speaker_info["mac_address"] - self.model_name = speaker_info["model_name"] - self.model_number = speaker_info["model_number"] - self.uid = speaker_info["uid"] - self.version = speaker_info["display_version"] - self.zone_name = speaker_info["zone_name"] + self.hardware_version: str = speaker_info["hardware_version"] + self.software_version: str = speaker_info["software_version"] + self.mac_address: str = speaker_info["mac_address"] + self.model_name: str = speaker_info["model_name"] + self.model_number: str = speaker_info["model_number"] + self.uid: str = speaker_info["uid"] + self.version: str = speaker_info["display_version"] + self.zone_name: str = speaker_info["zone_name"] # Subscriptions and events self.subscriptions_failed: bool = False @@ -160,12 +160,12 @@ class SonosSpeaker: self.sonos_group: list[SonosSpeaker] = [self] self.sonos_group_entities: list[str] = [] self.soco_snapshot: Snapshot | None = None - self.snapshot_group: list[SonosSpeaker] | None = None + self.snapshot_group: list[SonosSpeaker] = [] self._group_members_missing: set[str] = set() async def async_setup_dispatchers(self, entry: ConfigEntry) -> None: """Connect dispatchers in async context during setup.""" - dispatch_pairs = ( + dispatch_pairs: tuple[tuple[str, Callable[..., Any]], ...] = ( (SONOS_CHECK_ACTIVITY, self.async_check_activity), (SONOS_SPEAKER_ADDED, self.update_group_for_uid), (f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted), @@ -283,18 +283,17 @@ class SonosSpeaker: return self._share_link_plugin @property - def subscription_address(self) -> str | None: - """Return the current subscription callback address if any.""" - if self._subscriptions: - addr, port = self._subscriptions[0].event_listener.address - return ":".join([addr, str(port)]) - return None + def subscription_address(self) -> str: + """Return the current subscription callback address.""" + assert len(self._subscriptions) > 0 + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) # # Subscription handling and event dispatchers # def log_subscription_result( - self, result: Any, event: str, level: str = logging.DEBUG + self, result: Any, event: str, level: int = logging.DEBUG ) -> None: """Log a message if a subscription action (create/renew/stop) results in an exception.""" if not isinstance(result, Exception): @@ -304,7 +303,7 @@ class SonosSpeaker: message = "Request timed out" exc_info = None else: - message = result + message = str(result) exc_info = result if not str(result) else None _LOGGER.log( @@ -554,7 +553,7 @@ class SonosSpeaker: ) @callback - def speaker_activity(self, source): + def speaker_activity(self, source: str) -> None: """Track the last activity on this speaker, set availability and resubscribe.""" if self._resub_cooldown_expires_at: if time.monotonic() < self._resub_cooldown_expires_at: @@ -593,6 +592,7 @@ class SonosSpeaker: async def async_offline(self) -> None: """Handle removal of speaker when unavailable.""" + assert self._subscription_lock is not None async with self._subscription_lock: await self._async_offline() @@ -826,8 +826,8 @@ class SonosSpeaker: if speaker: self._group_members_missing.discard(uid) sonos_group.append(speaker) - entity_id = entity_registry.async_get_entity_id( - MP_DOMAIN, DOMAIN, uid + entity_id = cast( + str, entity_registry.async_get_entity_id(MP_DOMAIN, DOMAIN, uid) ) sonos_group_entities.append(entity_id) else: @@ -850,7 +850,9 @@ class SonosSpeaker: self.async_write_entity_states() for joined_uid in group[1:]: - joined_speaker = self.hass.data[DATA_SONOS].discovered.get(joined_uid) + joined_speaker: SonosSpeaker = self.hass.data[ + DATA_SONOS + ].discovered.get(joined_uid) if joined_speaker: joined_speaker.coordinator = self joined_speaker.sonos_group = sonos_group @@ -936,7 +938,7 @@ class SonosSpeaker: if with_group: self.snapshot_group = self.sonos_group.copy() else: - self.snapshot_group = None + self.snapshot_group = [] @staticmethod async def snapshot_multi( @@ -969,7 +971,7 @@ class SonosSpeaker: _LOGGER.warning("Error on restore %s: %s", self.zone_name, ex) self.soco_snapshot = None - self.snapshot_group = None + self.snapshot_group = [] @staticmethod async def restore_multi( @@ -996,7 +998,7 @@ class SonosSpeaker: exc_info=exc, ) - groups = [] + groups: list[list[SonosSpeaker]] = [] if not with_group: return groups @@ -1022,7 +1024,7 @@ class SonosSpeaker: # Bring back the original group topology for speaker in (s for s in speakers if s.snapshot_group): - assert speaker.snapshot_group is not None + assert len(speaker.snapshot_group) if speaker.snapshot_group[0] == speaker: if speaker.snapshot_group not in (speaker.sonos_group, [speaker]): speaker.join(speaker.snapshot_group) @@ -1047,7 +1049,7 @@ class SonosSpeaker: if with_group: for speaker in [s for s in speakers_set if s.snapshot_group]: - assert speaker.snapshot_group is not None + assert len(speaker.snapshot_group) speakers_set.update(speaker.snapshot_group) async with hass.data[DATA_SONOS].topology_condition: diff --git a/homeassistant/components/sonos/statistics.py b/homeassistant/components/sonos/statistics.py index a850e5a8caf..b761469aea5 100644 --- a/homeassistant/components/sonos/statistics.py +++ b/homeassistant/components/sonos/statistics.py @@ -14,7 +14,7 @@ class SonosStatistics: def __init__(self, zone_name: str, kind: str) -> None: """Initialize SonosStatistics.""" - self._stats = {} + self._stats: dict[str, dict[str, int | float]] = {} self._stat_type = kind self.zone_name = zone_name diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index a348b40cb0f..acf33ea34aa 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -3,8 +3,9 @@ from __future__ import annotations import datetime import logging -from typing import Any +from typing import Any, cast +from soco.alarms import Alarm from soco.exceptions import SoCoSlaveException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity @@ -183,14 +184,14 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): def is_on(self) -> bool: """Return True if entity is on.""" if self.needs_coordinator and not self.speaker.is_coordinator: - return getattr(self.speaker.coordinator, self.feature_type) - return getattr(self.speaker, self.feature_type) + return cast(bool, getattr(self.speaker.coordinator, self.feature_type)) + return cast(bool, getattr(self.speaker, self.feature_type)) - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self.send_command(True) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self.send_command(False) @@ -233,7 +234,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): ) @property - def alarm(self): + def alarm(self) -> Alarm: """Return the alarm instance.""" return self.hass.data[DATA_SONOS].alarms[self.household_id].get(self.alarm_id) @@ -247,7 +248,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll() @callback - def async_check_if_available(self): + def async_check_if_available(self) -> bool: """Check if alarm exists and remove alarm entity if not available.""" if self.alarm: return True @@ -279,7 +280,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): self.async_write_ha_state() @callback - def _async_update_device(self): + def _async_update_device(self) -> None: """Update the device, since this alarm moved to a different player.""" device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) @@ -288,22 +289,20 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): if entity is None: raise RuntimeError("Alarm has been deleted by accident.") - entry_id = entity.config_entry_id - new_device = device_registry.async_get_or_create( - config_entry_id=entry_id, + config_entry_id=cast(str, entity.config_entry_id), identifiers={(SONOS_DOMAIN, self.soco.uid)}, connections={(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address)}, ) - if not entity_registry.async_get(self.entity_id).device_id == new_device.id: + if ( + device := entity_registry.async_get(self.entity_id) + ) and device.device_id != new_device.id: _LOGGER.debug("%s is moving to %s", self.entity_id, new_device.name) - # pylint: disable=protected-access - entity_registry._async_update_entity( - self.entity_id, device_id=new_device.id - ) + entity_registry.async_update_entity(self.entity_id, device_id=new_device.id) @property - def _is_today(self): + def _is_today(self) -> bool: + """Return whether this alarm is scheduled for today.""" recurrence = self.alarm.recurrence timestr = int(datetime.datetime.today().strftime("%w")) return ( @@ -321,12 +320,12 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): return (self.alarm is not None) and self.speaker.available @property - def is_on(self): + def is_on(self) -> bool: """Return state of Sonos alarm switch.""" return self.alarm.enabled @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return attributes of Sonos alarm switch.""" return { ATTR_ID: str(self.alarm_id), diff --git a/mypy.ini b/mypy.ini index d6665cb40c8..957da7254eb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2589,39 +2589,3 @@ disallow_untyped_decorators = false disallow_untyped_defs = false warn_return_any = false warn_unreachable = false - -[mypy-homeassistant.components.sonos] -ignore_errors = true - -[mypy-homeassistant.components.sonos.alarms] -ignore_errors = true - -[mypy-homeassistant.components.sonos.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.sonos.diagnostics] -ignore_errors = true - -[mypy-homeassistant.components.sonos.entity] -ignore_errors = true - -[mypy-homeassistant.components.sonos.favorites] -ignore_errors = true - -[mypy-homeassistant.components.sonos.media_browser] -ignore_errors = true - -[mypy-homeassistant.components.sonos.media_player] -ignore_errors = true - -[mypy-homeassistant.components.sonos.number] -ignore_errors = true - -[mypy-homeassistant.components.sonos.sensor] -ignore_errors = true - -[mypy-homeassistant.components.sonos.speaker] -ignore_errors = true - -[mypy-homeassistant.components.sonos.statistics] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b6c31751e12..0c598df9cd1 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -15,20 +15,7 @@ from .model import Config, Integration # If you are an author of component listed here, please fix these errors and # remove your component from this list to enable type checks. # Do your best to not add anything new here. -IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.sonos", - "homeassistant.components.sonos.alarms", - "homeassistant.components.sonos.binary_sensor", - "homeassistant.components.sonos.diagnostics", - "homeassistant.components.sonos.entity", - "homeassistant.components.sonos.favorites", - "homeassistant.components.sonos.media_browser", - "homeassistant.components.sonos.media_player", - "homeassistant.components.sonos.number", - "homeassistant.components.sonos.sensor", - "homeassistant.components.sonos.speaker", - "homeassistant.components.sonos.statistics", -] +IGNORED_MODULES: Final[list[str]] = [] # Component modules which should set no_implicit_reexport = true. NO_IMPLICIT_REEXPORT_MODULES: set[str] = { From e07554dc25f89134f66e2917a1be83a54538a6d2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Sep 2022 21:43:38 +0200 Subject: [PATCH 864/903] Bump yale_smart_alarm_client to 0.3.9 (#77797) --- homeassistant/components/yale_smart_alarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index 0b1a5a94da0..865751d18e0 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -2,7 +2,7 @@ "domain": "yale_smart_alarm", "name": "Yale Smart Living", "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", - "requirements": ["yalesmartalarmclient==0.3.8"], + "requirements": ["yalesmartalarmclient==0.3.9"], "codeowners": ["@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 609b032cfe3..fd169055887 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2539,7 +2539,7 @@ xmltodict==0.13.0 xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.8 +yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble yalexs-ble==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d12689484d6..7069c0fb3fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1743,7 +1743,7 @@ xknx==1.0.2 xmltodict==0.13.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.8 +yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble yalexs-ble==1.6.4 From 1231ba4d03292b3ada972608f6cf301a7fcb55fc Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 5 Sep 2022 13:52:50 +0200 Subject: [PATCH 865/903] Rename BThome to BTHome (#77807) Co-authored-by: Paulus Schoutsen Co-authored-by: J. Nick Koston --- homeassistant/components/bthome/__init__.py | 10 +++++----- homeassistant/components/bthome/config_flow.py | 10 +++++----- homeassistant/components/bthome/const.py | 2 +- homeassistant/components/bthome/device.py | 2 +- homeassistant/components/bthome/manifest.json | 4 ++-- homeassistant/components/bthome/sensor.py | 10 +++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bthome/__init__.py | 2 +- tests/components/bthome/test_config_flow.py | 6 +++--- tests/components/bthome/test_sensor.py | 2 +- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/bthome/__init__.py b/homeassistant/components/bthome/__init__.py index 4cc3b5cf4da..93ebd7b288f 100644 --- a/homeassistant/components/bthome/__init__.py +++ b/homeassistant/components/bthome/__init__.py @@ -1,9 +1,9 @@ -"""The BThome Bluetooth integration.""" +"""The BTHome Bluetooth integration.""" from __future__ import annotations import logging -from bthome_ble import BThomeBluetoothDeviceData, SensorUpdate +from bthome_ble import BTHomeBluetoothDeviceData, SensorUpdate from bthome_ble.parser import EncryptionScheme from homeassistant.components.bluetooth import ( @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) def process_service_info( hass: HomeAssistant, entry: ConfigEntry, - data: BThomeBluetoothDeviceData, + data: BTHomeBluetoothDeviceData, service_info: BluetoothServiceInfoBleak, ) -> SensorUpdate: """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" @@ -40,14 +40,14 @@ def process_service_info( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up BThome Bluetooth from a config entry.""" + """Set up BTHome Bluetooth from a config entry.""" address = entry.unique_id assert address is not None kwargs = {} if bindkey := entry.data.get("bindkey"): kwargs["bindkey"] = bytes.fromhex(bindkey) - data = BThomeBluetoothDeviceData(**kwargs) + data = BTHomeBluetoothDeviceData(**kwargs) coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id diff --git a/homeassistant/components/bthome/config_flow.py b/homeassistant/components/bthome/config_flow.py index e8e49cab566..6514f2c5396 100644 --- a/homeassistant/components/bthome/config_flow.py +++ b/homeassistant/components/bthome/config_flow.py @@ -1,11 +1,11 @@ -"""Config flow for BThome Bluetooth integration.""" +"""Config flow for BTHome Bluetooth integration.""" from __future__ import annotations from collections.abc import Mapping import dataclasses from typing import Any -from bthome_ble import BThomeBluetoothDeviceData as DeviceData +from bthome_ble import BTHomeBluetoothDeviceData as DeviceData from bthome_ble.parser import EncryptionScheme import voluptuous as vol @@ -34,8 +34,8 @@ def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name -class BThomeConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for BThome Bluetooth.""" +class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for BTHome Bluetooth.""" VERSION = 1 @@ -68,7 +68,7 @@ class BThomeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_get_encryption_key( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Enter a bindkey for an encrypted BThome device.""" + """Enter a bindkey for an encrypted BTHome device.""" assert self._discovery_info assert self._discovered_device diff --git a/homeassistant/components/bthome/const.py b/homeassistant/components/bthome/const.py index e397e288071..e46aa50e148 100644 --- a/homeassistant/components/bthome/const.py +++ b/homeassistant/components/bthome/const.py @@ -1,3 +1,3 @@ -"""Constants for the BThome Bluetooth integration.""" +"""Constants for the BTHome Bluetooth integration.""" DOMAIN = "bthome" diff --git a/homeassistant/components/bthome/device.py b/homeassistant/components/bthome/device.py index f16b2f49998..bd011752db1 100644 --- a/homeassistant/components/bthome/device.py +++ b/homeassistant/components/bthome/device.py @@ -1,4 +1,4 @@ -"""Support for BThome Bluetooth devices.""" +"""Support for BTHome Bluetooth devices.""" from __future__ import annotations from bthome_ble import DeviceKey, SensorDeviceInfo diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index bdb4b75bfa9..597d52c72e4 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -1,6 +1,6 @@ { "domain": "bthome", - "name": "BThome", + "name": "BTHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bthome", "bluetooth": [ @@ -13,7 +13,7 @@ "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==0.5.2"], + "requirements": ["bthome-ble==1.0.0"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 71601fa24c0..a0068596b01 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -1,4 +1,4 @@ -"""Support for BThome sensors.""" +"""Support for BTHome sensors.""" from __future__ import annotations from typing import Optional, Union @@ -202,26 +202,26 @@ async def async_setup_entry( entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the BThome BLE sensors.""" + """Set up the BTHome BLE sensors.""" coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( - BThomeBluetoothSensorEntity, async_add_entities + BTHomeBluetoothSensorEntity, async_add_entities ) ) entry.async_on_unload(coordinator.async_register_processor(processor)) -class BThomeBluetoothSensorEntity( +class BTHomeBluetoothSensorEntity( PassiveBluetoothProcessorEntity[ PassiveBluetoothDataProcessor[Optional[Union[float, int]]] ], SensorEntity, ): - """Representation of a BThome BLE sensor.""" + """Representation of a BTHome BLE sensor.""" @property def native_value(self) -> int | float | None: diff --git a/requirements_all.txt b/requirements_all.txt index fd169055887..6a44809cd96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ bsblan==0.5.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==0.5.2 +bthome-ble==1.0.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7069c0fb3fe..30a8fe4ccdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ brunt==1.2.0 bsblan==0.5.0 # homeassistant.components.bthome -bthome-ble==0.5.2 +bthome-ble==1.0.0 # homeassistant.components.buienradar buienradar==1.0.5 diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index be59cd7e8cb..e480c0a3810 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -1,4 +1,4 @@ -"""Tests for the BThome integration.""" +"""Tests for the BTHome integration.""" from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData diff --git a/tests/components/bthome/test_config_flow.py b/tests/components/bthome/test_config_flow.py index fd8f8dfaa35..64a298e3460 100644 --- a/tests/components/bthome/test_config_flow.py +++ b/tests/components/bthome/test_config_flow.py @@ -1,8 +1,8 @@ -"""Test the BThome config flow.""" +"""Test the BTHome config flow.""" from unittest.mock import patch -from bthome_ble import BThomeBluetoothDeviceData as DeviceData +from bthome_ble import BTHomeBluetoothDeviceData as DeviceData from homeassistant import config_entries from homeassistant.components.bluetooth import BluetoothChange @@ -167,7 +167,7 @@ async def test_async_step_user_no_devices_found_2(hass): """ Test setup from service info cache with no devices found. - This variant tests with a non-BThome device known to us. + This variant tests with a non-BTHome device known to us. """ with patch( "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index f73d3bf379c..bb0c5b3f459 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -1,4 +1,4 @@ -"""Test the BThome sensors.""" +"""Test the BTHome sensors.""" from unittest.mock import patch From f3e811417f25aa40c6c18a20af9b0b1ba0088dbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 20:57:40 -0400 Subject: [PATCH 866/903] Prefilter noisy apple devices from bluetooth (#77808) --- homeassistant/components/bluetooth/manager.py | 18 ++- tests/components/bluetooth/test_init.py | 128 ++++++++++++------ 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index d274939c610..9fc00aa159b 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -54,6 +54,10 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" +APPLE_MFR_ID: Final = 76 +APPLE_HOMEKIT_START_BYTE: Final = 0x06 # homekit_controller +APPLE_DEVICE_ID_START_BYTE: Final = 0x10 # bluetooth_le_tracker +APPLE_START_BYTES_WANTED: Final = {APPLE_DEVICE_ID_START_BYTE, APPLE_HOMEKIT_START_BYTE} RSSI_SWITCH_THRESHOLD = 6 @@ -290,6 +294,19 @@ class BluetoothManager: than the source from the history or the timestamp in the history is older than 180s """ + + # Pre-filter noisy apple devices as they can account for 20-35% of the + # traffic on a typical network. + advertisement_data = service_info.advertisement + manufacturer_data = advertisement_data.manufacturer_data + if ( + len(manufacturer_data) == 1 + and (apple_data := manufacturer_data.get(APPLE_MFR_ID)) + and apple_data[0] not in APPLE_START_BYTES_WANTED + and not advertisement_data.service_data + ): + return + device = service_info.device connectable = service_info.connectable address = device.address @@ -299,7 +316,6 @@ class BluetoothManager: return self._history[address] = service_info - advertisement_data = service_info.advertisement source = service_info.source if connectable: diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ade68fdb94d..e4b84b943b4 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1291,16 +1291,16 @@ async def test_register_callback_by_manufacturer_id( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {MANUFACTURER_ID: 76}, + {MANUFACTURER_ID: 21}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) inject_advertisement(hass, apple_device, apple_adv) @@ -1316,9 +1316,59 @@ async def test_register_callback_by_manufacturer_id( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 + + +async def test_filtering_noisy_apple_devices( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test filtering noisy apple devices.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {MANUFACTURER_ID: 21}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") + apple_adv = AdvertisementData( + local_name="noisy", + manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + inject_advertisement(hass, empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 0 async def test_register_callback_by_address_connectable_manufacturer_id( @@ -1346,21 +1396,21 @@ async def test_register_callback_by_address_connectable_manufacturer_id( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {MANUFACTURER_ID: 76, CONNECTABLE: False, ADDRESS: "44:44:33:11:23:45"}, + {MANUFACTURER_ID: 21, CONNECTABLE: False, ADDRESS: "44:44:33:11:23:45"}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) inject_advertisement(hass, apple_device, apple_adv) - apple_device_wrong_address = BLEDevice("44:44:33:11:23:46", "apple") + apple_device_wrong_address = BLEDevice("44:44:33:11:23:46", "rtx") inject_advertisement(hass, apple_device_wrong_address, apple_adv) await hass.async_block_till_done() @@ -1370,9 +1420,9 @@ async def test_register_callback_by_address_connectable_manufacturer_id( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 async def test_register_callback_by_manufacturer_id_and_address( @@ -1400,19 +1450,19 @@ async def test_register_callback_by_manufacturer_id_and_address( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {MANUFACTURER_ID: 76, ADDRESS: "44:44:33:11:23:45"}, + {MANUFACTURER_ID: 21, ADDRESS: "44:44:33:11:23:45"}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") - apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + rtx_device = BLEDevice("44:44:33:11:23:45", "rtx") + rtx_adv = AdvertisementData( + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) - inject_advertisement(hass, apple_device, apple_adv) + inject_advertisement(hass, rtx_device, rtx_adv) yale_device = BLEDevice("44:44:33:11:23:45", "apple") yale_adv = AdvertisementData( @@ -1426,7 +1476,7 @@ async def test_register_callback_by_manufacturer_id_and_address( other_apple_device = BLEDevice("44:44:33:11:23:22", "apple") other_apple_adv = AdvertisementData( local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) inject_advertisement(hass, other_apple_device, other_apple_adv) @@ -1435,9 +1485,9 @@ async def test_register_callback_by_manufacturer_id_and_address( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 async def test_register_callback_by_service_uuid_and_address( @@ -1603,31 +1653,31 @@ async def test_register_callback_by_local_name( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {LOCAL_NAME: "apple"}, + {LOCAL_NAME: "rtx"}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") - apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + rtx_device = BLEDevice("44:44:33:11:23:45", "rtx") + rtx_adv = AdvertisementData( + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) - inject_advertisement(hass, apple_device, apple_adv) + inject_advertisement(hass, rtx_device, rtx_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") inject_advertisement(hass, empty_device, empty_adv) - apple_device_2 = BLEDevice("44:44:33:11:23:45", "apple") - apple_adv_2 = AdvertisementData( - local_name="apple2", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + rtx_device_2 = BLEDevice("44:44:33:11:23:45", "rtx") + rtx_adv_2 = AdvertisementData( + local_name="rtx2", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) - inject_advertisement(hass, apple_device_2, apple_adv_2) + inject_advertisement(hass, rtx_device_2, rtx_adv_2) await hass.async_block_till_done() @@ -1636,9 +1686,9 @@ async def test_register_callback_by_local_name( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 async def test_register_callback_by_local_name_overly_broad( From b1241bf0f2b06645ec31172fddc74cae3b6d778c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 01:45:35 -0500 Subject: [PATCH 867/903] Fix isy994 calling sync api in async context (#77812) --- homeassistant/components/isy994/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 54ee9a2ded5..61f42a60a6e 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -75,7 +75,7 @@ class ISYEntity(Entity): # New state attributes may be available, update the state. self.async_write_ha_state() - self.hass.bus.fire("isy994_control", event_data) + self.hass.bus.async_fire("isy994_control", event_data) @property def device_info(self) -> DeviceInfo | None: From e8ab4eef44a391cc7b7bc41749f0a23662f8f69b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 5 Sep 2022 06:15:14 -0400 Subject: [PATCH 868/903] Fix device info for zwave_js device entities (#77821) --- homeassistant/components/zwave_js/button.py | 10 ++++------ homeassistant/components/zwave_js/helpers.py | 13 +++++++++++++ homeassistant/components/zwave_js/sensor.py | 10 ++++------ homeassistant/components/zwave_js/update.py | 13 +++---------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index cef64f1724a..1d97ed05da5 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -9,11 +9,11 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DOMAIN, LOGGER -from .helpers import get_device_id, get_valueless_base_unique_id +from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 0 @@ -58,10 +58,8 @@ class ZWaveNodePingButton(ButtonEntity): self._attr_name = f"{name}: Ping" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.ping" - # device is precreated in main handler - self._attr_device_info = DeviceInfo( - identifiers={get_device_id(driver, node)}, - ) + # device may not be precreated in main handler yet + self._attr_device_info = get_device_info(driver, node) async def async_poll_value(self, _: bool) -> None: """Poll a value.""" diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index c047a3a9903..6175b7db353 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -30,6 +30,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from .const import ( @@ -413,3 +414,15 @@ def get_value_state_schema( vol.Coerce(int), vol.Range(min=value.metadata.min, max=value.metadata.max), ) + + +def get_device_info(driver: Driver, node: ZwaveNode) -> DeviceInfo: + """Get DeviceInfo for node.""" + return DeviceInfo( + identifiers={get_device_id(driver, 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, + suggested_area=node.location if node.location else None, + ) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 22fbfdab728..75d8066d595 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -28,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -63,7 +63,7 @@ from .discovery_data_template import ( NumericSensorDataTemplateData, ) from .entity import ZWaveBaseEntity -from .helpers import get_device_id, get_valueless_base_unique_id +from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 0 @@ -493,10 +493,8 @@ class ZWaveNodeStatusSensor(SensorEntity): self._attr_name = f"{name}: Node Status" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.node_status" - # device is precreated in main handler - self._attr_device_info = DeviceInfo( - identifiers={get_device_id(driver, self.node)}, - ) + # device may not be precreated in main handler yet + self._attr_device_info = get_device_info(driver, node) self._attr_native_value: str = node.status.name.lower() async def async_poll_value(self, _: bool) -> None: diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 1f04c3acc47..7f25788e0be 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -19,11 +19,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER -from .helpers import get_device_id, get_valueless_base_unique_id +from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 1 SCAN_INTERVAL = timedelta(days=1) @@ -75,14 +75,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.firmware_update" # device may not be precreated in main handler yet - self._attr_device_info = DeviceInfo( - identifiers={get_device_id(driver, 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, - suggested_area=node.location if node.location else None, - ) + self._attr_device_info = get_device_info(driver, node) self._attr_installed_version = self._attr_latest_version = node.firmware_version From ad8cd9c95798ed0d9672335936d8956de39bc277 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Mon, 5 Sep 2022 17:04:33 +0300 Subject: [PATCH 869/903] Bump pybravia to 0.2.1 (#77832) --- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 8a18cac5a99..fa172957781 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.0"], + "requirements": ["pybravia==0.2.1"], "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 6a44809cd96..db3390b3eb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,7 +1443,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.0 +pybravia==0.2.1 # homeassistant.components.nissan_leaf pycarwings2==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30a8fe4ccdd..b209c9b163d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1019,7 +1019,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.0 +pybravia==0.2.1 # homeassistant.components.cloudflare pycfdns==1.2.2 From 605e350159228ab068416d86c63f2fc09cad7dbd Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 5 Sep 2022 09:20:37 -0400 Subject: [PATCH 870/903] Add remoteAdminPasswordEnd to redacted keys in fully_kiosk diagnostics (#77837) Add remoteAdminPasswordEnd to redacted keys in diagnostics --- homeassistant/components/fully_kiosk/diagnostics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fully_kiosk/diagnostics.py b/homeassistant/components/fully_kiosk/diagnostics.py index 89a894d5353..121621186cd 100644 --- a/homeassistant/components/fully_kiosk/diagnostics.py +++ b/homeassistant/components/fully_kiosk/diagnostics.py @@ -51,6 +51,7 @@ SETTINGS_TO_REDACT = { "sebExamKey", "sebConfigKey", "kioskPinEnc", + "remoteAdminPasswordEnc", } From b0ff4fc057229552e180535f824ea67f62d76f72 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 5 Sep 2022 15:33:10 +0100 Subject: [PATCH 871/903] Less verbose error logs for bleak connection errors in ActiveBluetoothProcessorCoordinator (#77839) Co-authored-by: J. Nick Koston --- .../bluetooth/active_update_coordinator.py | 9 +++ .../test_active_update_coordinator.py | 76 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index e73414fe79f..b207f6fa2e1 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -6,6 +6,8 @@ import logging import time from typing import Any, Generic, TypeVar +from bleak import BleakError + from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer @@ -109,6 +111,13 @@ class ActiveBluetoothProcessorCoordinator( try: update = await self._async_poll_data(self._last_service_info) + except BleakError as exc: + if self.last_poll_successful: + self.logger.error( + "%s: Bluetooth error whilst polling: %s", self.address, str(exc) + ) + self.last_poll_successful = False + return except Exception: # pylint: disable=broad-except if self.last_poll_successful: self.logger.exception("%s: Failure while polling", self.address) diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 24ad96c523e..7677584e890 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -5,6 +5,8 @@ import asyncio import logging from unittest.mock import MagicMock, call, patch +from bleak import BleakError + from homeassistant.components.bluetooth import ( DOMAIN, BluetoothChange, @@ -162,6 +164,80 @@ async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start cancel() +async def test_bleak_error_and_recover( + hass: HomeAssistant, mock_bleak_scanner_start, caplog +): + """Test bleak error handling and recovery.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + flag = True + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + def _poll_needed(*args, **kwargs): + return True + + async def _poll(*args, **kwargs): + nonlocal flag + if flag: + raise BleakError("Connection was aborted") + return {"testdata": flag} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + poll_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=0, + immediate=True, + ), + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + # First poll fails + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": None}) + + assert ( + "aa:bb:cc:dd:ee:ff: Bluetooth error whilst polling: Connection was aborted" + in caplog.text + ) + + # Second poll works + flag = False + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": False}) + + cancel() + + async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_start): """Test error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) From 40421b41f7fe07a29ba40c4a3e1cc488dc64dc2a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Sep 2022 16:18:49 +0200 Subject: [PATCH 872/903] Add the hardware integration to default_config (#77840) --- homeassistant/components/default_config/manifest.json | 3 ++- homeassistant/package_constraints.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 593ac26dbc9..6701e62c71f 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -11,8 +11,9 @@ "dhcp", "energy", "frontend", - "homeassistant_alerts", + "hardware", "history", + "homeassistant_alerts", "input_boolean", "input_button", "input_datetime", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e41ae13458..8b65b9c0285 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,6 +28,7 @@ orjson==3.7.11 paho-mqtt==1.6.1 pillow==9.2.0 pip>=21.0,<22.3 +psutil-home-assistant==0.0.1 pyserial==3.5 python-slugify==4.0.1 pyudev==0.23.2 From 4f8421617eb567276dab38968e84db07831c118c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 13:05:37 -0500 Subject: [PATCH 873/903] Bump led-ble to 0.7.0 (#77845) --- homeassistant/components/led_ble/manifest.json | 5 +++-- homeassistant/generated/bluetooth.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index a0f5e3481d5..273fbfedc04 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -3,7 +3,7 @@ "name": "LED BLE", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ble_ble", - "requirements": ["led-ble==0.6.0"], + "requirements": ["led-ble==0.7.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ @@ -13,7 +13,8 @@ { "local_name": "Triones*" }, { "local_name": "LEDBlue*" }, { "local_name": "Dream~*" }, - { "local_name": "QHM-*" } + { "local_name": "QHM-*" }, + { "local_name": "AP-*" } ], "iot_class": "local_polling" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 83bfd3ab5eb..d7230213302 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -164,6 +164,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "led_ble", "local_name": "QHM-*" }, + { + "domain": "led_ble", + "local_name": "AP-*" + }, { "domain": "melnor", "manufacturer_data_start": [ diff --git a/requirements_all.txt b/requirements_all.txt index db3390b3eb9..460216f6394 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.6.0 +led-ble==0.7.0 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b209c9b163d..77f28f755e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.6.0 +led-ble==0.7.0 # homeassistant.components.foscam libpyfoscam==1.0 From bca9dc1f6160a915bcb62b96cc53da421cb96fb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 13:05:53 -0500 Subject: [PATCH 874/903] Bump govee-ble to 0.17.2 (#77849) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/govee_ble/__init__.py | 4 ++-- tests/components/govee_ble/test_config_flow.py | 6 +++--- tests/components/govee_ble/test_sensor.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index e24e3bfea14..2ce68498968 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -53,7 +53,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.17.1"], + "requirements": ["govee-ble==0.17.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 460216f6394..2976fb6a630 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -772,7 +772,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.1 +govee-ble==0.17.2 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77f28f755e0..008a6b39079 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -573,7 +573,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.1 +govee-ble==0.17.2 # homeassistant.components.gree greeclimate==1.3.0 diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index 3baea5e1140..c440317fa43 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -14,7 +14,7 @@ NOT_GOVEE_SERVICE_INFO = BluetoothServiceInfo( ) GVH5075_SERVICE_INFO = BluetoothServiceInfo( - name="GVH5075_2762", + name="GVH5075 2762", address="61DE521B-F0BF-9F44-64D4-75BBE1738105", rssi=-63, manufacturer_data={ @@ -26,7 +26,7 @@ GVH5075_SERVICE_INFO = BluetoothServiceInfo( ) GVH5177_SERVICE_INFO = BluetoothServiceInfo( - name="GVH5177_2EC8", + name="GVH5177 2EC8", address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", rssi=-56, manufacturer_data={ diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index 188672cdf18..73cbb903f31 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "H5075_2762" + assert result2["title"] == "H5075 2762" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "H5177_2EC8" + assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -192,7 +192,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "H5177_2EC8" + assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index da67d32e681..e7828fdc496 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -42,7 +42,7 @@ async def test_sensors(hass): temp_sensor = hass.states.get("sensor.h5075_2762_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "21.34" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075_2762 Temperature" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075 2762 Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From e8c4711d884a9c65990ee00114ecb5e51d16eb37 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 5 Sep 2022 20:27:48 +0200 Subject: [PATCH 875/903] Update frontend to 20220905.0 (#77854) --- 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 8459d08eab7..416634053d6 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==20220902.0"], + "requirements": ["home-assistant-frontend==20220905.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b65b9c0285..3bf00427954 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220902.0 +home-assistant-frontend==20220905.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2976fb6a630..77cd2faa49e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220902.0 +home-assistant-frontend==20220905.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 008a6b39079..a518ff1d9cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220902.0 +home-assistant-frontend==20220905.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 6c36d5acaaf8dfadb18095f3e053477ed425b759 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 Sep 2022 14:28:36 -0400 Subject: [PATCH 876/903] Bumped version to 2022.9.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7ae1fccf7f1..c4c15302a44 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index fc5260f41df..0a8db19d4ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b4" +version = "2022.9.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 74ddc336cad1fdfd5cb59310c528a22fbcb62131 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 6 Sep 2022 17:33:16 +0200 Subject: [PATCH 877/903] Use identifiers host and serial number to match device (#75657) --- homeassistant/components/upnp/__init__.py | 29 ++++++++++------ homeassistant/components/upnp/config_flow.py | 8 +++-- homeassistant/components/upnp/const.py | 5 +-- homeassistant/components/upnp/device.py | 8 ++++- tests/components/upnp/conftest.py | 7 ++-- tests/components/upnp/test_config_flow.py | 36 +++++++++++++++++++- 6 files changed, 75 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index a45e58f28bc..95531450e5a 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -25,15 +25,18 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, DOMAIN, + IDENTIFIER_HOST, + IDENTIFIER_SERIAL_NUMBER, LOGGER, ) -from .device import Device, async_create_device, async_get_mac_address_from_host +from .device import Device, async_create_device NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" @@ -106,24 +109,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device.original_udn = entry.data[CONFIG_ENTRY_ORIGINAL_UDN] # Store mac address for changed UDN matching. - if device.host: - device.mac_address = await async_get_mac_address_from_host(hass, device.host) - if device.mac_address and not entry.data.get("CONFIG_ENTRY_MAC_ADDRESS"): + device_mac_address = await device.async_get_mac_address() + if device_mac_address and not entry.data.get(CONFIG_ENTRY_MAC_ADDRESS): hass.config_entries.async_update_entry( entry=entry, data={ **entry.data, - CONFIG_ENTRY_MAC_ADDRESS: device.mac_address, + CONFIG_ENTRY_MAC_ADDRESS: device_mac_address, + CONFIG_ENTRY_HOST: device.host, }, ) + identifiers = {(DOMAIN, device.usn)} + if device.host: + identifiers.add((IDENTIFIER_HOST, device.host)) + if device.serial_number: + identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number)) + connections = {(dr.CONNECTION_UPNP, device.udn)} - if device.mac_address: - connections.add((dr.CONNECTION_NETWORK_MAC, device.mac_address)) + if device_mac_address: + connections.add((dr.CONNECTION_NETWORK_MAC, device_mac_address)) device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device( - identifiers=set(), connections=connections + identifiers=identifiers, connections=connections ) if device_entry: LOGGER.debug( @@ -136,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections=connections, - identifiers={(DOMAIN, device.usn)}, + identifiers=identifiers, name=device.name, manufacturer=device.manufacturer, model=device.model_name, @@ -148,7 +157,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Update identifier. device_entry = device_registry.async_update_device( device_entry.id, - new_identifiers={(DOMAIN, device.usn)}, + new_identifiers=identifiers, ) assert device_entry diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 7d4e768e855..3386cf40711 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from .const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, @@ -161,22 +162,25 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): unique_id = discovery_info.ssdp_usn await self.async_set_unique_id(unique_id) mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info) + host = discovery_info.ssdp_headers["_host"] self._abort_if_unique_id_configured( # Store mac address for older entries. # The location is stored in the config entry such that when the location changes, the entry is reloaded. updates={ CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location, + CONFIG_ENTRY_HOST: host, }, ) # Handle devices changing their UDN, only allow a single host. for entry in self._async_current_entries(include_ignore=True): entry_mac_address = entry.data.get(CONFIG_ENTRY_MAC_ADDRESS) - entry_st = entry.data.get(CONFIG_ENTRY_ST) - if entry_mac_address != mac_address: + entry_host = entry.data.get(CONFIG_ENTRY_HOST) + if entry_mac_address != mac_address and entry_host != host: continue + entry_st = entry.data.get(CONFIG_ENTRY_ST) if discovery_info.ssdp_st != entry_st: # Check ssdp_st to prevent swapping between IGDv1 and IGDv2. continue diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index e673922d1c2..023ec82a487 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -6,7 +6,6 @@ from homeassistant.const import TIME_SECONDS LOGGER = logging.getLogger(__package__) -CONF_LOCAL_IP = "local_ip" DOMAIN = "upnp" BYTES_RECEIVED = "bytes_received" BYTES_SENT = "bytes_sent" @@ -24,7 +23,9 @@ CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_ORIGINAL_UDN = "original_udn" CONFIG_ENTRY_MAC_ADDRESS = "mac_address" CONFIG_ENTRY_LOCATION = "location" +CONFIG_ENTRY_HOST = "host" +IDENTIFIER_HOST = "upnp_host" +IDENTIFIER_SERIAL_NUMBER = "upnp_serial_number" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).total_seconds() ST_IGD_V1 = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" ST_IGD_V2 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2" -SSDP_SEARCH_TIMEOUT = 4 diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 3a688b8571d..e06ada02b77 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -69,9 +69,15 @@ class Device: self.hass = hass self._igd_device = igd_device self.coordinator: DataUpdateCoordinator | None = None - self.mac_address: str | None = None self.original_udn: str | None = None + async def async_get_mac_address(self) -> str | None: + """Get mac address.""" + if not self.host: + return None + + return await async_get_mac_address_from_host(self.hass, self.host) + @property def udn(self) -> str: """Get the UDN.""" diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index e7cd24d0c7c..b159a371d9a 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -25,7 +25,7 @@ TEST_UDN = "uuid:device" TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" TEST_USN = f"{TEST_UDN}::{TEST_ST}" TEST_LOCATION = "http://192.168.1.1/desc.xml" -TEST_HOSTNAME = urlparse(TEST_LOCATION).hostname +TEST_HOST = urlparse(TEST_LOCATION).hostname TEST_FRIENDLY_NAME = "mock-name" TEST_MAC_ADDRESS = "00:11:22:33:44:55" TEST_DISCOVERY = ssdp.SsdpServiceInfo( @@ -41,10 +41,11 @@ TEST_DISCOVERY = ssdp.SsdpServiceInfo( ssdp.ATTR_UPNP_FRIENDLY_NAME: TEST_FRIENDLY_NAME, ssdp.ATTR_UPNP_MANUFACTURER: "mock-manufacturer", ssdp.ATTR_UPNP_MODEL_NAME: "mock-model-name", + ssdp.ATTR_UPNP_SERIAL: "mock-serial", ssdp.ATTR_UPNP_UDN: TEST_UDN, }, ssdp_headers={ - "_host": TEST_HOSTNAME, + "_host": TEST_HOST, }, ) @@ -54,8 +55,10 @@ def mock_igd_device() -> IgdDevice: """Mock async_upnp_client device.""" mock_upnp_device = create_autospec(UpnpDevice, instance=True) mock_upnp_device.device_url = TEST_DISCOVERY.ssdp_location + mock_upnp_device.serial_number = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_SERIAL] mock_igd_device = create_autospec(IgdDevice) + mock_igd_device.device_type = TEST_DISCOVERY.ssdp_st mock_igd_device.name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] mock_igd_device.manufacturer = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MANUFACTURER] mock_igd_device.model_name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MODEL_NAME] diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index e89b8274c18..f0a1de1ce37 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, @@ -21,6 +22,7 @@ from homeassistant.core import HomeAssistant from .conftest import ( TEST_DISCOVERY, TEST_FRIENDLY_NAME, + TEST_HOST, TEST_LOCATION, TEST_MAC_ADDRESS, TEST_ST, @@ -140,7 +142,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): @pytest.mark.usefixtures("mock_mac_address_from_host") -async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): +async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant): """Test config flow: discovery through ssdp, same device, but new UDN, matched on mac address.""" entry = MockConfigEntry( domain=DOMAIN, @@ -171,6 +173,38 @@ async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): assert result["reason"] == "config_entry_updated" +@pytest.mark.usefixtures("mock_mac_address_from_host") +async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant): + """Test config flow: discovery through ssdp, same device, but new UDN, matched on mac address.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_USN, + data={ + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_HOST: TEST_HOST, + }, + source=config_entries.SOURCE_SSDP, + state=config_entries.ConfigEntryState.LOADED, + ) + entry.add_to_hass(hass) + + # New discovery via step ssdp. + new_udn = TEST_UDN + "2" + new_discovery = deepcopy(TEST_DISCOVERY) + new_discovery.ssdp_usn = f"{new_udn}::{TEST_ST}" + new_discovery.upnp["_udn"] = new_udn + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=new_discovery, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "config_entry_updated" + + @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", From 3240f8f93864b6bae9272bb011082bf04a031780 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 3 Sep 2022 23:19:05 +0200 Subject: [PATCH 878/903] Refactor zwave_js event handling (#77732) * Refactor zwave_js event handling * Clean up --- homeassistant/components/zwave_js/__init__.py | 585 ++++++++++-------- homeassistant/components/zwave_js/const.py | 1 - 2 files changed, 338 insertions(+), 248 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 538fe911dd0..03a8ee5fce2 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Callable +from collections.abc import Coroutine from typing import Any from async_timeout import timeout @@ -79,7 +79,6 @@ from .const import ( CONF_USB_PATH, CONF_USE_ADDON, DATA_CLIENT, - DATA_PLATFORM_SETUP, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, @@ -104,7 +103,8 @@ from .services import ZWaveServices CONNECT_TIMEOUT = 10 DATA_CLIENT_LISTEN_TASK = "client_listen_task" -DATA_START_PLATFORM_TASK = "start_platform_task" +DATA_DRIVER_EVENTS = "driver_events" +DATA_START_CLIENT_TASK = "start_client_task" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -118,51 +118,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@callback -def register_node_in_dev_reg( - hass: HomeAssistant, - entry: ConfigEntry, - dev_reg: device_registry.DeviceRegistry, - driver: Driver, - node: ZwaveNode, - remove_device_func: Callable[[device_registry.DeviceEntry], None], -) -> device_registry.DeviceEntry: - """Register node in dev reg.""" - device_id = get_device_id(driver, node) - device_id_ext = get_device_id_ext(driver, node) - device = dev_reg.async_get_device({device_id}) - - # Replace the device if it can be determined that this node is not the - # same product as it was previously. - if ( - device_id_ext - and device - and len(device.identifiers) == 2 - and device_id_ext not in device.identifiers - ): - remove_device_func(device) - device = None - - if device_id_ext: - ids = {device_id, device_id_ext} - else: - ids = {device_id} - - device = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers=ids, - 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, - suggested_area=node.location if node.location else UNDEFINED, - ) - - async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) - - return device - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" if use_addon := entry.data.get(CONF_USE_ADDON): @@ -191,37 +146,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up websocket API async_register_api(hass) - platform_task = hass.async_create_task(start_platforms(hass, entry, client)) + # Create a task to allow the config entry to be unloaded before the driver is ready. + # Unloading the config entry is needed if the client listen task errors. + start_client_task = hass.async_create_task(start_client(hass, entry, client)) hass.data[DOMAIN].setdefault(entry.entry_id, {})[ - DATA_START_PLATFORM_TASK - ] = platform_task + DATA_START_CLIENT_TASK + ] = start_client_task return True -async def start_platforms( +async def start_client( hass: HomeAssistant, entry: ConfigEntry, client: ZwaveClient ) -> None: - """Start platforms and perform discovery.""" + """Start listening with the client.""" entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) entry_hass_data[DATA_CLIENT] = client - entry_hass_data[DATA_PLATFORM_SETUP] = {} - driver_ready = asyncio.Event() + driver_events = entry_hass_data[DATA_DRIVER_EVENTS] = DriverEvents(hass, entry) async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" await disconnect_client(hass, entry) - listen_task = asyncio.create_task(client_listen(hass, entry, client, driver_ready)) + listen_task = asyncio.create_task( + client_listen(hass, entry, client, driver_events.ready) + ) entry_hass_data[DATA_CLIENT_LISTEN_TASK] = listen_task entry.async_on_unload( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) ) try: - await driver_ready.wait() + await driver_events.ready.wait() except asyncio.CancelledError: - LOGGER.debug("Cancelling start platforms") + LOGGER.debug("Cancelling start client") return LOGGER.info("Connection to Zwave JS Server initialized") @@ -229,37 +187,289 @@ async def start_platforms( if client.driver is None: raise RuntimeError("Driver not ready.") - await setup_driver(hass, entry, client, client.driver) + await driver_events.setup(client.driver) -async def setup_driver( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry, client: ZwaveClient, driver: Driver -) -> None: - """Set up devices using the ready driver.""" - dev_reg = device_registry.async_get(hass) - ent_reg = entity_registry.async_get(hass) - entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) - platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] - registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) - discovered_value_ids: dict[str, set[str]] = defaultdict(set) +class DriverEvents: + """Represent driver events.""" - async def async_setup_platform(platform: Platform) -> None: - """Set up platform if needed.""" - if platform not in platform_setup_tasks: - platform_setup_tasks[platform] = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) + driver: Driver + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Set up the driver events instance.""" + self.config_entry = entry + self.dev_reg = device_registry.async_get(hass) + self.hass = hass + self.platform_setup_tasks: dict[str, asyncio.Task] = {} + self.ready = asyncio.Event() + # Make sure to not pass self to ControllerEvents until all attributes are set. + self.controller_events = ControllerEvents(hass, self) + + async def setup(self, driver: Driver) -> None: + """Set up devices using the ready driver.""" + self.driver = driver + + # If opt in preference hasn't been specified yet, we do nothing, otherwise + # we apply the preference + if opted_in := self.config_entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): + await async_enable_statistics(driver) + elif opted_in is False: + await driver.async_disable_statistics() + + # Check for nodes that no longer exist and remove them + stored_devices = device_registry.async_entries_for_config_entry( + self.dev_reg, self.config_entry.entry_id + ) + known_devices = [ + self.dev_reg.async_get_device({get_device_id(driver, node)}) + for node in 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: + self.dev_reg.async_remove_device(device.id) + + # run discovery on all ready nodes + await asyncio.gather( + *( + self.controller_events.async_on_node_added(node) + for node in driver.controller.nodes.values() ) - await platform_setup_tasks[platform] + ) + + # listen for new nodes being added to the mesh + self.config_entry.async_on_unload( + driver.controller.on( + "node added", + lambda event: self.hass.async_create_task( + self.controller_events.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 + self.config_entry.async_on_unload( + driver.controller.on( + "node removed", self.controller_events.async_on_node_removed + ) + ) + + async def async_setup_platform(self, platform: Platform) -> None: + """Set up platform if needed.""" + if platform not in self.platform_setup_tasks: + self.platform_setup_tasks[platform] = self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) + ) + await self.platform_setup_tasks[platform] + + +class ControllerEvents: + """Represent controller events. + + Handle the following events: + - node added + - node removed + """ + + def __init__(self, hass: HomeAssistant, driver_events: DriverEvents) -> None: + """Set up the controller events instance.""" + self.hass = hass + self.config_entry = driver_events.config_entry + self.discovered_value_ids: dict[str, set[str]] = defaultdict(set) + self.driver_events = driver_events + self.dev_reg = driver_events.dev_reg + self.registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) + self.node_events = NodeEvents(hass, self) @callback - def remove_device(device: device_registry.DeviceEntry) -> None: + def remove_device(self, device: device_registry.DeviceEntry) -> None: """Remove device from registry.""" # note: removal of entity registry entry is handled by core - dev_reg.async_remove_device(device.id) - registered_unique_ids.pop(device.id, None) - discovered_value_ids.pop(device.id, None) + self.dev_reg.async_remove_device(device.id) + self.registered_unique_ids.pop(device.id, None) + self.discovered_value_ids.pop(device.id, None) + + async def async_on_node_added(self, node: ZwaveNode) -> None: + """Handle node added event.""" + # No need for a ping button or node status sensor for controller nodes + if not node.is_controller_node: + # Create a node status sensor for each device + await self.driver_events.async_setup_platform(Platform.SENSOR) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_node_status_sensor", + node, + ) + + # Create a ping button for each device + await self.driver_events.async_setup_platform(Platform.BUTTON) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_ping_button_entity", + node, + ) + + # Create a firmware update entity for each device + await self.driver_events.async_setup_platform(Platform.UPDATE) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity", + node, + ) + + # we only want to run discovery when the node has reached ready state, + # otherwise we'll have all kinds of missing info issues. + if node.ready: + await self.node_events.async_on_node_ready(node) + return + # if node is not yet ready, register one-time callback for ready state + LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) + node.once( + "ready", + lambda event: self.hass.async_create_task( + self.node_events.async_on_node_ready(event["node"]) + ), + ) + # we do submit the node to device registry so user has + # some visual feedback that something is (in the process of) being added + self.register_node_in_dev_reg(node) + + @callback + def async_on_node_removed(self, event: dict) -> None: + """Handle node removed event.""" + node: ZwaveNode = event["node"] + replaced: bool = event.get("replaced", False) + # grab device in device registry attached to this node + dev_id = get_device_id(self.driver_events.driver, node) + device = self.dev_reg.async_get_device({dev_id}) + # We assert because we know the device exists + assert device + if replaced: + self.discovered_value_ids.pop(device.id, None) + + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{get_valueless_base_unique_id(self.driver_events.driver, node)}_remove_entity", + ) + else: + self.remove_device(device) + + @callback + def register_node_in_dev_reg(self, node: ZwaveNode) -> device_registry.DeviceEntry: + """Register node in dev reg.""" + driver = self.driver_events.driver + device_id = get_device_id(driver, node) + device_id_ext = get_device_id_ext(driver, node) + device = self.dev_reg.async_get_device({device_id}) + + # Replace the device if it can be determined that this node is not the + # same product as it was previously. + if ( + device_id_ext + and device + and len(device.identifiers) == 2 + and device_id_ext not in device.identifiers + ): + self.remove_device(device) + device = None + + if device_id_ext: + ids = {device_id, device_id_ext} + else: + ids = {device_id} + + device = self.dev_reg.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers=ids, + 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, + suggested_area=node.location if node.location else UNDEFINED, + ) + + async_dispatcher_send(self.hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) + + return device + + +class NodeEvents: + """Represent node events. + + Handle the following events: + - ready + - value added + - value updated + - metadata updated + - value notification + - notification + """ + + def __init__( + self, hass: HomeAssistant, controller_events: ControllerEvents + ) -> None: + """Set up the node events instance.""" + self.config_entry = controller_events.config_entry + self.controller_events = controller_events + self.dev_reg = controller_events.dev_reg + self.ent_reg = entity_registry.async_get(hass) + self.hass = hass + + async def async_on_node_ready(self, node: ZwaveNode) -> None: + """Handle node ready event.""" + LOGGER.debug("Processing node %s", node) + # register (or update) node in device registry + device = self.controller_events.register_node_in_dev_reg(node) + # We only want to create the defaultdict once, even on reinterviews + if device.id not in self.controller_events.registered_unique_ids: + self.controller_events.registered_unique_ids[device.id] = defaultdict(set) + + value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} + + # run discovery on all node values and create/update entities + await asyncio.gather( + *( + self.async_handle_discovery_info( + device, disc_info, value_updates_disc_info + ) + for disc_info in async_discover_node_values( + node, device, self.controller_events.discovered_value_ids + ) + ) + ) + + # add listeners to handle new values that get added later + for event in ("value added", "value updated", "metadata updated"): + self.config_entry.async_on_unload( + node.on( + event, + lambda event: self.hass.async_create_task( + self.async_on_value_added( + value_updates_disc_info, event["value"] + ) + ), + ) + ) + + # add listener for stateless node value notification events + self.config_entry.async_on_unload( + node.on( + "value notification", + lambda event: self.async_on_value_notification( + event["value_notification"] + ), + ) + ) + # add listener for stateless node notification events + self.config_entry.async_on_unload( + node.on("notification", self.async_on_notification) + ) async def async_handle_discovery_info( + self, device: device_registry.DeviceEntry, disc_info: ZwaveDiscoveryInfo, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], @@ -269,20 +479,22 @@ async def setup_driver( # noqa: C901 # the value_id format. Some time in the future, this call (as well as the # helper functions) can be removed. async_migrate_discovered_value( - hass, - ent_reg, - registered_unique_ids[device.id][disc_info.platform], + self.hass, + self.ent_reg, + self.controller_events.registered_unique_ids[device.id][disc_info.platform], device, - driver, + self.controller_events.driver_events.driver, disc_info, ) platform = disc_info.platform - await async_setup_platform(platform) + await self.controller_events.driver_events.async_setup_platform(platform) LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_{platform}", disc_info + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_{platform}", + disc_info, ) # If we don't need to watch for updates return early @@ -294,151 +506,57 @@ async def setup_driver( # noqa: C901 if len(value_updates_disc_info) != 1: return # add listener for value updated events - entry.async_on_unload( + self.config_entry.async_on_unload( disc_info.node.on( "value updated", - lambda event: async_on_value_updated_fire_event( + lambda event: self.async_on_value_updated_fire_event( value_updates_disc_info, event["value"] ), ) ) - async def async_on_node_ready(node: ZwaveNode) -> None: - """Handle node ready event.""" - LOGGER.debug("Processing node %s", node) - # register (or update) node in device registry - device = register_node_in_dev_reg( - hass, entry, dev_reg, driver, node, remove_device - ) - # We only want to create the defaultdict once, even on reinterviews - if device.id not in registered_unique_ids: - registered_unique_ids[device.id] = defaultdict(set) - - value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} - - # run discovery on all node values and create/update entities - await asyncio.gather( - *( - async_handle_discovery_info(device, disc_info, value_updates_disc_info) - for disc_info in async_discover_node_values( - node, device, discovered_value_ids - ) - ) - ) - - # add listeners to handle new values that get added later - for event in ("value added", "value updated", "metadata updated"): - entry.async_on_unload( - node.on( - event, - lambda event: hass.async_create_task( - async_on_value_added(value_updates_disc_info, event["value"]) - ), - ) - ) - - # add listener for stateless node value notification events - entry.async_on_unload( - node.on( - "value notification", - lambda event: async_on_value_notification(event["value_notification"]), - ) - ) - # add listener for stateless node notification events - entry.async_on_unload(node.on("notification", async_on_notification)) - - async def async_on_node_added(node: ZwaveNode) -> None: - """Handle node added event.""" - # No need for a ping button or node status sensor for controller nodes - if not node.is_controller_node: - # Create a node status sensor for each device - await async_setup_platform(Platform.SENSOR) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node - ) - - # Create a ping button for each device - await async_setup_platform(Platform.BUTTON) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node - ) - - # Create a firmware update entity for each device - await async_setup_platform(Platform.UPDATE) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_firmware_update_entity", node - ) - - # we only want to run discovery when the node has reached ready state, - # otherwise we'll have all kinds of missing info issues. - if node.ready: - await async_on_node_ready(node) - return - # if node is not yet ready, register one-time callback for ready state - LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) - node.once( - "ready", - lambda event: hass.async_create_task(async_on_node_ready(event["node"])), - ) - # we do submit the node to device registry so user has - # some visual feedback that something is (in the process of) being added - register_node_in_dev_reg(hass, entry, dev_reg, driver, node, remove_device) - async def async_on_value_added( - value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value + self, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value ) -> None: """Fire value updated event.""" # If node isn't ready or a device for this node doesn't already exist, we can # let the node ready event handler perform discovery. If a value has already # been processed, we don't need to do it again - device_id = get_device_id(driver, value.node) + device_id = get_device_id( + self.controller_events.driver_events.driver, value.node + ) if ( not value.node.ready - or not (device := dev_reg.async_get_device({device_id})) - or value.value_id in discovered_value_ids[device.id] + or not (device := self.dev_reg.async_get_device({device_id})) + or value.value_id in self.controller_events.discovered_value_ids[device.id] ): return LOGGER.debug("Processing node %s added value %s", value.node, value) await asyncio.gather( *( - async_handle_discovery_info(device, disc_info, value_updates_disc_info) + self.async_handle_discovery_info( + device, disc_info, value_updates_disc_info + ) for disc_info in async_discover_single_value( - value, device, discovered_value_ids + value, device, self.controller_events.discovered_value_ids ) ) ) @callback - def async_on_node_removed(event: dict) -> None: - """Handle node removed event.""" - node: ZwaveNode = event["node"] - replaced: bool = event.get("replaced", False) - # grab device in device registry attached to this node - dev_id = get_device_id(driver, node) - device = dev_reg.async_get_device({dev_id}) - # We assert because we know the device exists - assert device - if replaced: - discovered_value_ids.pop(device.id, None) - - async_dispatcher_send( - hass, - f"{DOMAIN}_{get_valueless_base_unique_id(driver, node)}_remove_entity", - ) - else: - remove_device(device) - - @callback - def async_on_value_notification(notification: ValueNotification) -> None: + def async_on_value_notification(self, notification: ValueNotification) -> None: """Relay stateless value notification events from Z-Wave nodes to hass.""" - device = dev_reg.async_get_device({get_device_id(driver, notification.node)}) + driver = self.controller_events.driver_events.driver + device = self.dev_reg.async_get_device( + {get_device_id(driver, notification.node)} + ) # We assert because we know the device exists assert device raw_value = value = notification.value if notification.metadata.states: value = notification.metadata.states.get(str(value), value) - hass.bus.async_fire( + self.hass.bus.async_fire( ZWAVE_JS_VALUE_NOTIFICATION_EVENT, { ATTR_DOMAIN: DOMAIN, @@ -459,15 +577,19 @@ async def setup_driver( # noqa: C901 ) @callback - def async_on_notification(event: dict[str, Any]) -> None: + def async_on_notification(self, event: dict[str, Any]) -> None: """Relay stateless notification events from Z-Wave nodes to hass.""" if "notification" not in event: LOGGER.info("Unknown notification: %s", event) return + + driver = self.controller_events.driver_events.driver notification: EntryControlNotification | NotificationNotification | PowerLevelNotification | MultilevelSwitchNotification = event[ "notification" ] - device = dev_reg.async_get_device({get_device_id(driver, notification.node)}) + device = self.dev_reg.async_get_device( + {get_device_id(driver, notification.node)} + ) # We assert because we know the device exists assert device event_data = { @@ -521,31 +643,35 @@ async def setup_driver( # noqa: C901 else: raise TypeError(f"Unhandled notification type: {notification}") - hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) + self.hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) @callback def async_on_value_updated_fire_event( - value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value + self, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value ) -> None: """Fire value updated event.""" # Get the discovery info for the value that was updated. If there is # no discovery info for this value, we don't need to fire an event if value.value_id not in value_updates_disc_info: return + + driver = self.controller_events.driver_events.driver disc_info = value_updates_disc_info[value.value_id] - device = dev_reg.async_get_device({get_device_id(driver, value.node)}) + device = self.dev_reg.async_get_device({get_device_id(driver, value.node)}) # We assert because we know the device exists assert device unique_id = get_unique_id(driver, disc_info.primary_value.value_id) - entity_id = ent_reg.async_get_entity_id(disc_info.platform, DOMAIN, unique_id) + entity_id = self.ent_reg.async_get_entity_id( + disc_info.platform, DOMAIN, unique_id + ) raw_value = value_ = value.value if value.metadata.states: value_ = value.metadata.states.get(str(value), value_) - hass.bus.async_fire( + self.hass.bus.async_fire( ZWAVE_JS_VALUE_UPDATED_EVENT, { ATTR_NODE_ID: value.node.node_id, @@ -564,43 +690,6 @@ async def setup_driver( # noqa: C901 }, ) - # If opt in preference hasn't been specified yet, we do nothing, otherwise - # we apply the preference - if opted_in := entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): - await async_enable_statistics(driver) - elif opted_in is False: - await driver.async_disable_statistics() - - # 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(driver, node)}) - for node in 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 - await asyncio.gather( - *(async_on_node_added(node) for node in driver.controller.nodes.values()) - ) - - # listen for new nodes being added to the mesh - entry.async_on_unload( - driver.controller.on( - "node added", - lambda event: hass.async_create_task(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 - entry.async_on_unload(driver.controller.on("node removed", async_on_node_removed)) - async def client_listen( hass: HomeAssistant, @@ -633,14 +722,15 @@ async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None: data = hass.data[DOMAIN][entry.entry_id] client: ZwaveClient = data[DATA_CLIENT] listen_task: asyncio.Task = data[DATA_CLIENT_LISTEN_TASK] - platform_task: asyncio.Task = data[DATA_START_PLATFORM_TASK] + start_client_task: asyncio.Task = data[DATA_START_CLIENT_TASK] + driver_events: DriverEvents = data[DATA_DRIVER_EVENTS] listen_task.cancel() - platform_task.cancel() - platform_setup_tasks = data.get(DATA_PLATFORM_SETUP, {}).values() + start_client_task.cancel() + platform_setup_tasks = driver_events.platform_setup_tasks.values() for task in platform_setup_tasks: task.cancel() - await asyncio.gather(listen_task, platform_task, *platform_setup_tasks) + await asyncio.gather(listen_task, start_client_task, *platform_setup_tasks) if client.connected: await client.disconnect() @@ -650,9 +740,10 @@ async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" info = hass.data[DOMAIN][entry.entry_id] + driver_events: DriverEvents = info[DATA_DRIVER_EVENTS] - tasks = [] - for platform, task in info[DATA_PLATFORM_SETUP].items(): + tasks: list[asyncio.Task | Coroutine] = [] + for platform, task in driver_events.platform_setup_tasks.items(): if task.done(): tasks.append( hass.config_entries.async_forward_entry_unload(entry, platform) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index ddd4917e596..db3da247e7d 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -21,7 +21,6 @@ CONF_DATA_COLLECTION_OPTED_IN = "data_collection_opted_in" DOMAIN = "zwave_js" DATA_CLIENT = "client" -DATA_PLATFORM_SETUP = "platform_setup" EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" From 2bfcdc66b6ce6c38a4a3df431321eb1af2a10b96 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 5 Sep 2022 21:50:47 +0200 Subject: [PATCH 879/903] Allow empty db in SQL options flow (#77777) --- homeassistant/components/sql/config_flow.py | 7 +- tests/components/sql/test_config_flow.py | 75 ++++++++++++++++++--- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index dc3a839ef1d..bcbece9f7f6 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -147,9 +147,12 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage SQL options.""" errors = {} + db_url_default = DEFAULT_URL.format( + hass_config_path=self.hass.config.path(DEFAULT_DB_FILE) + ) if user_input is not None: - db_url = user_input[CONF_DB_URL] + db_url = user_input.get(CONF_DB_URL, db_url_default) query = user_input[CONF_QUERY] column = user_input[CONF_COLUMN_NAME] @@ -176,7 +179,7 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): step_id="init", data_schema=vol.Schema( { - vol.Required( + vol.Optional( CONF_DB_URL, description={ "suggested_value": self.entry.options[CONF_DB_URL] diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 7c3571f8f19..96402e1bc7a 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -20,7 +20,7 @@ from . import ( from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: +async def test_form(hass: HomeAssistant, recorder_mock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -52,7 +52,7 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_success(hass: HomeAssistant) -> None: +async def test_import_flow_success(hass: HomeAssistant, recorder_mock) -> None: """Test a successful import of yaml.""" with patch( @@ -79,7 +79,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: +async def test_import_flow_already_exist(hass: HomeAssistant, recorder_mock) -> None: """Test import of yaml already exist.""" MockConfigEntry( @@ -102,7 +102,7 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: assert result3["reason"] == "already_configured" -async def test_flow_fails_db_url(hass: HomeAssistant) -> None: +async def test_flow_fails_db_url(hass: HomeAssistant, recorder_mock) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -123,7 +123,7 @@ async def test_flow_fails_db_url(hass: HomeAssistant) -> None: assert result4["errors"] == {"db_url": "db_url_invalid"} -async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: +async def test_flow_fails_invalid_query(hass: HomeAssistant, recorder_mock) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -169,7 +169,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: } -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow(hass: HomeAssistant, recorder_mock) -> None: """Test options config flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -218,7 +218,9 @@ async def test_options_flow(hass: HomeAssistant) -> None: } -async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None: +async def test_options_flow_name_previously_removed( + hass: HomeAssistant, recorder_mock +) -> None: """Test options config flow where the name was missing.""" entry = MockConfigEntry( domain=DOMAIN, @@ -269,7 +271,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None } -async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None: +async def test_options_flow_fails_db_url(hass: HomeAssistant, recorder_mock) -> None: """Test options flow fails incorrect db url.""" entry = MockConfigEntry( domain=DOMAIN, @@ -312,7 +314,7 @@ async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None: async def test_options_flow_fails_invalid_query( - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock ) -> None: """Test options flow fails incorrect query and template.""" entry = MockConfigEntry( @@ -367,3 +369,58 @@ async def test_options_flow_fails_invalid_query( "column": "size", "unit_of_measurement": "MiB", } + + +async def test_options_flow_db_url_empty(hass: HomeAssistant, recorder_mock) -> None: + """Test options config flow with leaving db_url empty.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "db_url": "sqlite://", + "name": "Get Value", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + "value_template": None, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "query": "SELECT 5 as size", + "column": "size", + "unit_of_measurement": "MiB", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "name": "Get Value", + "db_url": "sqlite://", + "query": "SELECT 5 as size", + "column": "size", + "value_template": None, + "unit_of_measurement": "MiB", + } From f5e61ecdec449373c9877fe3a0247109311a2956 Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Tue, 6 Sep 2022 17:34:11 +0200 Subject: [PATCH 880/903] Handle exception on projector being unavailable (#77802) --- homeassistant/components/epson/manifest.json | 2 +- homeassistant/components/epson/media_player.py | 14 +++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 82b74486377..0ba8351fd15 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -3,7 +3,7 @@ "name": "Epson", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/epson", - "requirements": ["epson-projector==0.4.6"], + "requirements": ["epson-projector==0.5.0"], "codeowners": ["@pszafer"], "iot_class": "local_polling", "loggers": ["epson_projector"] diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 57bb0165f6a..0e70984ac31 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from epson_projector import Projector +from epson_projector import Projector, ProjectorUnavailableError from epson_projector.const import ( BACK, BUSY, @@ -20,7 +20,6 @@ from epson_projector.const import ( POWER, SOURCE, SOURCE_LIST, - STATE_UNAVAILABLE as EPSON_STATE_UNAVAILABLE, TURN_OFF, TURN_ON, VOL_DOWN, @@ -123,11 +122,16 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): async def async_update(self) -> None: """Update state of device.""" - power_state = await self._projector.get_power() - _LOGGER.debug("Projector status: %s", power_state) - if not power_state or power_state == EPSON_STATE_UNAVAILABLE: + try: + power_state = await self._projector.get_power() + except ProjectorUnavailableError as ex: + _LOGGER.debug("Projector is unavailable: %s", ex) self._attr_available = False return + if not power_state: + self._attr_available = False + return + _LOGGER.debug("Projector status: %s", power_state) self._attr_available = True if power_state == EPSON_CODES[POWER]: self._attr_state = STATE_ON diff --git a/requirements_all.txt b/requirements_all.txt index 77cd2faa49e..496101c9703 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -630,7 +630,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.6 +epson-projector==0.5.0 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a518ff1d9cc..30cf9d2edb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -477,7 +477,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.6 +epson-projector==0.5.0 # homeassistant.components.faa_delays faadelays==0.0.7 From 61ee621c90cb5294d8d7bef426840ac2a9ec4fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20V=C3=B6lker?= Date: Tue, 6 Sep 2022 11:10:35 +0200 Subject: [PATCH 881/903] Adjust Renault default scan interval (#77823) raise DEFAULT_SCAN_INTERVAL to 7 minutes This PR is raising the default scan interval for the Renault API from 5 minutes to 7 minutes. Lower intervals fail sometimes, maybe due to quota limitations. This seems to be a working interval as described in home-assistant#73220 --- homeassistant/components/renault/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index 89bf322c2bf..b29f4ad0701 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -6,7 +6,7 @@ DOMAIN = "renault" CONF_LOCALE = "locale" CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id" -DEFAULT_SCAN_INTERVAL = 300 # 5 minutes +DEFAULT_SCAN_INTERVAL = 420 # 7 minutes PLATFORMS = [ Platform.BINARY_SENSOR, From 31d085cdf896c32a3cb1ea71c41b0eec6525e687 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 13:56:27 -0500 Subject: [PATCH 882/903] Fix history stats device class when type is not time (#77855) --- .../components/history_stats/sensor.py | 3 +- tests/components/history_stats/test_sensor.py | 45 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index a42c516f12b..642c327e29d 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -143,7 +143,6 @@ class HistoryStatsSensorBase( class HistoryStatsSensor(HistoryStatsSensorBase): """A HistoryStats sensor.""" - _attr_device_class = SensorDeviceClass.DURATION _attr_state_class = SensorStateClass.MEASUREMENT def __init__( @@ -157,6 +156,8 @@ class HistoryStatsSensor(HistoryStatsSensorBase): self._attr_native_unit_of_measurement = UNITS[sensor_type] self._type = sensor_type self._process_update() + if self._type == CONF_TYPE_TIME: + self._attr_device_class = SensorDeviceClass.DURATION @callback def _process_update(self) -> None: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 8907f381a6c..5de74f71d1e 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -9,7 +9,7 @@ import pytest from homeassistant import config as hass_config from homeassistant.components.history_stats import DOMAIN -from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN +from homeassistant.const import ATTR_DEVICE_CLASS, SERVICE_RELOAD, STATE_UNKNOWN import homeassistant.core as ha from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component, setup_component @@ -1496,3 +1496,46 @@ async def test_end_time_with_microseconds_zeroed(time_zone, hass, recorder_mock) async_fire_time_changed(hass, rolled_to_next_day_plus_18) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0" + + +async def test_device_classes(hass, recorder_mock): + """Test the device classes.""" + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "time", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "count", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "ratio", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get("sensor.time").attributes[ATTR_DEVICE_CLASS] == "duration" + assert ATTR_DEVICE_CLASS not in hass.states.get("sensor.ratio").attributes + assert ATTR_DEVICE_CLASS not in hass.states.get("sensor.count").attributes From 6989b16274c60c0e5b99c261c18b1e1f07bde9df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 08:00:05 -0500 Subject: [PATCH 883/903] Bump zeroconf to 0.39.1 (#77859) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 4438a22040d..ec670558e66 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.39.0"], + "requirements": ["zeroconf==0.39.1"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3bf00427954..1299abcd61b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -40,7 +40,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.7.2 -zeroconf==0.39.0 +zeroconf==0.39.1 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 496101c9703..b82217b6937 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2566,7 +2566,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.39.0 +zeroconf==0.39.1 # homeassistant.components.zha zha-quirks==0.0.79 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30cf9d2edb9..62dbd57e577 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1761,7 +1761,7 @@ yolink-api==0.0.9 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.39.0 +zeroconf==0.39.1 # homeassistant.components.zha zha-quirks==0.0.79 From 62dcbc4d4a33cef00f24a7c7c4caa99f4e5bbbbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 02:54:52 -0500 Subject: [PATCH 884/903] Add RSSI to the bluetooth debug log (#77860) --- homeassistant/components/bluetooth/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 9fc00aa159b..80817deb2a1 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -327,12 +327,13 @@ class BluetoothManager: matched_domains = self._integration_matcher.match_domains(service_info) _LOGGER.debug( - "%s: %s %s connectable: %s match: %s", + "%s: %s %s connectable: %s match: %s rssi: %s", source, address, advertisement_data, connectable, matched_domains, + device.rssi, ) for match in self._callback_index.match_callbacks(service_info): From 319b0b8902ab94d7d60b600947e0affb69db20b2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Sep 2022 23:39:42 +0200 Subject: [PATCH 885/903] Pin astroid to fix pylint (#77862) --- requirements_test.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_test.txt b/requirements_test.txt index d15431a1d85..99e2f8d8402 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,6 +7,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt +astroid==2.12.5 codecov==2.1.12 coverage==6.4.4 freezegun==1.2.1 From d98687b78914285b301914d7f28dc06fde2274ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 02:55:43 -0500 Subject: [PATCH 886/903] Bump thermopro-ble to 0.4.3 (#77863) * Bump thermopro-ble to 0.4.2 - Turns on rounding of long values - Uses bluetooth-data-tools under the hood - Adds the TP393 since it works without any changes to the parser Changelog: https://github.com/Bluetooth-Devices/thermopro-ble/compare/v0.4.0...v0.4.2 * bump again for device detection fix --- homeassistant/components/thermopro/manifest.json | 7 +++++-- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/thermopro/manifest.json b/homeassistant/components/thermopro/manifest.json index 912a070ccf1..dca643a28cf 100644 --- a/homeassistant/components/thermopro/manifest.json +++ b/homeassistant/components/thermopro/manifest.json @@ -3,9 +3,12 @@ "name": "ThermoPro", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/thermopro", - "bluetooth": [{ "local_name": "TP35*", "connectable": false }], + "bluetooth": [ + { "local_name": "TP35*", "connectable": false }, + { "local_name": "TP39*", "connectable": false } + ], "dependencies": ["bluetooth"], - "requirements": ["thermopro-ble==0.4.0"], + "requirements": ["thermopro-ble==0.4.3"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index d7230213302..b2400f733da 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -269,6 +269,11 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "local_name": "TP35*", "connectable": False }, + { + "domain": "thermopro", + "local_name": "TP39*", + "connectable": False + }, { "domain": "xiaomi_ble", "connectable": False, diff --git a/requirements_all.txt b/requirements_all.txt index b82217b6937..18612e9ee5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2366,7 +2366,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.3.1 # homeassistant.components.thermopro -thermopro-ble==0.4.0 +thermopro-ble==0.4.3 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62dbd57e577..9b6edc09648 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1615,7 +1615,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.3.1 # homeassistant.components.thermopro -thermopro-ble==0.4.0 +thermopro-ble==0.4.3 # homeassistant.components.todoist todoist-python==8.0.0 From a13438c5b06986a83b3abb12a7bd6f3f916150a1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 6 Sep 2022 09:40:20 -0400 Subject: [PATCH 887/903] Improve performance impact of zwave_js update entity and other tweaks (#77866) * Improve performance impact of zwave_js update entity and other tweaks * Reduce concurrent polls * we need to write state after setting in progress to false * Fix existing tests * Fix tests by fixing fixtures * remove redundant conditional * Add test for delayed startup * tweaks * outdent happy path * Add missing PROGRESS feature support * Update homeassistant/components/zwave_js/update.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/update.py Co-authored-by: Martin Hjelmare * Fix tests by reverting outdent, PR comments, mark callback * Remove redundant conditional * make more readable * Remove unused SCAN_INTERVAL * Catch FailedZWaveCommand * Add comment and remove poll unsub on update * Fix catching error and add test * readability * Fix tests * Add assertions * rely on built in progress indicator Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 25 +- homeassistant/components/zwave_js/update.py | 98 +++--- tests/components/zwave_js/conftest.py | 3 +- .../aeotec_radiator_thermostat_state.json | 8 +- .../fixtures/aeotec_zw164_siren_state.json | 174 +++++------ .../fixtures/bulb_6_multi_color_state.json | 4 +- .../fixtures/chain_actuator_zws12_state.json | 9 +- .../fixtures/climate_adc_t3000_state.json | 294 +++++++++--------- .../fixtures/climate_danfoss_lc_13_state.json | 56 ---- .../climate_eurotronic_spirit_z_state.json | 3 +- .../climate_heatit_z_trm2fx_state.json | 186 +++++------ .../climate_heatit_z_trm3_no_value_state.json | 198 ++++++------ .../fixtures/climate_heatit_z_trm3_state.json | 4 +- ...setpoint_on_different_endpoints_state.json | 174 +++++------ ..._ct100_plus_different_endpoints_state.json | 116 ------- ...ostat_ct101_multiple_temp_units_state.json | 174 +++++------ .../cover_aeotec_nano_shutter_state.json | 174 +++++------ .../fixtures/cover_fibaro_fgr222_state.json | 138 ++++---- .../fixtures/cover_iblinds_v2_state.json | 4 +- .../fixtures/cover_qubino_shutter_state.json | 150 ++++----- .../zwave_js/fixtures/cover_zw062_state.json | 4 +- .../fixtures/eaton_rf9640_dimmer_state.json | 4 +- .../fixtures/ecolink_door_sensor_state.json | 4 +- .../express_controls_ezmultipli_state.json | 162 +++++----- .../zwave_js/fixtures/fan_ge_12730_state.json | 4 +- .../zwave_js/fixtures/fan_generic_state.json | 4 +- .../zwave_js/fixtures/fan_hs_fc200_state.json | 198 ++++++------ .../fixtures/fortrezz_ssa1_siren_state.json | 66 ++-- .../fixtures/fortrezz_ssa3_siren_state.json | 66 ++-- .../fixtures/inovelli_lzw36_state.json | 210 ++++++------- .../light_color_null_values_state.json | 138 ++++---- .../fixtures/lock_august_asl03_state.json | 4 +- .../fixtures/lock_id_lock_as_id150_state.json | 162 +++++----- ...pp_electric_strike_lock_control_state.json | 162 +++++----- .../fixtures/multisensor_6_state.json | 4 +- .../nortek_thermostat_added_event.json | 4 +- .../nortek_thermostat_removed_event.json | 4 +- .../fixtures/nortek_thermostat_state.json | 4 +- .../fixtures/null_name_check_state.json | 150 ++++----- .../fixtures/srt321_hrt4_zw_state.json | 54 ++-- .../vision_security_zl7432_state.json | 66 ++-- .../zwave_js/fixtures/zen_31_state.json | 246 +++++++-------- .../fixtures/zp3111-5_not_ready_state.json | 4 +- .../zwave_js/fixtures/zp3111-5_state.json | 162 +++++----- tests/components/zwave_js/test_init.py | 8 +- tests/components/zwave_js/test_update.py | 82 ++++- 46 files changed, 1941 insertions(+), 2027 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 03a8ee5fce2..98219520693 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -8,6 +8,7 @@ from typing import Any from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode @@ -312,14 +313,6 @@ class ControllerEvents: node, ) - # Create a firmware update entity for each device - await self.driver_events.async_setup_platform(Platform.UPDATE) - async_dispatcher_send( - self.hass, - f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity", - node, - ) - # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: @@ -463,11 +456,27 @@ class NodeEvents: ), ) ) + # add listener for stateless node notification events self.config_entry.async_on_unload( node.on("notification", self.async_on_notification) ) + # Create a firmware update entity for each non-controller device that + # supports firmware updates + if not node.is_controller_node and any( + CommandClass.FIRMWARE_UPDATE_MD.value == cc.id + for cc in node.command_classes + ): + await self.controller_events.driver_events.async_setup_platform( + Platform.UPDATE + ) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity", + node, + ) + async def async_handle_discovery_info( self, device: device_registry.DeviceEntry, diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 7f25788e0be..97c14746dd9 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -1,14 +1,15 @@ """Representation of Z-Wave updates.""" from __future__ import annotations +import asyncio from collections.abc import Callable -from datetime import timedelta +from datetime import datetime, timedelta from typing import Any from awesomeversion import AwesomeVersion from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import NodeStatus -from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver from zwave_js_server.model.firmware import FirmwareUpdateInfo from zwave_js_server.model.node import Node as ZwaveNode @@ -21,12 +22,13 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.start import async_at_start from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 1 -SCAN_INTERVAL = timedelta(days=1) async def async_setup_entry( @@ -37,12 +39,14 @@ async def async_setup_entry( """Set up Z-Wave button from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + semaphore = asyncio.Semaphore(3) + @callback def async_add_firmware_update_entity(node: ZwaveNode) -> None: """Add firmware update entity.""" driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. - async_add_entities([ZWaveNodeFirmwareUpdate(driver, node)], True) + async_add_entities([ZWaveNodeFirmwareUpdate(driver, node, semaphore)]) config_entry.async_on_unload( async_dispatcher_connect( @@ -62,30 +66,36 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES ) _attr_has_entity_name = True + _attr_should_poll = False - def __init__(self, driver: Driver, node: ZwaveNode) -> None: + def __init__( + self, driver: Driver, node: ZwaveNode, semaphore: asyncio.Semaphore + ) -> None: """Initialize a Z-Wave device firmware update entity.""" self.driver = driver self.node = node + self.semaphore = semaphore self._latest_version_firmware: FirmwareUpdateInfo | None = None self._status_unsub: Callable[[], None] | None = None + self._poll_unsub: Callable[[], None] | None = None # Entity class attributes self._attr_name = "Firmware" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.firmware_update" + self._attr_installed_version = self._attr_latest_version = node.firmware_version # device may not be precreated in main handler yet self._attr_device_info = get_device_info(driver, node) - self._attr_installed_version = self._attr_latest_version = node.firmware_version - + @callback def _update_on_status_change(self, _: dict[str, Any]) -> None: """Update the entity when node is awake.""" self._status_unsub = None - self.hass.async_create_task(self.async_update(True)) + self.hass.async_create_task(self._async_update()) - async def async_update(self, write_state: bool = False) -> None: + async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: """Update the entity.""" + self._poll_unsub = None for status, event_name in ( (NodeStatus.ASLEEP, "wake up"), (NodeStatus.DEAD, "alive"), @@ -97,34 +107,38 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) return - if available_firmware_updates := ( - await self.driver.controller.async_get_available_firmware_updates( - self.node, API_KEY_FIRMWARE_UPDATE_SERVICE + try: + async with self.semaphore: + available_firmware_updates = ( + await self.driver.controller.async_get_available_firmware_updates( + self.node, API_KEY_FIRMWARE_UPDATE_SERVICE + ) + ) + except FailedZWaveCommand as err: + LOGGER.debug( + "Failed to get firmware updates for node %s: %s", + self.node.node_id, + err, ) - ): - self._latest_version_firmware = max( - available_firmware_updates, - key=lambda x: AwesomeVersion(x.version), - ) - self._async_process_available_updates(write_state) - - @callback - def _async_process_available_updates(self, write_state: bool = True) -> None: - """ - Process available firmware updates. - - Sets latest version attribute and FirmwareUpdateInfo instance. - """ - # If we have an available firmware update that is a higher version than what's - # on the node, we should advertise it, otherwise we are on the latest version - if (firmware := self._latest_version_firmware) and AwesomeVersion( - firmware.version - ) > AwesomeVersion(self.node.firmware_version): - self._attr_latest_version = firmware.version else: - self._attr_latest_version = self._attr_installed_version - if write_state: - self.async_write_ha_state() + if available_firmware_updates: + self._latest_version_firmware = latest_firmware = max( + available_firmware_updates, + key=lambda x: AwesomeVersion(x.version), + ) + + # If we have an available firmware update that is a higher version than + # what's on the node, we should advertise it, otherwise there is + # nothing to do. + new_version = latest_firmware.version + current_version = self.node.firmware_version + if AwesomeVersion(new_version) > AwesomeVersion(current_version): + self._attr_latest_version = new_version + self.async_write_ha_state() + finally: + self._poll_unsub = async_call_later( + self.hass, timedelta(days=1), self._async_update + ) async def async_release_notes(self) -> str | None: """Get release notes.""" @@ -138,8 +152,6 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - self._attr_in_progress = True - self.async_write_ha_state() try: for file in firmware.files: await self.driver.controller.async_begin_ota_firmware_update( @@ -148,11 +160,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): except BaseZwaveJSServerError as err: raise HomeAssistantError(err) from err else: - self._attr_installed_version = firmware.version + self._attr_installed_version = self._attr_latest_version = firmware.version self._latest_version_firmware = None - self._async_process_available_updates() - finally: - self._attr_in_progress = False + self.async_write_ha_state() async def async_poll_value(self, _: bool) -> None: """Poll a value.""" @@ -179,8 +189,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) ) + self.async_on_remove(async_at_start(self.hass, self._async_update)) + async def async_will_remove_from_hass(self) -> None: """Call when entity will be removed.""" if self._status_unsub: self._status_unsub() self._status_unsub = None + + if self._poll_unsub: + self._poll_unsub() + self._poll_unsub = None diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 7131b1ade69..04a9c5671f9 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -584,7 +584,8 @@ def mock_client_fixture(controller_state, version_state, log_config_state): async def listen(driver_ready: asyncio.Event) -> None: driver_ready.set() - await asyncio.sleep(30) + listen_block = asyncio.Event() + await listen_block.wait() assert False, "Listen wasn't canceled!" async def disconnect(): diff --git a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json index 789a72c98fa..111714560b5 100644 --- a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json @@ -39,7 +39,13 @@ "neighbors": [6, 7, 45, 67], "interviewAttempts": 1, "endpoints": [ - { "nodeId": 4, "index": 0, "installerIcon": 4608, "userIcon": 4608 } + { + "nodeId": 4, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "commandClasses": [] + } ], "values": [ { diff --git a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json index 0f0dde61c83..f2b43878990 100644 --- a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json @@ -77,7 +77,93 @@ 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 ], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 121, + "name": "Sound Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": false + } + ] }, { "nodeId": 2, @@ -3665,92 +3751,6 @@ ], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 121, - "name": "Sound Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 8, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3", "isControllerNode": false diff --git a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json index 7243cbe9383..172580f563e 100644 --- a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json +++ b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json @@ -57,10 +57,10 @@ "nodeId": 39, "index": 0, "installerIcon": 1536, - "userIcon": 1536 + "userIcon": 1536, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json index d17385f7d1e..f89fce5561e 100644 --- a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json +++ b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json @@ -44,9 +44,14 @@ "neighbors": [1, 2], "interviewAttempts": 1, "endpoints": [ - { "nodeId": 6, "index": 0, "installerIcon": 6656, "userIcon": 6656 } + { + "nodeId": 6, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "commandClasses": [] + } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json index bc19c034099..ab80b46069c 100644 --- a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json +++ b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json @@ -58,7 +58,153 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": true + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": true + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": true + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": true + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 3, + "isSecure": true + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": true + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 100, + "name": "Humidity Control Setpoint", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 109, + "name": "Humidity Control Mode", + "version": 2, + "isSecure": true + }, + { + "id": 110, + "name": "Humidity Control Operating State", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 7, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -3940,152 +4086,6 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 11, - "isSecure": true - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": true - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": true - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 3, - "isSecure": true - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 3, - "isSecure": true - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": true - }, - { - "id": 85, - "name": "Transport Service", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": true - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 100, - "name": "Humidity Control Setpoint", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 109, - "name": "Humidity Control Mode", - "version": 2, - "isSecure": true - }, - { - "id": 110, - "name": "Humidity Control Operating State", - "version": 1, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 113, - "name": "Notification", - "version": 7, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": true - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": true - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": true - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": true - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 159, - "name": "Security 2", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0190:0x0006:0x0001:1.44", "statistics": { diff --git a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json index cb8e78881df..8a88c1fc6e2 100644 --- a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json +++ b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json @@ -67,62 +67,6 @@ "neighbors": [1, 14], "interviewAttempts": 1, "interviewStage": 7, - "commandClasses": [ - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 70, - "name": "Climate Control Schedule", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 132, - "name": "Wake Up", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 143, - "name": "Multi Command", - "version": 1, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 5, diff --git a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json index dfca647ae67..7025c27182f 100644 --- a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json +++ b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json @@ -72,7 +72,8 @@ "nodeId": 8, "index": 0, "installerIcon": 4608, - "userIcon": 4608 + "userIcon": 4608, + "commandClasses": [] } ], "values": [ diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json index c99898fb595..d1a5fb8c1ee 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json @@ -66,7 +66,99 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 26, @@ -1348,98 +1440,6 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 3, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 3, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 3, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - } - ], "interviewStage": "Complete", "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json index 61b138ebbe7..5c95fe1ffc0 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json @@ -68,7 +68,105 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 1, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] }, { "nodeId": 74, @@ -1148,104 +1246,6 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 3, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 1, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 3, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json index da6876dceaa..bd3bf2d560e 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json @@ -66,7 +66,8 @@ "nodeId": 24, "index": 0, "installerIcon": 4608, - "userIcon": 4609 + "userIcon": 4609, + "commandClasses": [] }, { "nodeId": 24, @@ -93,7 +94,6 @@ "userIcon": 3329 } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json index d5f540e8343..cbd25aa4ffd 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json @@ -62,7 +62,93 @@ "endpoints": [ { "nodeId": 8, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + } + ] }, { "nodeId": 8, @@ -741,91 +827,5 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 2, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 3, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index ca0efb56711..21b8a7457eb 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -1316,121 +1316,5 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 5, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json index ba87b585b3c..98a0fab8dbb 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json @@ -54,7 +54,93 @@ "endpoints": [ { "nodeId": 4, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + } + ] }, { "nodeId": 4, @@ -873,91 +959,5 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 2, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 3, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json index cd6ceb2f192..48e692b2395 100644 --- a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json @@ -63,7 +63,93 @@ }, "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": true + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -386,92 +472,6 @@ "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": true - }, - { - "id": 43, - "name": "Scene Activation", - "version": 1, - "isSecure": true - }, - { - "id": 44, - "name": "Scene Actuator Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": true - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": true - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1", "isControllerNode": false diff --git a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json index 4e50345195b..54b976a94a6 100644 --- a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json +++ b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json @@ -74,7 +74,75 @@ }, "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 2, + "isSecure": false + }, + { + "id": 145, + "name": "Manufacturer Proprietary", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -1029,74 +1097,6 @@ "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 3, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 2, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 2, - "isSecure": false - }, - { - "id": 145, - "name": "Manufacturer Proprietary", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x010f:0x0302:0x1000:25.25", "statistics": { diff --git a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json index f1e08bf7795..d71f719bc3e 100644 --- a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json +++ b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json @@ -54,10 +54,10 @@ "nodeId": 54, "index": 0, "installerIcon": 6400, - "userIcon": 6400 + "userIcon": 6400, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json index 4c9320085c3..015e4b91cd5 100644 --- a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json @@ -57,7 +57,81 @@ }, "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 3, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 4, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] } ], "values": [ @@ -792,80 +866,6 @@ "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 3, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 4, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 5, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0159:0x0003:0x0052:71.0", "statistics": { diff --git a/tests/components/zwave_js/fixtures/cover_zw062_state.json b/tests/components/zwave_js/fixtures/cover_zw062_state.json index a2033e30bd6..8e819faa347 100644 --- a/tests/components/zwave_js/fixtures/cover_zw062_state.json +++ b/tests/components/zwave_js/fixtures/cover_zw062_state.json @@ -62,10 +62,10 @@ "nodeId": 12, "index": 0, "installerIcon": 7680, - "userIcon": 7680 + "userIcon": 7680, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json index 23f8628b6d3..6885c1aa342 100644 --- a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json +++ b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json @@ -55,10 +55,10 @@ "nodeId": 19, "index": 0, "installerIcon": 1536, - "userIcon": 1536 + "userIcon": 1536, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json index 225f532dfb8..9633b84c394 100644 --- a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json +++ b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json @@ -47,10 +47,10 @@ "endpoints": [ { "nodeId": 2, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json index ea267d86b8c..502dd573420 100644 --- a/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json +++ b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json @@ -60,7 +60,87 @@ }, "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 6, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 2, + "isSecure": false + }, + { + "id": 119, + "name": "Node Naming and Location", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -578,86 +658,6 @@ "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 3, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 6, - "isSecure": false - }, - { - "id": 51, - "name": "Color Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 2, - "isSecure": false - }, - { - "id": 119, - "name": "Node Naming and Location", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x001e:0x0004:0x0001:1.8", "statistics": { diff --git a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json index a1fa0294fd5..59aff4035da 100644 --- a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json +++ b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json @@ -47,10 +47,10 @@ "endpoints": [ { "nodeId": 24, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/fan_generic_state.json b/tests/components/zwave_js/fixtures/fan_generic_state.json index a13b99d882f..29f49bb50dc 100644 --- a/tests/components/zwave_js/fixtures/fan_generic_state.json +++ b/tests/components/zwave_js/fixtures/fan_generic_state.json @@ -57,10 +57,10 @@ "nodeId": 17, "index": 0, "installerIcon": 1024, - "userIcon": 1024 + "userIcon": 1024, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json index a47904a6833..d8ae5fc899a 100644 --- a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json +++ b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json @@ -63,7 +63,105 @@ }, "mandatorySupportedCCs": [32, 38, 133, 89, 114, 115, 134, 94], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -9859,104 +9957,6 @@ "mandatorySupportedCCs": [32, 38, 133, 89, 114, 115, 134, 94], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": false - }, - { - "id": 43, - "name": "Scene Activation", - "version": 1, - "isSecure": false - }, - { - "id": 44, - "name": "Scene Actuator Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 85, - "name": "Transport Service", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 159, - "name": "Security 2", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x000c:0x0203:0x0001:50.5", "statistics": { diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json index f24f611ebe9..98f26c9a669 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json @@ -62,7 +62,39 @@ }, "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -306,38 +338,6 @@ "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0084:0x0313:0x010b:1.11", "statistics": { diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json index 5768510fb3d..86d04a3fa59 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json @@ -56,7 +56,39 @@ }, "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -298,38 +330,6 @@ "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0084:0x0331:0x010b:1.11", "statistics": { diff --git a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json index b5986aaf35d..2dbbadcf138 100644 --- a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json +++ b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json @@ -65,116 +65,116 @@ "aggregatedEndpointCount": 0, "interviewAttempts": 1, "interviewStage": 7, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 3, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 5, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 3, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 19, "index": 0, "installerIcon": 7168, - "userIcon": 7168 + "userIcon": 7168, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 19, diff --git a/tests/components/zwave_js/fixtures/light_color_null_values_state.json b/tests/components/zwave_js/fixtures/light_color_null_values_state.json index 6f4055a66fa..92e7e4ef30c 100644 --- a/tests/components/zwave_js/fixtures/light_color_null_values_state.json +++ b/tests/components/zwave_js/fixtures/light_color_null_values_state.json @@ -83,7 +83,75 @@ }, "mandatorySupportedCCs": [32], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 2, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -616,73 +684,5 @@ "mandatorySupportedCCs": [32], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 2, - "isSecure": false - }, - { - "id": 51, - "name": "Color Switch", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json index 2b092b9d3b0..07c4a441d02 100644 --- a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json +++ b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json @@ -58,10 +58,10 @@ "nodeId": 6, "index": 0, "installerIcon": 768, - "userIcon": 768 + "userIcon": 768, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Door Lock", diff --git a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json index 5e64724ba3b..3e3e9a7e0df 100644 --- a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json +++ b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json @@ -48,7 +48,87 @@ "nodeId": 60, "index": 0, "installerIcon": 768, - "userIcon": 768 + "userIcon": 768, + "commandClasses": [ + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 98, + "name": "Door Lock", + "version": 2, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -2836,85 +2916,5 @@ "mandatorySupportedCCs": [32, 98, 99, 114, 152, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 98, - "name": "Door Lock", - "version": 2, - "isSecure": true - }, - { - "id": 99, - "name": "User Code", - "version": 1, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 4, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": true - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json index 9ae86f1d581..e0d583764cf 100644 --- a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json +++ b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json @@ -35,7 +35,87 @@ }, "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 48, + "name": "Binary Sensor", + "version": 2, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": true + }, + { + "id": 98, + "name": "Door Lock", + "version": 2, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -476,86 +556,6 @@ "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 48, - "name": "Binary Sensor", - "version": 2, - "isSecure": true - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": true - }, - { - "id": 98, - "name": "Door Lock", - "version": 2, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 113, - "name": "Notification", - "version": 5, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": true - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0154:0x0005:0x0001:1.3", "statistics": { diff --git a/tests/components/zwave_js/fixtures/multisensor_6_state.json b/tests/components/zwave_js/fixtures/multisensor_6_state.json index 62535414b5b..580393ae6cd 100644 --- a/tests/components/zwave_js/fixtures/multisensor_6_state.json +++ b/tests/components/zwave_js/fixtures/multisensor_6_state.json @@ -61,10 +61,10 @@ "nodeId": 52, "index": 0, "installerIcon": 3079, - "userIcon": 3079 + "userIcon": 3079, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json index c2a2802d273..73515b1c2ac 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json @@ -18,10 +18,10 @@ "endpoints": [ { "nodeId": 67, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json index 48885802751..8491e65c037 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json @@ -53,10 +53,10 @@ "endpoints": [ { "nodeId": 67, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json index 912cbe30574..a0cd7867b1a 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json @@ -61,10 +61,10 @@ "nodeId": 67, "index": 0, "installerIcon": 4608, - "userIcon": 4608 + "userIcon": 4608, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/components/zwave_js/fixtures/null_name_check_state.json b/tests/components/zwave_js/fixtures/null_name_check_state.json index b283041c3c6..b0ee80b146b 100644 --- a/tests/components/zwave_js/fixtures/null_name_check_state.json +++ b/tests/components/zwave_js/fixtures/null_name_check_state.json @@ -31,7 +31,81 @@ "nodeId": 10, "index": 0, "installerIcon": 3328, - "userIcon": 3328 + "userIcon": 3328, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 7, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 10, @@ -337,79 +411,5 @@ "mandatorySupportedCCs": [32, 49], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 7, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json index 836cb20cf34..ac5232d55e0 100644 --- a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json +++ b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json @@ -53,36 +53,36 @@ "neighbors": [1, 5, 10, 12, 13, 14, 15, 18, 21], "interviewAttempts": 1, "interviewStage": 7, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 20, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + } + ] } ], "values": [ diff --git a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json index c510c60a479..4878a26beab 100644 --- a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json +++ b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json @@ -60,7 +60,39 @@ }, "mandatorySupportedCCs": [32, 37, 39], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + } + ] }, { "nodeId": 7, @@ -363,37 +395,5 @@ "mandatorySupportedCCs": [32, 37, 39], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zen_31_state.json b/tests/components/zwave_js/fixtures/zen_31_state.json index 982e96d9adf..0d307154359 100644 --- a/tests/components/zwave_js/fixtures/zen_31_state.json +++ b/tests/components/zwave_js/fixtures/zen_31_state.json @@ -66,7 +66,129 @@ 32, 38, 133, 89, 51, 90, 114, 115, 159, 108, 85, 134, 94 ], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 3, + "isSecure": false + }, + { + "id": 86, + "name": "CRC-16 Encapsulation", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] }, { "nodeId": 94, @@ -2587,127 +2709,5 @@ ], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 11, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - }, - { - "id": 51, - "name": "Color Switch", - "version": 3, - "isSecure": false - }, - { - "id": 86, - "name": "CRC-16 Encapsulation", - "version": 1, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 8, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json index 1c4805b5c22..4e7d5f6a9dc 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json @@ -26,7 +26,8 @@ }, "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [] } ], "values": [], @@ -53,7 +54,6 @@ "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] }, - "commandClasses": [], "interviewStage": "ProtocolInfo", "statistics": { "commandsTX": 0, diff --git a/tests/components/zwave_js/fixtures/zp3111-5_state.json b/tests/components/zwave_js/fixtures/zp3111-5_state.json index c9d37b74c29..54f37d389dd 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_state.json @@ -63,7 +63,87 @@ }, "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 7, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 132, + "name": "Wake Up", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + } + ] } ], "values": [ @@ -607,86 +687,6 @@ "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 4, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 7, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 132, - "name": "Wake Up", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0109:0x2021:0x2101:5.1", "statistics": { diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 57f552c9502..d038949d494 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -211,8 +211,8 @@ async def test_on_node_added_not_ready( client.driver.receive_event(event) await hass.async_block_till_done() - # the only entities are the node status sensor, ping button, and firmware update - assert len(hass.states.async_all()) == 3 + # the only entities are the node status sensor and ping button + assert len(hass.states.async_all()) == 2 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device @@ -254,8 +254,8 @@ async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integrati assert not device.model assert not device.sw_version - # the only entities are the node status sensor, ping button, and firmware update - assert len(hass.states.async_all()) == 3 + # the only entities are the node status sensor and ping button + assert len(hass.states.async_all()) == 2 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index c9ec8fa68c6..76fecfdee6d 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -19,9 +19,9 @@ from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_registry import async_get -from homeassistant.util import datetime as dt_util +from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed UPDATE_ENTITY = "update.z_wave_thermostat_firmware" FIRMWARE_UPDATES = { @@ -162,14 +162,12 @@ async def test_update_entity_success( client.async_send_command.reset_mock() -async def test_update_entity_failure( +async def test_update_entity_install_failure( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, controller_node, integration, - caplog, - hass_ws_client, ): """Test update entity failed install.""" client.async_send_command.return_value = FIRMWARE_UPDATES @@ -194,15 +192,15 @@ async def test_update_entity_failure( async def test_update_entity_sleep( hass, client, - multisensor_6, + zen_31, integration, ): """Test update occurs when device is asleep after it wakes up.""" event = Event( "sleep", - data={"source": "node", "event": "sleep", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "sleep", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) client.async_send_command.reset_mock() client.async_send_command.return_value = FIRMWARE_UPDATES @@ -215,9 +213,9 @@ async def test_update_entity_sleep( event = Event( "wake up", - data={"source": "node", "event": "wake up", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "wake up", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) await hass.async_block_till_done() # Now that the node is up we can check for updates @@ -225,21 +223,21 @@ async def test_update_entity_sleep( args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == multisensor_6.node_id + assert args["nodeId"] == zen_31.node_id async def test_update_entity_dead( hass, client, - multisensor_6, + zen_31, integration, ): """Test update occurs when device is dead after it becomes alive.""" event = Event( "dead", - data={"source": "node", "event": "dead", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "dead", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) client.async_send_command.reset_mock() client.async_send_command.return_value = FIRMWARE_UPDATES @@ -252,9 +250,9 @@ async def test_update_entity_dead( event = Event( "alive", - data={"source": "node", "event": "alive", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "alive", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) await hass.async_block_till_done() # Now that the node is up we can check for updates @@ -262,4 +260,54 @@ async def test_update_entity_dead( args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == multisensor_6.node_id + assert args["nodeId"] == zen_31.node_id + + +async def test_update_entity_ha_not_running( + hass, + client, + zen_31, + hass_ws_client, +): + """Test update occurs after HA starts.""" + await hass.async_stop() + + 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 len(client.async_send_command.call_args_list) == 0 + + await hass.async_start() + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == zen_31.node_id + + +async def test_update_entity_failure( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + controller_node, + integration, +): + """Test update entity update failed.""" + assert len(client.async_send_command.call_args_list) == 0 + client.async_send_command.side_effect = FailedZWaveCommand("test", 260, "test") + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert ( + args["nodeId"] + == climate_radio_thermostat_ct100_plus_different_endpoints.node_id + ) From 1dbcf88e156d44865ac0a4dd84511dc21b6ef194 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Tue, 6 Sep 2022 10:52:27 +0300 Subject: [PATCH 888/903] Bump pybravia to 0.2.2 (#77867) --- homeassistant/components/braviatv/coordinator.py | 12 +++++++++++- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index bdacddcdb2f..49c902e0d44 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -7,7 +7,13 @@ from functools import wraps import logging from typing import Any, Final, TypeVar -from pybravia import BraviaTV, BraviaTVError, BraviaTVNotFound +from pybravia import ( + BraviaTV, + BraviaTVConnectionError, + BraviaTVConnectionTimeout, + BraviaTVError, + BraviaTVNotFound, +) from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player.const import ( @@ -130,6 +136,10 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): _LOGGER.debug("Update skipped, Bravia API service is reloading") return raise UpdateFailed("Error communicating with device") from err + except (BraviaTVConnectionError, BraviaTVConnectionTimeout): + self.is_on = False + self.connected = False + _LOGGER.debug("Update skipped, Bravia TV is off") except BraviaTVError as err: self.is_on = False self.connected = False diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index fa172957781..dca9d65cff0 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.1"], + "requirements": ["pybravia==0.2.2"], "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 18612e9ee5c..d3637791e7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,7 +1443,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.1 +pybravia==0.2.2 # homeassistant.components.nissan_leaf pycarwings2==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b6edc09648..e34022971fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1019,7 +1019,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.1 +pybravia==0.2.2 # homeassistant.components.cloudflare pycfdns==1.2.2 From e1e153f391633a41955566454783ee60b168f312 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 08:48:39 -0500 Subject: [PATCH 889/903] Bump bluetooth-auto-recovery to 0.3.1 (#77898) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index b98312040f0..ca6a76c55ae 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.16.0", "bluetooth-adapters==0.3.4", - "bluetooth-auto-recovery==0.3.0" + "bluetooth-auto-recovery==0.3.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1299abcd61b..09102494074 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 bluetooth-adapters==0.3.4 -bluetooth-auto-recovery==0.3.0 +bluetooth-auto-recovery==0.3.1 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index d3637791e7c..0ab025b66c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.0 +bluetooth-auto-recovery==0.3.1 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e34022971fd..13c3ad85d9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.0 +bluetooth-auto-recovery==0.3.1 # homeassistant.components.bond bond-async==0.1.22 From 9155f669e9f39f5f1ab4b8c0f7c40cc7c76f71ee Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 6 Sep 2022 18:54:53 +0200 Subject: [PATCH 890/903] Update frontend to 20220906.0 (#77910) --- 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 416634053d6..8abc8fd4e32 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==20220905.0"], + "requirements": ["home-assistant-frontend==20220906.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 09102494074..58a000fbc25 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220905.0 +home-assistant-frontend==20220906.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 0ab025b66c5..ed85d669deb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220905.0 +home-assistant-frontend==20220906.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13c3ad85d9c..81a71d5df1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220905.0 +home-assistant-frontend==20220906.0 # homeassistant.components.home_connect homeconnect==0.7.2 From c8ad8a6d86323482e479e5aa17ca267f9054b5e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Sep 2022 12:55:44 -0400 Subject: [PATCH 891/903] Bumped version to 2022.9.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c4c15302a44..156ba1d133d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 0a8db19d4ee..1fdc8d87e07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b5" +version = "2022.9.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d1b637ea7a17d75cc3c6936c909c19f5de839893 Mon Sep 17 00:00:00 2001 From: Matthew Simpson Date: Tue, 6 Sep 2022 20:50:03 +0100 Subject: [PATCH 892/903] Bump btsmarthub_devicelist to 0.2.2 (#77609) --- homeassistant/components/bt_smarthub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 6a0453752e9..fb34117eb6b 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -2,7 +2,7 @@ "domain": "bt_smarthub", "name": "BT Smart Hub", "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", - "requirements": ["btsmarthub_devicelist==0.2.0"], + "requirements": ["btsmarthub_devicelist==0.2.2"], "codeowners": ["@jxwolstenholme"], "iot_class": "local_polling", "loggers": ["btsmarthub_devicelist"] diff --git a/requirements_all.txt b/requirements_all.txt index ed85d669deb..63eee62c878 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -470,7 +470,7 @@ bthome-ble==1.0.0 bthomehub5-devicelist==0.1.1 # homeassistant.components.bt_smarthub -btsmarthub_devicelist==0.2.0 +btsmarthub_devicelist==0.2.2 # homeassistant.components.buienradar buienradar==1.0.5 From 9aa87761cf5c9a7f1fc729cbc6809bf7c7791f02 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 7 Sep 2022 11:10:24 -0400 Subject: [PATCH 893/903] Fix ZHA lighting initial hue/saturation attribute read (#77727) * Handle the case of `current_hue` being `None` * WIP unit tests --- homeassistant/components/zha/light.py | 18 +++--- tests/components/zha/common.py | 21 +++++- tests/components/zha/test_light.py | 93 ++++++++++++++++++++++++--- 3 files changed, 114 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 5a8011e2386..9858c6803f9 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -610,16 +610,18 @@ class Light(BaseLight, ZhaEntity): and self._color_channel.enhanced_current_hue is not None ): curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 - else: + elif self._color_channel.current_hue is not None: curr_hue = self._color_channel.current_hue * 254 / 360 - curr_saturation = self._color_channel.current_saturation - if curr_hue is not None and curr_saturation is not None: - self._attr_hs_color = ( - int(curr_hue), - int(curr_saturation * 2.54), - ) else: - self._attr_hs_color = (0, 0) + curr_hue = 0 + + if (curr_saturation := self._color_channel.current_saturation) is None: + curr_saturation = 0 + + self._attr_hs_color = ( + int(curr_hue), + int(curr_saturation * 2.54), + ) if self._color_channel.color_loop_supported: self._attr_supported_features |= light.LightEntityFeature.EFFECT diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index cad8020267f..56197fa39ec 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -2,12 +2,14 @@ import asyncio from datetime import timedelta import math -from unittest.mock import AsyncMock, Mock +from typing import Any +from unittest.mock import AsyncMock, Mock, patch import zigpy.zcl import zigpy.zcl.foundation as zcl_f import homeassistant.components.zha.core.const as zha_const +from homeassistant.components.zha.core.helpers import async_get_zha_config_value from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util @@ -243,3 +245,20 @@ async def async_shift_time(hass): next_update = dt_util.utcnow() + timedelta(seconds=11) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + + +def patch_zha_config(component: str, overrides: dict[tuple[str, str], Any]): + """Patch the ZHA custom configuration defaults.""" + + def new_get_config(config_entry, section, config_key, default): + if (section, config_key) in overrides: + return overrides[section, config_key] + else: + return async_get_zha_config_value( + config_entry, section, config_key, default + ) + + return patch( + f"homeassistant.components.zha.{component}.async_get_zha_config_value", + side_effect=new_get_config, + ) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 156f692aa14..16678393f52 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -14,6 +14,10 @@ from homeassistant.components.light import ( FLASH_SHORT, ColorMode, ) +from homeassistant.components.zha.core.const import ( + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + ZHA_OPTIONS, +) from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform @@ -26,6 +30,7 @@ from .common import ( async_test_rejoin, find_entity_id, get_zha_gateway, + patch_zha_config, send_attributes_report, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -342,7 +347,11 @@ async def test_light( if cluster_identify: await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_SHORT) - # test turning the lights on and off from the HA + # test long flashing the lights from the HA + if cluster_identify: + await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) + + # test dimming the lights on and off from the HA if cluster_level: await async_test_level_on_off_from_hass( hass, cluster_on_off, cluster_level, entity_id @@ -357,16 +366,82 @@ async def test_light( # test rejoin await async_test_off_from_hass(hass, cluster_on_off, entity_id) - clusters = [cluster_on_off] - if cluster_level: - clusters.append(cluster_level) - if cluster_color: - clusters.append(cluster_color) + clusters = [c for c in (cluster_on_off, cluster_level, cluster_color) if c] await async_test_rejoin(hass, zigpy_device, clusters, reporting) - # test long flashing the lights from the HA - if cluster_identify: - await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) + +@pytest.mark.parametrize( + "plugged_attr_reads, config_override, expected_state", + [ + # HS light without cached hue or saturation + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with cached hue + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_hue": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with cached saturation + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_saturation": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with both + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_hue": 100, + "current_saturation": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + ], +) +async def test_light_initialization( + hass, + zigpy_device_mock, + zha_device_joined_restored, + plugged_attr_reads, + config_override, + expected_state, +): + """Test zha light initialization with cached attributes and color modes.""" + + # create zigpy devices + zigpy_device = zigpy_device_mock(LIGHT_COLOR) + + # mock attribute reads + zigpy_device.endpoints[1].light_color.PLUGGED_ATTR_READS = plugged_attr_reads + + with patch_zha_config("light", config_override): + zha_device = await zha_device_joined_restored(zigpy_device) + entity_id = await find_entity_id(Platform.LIGHT, zha_device, hass) + + assert entity_id is not None + + # TODO ensure hue and saturation are properly set on startup @patch( From a4f528e90855884933d15a0e9d8c14c6406117ce Mon Sep 17 00:00:00 2001 From: Chris McCurdy Date: Wed, 7 Sep 2022 11:43:05 -0400 Subject: [PATCH 894/903] Add additional method of retrieving UUID for LG soundbar configuration (#77856) --- .../components/lg_soundbar/config_flow.py | 19 +- .../lg_soundbar/test_config_flow.py | 168 +++++++++++++++++- 2 files changed, 182 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py index bd9a727d1f4..0606bad2d67 100644 --- a/homeassistant/components/lg_soundbar/config_flow.py +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the LG Soundbar integration.""" -from queue import Queue +from queue import Full, Queue import socket import temescal @@ -20,18 +20,29 @@ def test_connect(host, port): uuid_q = Queue(maxsize=1) name_q = Queue(maxsize=1) + def queue_add(attr_q, data): + try: + attr_q.put_nowait(data) + except Full: + pass + def msg_callback(response): - if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]: - uuid_q.put_nowait(response["data"]["s_uuid"]) + if ( + response["msg"] in ["MAC_INFO_DEV", "PRODUCT_INFO"] + and "s_uuid" in response["data"] + ): + queue_add(uuid_q, response["data"]["s_uuid"]) if ( response["msg"] == "SPK_LIST_VIEW_INFO" and "s_user_name" in response["data"] ): - name_q.put_nowait(response["data"]["s_user_name"]) + queue_add(name_q, response["data"]["s_user_name"]) try: connection = temescal.temescal(host, port=port, callback=msg_callback) connection.get_mac_info() + if uuid_q.empty(): + connection.get_product_info() connection.get_info() details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)} return details diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py index 3fafc2c7628..8bcf817cbba 100644 --- a/tests/components/lg_soundbar/test_config_flow.py +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -1,5 +1,5 @@ """Test the lg_soundbar config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import DEFAULT, MagicMock, Mock, call, patch from homeassistant import config_entries from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN @@ -43,6 +43,172 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_uuid_missing_from_mac_info(hass): + """Test we get the form, but uuid is missing from the initial get_mac_info function call.""" + + 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.lg_soundbar.config_flow.temescal", return_value=Mock() + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + tmock = mock_temescal.temescal + tmock.return_value = Mock() + instance = tmock.return_value + + def temescal_side_effect(addr, port, callback): + product_info = {"msg": "PRODUCT_INFO", "data": {"s_uuid": "uuid"}} + instance.get_product_info.side_effect = lambda: callback(product_info) + info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} + instance.get_info.side_effect = lambda: callback(info) + return DEFAULT + + tmock.side_effect = temescal_side_effect + + 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"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_uuid_present_in_both_functions_uuid_q_empty(hass): + """Get the form, uuid present in both get_mac_info and get_product_info calls. + + Value from get_mac_info is not added to uuid_q before get_product_info is run. + """ + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_uuid_q = MagicMock() + mock_name_q = MagicMock() + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.config_flow.Queue", + return_value=MagicMock(), + ) as mock_q, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_q.side_effect = [mock_uuid_q, mock_name_q] + mock_uuid_q.empty.return_value = True + mock_uuid_q.get.return_value = "uuid" + mock_name_q.get.return_value = "name" + tmock = mock_temescal.temescal + tmock.return_value = Mock() + instance = tmock.return_value + + def temescal_side_effect(addr, port, callback): + mac_info = {"msg": "MAC_INFO_DEV", "data": {"s_uuid": "uuid"}} + instance.get_mac_info.side_effect = lambda: callback(mac_info) + product_info = {"msg": "PRODUCT_INFO", "data": {"s_uuid": "uuid"}} + instance.get_product_info.side_effect = lambda: callback(product_info) + info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} + instance.get_info.side_effect = lambda: callback(info) + return DEFAULT + + tmock.side_effect = temescal_side_effect + + 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"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + mock_uuid_q.empty.assert_called_once() + mock_uuid_q.put_nowait.has_calls([call("uuid"), call("uuid")]) + mock_uuid_q.get.assert_called_once() + + +async def test_form_uuid_present_in_both_functions_uuid_q_not_empty(hass): + """Get the form, uuid present in both get_mac_info and get_product_info calls. + + Value from get_mac_info is added to uuid_q before get_product_info is run. + """ + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_uuid_q = MagicMock() + mock_name_q = MagicMock() + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.config_flow.Queue", + return_value=MagicMock(), + ) as mock_q, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_q.side_effect = [mock_uuid_q, mock_name_q] + mock_uuid_q.empty.return_value = False + mock_uuid_q.get.return_value = "uuid" + mock_name_q.get.return_value = "name" + tmock = mock_temescal.temescal + tmock.return_value = Mock() + instance = tmock.return_value + + def temescal_side_effect(addr, port, callback): + mac_info = {"msg": "MAC_INFO_DEV", "data": {"s_uuid": "uuid"}} + instance.get_mac_info.side_effect = lambda: callback(mac_info) + info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} + instance.get_info.side_effect = lambda: callback(info) + return DEFAULT + + tmock.side_effect = temescal_side_effect + + 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"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + mock_uuid_q.empty.assert_called_once() + mock_uuid_q.put_nowait.assert_called_once() + mock_uuid_q.get.assert_called_once() + + async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( From 9901b31316f591ffd5840c2924c536751009f1f5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 7 Sep 2022 01:28:47 -0400 Subject: [PATCH 895/903] Bump zwave-js-server-python to 0.41.1 (#77915) * Bump zwave-js-server-python to 0.41.1 * Fix fixture --- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/climate_adc_t3000_state.json | 112 +++++------------- 4 files changed, 31 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index b906efec96c..7c569301831 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.41.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.41.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 63eee62c878..7a945fc9deb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2596,7 +2596,7 @@ zigpy==0.50.2 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.0 +zwave-js-server-python==0.41.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81a71d5df1d..6a76db50147 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1782,7 +1782,7 @@ zigpy-znp==0.8.2 zigpy==0.50.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.0 +zwave-js-server-python==0.41.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json index ab80b46069c..b6a235ad45e 100644 --- a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json +++ b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json @@ -227,9 +227,7 @@ "unit": "\u00b0F" }, "value": 72, - "nodeId": 68, - "newValue": 73, - "prevValue": 72.5 + "nodeId": 68 }, { "endpoint": 0, @@ -250,9 +248,7 @@ "unit": "%" }, "value": 34, - "nodeId": 68, - "newValue": 34, - "prevValue": 34 + "nodeId": 68 }, { "endpoint": 0, @@ -448,9 +444,7 @@ "8": "Quiet circulation mode" } }, - "value": 0, - "newValue": 1, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -581,9 +575,7 @@ "2": "De-humidifying" } }, - "value": 0, - "newValue": 1, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1295,9 +1287,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1323,9 +1313,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1351,9 +1339,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1379,9 +1365,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1407,9 +1391,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 0 + "value": 1 }, { "endpoint": 0, @@ -1435,9 +1417,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1463,9 +1443,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1491,9 +1469,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1519,9 +1495,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1547,9 +1521,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1575,9 +1547,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1603,9 +1573,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1631,9 +1599,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1659,9 +1625,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1687,9 +1651,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1715,9 +1677,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1743,9 +1703,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1771,9 +1729,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1799,9 +1755,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1827,9 +1781,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1855,9 +1807,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1883,9 +1833,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1911,9 +1859,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1939,9 +1885,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, From 8d0ebdd1f97080bc7fc4a240336f5977bebc935a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Sep 2022 20:13:01 +0200 Subject: [PATCH 896/903] Revert "Add ability to ignore devices for UniFi Protect" (#77916) --- .../components/unifiprotect/__init__.py | 30 ++----- .../components/unifiprotect/config_flow.py | 78 +++++++------------ .../components/unifiprotect/const.py | 1 - homeassistant/components/unifiprotect/data.py | 65 +--------------- .../components/unifiprotect/services.py | 6 +- .../components/unifiprotect/strings.json | 6 +- .../unifiprotect/translations/en.json | 4 - .../components/unifiprotect/utils.py | 46 ++++------- tests/components/unifiprotect/conftest.py | 1 - .../unifiprotect/test_config_flow.py | 45 +---------- tests/components/unifiprotect/test_init.py | 39 +++------- tests/components/unifiprotect/utils.py | 12 +-- 12 files changed, 75 insertions(+), 258 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 60829223e2f..30b1d1ad56d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -26,7 +26,6 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( CONF_ALL_UPDATES, - CONF_IGNORED, CONF_OVERRIDE_CHOST, DEFAULT_SCAN_INTERVAL, DEVICES_FOR_SUBSCRIBE, @@ -36,11 +35,11 @@ from .const import ( OUTDATED_LOG_MESSAGE, PLATFORMS, ) -from .data import ProtectData +from .data import ProtectData, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services -from .utils import async_unifi_mac, convert_mac_list +from .utils import _async_unifi_mac_from_hass, async_get_devices from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) @@ -107,19 +106,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" - - data: ProtectData = hass.data[DOMAIN][entry.entry_id] - changed = data.async_get_changed_options(entry) - - if len(changed) == 1 and CONF_IGNORED in changed: - new_macs = convert_mac_list(entry.options.get(CONF_IGNORED, "")) - added_macs = new_macs - data.ignored_macs - removed_macs = data.ignored_macs - new_macs - # if only ignored macs are added, we can handle without reloading - if not removed_macs and added_macs: - data.async_add_new_ignored_macs(added_macs) - return - await hass.config_entries.async_reload(entry.entry_id) @@ -139,15 +125,15 @@ async def async_remove_config_entry_device( ) -> bool: """Remove ufp config entry from a device.""" unifi_macs = { - async_unifi_mac(connection[1]) + _async_unifi_mac_from_hass(connection[1]) for connection in device_entry.connections if connection[0] == dr.CONNECTION_NETWORK_MAC } - data: ProtectData = hass.data[DOMAIN][config_entry.entry_id] - if data.api.bootstrap.nvr.mac in unifi_macs: + api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id}) + assert api is not None + if api.bootstrap.nvr.mac in unifi_macs: return False - for device in data.get_by_types(DEVICES_THAT_ADOPT): + for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT): if device.is_adopted_by_us and device.mac in unifi_macs: - data.async_ignore_mac(device.mac) - break + return False return True diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 1907a201c8d..f07ca923a53 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -35,7 +35,6 @@ from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_MAX_MEDIA, CONF_OVERRIDE_CHOST, DEFAULT_MAX_MEDIA, @@ -47,7 +46,7 @@ from .const import ( ) from .data import async_last_update_was_successful from .discovery import async_start_discovery -from .utils import _async_resolve, async_short_mac, async_unifi_mac, convert_mac_list +from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass _LOGGER = logging.getLogger(__name__) @@ -121,7 +120,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle integration discovery.""" self._discovered_device = discovery_info - mac = async_unifi_mac(discovery_info["hw_addr"]) + mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"]) await self.async_set_unique_id(mac) source_ip = discovery_info["source_ip"] direct_connect_domain = discovery_info["direct_connect_domain"] @@ -183,7 +182,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders = { "name": discovery_info["hostname"] or discovery_info["platform"] - or f"NVR {async_short_mac(discovery_info['hw_addr'])}", + or f"NVR {_async_short_mac(discovery_info['hw_addr'])}", "ip_address": discovery_info["source_ip"], } self.context["title_placeholders"] = placeholders @@ -225,7 +224,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_ALL_UPDATES: False, CONF_OVERRIDE_CHOST: False, CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA, - CONF_IGNORED: "", }, ) @@ -367,53 +365,33 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" - - values = user_input or self.config_entry.options - schema = vol.Schema( - { - vol.Optional( - CONF_DISABLE_RTSP, - description={ - "suggested_value": values.get(CONF_DISABLE_RTSP, False) - }, - ): bool, - vol.Optional( - CONF_ALL_UPDATES, - description={ - "suggested_value": values.get(CONF_ALL_UPDATES, False) - }, - ): bool, - vol.Optional( - CONF_OVERRIDE_CHOST, - description={ - "suggested_value": values.get(CONF_OVERRIDE_CHOST, False) - }, - ): bool, - vol.Optional( - CONF_MAX_MEDIA, - description={ - "suggested_value": values.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) - }, - ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), - vol.Optional( - CONF_IGNORED, - description={"suggested_value": values.get(CONF_IGNORED, "")}, - ): str, - } - ) - errors: dict[str, str] = {} - if user_input is not None: - try: - convert_mac_list(user_input.get(CONF_IGNORED, ""), raise_exception=True) - except vol.Invalid: - errors[CONF_IGNORED] = "invalid_mac_list" - - if not errors: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(title="", data=user_input) return self.async_show_form( step_id="init", - data_schema=schema, - errors=errors, + data_schema=vol.Schema( + { + vol.Optional( + CONF_DISABLE_RTSP, + default=self.config_entry.options.get(CONF_DISABLE_RTSP, False), + ): bool, + vol.Optional( + CONF_ALL_UPDATES, + default=self.config_entry.options.get(CONF_ALL_UPDATES, False), + ): bool, + vol.Optional( + CONF_OVERRIDE_CHOST, + default=self.config_entry.options.get( + CONF_OVERRIDE_CHOST, False + ), + ): bool, + vol.Optional( + CONF_MAX_MEDIA, + default=self.config_entry.options.get( + CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA + ), + ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), + } + ), ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 080dc41f358..93a0fa5ff74 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -20,7 +20,6 @@ CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" -CONF_IGNORED = "ignored_devices" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index c17b99d639f..20b5747a342 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -28,7 +28,6 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA, DEVICES_THAT_ADOPT, @@ -37,11 +36,7 @@ from .const import ( DISPATCH_CHANNELS, DOMAIN, ) -from .utils import ( - async_dispatch_id as _ufpd, - async_get_devices_by_type, - convert_mac_list, -) +from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] @@ -72,7 +67,6 @@ class ProtectData: self._hass = hass self._entry = entry - self._existing_options = dict(entry.options) self._hass = hass self._update_interval = update_interval self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {} @@ -80,8 +74,6 @@ class ProtectData: self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None self._auth_failures = 0 - self._ignored_macs: set[str] | None = None - self._ignore_update_cancel: Callable[[], None] | None = None self.last_update_success = False self.api = protect @@ -96,47 +88,6 @@ class ProtectData: """Max number of events to load at once.""" return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) - @property - def ignored_macs(self) -> set[str]: - """Set of ignored MAC addresses.""" - - if self._ignored_macs is None: - self._ignored_macs = convert_mac_list( - self._entry.options.get(CONF_IGNORED, "") - ) - - return self._ignored_macs - - @callback - def async_get_changed_options(self, entry: ConfigEntry) -> dict[str, Any]: - """Get changed options for when entry is updated.""" - - return dict( - set(self._entry.options.items()) - set(self._existing_options.items()) - ) - - @callback - def async_ignore_mac(self, mac: str) -> None: - """Ignores a MAC address for a UniFi Protect device.""" - - new_macs = (self._ignored_macs or set()).copy() - new_macs.add(mac) - _LOGGER.debug("Updating ignored_devices option: %s", self.ignored_macs) - options = dict(self._entry.options) - options[CONF_IGNORED] = ",".join(new_macs) - self._hass.config_entries.async_update_entry(self._entry, options=options) - - @callback - def async_add_new_ignored_macs(self, new_macs: set[str]) -> None: - """Add new ignored MAC addresses and ensures the devices are removed.""" - - for mac in new_macs: - device = self.api.bootstrap.get_device_from_mac(mac) - if device is not None: - self._async_remove_device(device) - self._ignored_macs = None - self._existing_options = dict(self._entry.options) - def get_by_types( self, device_types: Iterable[ModelType], ignore_unadopted: bool = True ) -> Generator[ProtectAdoptableDeviceModel, None, None]: @@ -148,8 +99,6 @@ class ProtectData: for device in devices: if ignore_unadopted and not device.is_adopted_by_us: continue - if device.mac in self.ignored_macs: - continue yield device async def async_setup(self) -> None: @@ -159,11 +108,6 @@ class ProtectData: ) await self.async_refresh() - for mac in self.ignored_macs: - device = self.api.bootstrap.get_device_from_mac(mac) - if device is not None: - self._async_remove_device(device) - async def async_stop(self, *args: Any) -> None: """Stop processing data.""" if self._unsub_websocket: @@ -228,7 +172,6 @@ class ProtectData: @callback def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None: - registry = dr.async_get(self._hass) device_entry = registry.async_get_device( identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} @@ -353,13 +296,13 @@ class ProtectData: @callback -def async_ufp_data_for_config_entry_ids( +def async_ufp_instance_for_config_entry_ids( hass: HomeAssistant, config_entry_ids: set[str] -) -> ProtectData | None: +) -> ProtectApiClient | None: """Find the UFP instance for the config entry ids.""" domain_data = hass.data[DOMAIN] for config_entry_id in config_entry_ids: if config_entry_id in domain_data: protect_data: ProtectData = domain_data[config_entry_id] - return protect_data + return protect_data.api return None diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 914803e9c45..915c51b6c0a 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -25,7 +25,7 @@ from homeassistant.helpers.service import async_extract_referenced_entity_ids from homeassistant.util.read_only_dict import ReadOnlyDict from .const import ATTR_MESSAGE, DOMAIN -from .data import async_ufp_data_for_config_entry_ids +from .data import async_ufp_instance_for_config_entry_ids SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" @@ -70,8 +70,8 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl return _async_get_ufp_instance(hass, device_entry.via_device_id) config_entry_ids = device_entry.config_entries - if ufp_data := async_ufp_data_for_config_entry_ids(hass, config_entry_ids): - return ufp_data.api + if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids): + return ufp_instance raise HomeAssistantError(f"No device found for device id: {device_id}") diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d9750d31ae1..d3cfe24abd2 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -50,13 +50,9 @@ "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "override_connection_host": "Override Connection Host", - "max_media": "Max number of event to load for Media Browser (increases RAM usage)", - "ignored_devices": "Comma separated list of MAC addresses of devices to ignore" + "max_media": "Max number of event to load for Media Browser (increases RAM usage)" } } - }, - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index c6050d05284..5d690e3fd3e 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -42,15 +42,11 @@ } }, "options": { - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" - }, "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "disable_rtsp": "Disable the RTSP stream", - "ignored_devices": "Comma separated list of MAC addresses of devices to ignore", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 8c368da1c40..808117aac9e 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -1,9 +1,9 @@ """UniFi Protect Integration utils.""" from __future__ import annotations +from collections.abc import Generator, Iterable import contextlib from enum import Enum -import re import socket from typing import Any @@ -14,16 +14,12 @@ from pyunifiprotect.data import ( LightModeType, ProtectAdoptableDeviceModel, ) -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv from .const import DOMAIN, ModelType -MAC_RE = re.compile(r"[0-9A-F]{12}") - def get_nested_attr(obj: Any, attr: str) -> Any: """Fetch a nested attribute.""" @@ -42,16 +38,15 @@ def get_nested_attr(obj: Any, attr: str) -> Any: @callback -def async_unifi_mac(mac: str) -> str: - """Convert MAC address to format from UniFi Protect.""" +def _async_unifi_mac_from_hass(mac: str) -> str: # MAC addresses in UFP are always caps - return mac.replace(":", "").replace("-", "").replace("_", "").upper() + return mac.replace(":", "").upper() @callback -def async_short_mac(mac: str) -> str: +def _async_short_mac(mac: str) -> str: """Get the short mac address from the full mac.""" - return async_unifi_mac(mac)[-6:] + return _async_unifi_mac_from_hass(mac)[-6:] async def _async_resolve(hass: HomeAssistant, host: str) -> str | None: @@ -82,6 +77,18 @@ def async_get_devices_by_type( return devices +@callback +def async_get_devices( + bootstrap: Bootstrap, model_type: Iterable[ModelType] +) -> Generator[ProtectAdoptableDeviceModel, None, None]: + """Return all device by type.""" + return ( + device + for device_type in model_type + for device in async_get_devices_by_type(bootstrap, device_type).values() + ) + + @callback def async_get_light_motion_current(obj: Light) -> str: """Get light motion mode for Flood Light.""" @@ -99,22 +106,3 @@ def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str: """Generate entry specific dispatch ID.""" return f"{DOMAIN}.{entry.entry_id}.{dispatch}" - - -@callback -def convert_mac_list(option: str, raise_exception: bool = False) -> set[str]: - """Convert csv list of MAC addresses.""" - - macs = set() - values = cv.ensure_list_csv(option) - for value in values: - if value == "": - continue - value = async_unifi_mac(value) - if not MAC_RE.match(value): - if raise_exception: - raise vol.Invalid("invalid_mac_list") - continue - macs.add(value) - - return macs diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index fa245e8b1cc..b006dfbd004 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -68,7 +68,6 @@ def mock_ufp_config_entry(): "port": 443, "verify_ssl": False, }, - options={"ignored_devices": "FFFFFFFFFFFF,test"}, version=2, ) diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 26a9dd73ee8..d0fb0dba9f2 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -14,7 +14,6 @@ from homeassistant.components import dhcp, ssdp from homeassistant.components.unifiprotect.const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_OVERRIDE_CHOST, DOMAIN, ) @@ -270,52 +269,10 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "all_updates": True, "disable_rtsp": True, "override_connection_host": True, + "max_media": 1000, } -async def test_form_options_invalid_mac( - hass: HomeAssistant, ufp_client: ProtectApiClient -) -> None: - """Test we handle options flows.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - "id": "UnifiProtect", - "port": 443, - "verify_ssl": False, - "max_media": 1000, - }, - version=2, - unique_id=dr.format_mac(MAC_ADDR), - ) - mock_config.add_to_hass(hass) - - with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.ProtectApiClient" - ) as mock_api: - mock_api.return_value = ufp_client - - await hass.config_entries.async_setup(mock_config.entry_id) - await hass.async_block_till_done() - assert mock_config.state == config_entries.ConfigEntryState.LOADED - - result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM - assert not result["errors"] - assert result["step_id"] == "init" - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - {CONF_IGNORED: "test,test2"}, - ) - - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {CONF_IGNORED: "invalid_mac_list"} - - @pytest.mark.parametrize( "source, data", [ diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 7a1e590b87d..9392caa30ac 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -7,21 +7,20 @@ from unittest.mock import AsyncMock, patch import aiohttp from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient -from pyunifiprotect.data import NVR, Bootstrap, Doorlock, Light, Sensor +from pyunifiprotect.data import NVR, Bootstrap, Light from homeassistant.components.unifiprotect.const import ( CONF_DISABLE_RTSP, - CONF_IGNORED, DEFAULT_SCAN_INTERVAL, DOMAIN, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import _patch_discovery -from .utils import MockUFPFixture, get_device_from_ufp_device, init_entry, time_changed +from .utils import MockUFPFixture, init_entry, time_changed from tests.common import MockConfigEntry @@ -212,38 +211,28 @@ async def test_device_remove_devices( hass: HomeAssistant, ufp: MockUFPFixture, light: Light, - doorlock: Doorlock, - sensor: Sensor, hass_ws_client: Callable[ [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] ], ) -> None: """Test we can only remove a device that no longer exists.""" - sensor.mac = "FFFFFFFFFFFF" - - await init_entry(hass, ufp, [light, doorlock, sensor], regenerate_ids=False) + await init_entry(hass, ufp, [light]) assert await async_setup_component(hass, "config", {}) - + entity_id = "light.test_light" entry_id = ufp.entry.entry_id + + registry: er.EntityRegistry = er.async_get(hass) + entity = registry.async_get(entity_id) + assert entity is not None device_registry = dr.async_get(hass) - light_device = get_device_from_ufp_device(hass, light) - assert light_device is not None + live_device_entry = device_registry.async_get(entity.device_id) assert ( - await remove_device(await hass_ws_client(hass), light_device.id, entry_id) - is True + await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id) + is False ) - doorlock_device = get_device_from_ufp_device(hass, doorlock) - assert ( - await remove_device(await hass_ws_client(hass), doorlock_device.id, entry_id) - is True - ) - - sensor_device = get_device_from_ufp_device(hass, sensor) - assert sensor_device is None - dead_device_entry = device_registry.async_get_or_create( config_entry_id=entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")}, @@ -253,10 +242,6 @@ async def test_device_remove_devices( is True ) - await time_changed(hass, 60) - entry = hass.config_entries.async_get_entry(entry_id) - entry.options[CONF_IGNORED] == f"{light.mac},{doorlock.mac}" - async def test_device_remove_devices_nvr( hass: HomeAssistant, diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 3376db4ec51..bee479b8e2b 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -23,7 +23,7 @@ from pyunifiprotect.test_util.anonymize import random_hex from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityDescription import homeassistant.util.dt as dt_util @@ -229,13 +229,3 @@ async def adopt_devices( ufp.ws_msg(mock_msg) await hass.async_block_till_done() - - -def get_device_from_ufp_device( - hass: HomeAssistant, device: ProtectAdoptableDeviceModel -) -> dr.DeviceEntry | None: - """Return all device by type.""" - registry = dr.async_get(hass) - return registry.async_get_device( - identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} - ) From 2eeab820b724427d20b2f0b1ef436573b2cb1f26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 16:43:18 -0500 Subject: [PATCH 897/903] Bump aiohomekit to 1.5.2 (#77927) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index e3526dd870a..08eac050c98 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.5.1"], + "requirements": ["aiohomekit==1.5.2"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 7a945fc9deb..2c26df6547f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.5.1 +aiohomekit==1.5.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a76db50147..3b6adffe4a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.5.1 +aiohomekit==1.5.2 # homeassistant.components.emulated_hue # homeassistant.components.http From 941a5e382046d4fe7f2b1b12a8a8cc7a160d2b53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Sep 2022 03:22:19 -0500 Subject: [PATCH 898/903] Bump led-ble to 0.7.1 (#77931) --- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 273fbfedc04..1dd289daa4d 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -3,7 +3,7 @@ "name": "LED BLE", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ble_ble", - "requirements": ["led-ble==0.7.0"], + "requirements": ["led-ble==0.7.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2c26df6547f..1dc6a08de26 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.0 +led-ble==0.7.1 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b6adffe4a5..1e5013eb5f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.0 +led-ble==0.7.1 # homeassistant.components.foscam libpyfoscam==1.0 From a3edbfc6017e80376f1b2c7649e20f0a9a6b8980 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 18:12:32 -0500 Subject: [PATCH 899/903] Small tweaks to improve performance of bluetooth matching (#77934) * Small tweaks to improve performance of bluetooth matching * Small tweaks to improve performance of bluetooth matching * cleanup --- homeassistant/components/bluetooth/match.py | 68 +++++++++++---------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 813acfc8cda..dd1c9c1fa3c 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -180,12 +180,20 @@ class BluetoothMatcherIndexBase(Generic[_T]): We put them in the bucket that they are most likely to match. """ + # Local name is the cheapest to match since its just a dict lookup if LOCAL_NAME in matcher: self.local_name.setdefault( _local_name_to_index_key(matcher[LOCAL_NAME]), [] ).append(matcher) return + # Manufacturer data is 2nd cheapest since its all ints + if MANUFACTURER_ID in matcher: + self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( + matcher + ) + return + if SERVICE_UUID in matcher: self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher) return @@ -196,12 +204,6 @@ class BluetoothMatcherIndexBase(Generic[_T]): ) return - if MANUFACTURER_ID in matcher: - self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( - matcher - ) - return - def remove(self, matcher: _T) -> None: """Remove a matcher from the index. @@ -214,6 +216,10 @@ class BluetoothMatcherIndexBase(Generic[_T]): ) return + if MANUFACTURER_ID in matcher: + self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) + return + if SERVICE_UUID in matcher: self.service_uuid[matcher[SERVICE_UUID]].remove(matcher) return @@ -222,10 +228,6 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher) return - if MANUFACTURER_ID in matcher: - self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) - return - def build(self) -> None: """Rebuild the index sets.""" self.service_uuid_set = set(self.service_uuid) @@ -235,33 +237,36 @@ class BluetoothMatcherIndexBase(Generic[_T]): def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]: """Check for a match.""" matches = [] - if len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH: + if service_info.name and len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH: for matcher in self.local_name.get( service_info.name[:LOCAL_NAME_MIN_MATCH_LENGTH], [] ): if ble_device_matches(matcher, service_info): matches.append(matcher) - for service_data_uuid in self.service_data_uuid_set.intersection( - service_info.service_data - ): - for matcher in self.service_data_uuid[service_data_uuid]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + if self.service_data_uuid_set and service_info.service_data: + for service_data_uuid in self.service_data_uuid_set.intersection( + service_info.service_data + ): + for matcher in self.service_data_uuid[service_data_uuid]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) - for manufacturer_id in self.manufacturer_id_set.intersection( - service_info.manufacturer_data - ): - for matcher in self.manufacturer_id[manufacturer_id]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + if self.manufacturer_id_set and service_info.manufacturer_data: + for manufacturer_id in self.manufacturer_id_set.intersection( + service_info.manufacturer_data + ): + for matcher in self.manufacturer_id[manufacturer_id]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) - for service_uuid in self.service_uuid_set.intersection( - service_info.service_uuids - ): - for matcher in self.service_uuid[service_uuid]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + if self.service_uuid_set and service_info.service_uuids: + for service_uuid in self.service_uuid_set.intersection( + service_info.service_uuids + ): + for matcher in self.service_uuid[service_uuid]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) return matches @@ -347,8 +352,6 @@ def ble_device_matches( service_info: BluetoothServiceInfoBleak, ) -> bool: """Check if a ble device and advertisement_data matches the matcher.""" - device = service_info.device - # Don't check address here since all callers already # check the address and we don't want to double check # since it would result in an unreachable reject case. @@ -379,7 +382,8 @@ def ble_device_matches( return False if (local_name := matcher.get(LOCAL_NAME)) and ( - (device_name := advertisement_data.local_name or device.name) is None + (device_name := advertisement_data.local_name or service_info.device.name) + is None or not _memorized_fnmatch( device_name, local_name, From 3acc3af38c5b0535ee4be4c5baa2b165157645d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Sep 2022 06:12:17 -0500 Subject: [PATCH 900/903] Bump PySwitchbot to 0.18.25 (#77935) --- homeassistant/components/switchbot/__init__.py | 2 ++ homeassistant/components/switchbot/const.py | 2 ++ homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 59ed071f325..345190d8933 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -31,6 +31,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { SupportedModels.BULB.value: [Platform.SENSOR, Platform.LIGHT], SupportedModels.LIGHT_STRIP.value: [Platform.SENSOR, Platform.LIGHT], + SupportedModels.CEILING_LIGHT.value: [Platform.SENSOR, Platform.LIGHT], SupportedModels.BOT.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.PLUG.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.CURTAIN.value: [ @@ -43,6 +44,7 @@ PLATFORMS_BY_TYPE = { SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR], } CLASS_BY_DEVICE = { + SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, SupportedModels.CURTAIN.value: switchbot.SwitchbotCurtain, SupportedModels.BOT.value: switchbot.Switchbot, SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index aa334120b85..ecd86e1bef5 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -16,6 +16,7 @@ class SupportedModels(StrEnum): BOT = "bot" BULB = "bulb" + CEILING_LIGHT = "ceiling_light" CURTAIN = "curtain" HYGROMETER = "hygrometer" LIGHT_STRIP = "light_strip" @@ -30,6 +31,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.PLUG_MINI: SupportedModels.PLUG, SwitchbotModel.COLOR_BULB: SupportedModels.BULB, SwitchbotModel.LIGHT_STRIP: SupportedModels.LIGHT_STRIP, + SwitchbotModel.CEILING_LIGHT: SupportedModels.CEILING_LIGHT, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 9fb73a62dd6..040e76391bd 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.22"], + "requirements": ["PySwitchbot==0.18.25"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 1dc6a08de26..c8723cc6737 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.22 +PySwitchbot==0.18.25 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e5013eb5f4..bb8403744b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.22 +PySwitchbot==0.18.25 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 10f7e2ff8a775400a28384ec7e2d762f670c336b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Sep 2022 09:54:21 -0500 Subject: [PATCH 901/903] Handle stale switchbot advertisement data while connected (#77956) --- homeassistant/components/switchbot/cover.py | 3 +++ homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/switch.py | 13 +++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index c2b6cb1a4c7..df716be6ff3 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging from typing import Any +import switchbot + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, @@ -36,6 +38,7 @@ async def async_setup_entry( class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Representation of a Switchbot.""" + _device: switchbot.SwitchbotCurtain _attr_device_class = CoverDeviceClass.CURTAIN _attr_supported_features = ( CoverEntityFeature.OPEN diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 040e76391bd..f322734ba54 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.25"], + "requirements": ["PySwitchbot==0.18.27"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 17235135cfa..d524a7100f0 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging from typing import Any +import switchbot + from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON @@ -34,6 +36,7 @@ class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): """Representation of a Switchbot switch.""" _attr_device_class = SwitchDeviceClass.SWITCH + _device: switchbot.Switchbot def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot.""" @@ -69,21 +72,19 @@ class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): @property def assumed_state(self) -> bool: """Return true if unable to access real state of entity.""" - if not self.data["data"]["switchMode"]: - return True - return False + return not self._device.switch_mode() @property def is_on(self) -> bool | None: """Return true if device is on.""" - if not self.data["data"]["switchMode"]: + if not self._device.switch_mode(): return self._attr_is_on - return self.data["data"]["isOn"] + return self._device.is_on() @property def extra_state_attributes(self) -> dict: """Return the state attributes.""" return { **super().extra_state_attributes, - "switch_mode": self.data["data"]["switchMode"], + "switch_mode": self._device.switch_mode(), } diff --git a/requirements_all.txt b/requirements_all.txt index c8723cc6737..0119639e3fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.25 +PySwitchbot==0.18.27 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb8403744b4..0878fbf4a16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.25 +PySwitchbot==0.18.27 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From e69fde68757542abdadbc46230259148f137f3f6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Sep 2022 17:31:53 +0200 Subject: [PATCH 902/903] Update frontend to 20220907.0 (#77963) --- 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 8abc8fd4e32..07822979683 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==20220906.0"], + "requirements": ["home-assistant-frontend==20220907.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 58a000fbc25..7c11725e460 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220906.0 +home-assistant-frontend==20220907.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 0119639e3fc..a3c604dfc2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220906.0 +home-assistant-frontend==20220907.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0878fbf4a16..d974a7a181f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220906.0 +home-assistant-frontend==20220907.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 4ab5cdcb795845a5dbc83fea72d00edd788986fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Sep 2022 17:46:53 +0200 Subject: [PATCH 903/903] Bumped version to 2022.9.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 156ba1d133d..c3a59a88aae 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 1fdc8d87e07..9b05ae89191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b6" +version = "2022.9.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"